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: 1 addition & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
BITTE_API_KEY=
NEAR_PK=
NEXT_PUBLIC_ACCOUNT_ID=
CRON_SECRET=
DEPLOYMENT_CONFIG=
16 changes: 16 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const nextJest = require('next/jest')

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})

// Add any custom config to be passed to Jest
const customJestConfig = {
testEnvironment: 'node',
}

process.env.TZ = 'UTC'
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
4 changes: 2 additions & 2 deletions lib/agent-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { calculatePositionsPnL, fetchMarketPrices, fetchPortfolioBalances } from
import { getCurrentPositions } from './api-helpers'
import type { AgentContext, PositionWithPnL } from './types'
import { TOKEN_LIST } from './utils'
import { getEnvStrategy } from './strategies'
import { loadConfig } from './config'

export async function buildAgentContext(
accountId: string,
Expand Down Expand Up @@ -84,7 +84,7 @@ function generateSystemPrompt(
marketOverviewData: string,
accountId: string
): string {
const strategy = getEnvStrategy()
const strategy = loadConfig().strategy

const tradingPositions = positionsWithPnl.filter(
(pos) => pos.symbol !== 'USDC' && Number(pos.rawBalance) >= 1000
Expand Down
5 changes: 3 additions & 2 deletions lib/api-auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { NextRequest, NextResponse } from 'next/server'
import { getEnvVar } from './env'
import { loadConfig } from './config'

type AuthenticatedCallHandler = (req: NextRequest) => Promise<NextResponse>

export function withCronSecret(handler: AuthenticatedCallHandler) {
return async (req: NextRequest): Promise<NextResponse> => {
const { cronSecret } = loadConfig()
const authHeader = req.headers.get('Authorization')
if (authHeader !== `Bearer ${getEnvVar('CRON_SECRET')}`) {
if (authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

Expand Down
5 changes: 3 additions & 2 deletions lib/api-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getEnvVar } from './env'
import { loadConfig } from './config'
import { Quote, PositionWithPnL, CurrentPosition } from './types'
import { TOKEN_LIST } from './utils'

Expand All @@ -10,9 +10,10 @@ interface ApiCallOptions {
}

async function makeApiCall(endpoint: string, options: ApiCallOptions) {
const { bitteKey } = loadConfig()
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${getEnvVar('BITTE_API_KEY')}`,
Authorization: `Bearer ${bitteKey}`,
...options.headers,
}

Expand Down
67 changes: 67 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { z } from 'zod'
import { getEnvVar } from './env'

const StrategyConfigSchema = z.object({
overview: z.string(),
riskParams: z.object({
profitTarget: z.number(),
stopLoss: z.number(),
maxPositions: z.number(),
positionSize: z.string(),
}),
step1Rules: z.string(),
step2Rules: z.string(),
step3Rules: z.string(),
})

export const DEFAULT_STRATEGY: StrategyConfig = {
overview:
'Wall Street 3-Step: Data-driven day trading with clear profit/loss targets and risk management',
riskParams: {
profitTarget: 2,
stopLoss: -1.5,
maxPositions: 4,
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). Don't close positions with raw balance below 1000.",
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.',
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.',
}

const ed25519Pattern = /^ed25519:[1-9A-HJ-NP-Za-km-z]{43,87}$/

export const ed25519String = z
.string()
.regex(ed25519Pattern, 'Invalid NEAR private key: must be ed25519:<base58>')
.transform((val) => val as `ed25519:${string}`)

export const ServerConfigSchema = z.object({
strategy: StrategyConfigSchema.optional().default(DEFAULT_STRATEGY),
cronSecret: z.string(),
bitteKey: z.string(),
nearPk: ed25519String,
})

export type StrategyConfig = z.infer<typeof StrategyConfigSchema>
export type ServerConfig = z.infer<typeof ServerConfigSchema>

export function parseConfig(configString: string): ServerConfig {
return ServerConfigSchema.parse(JSON.parse(configString))
}

export function loadConfig(): ServerConfig {
try {
return parseConfig(getEnvVar('DEPLOYMENT_CONFIG'))
} catch (error: unknown) {
console.warn('Failed to parse DEPLOYMENT_CONFIG, trying individual env vars', String(error))
return {
strategy: JSON.parse(getEnvVar('STRATEGY', JSON.stringify(DEFAULT_STRATEGY))),
cronSecret: getEnvVar('CRON_SECRET'),
bitteKey: getEnvVar('BITTE_API_KEY'),
nearPk: getEnvVar('NEAR_PK') as `ed25519:${string}`,
}
}
}
5 changes: 3 additions & 2 deletions lib/near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { actionCreators } from '@near-js/transactions'
import { Account, KeyPair, keyStores, Near } from 'near-api-js'
import type { Quote } from './types'
import { INTENTS_CONTRACT_ID, NEAR_RPC_URL, TGas, USDC_CONTRACT } from './utils'
import { getEnvVar } from './env'
import { loadConfig } from './config'

const FIFTY_TGAS = BigInt(TGas * 50)
const ONE_YOCTO = BigInt(1)
Expand All @@ -22,7 +22,8 @@ export async function getTokenBalance(account: Account, assetId: string): Promis
}

export async function initializeNearAccount(accountId: string): Promise<Account> {
const keyPair = KeyPair.fromString(getEnvVar('NEAR_PK') as `ed25519:${string}`)
const { nearPk } = loadConfig()
const keyPair = KeyPair.fromString(nearPk)
const keyStore = new keyStores.InMemoryKeyStore()
keyStore.setKey('mainnet', accountId, keyPair)

Expand Down
33 changes: 0 additions & 33 deletions lib/strategies/index.ts

This file was deleted.

12 changes: 0 additions & 12 deletions lib/strategies/types.ts

This file was deleted.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"format": "next lint --fix && prettier . --write",
"fmt": "pnpm format",
"start": "next start",
"types": "tsc --noEmit"
"types": "tsc --noEmit",
"test": "jest"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.24",
Expand All @@ -21,15 +22,18 @@
"near-api-js": "^5.1.0",
"next": "15.2.4",
"react": "^19",
"react-dom": "^19"
"react-dom": "^19",
"zod": "^4.1.12"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@types/jest": "^30.0.0",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"jest": "^30.2.0",
"prettier": "^3.6.2",
"typescript": "^5"
}
Expand Down
Loading