Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: "22"
cache: "pnpm"
node-version: '22'
cache: 'pnpm'

- name: Install Dependencies
run: pnpm install --frozen-lockfile
Expand Down
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pnpm-lock.yaml
node_modules
.next
dist
build

8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"endOfLine": "lf"
}
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ The system comes with a proven Wall Street 3-step strategy:
// From lib/strategies/index.ts
export const DEFAULT_STRATEGY: StrategyConfig = {
overview:
"Wall Street 3-Step: Data-driven day trading with clear profit/loss targets and risk management",
'Wall Street 3-Step: Data-driven day trading with clear profit/loss targets and risk management',
riskParams: {
profitTarget: 2, // +2% profit target
stopLoss: -1.5, // -1.5% stop loss
maxPositions: 4, // Max 4 open positions
positionSize: "5-15% of USDC",
positionSize: '5-15% of USDC',
},
step1Rules:
"Risk targets: SELL at +2% profit OR -1.5% loss. Close losing positions faster than winners (cut losses, let profits run). .",
'Risk targets: SELL at +2% profit OR -1.5% loss. Close losing positions faster than winners (cut losses, let profits run). .',
step2Rules:
"Screen for high-probability setups: Price momentum >3% with volume confirmation, Fear/Greed extremes, Order book imbalances. Use 1 analysis tool only if market data insufficient. Only trade clear directional moves.",
'Screen for high-probability setups: Price momentum >3% with volume confirmation, Fear/Greed extremes, Order book imbalances. Use 1 analysis tool only if market data insufficient. Only trade clear directional moves.',
step3Rules:
"Dynamic sizing: 5-15% per trade (scales with account). Size calculation: Min($10, Max($5, USDC_balance * 0.10)). Account for slippage: Minimum $8 positions. Max 3-4 open positions at once.",
};
'Dynamic sizing: 5-15% per trade (scales with account). Size calculation: Min($10, Max($5, USDC_balance * 0.10)). Account for slippage: Minimum $8 positions. Max 3-4 open positions at once.',
}
```

### Available Tools
Expand All @@ -46,17 +46,17 @@ All strategies have access to these trading tools:

```typescript
const template: StrategyConfig = {
overview: "Your Strategy Name: What does your strategy do?",
overview: 'Your Strategy Name: What does your strategy do?',
riskParams: {
profitTarget: 2, // % profit to exit
stopLoss: -1.5, // % loss to exit
maxPositions: 4, // Max open positions
positionSize: "5-15% of USDC", // Position sizing
positionSize: '5-15% of USDC', // Position sizing
},
step1Rules: "When and how to close existing positions...",
step2Rules: "What market conditions to look for...",
step3Rules: "How to size and execute new positions...",
};
step1Rules: 'When and how to close existing positions...',
step2Rules: 'What market conditions to look for...',
step3Rules: 'How to size and execute new positions...',
}
```

### Core Infrastructure
Expand Down
50 changes: 22 additions & 28 deletions app/api/deposit/route.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,49 @@
import { NextRequest, NextResponse } from "next/server";
import { BALANCE_UPDATE_DELAY } from "@/lib/utils";
import { initializeNearAccount, depositUSDC, getUSDCBalance } from "@/lib/near";
import { formatUnits } from "@/lib/viem";
import { withCronSecret } from "@/lib/api-auth";
import { getEnvVar } from "@/lib/env";
import { NextRequest, NextResponse } from 'next/server'
import { BALANCE_UPDATE_DELAY } from '@/lib/utils'
import { initializeNearAccount, depositUSDC, getUSDCBalance } from '@/lib/near'
import { formatUnits } from '@/lib/viem'
import { withCronSecret } from '@/lib/api-auth'
import { getEnvVar } from '@/lib/env'

async function depositHandler(request: NextRequest) {
try {
const accountId = getEnvVar("NEXT_PUBLIC_ACCOUNT_ID");
const { searchParams } = new URL(request.url);
const depositStr = searchParams.get("amount");
const accountId = getEnvVar('NEXT_PUBLIC_ACCOUNT_ID')
const { searchParams } = new URL(request.url)
const depositStr = searchParams.get('amount')
if (!depositStr) {
return NextResponse.json(
{ error: "unspecified amount" },
{ status: 400 },
);
return NextResponse.json({ error: 'unspecified amount' }, { status: 400 })
}

const depositAmount = BigInt(depositStr);
const depositAmount = BigInt(depositStr)

const account = await initializeNearAccount(accountId);
const account = await initializeNearAccount(accountId)

const usdcBalance = await getUSDCBalance(account);
const usdcBalance = await getUSDCBalance(account)

if (usdcBalance < depositAmount) {
return NextResponse.json(
{
error: `Insufficient USDC balance (required: $${depositAmount}, available: $${usdcBalance})`,
},
{ status: 400 },
);
{ status: 400 }
)
}

const tx = await depositUSDC(account, depositAmount);
const tx = await depositUSDC(account, depositAmount)

await new Promise((resolve) => setTimeout(resolve, BALANCE_UPDATE_DELAY));
await new Promise((resolve) => setTimeout(resolve, BALANCE_UPDATE_DELAY))

const uiAmount = formatUnits(depositAmount, 6);
const uiAmount = formatUnits(depositAmount, 6)

return NextResponse.json({
message: `Successfully deposited $${uiAmount} USDC`,
transactionHash: tx.transaction.hash,
amount: uiAmount,
});
})
} catch (error) {
console.error("Error in deposit endpoint:", error);
return NextResponse.json(
{ error: "Failed to process deposit request" },
{ status: 500 },
);
console.error('Error in deposit endpoint:', error)
return NextResponse.json({ error: 'Failed to process deposit request' }, { status: 500 })
}
}

export const GET = withCronSecret(depositHandler);
export const GET = withCronSecret(depositHandler)
72 changes: 34 additions & 38 deletions app/api/trade/route.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,70 @@
import { NextResponse } from "next/server";
import { BALANCE_UPDATE_DELAY, logTradingAgentData } from "@/lib/utils";
import { storeTrade, storePortfolioSnapshot } from "@/lib/api-helpers";
import { buildTransactionPayload, initializeNearAccount } from "@/lib/near";
import { buildAgentContext } from "@/lib/agent-context";
import { callAgent } from "@bitte-ai/agent-sdk";
import { ToolResult } from "@/lib/types";
import { withCronSecret } from "@/lib/api-auth";
import { getEnvVar } from "@/lib/env";
import { NextResponse } from 'next/server'
import { BALANCE_UPDATE_DELAY, logTradingAgentData } from '@/lib/utils'
import { storeTrade, storePortfolioSnapshot } from '@/lib/api-helpers'
import { buildTransactionPayload, initializeNearAccount } from '@/lib/near'
import { AGENT_TRIGGER_MESSAGE, buildAgentContext } from '@/lib/agent-context'
import { callAgent } from '@bitte-ai/agent-sdk'
import { ToolResult } from '@/lib/types'
import { withCronSecret } from '@/lib/api-auth'
import { getEnvVar } from '@/lib/env'

async function tradeHandler(): Promise<NextResponse> {
try {
const accountId = getEnvVar("NEXT_PUBLIC_ACCOUNT_ID");
const agentId = "trading-agent-kappa.vercel.app";
const accountId = getEnvVar('NEXT_PUBLIC_ACCOUNT_ID')
const agentId = 'trading-agent-kappa.vercel.app'

const account = await initializeNearAccount(accountId);
const account = await initializeNearAccount(accountId)

const context = await buildAgentContext(accountId, account);
const context = await buildAgentContext(accountId, account)

const { content, toolResults } = await callAgent(
const { content, toolResults } = await callAgent({
accountId,
context.systemPrompt,
message: AGENT_TRIGGER_MESSAGE,
agentId,
);
systemPrompt: context.systemPrompt,
})

const quoteResult = (toolResults as ToolResult[]).find(
(callResult) => callResult.result?.data?.data?.quote,
);
const quote = quoteResult?.result?.data?.data?.quote;
(callResult) => callResult.result?.data?.data?.quote
)
const quote = quoteResult?.result?.data?.data?.quote

logTradingAgentData({
context,
content,
pnlUsd: context.totalPnl,
quoteResult,
});
})

if (quote) {
const tx = await account.signAndSendTransaction(
buildTransactionPayload(quote),
);
console.log("Trade executed:", tx.transaction.hash);
await new Promise((resolve) => setTimeout(resolve, BALANCE_UPDATE_DELAY));
await storeTrade(accountId, quote);
const tx = await account.signAndSendTransaction(buildTransactionPayload(quote))
console.log('Trade executed:', tx.transaction.hash)
await new Promise((resolve) => setTimeout(resolve, BALANCE_UPDATE_DELAY))
await storeTrade(accountId, quote)

const updatedContext = await buildAgentContext(accountId, account);
const updatedContext = await buildAgentContext(accountId, account)

await storePortfolioSnapshot(
accountId,
updatedContext.positionsWithPnl,
updatedContext.totalUsd,
context.totalUsd,
content,
);
content
)
} else {
await storePortfolioSnapshot(
accountId,
context.positionsWithPnl,
context.totalUsd,
context.totalUsd,
content,
);
content
)
}
return NextResponse.json({ content });
return NextResponse.json({ content })
} catch (error) {
console.error("Error in trading endpoint:", error);
return NextResponse.json(
{ error: "Failed to process trading request" },
{ status: 500 },
);
console.error('Error in trading endpoint:', error)
return NextResponse.json({ error: 'Failed to process trading request' }, { status: 500 })
}
}

export const GET = withCronSecret(tradeHandler);
export const GET = withCronSecret(tradeHandler)
66 changes: 25 additions & 41 deletions app/api/withdraw/route.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,56 @@
import { NextRequest, NextResponse } from "next/server";
import { BALANCE_UPDATE_DELAY, USDC_CONTRACT } from "@/lib/utils";
import {
initializeNearAccount,
withdrawToken,
intentsBalance,
} from "@/lib/near";
import { formatUnits } from "@/lib/viem";
import { NextRequest, NextResponse } from 'next/server'
import { BALANCE_UPDATE_DELAY, USDC_CONTRACT } from '@/lib/utils'
import { initializeNearAccount, withdrawToken, intentsBalance } from '@/lib/near'
import { formatUnits } from '@/lib/viem'

const bigIntMin = (a: bigint, b: bigint) => (a < b ? a : b);
const ZERO = BigInt(0);
const bigIntMin = (a: bigint, b: bigint) => (a < b ? a : b)
const ZERO = BigInt(0)

export async function GET(request: NextRequest) {
try {
if (process.env.CRON_SECRET) {
const authHeader = request.headers.get("Authorization");
const authHeader = request.headers.get('Authorization')
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
}

const { searchParams } = new URL(request.url);
const withdrawStr = searchParams.get("amount");
const { searchParams } = new URL(request.url)
const withdrawStr = searchParams.get('amount')
if (!withdrawStr) {
return NextResponse.json(
{ error: "unspecified amount" },
{ status: 400 },
);
return NextResponse.json({ error: 'unspecified amount' }, { status: 400 })
}
const token = searchParams.get("token") || USDC_CONTRACT;
const token = searchParams.get('token') || USDC_CONTRACT

const accountId = process.env.NEXT_PUBLIC_ACCOUNT_ID;
const accountId = process.env.NEXT_PUBLIC_ACCOUNT_ID
if (!accountId) {
return NextResponse.json(
{ error: "accountId is not configured" },
{ status: 500 },
);
return NextResponse.json({ error: 'accountId is not configured' }, { status: 500 })
}

const requestedWithdrawAmount = BigInt(withdrawStr);
const requestedWithdrawAmount = BigInt(withdrawStr)

const account = await initializeNearAccount(accountId);
const account = await initializeNearAccount(accountId)

const usdcBalance = await intentsBalance(account, token);
const withdrawAmount = bigIntMin(requestedWithdrawAmount, usdcBalance);
const usdcBalance = await intentsBalance(account, token)
const withdrawAmount = bigIntMin(requestedWithdrawAmount, usdcBalance)

if (withdrawAmount == ZERO) {
return NextResponse.json(
{ message: "Nothing to withdraw" },
{ status: 200 },
);
return NextResponse.json({ message: 'Nothing to withdraw' }, { status: 200 })
}

const tx = await withdrawToken(account, token, withdrawAmount);
const tx = await withdrawToken(account, token, withdrawAmount)

await new Promise((resolve) => setTimeout(resolve, BALANCE_UPDATE_DELAY));
await new Promise((resolve) => setTimeout(resolve, BALANCE_UPDATE_DELAY))

const uiAmount = formatUnits(withdrawAmount, 6);
const uiAmount = formatUnits(withdrawAmount, 6)

return NextResponse.json({
message: `Successfully withdrew $${uiAmount} USDC`,
transactionHash: tx.transaction.hash,
amount: uiAmount,
});
})
} catch (error) {
console.error("Error in deposit endpoint:", error);
return NextResponse.json(
{ error: "Failed to process deposit request" },
{ status: 500 },
);
console.error('Error in deposit endpoint:', error)
return NextResponse.json({ error: 'Failed to process deposit request' }, { status: 500 })
}
}
16 changes: 6 additions & 10 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const metadata = {
title: "Autonomous Trading Agent",
description: "Deployment status page",
};
title: 'Autonomous Trading Agent',
description: 'Deployment status page',
}

const styles = `
* { margin: 0; padding: 0; box-sizing: border-box; }
Expand All @@ -18,13 +18,9 @@ const styles = `
.description { font-size: 18px; }
.button { width: 100%; max-width: 280px; }
}
`;
`

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
Expand All @@ -38,5 +34,5 @@ export default function RootLayout({
{children}
</body>
</html>
);
)
}
Loading