Why TypeScript for Financial Agents?
TypeScript's type system is particularly valuable in financial applications where correctness is critical. When your agent is placing trades, sending USDC, or managing positions worth real money, a type error at compile time is vastly preferable to a runtime crash — or worse, a silent logic error that sends funds to the wrong address.
Typed interfaces for API responses mean your IDE catches mistakes before you run the code. A field renamed from agent_id to agentId becomes a compile-time error rather than an undefined variable at runtime. Generic HTTP client helpers ensure every endpoint returns the correct shape, and strict null checking forces you to handle every "this might be undefined" case explicitly.
This tutorial builds a complete TypeScript trading agent that integrates with Purple Flea's API. We'll define proper interfaces for all response types, build a type-safe API client class, implement a simple trading strategy, and then show how to expose it as an AI tool using the Vercel AI SDK. The same code runs unchanged on Deno and Bun with minimal modifications.
Project Setup
Create the project directory and initialize it:
mkdir pf-ts-agent && cd pf-ts-agent npm init -y
{
"name": "pf-ts-agent",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx src/agent.ts",
"start": "node dist/agent.js"
},
"dependencies": {
"ai": "^4.0.0",
"@ai-sdk/openai": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"tsx": "^4.0.0",
"@types/node": "^20.0.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
npm install mkdir src
Type Interfaces for the Purple Flea API
Every API response should have a corresponding TypeScript interface. This is the core value of using TypeScript — when the API shape is fully typed, your agent code becomes self-documenting and the compiler catches misuse immediately.
// src/types.ts — Purple Flea API response interfaces // ─── Agent ──────────────────────────────────────────── export interface RegisterResponse { agent_id: string; address: string; mnemonic: string; chain: string; } // ─── Faucet ─────────────────────────────────────────── export interface FaucetResponse { success: boolean; amount?: string; // USD amount awarded, e.g. "1.00" new_balance?: string; error?: string; } // ─── Wallet ─────────────────────────────────────────── export interface TokenBalance { formatted: string; // e.g. "1.234567" raw: string; // raw integer string (wei, lamports, satoshis) } export interface WalletBalance { eth?: TokenBalance; usdc: TokenBalance; usdt: TokenBalance; sol?: TokenBalance & { spendable: string }; native?: TokenBalance; // for non-ETH native tokens } // ─── Trading ────────────────────────────────────────── export interface Market { symbol: string; mark_price: string; funding_rate: string; max_leverage: number; min_size: string; volume_24h: string; price_change_24h: string; open_interest: string; } export type Direction = 'long' | 'short'; export interface OpenPositionRequest { agent_id: string; symbol: string; direction: Direction; margin: string; leverage: string; } export interface Position { position_id: string; symbol: string; direction: Direction; entry_price: string; mark_price: string; size_usd: string; margin: string; leverage: number; unrealized_pnl: string; liquidation_price: string; opened_at: string; // ISO 8601 } export interface ClosePositionResponse { position_id: string; exit_price: string; realized_pnl: string; new_balance: string; } // ─── Casino ─────────────────────────────────────────── export type CoinSide = 'heads' | 'tails'; export type DiceDir = 'over' | 'under'; export interface CoinFlipResult { outcome: CoinSide; payout: string; server_seed_hash: string; client_seed: string; nonce: number; server_seed?: string; // revealed after round } // ─── Generic API wrapper ────────────────────────────── export interface ApiError { error: string; code?: number; details?: string; } export type ApiResult<T> = T | ApiError; export function isError<T>(result: ApiResult<T>): result is ApiError { return 'error' in result; }
Type-Safe API Client Class
A client class centralizes all API calls, handles authentication, and enforces return types through generics. Any endpoint added later inherits the same type-safe fetch wrapper.
// src/client.ts — Purple Flea type-safe API client import type { RegisterResponse, FaucetResponse, WalletBalance, Market, OpenPositionRequest, Position, ClosePositionResponse, CoinFlipResult, CoinSide, DiceDir, ApiResult } from './types.js'; export class PurpleFleatClient { private readonly base: string; private readonly headers: Record<string, string>; constructor(apiKey: string, base = 'https://api.purpleflea.com') { this.base = base; this.headers = { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }; } /** Generic fetch wrapper with typed response */ private async request<T>( method: string, path: string, body?: unknown, params?: Record<string, string> ): Promise<ApiResult<T>> { const url = new URL(this.base + path); if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); const res = await fetch(url.toString(), { method, headers: this.headers, body: body != null ? JSON.stringify(body) : undefined }); return res.json() as Promise<ApiResult<T>>; } // ─── Agent ──────────────────────────────────────── async register(name: string, chain: string = 'ethereum') { return this.request<RegisterResponse>('POST', '/agent/register', { name, chain }); } // ─── Faucet ─────────────────────────────────────── async claimFaucet(agentId: string) { return this.request<FaucetResponse>('POST', '/faucet/claim', { agent_id: agentId }); } // ─── Wallet ─────────────────────────────────────── async getBalance(walletId: string, chain: string = 'ethereum') { return this.request<WalletBalance>('GET', '/wallet/balance', undefined, { wallet_id: walletId, chain }); } // ─── Trading ────────────────────────────────────── async getMarkets() { return this.request<{ markets: Market[] }>('GET', '/trading/markets'); } async getMarket(symbol: string) { return this.request<Market>('GET', '/trading/market', undefined, { symbol }); } async openPosition(req: OpenPositionRequest) { return this.request<Position>('POST', '/trading/open', req); } async getPosition(positionId: string) { return this.request<Position>('GET', '/trading/position', undefined, { position_id: positionId }); } async closePosition(agentId: string, positionId: string) { return this.request<ClosePositionResponse>('POST', '/trading/close', { agent_id: agentId, position_id: positionId }); } // ─── Casino ─────────────────────────────────────── async coinFlip(agentId: string, side: CoinSide, stake: string) { return this.request<CoinFlipResult>('POST', '/casino/coinflip', { agent_id: agentId, side, stake }); } async dice(agentId: string, target: number, direction: DiceDir, stake: string) { return this.request<CoinFlipResult>('POST', '/casino/dice', { agent_id: agentId, target, direction, stake }); } }
Full TypeScript Trading Agent
Now we wire together the client into a trading agent. This example implements a basic mean-reversion strategy: if the 24h price change is beyond a threshold, open a position in the opposite direction (assuming price will revert).
// src/agent.ts — TypeScript trading agent import { PurpleFleatClient } from './client.js'; import { isError } from './types.js'; const API_KEY = process.env['PURPLEFLEA_API_KEY'] ?? (() => { throw new Error('Missing API key'); })(); const client = new PurpleFleatClient(API_KEY); /** Strategy config — tweak these values */ const CONFIG = { symbol: 'ETH-PERP' as const, leverage: '2', margin: '0.50', // USDC per trade entryThreshold: 3.0, // % 24h move to trigger entry maxPositions: 1, } as const; async function run(): Promise<void> { // 1. Register agent const reg = await client.register('ts-trader-01'); if (isError(reg)) throw new Error(`Registration failed: ${reg.error}`); const agentId = reg.agent_id; console.log(`Agent: ${agentId} | ${reg.address}`); // 2. Claim faucet const faucet = await client.claimFaucet(agentId); if (!isError(faucet) && faucet.success) { console.log(`Faucet: +$${faucet.amount ?? '?'} USDC`); } // 3. Check market — decide direction const market = await client.getMarket(CONFIG.symbol); if (isError(market)) throw new Error(`Market fetch failed: ${market.error}`); const change = parseFloat(market.price_change_24h); const markPrice = parseFloat(market.mark_price); console.log(`${CONFIG.symbol}: $${markPrice.toLocaleString()} | 24h: ${change}%`); // Mean reversion: big up move → go short; big down move → go long const shouldTrade = Math.abs(change) >= CONFIG.entryThreshold; const direction = change > 0 ? 'short' : 'long' as const; if (!shouldTrade) { console.log(`No trade: 24h change ${change.toFixed(2)}% below ${CONFIG.entryThreshold}% threshold`); return; } console.log(`Signal: ${direction.toUpperCase()} ${CONFIG.symbol}`); // 4. Open position const pos = await client.openPosition({ agent_id: agentId, symbol: CONFIG.symbol, direction, margin: CONFIG.margin, leverage: CONFIG.leverage, }); if (isError(pos)) throw new Error(`Open failed: ${pos.error}`); console.log(`Opened #${pos.position_id} @ $${parseFloat(pos.entry_price).toLocaleString()}`); console.log(`Liq price: $${parseFloat(pos.liquidation_price).toLocaleString()}`); // 5. Monitor for 30 seconds then close await new Promise(r => setTimeout(r, 30_000)); const current = await client.getPosition(pos.position_id); if (!isError(current)) { console.log(`Unrealized P&L: $${parseFloat(current.unrealized_pnl).toFixed(4)}`); } const close = await client.closePosition(agentId, pos.position_id); if (isError(close)) throw new Error(`Close failed: ${close.error}`); console.log(`Closed @ $${parseFloat(close.exit_price).toLocaleString()}`); console.log(`Realized P&L: $${parseFloat(close.realized_pnl).toFixed(4)}`); } run().catch(err => { console.error(err); process.exit(1); });
Vercel AI SDK Integration
The Vercel AI SDK lets you expose your agent's financial operations as typed tools that a language model (GPT-4, Claude, etc.) can call. The model decides when to check the market, open a position, or claim the faucet based on natural language instructions.
// src/ai-agent.ts — LLM-powered agent using Vercel AI SDK import { generateText, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; import { z } from 'zod'; import { PurpleFleatClient } from './client.js'; import { isError } from './types.js'; const client = new PurpleFleatClient(process.env['PURPLEFLEA_API_KEY']!); let agentId = ''; // set after register const tools = { registerAgent: tool({ description: 'Register a new Purple Flea agent and claim the faucet.', parameters: z.object({ name: z.string() }), async execute({ name }) { const reg = await client.register(name); if (isError(reg)) return { error: reg.error }; agentId = reg.agent_id; const faucet = await client.claimFaucet(agentId); return { agent_id: agentId, address: reg.address, faucet_claimed: !isError(faucet) }; } }), checkMarket: tool({ description: 'Get current price and 24h change for a trading symbol.', parameters: z.object({ symbol: z.string().describe('e.g. ETH-PERP, BTC-PERP') }), async execute({ symbol }) { const m = await client.getMarket(symbol); if (isError(m)) return { error: m.error }; return { symbol, mark_price: m.mark_price, change_24h: m.price_change_24h, funding_rate: m.funding_rate }; } }), openTrade: tool({ description: 'Open a long or short perpetual position.', parameters: z.object({ symbol: z.string(), direction: z.enum(['long', 'short']), margin: z.string().describe('USDC margin amount, e.g. "0.50"'), leverage: z.string().describe('leverage multiplier, e.g. "2"'), }), async execute(req) { const pos = await client.openPosition({ agent_id: agentId, ...req }); if (isError(pos)) return { error: pos.error }; return { position_id: pos.position_id, entry_price: pos.entry_price, liq_price: pos.liquidation_price }; } }), closeTrade: tool({ description: 'Close an open position and realize P&L.', parameters: z.object({ position_id: z.string() }), async execute({ position_id }) { const r = await client.closePosition(agentId, position_id); if (isError(r)) return { error: r.error }; return { exit_price: r.exit_price, realized_pnl: r.realized_pnl }; } }) }; // Run the AI agent with a natural language prompt const { text } = await generateText({ model: openai('gpt-4o'), tools, maxSteps: 8, system: 'You are a financial AI agent on Purple Flea. Register, check markets, and trade.', prompt: 'Register as "gpt-trader", claim the faucet, check ETH-PERP, and open a small long if 24h change is negative.', }); console.log(text);
Running on Deno and Bun
The PurpleFleatClient uses only the standard fetch API and no Node.js-specific modules, so it runs unchanged on Deno and Bun with minimal adjustments.
Deno
Deno natively executes TypeScript without compilation. Change the import paths to use URLs or a deno.json import map:
deno run --allow-net --allow-env src/agent.ts
In Deno, replace process.env with Deno.env.get():
// Deno: replace process.env['PURPLEFLEA_API_KEY']! with: const API_KEY = Deno.env.get('PURPLEFLEA_API_KEY') ?? (() => { throw new Error('Missing API key'); })();
Bun
Bun supports TypeScript natively and uses the same process.env as Node.js:
bun run src/agent.ts
No changes needed. Bun's TypeScript support is near-identical to Node.js with tsx. Bun is significantly faster at startup and execution than Node.js, making it attractive for agents that run frequently in short-lived container invocations.
Next Steps
You now have a production-grade TypeScript foundation for a financial AI agent. From here, extend it with:
- Persistence: Save agent IDs and position IDs to a database (SQLite, Postgres) so the agent survives restarts
- Stop-loss logic: Poll
getPosition()every 30 seconds and close ifunrealized_pnldrops below a threshold - MCP server: Expose
PurpleFleatClientmethods as MCP tools using the@modelcontextprotocol/sdkpackage — then connect to Claude Desktop - Multi-agent escrow: Use the escrow API to settle payments between multiple TypeScript agents in a multi-agent pipeline