paper_buy
Execute a simulated buy order for a Solana token using real-time prices from Jupiter and DexScreener, with built-in risk management enforcing position limits, exposure caps, and minimum liquidity.
Instructions
Execute a paper (simulated) buy order for a Solana token. Uses real-time prices from Jupiter/DexScreener. Risk manager enforces position limits, exposure caps, and minimum liquidity.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| mint | Yes | Solana token mint address | |
| amount_sol | No | SOL amount to spend (default: max position size, confidence-scaled) | |
| symbol | No | Token symbol for display | |
| score | No | Signal score (used for confidence-based position sizing) | |
| liquidity_usd | No | Pool liquidity in USD (checked against minimum) |
Implementation Reference
- src/tools.js:140-155 (handler)Handler function for paper_buy tool: validates inputs from Zod schema, then delegates to either remote trader API (traderFetch) or local PaperEngine.buy() based on configuration.
async ({ mint, amount_sol, symbol, score, liquidity_usd }) => { try { let result; if (useRemoteTrader) { result = await traderFetch('/buy', { method: 'POST', body: JSON.stringify({ mint, amount_sol, symbol, score, liquidity_usd }), }); } else { result = await paper.buy(mint, amount_sol, { symbol, score, liquidity_usd }); } return text(JSON.stringify(result, null, 2)); } catch (err) { return error(err.message); } } - src/tools.js:121-132 (schema)Zod input schema for paper_buy: requires mint (base58 regex), optional amount_sol (0-10 SOL), symbol, score, and liquidity_usd.
{ mint: z.string().regex(MINT_REGEX) .describe('Solana token mint address'), amount_sol: z.number().positive().max(10).optional() .describe('SOL amount to spend (default: max position size, confidence-scaled)'), symbol: z.string().max(20).optional() .describe('Token symbol for display'), score: z.number().min(0).max(100).optional() .describe('Signal score (used for confidence-based position sizing)'), liquidity_usd: z.number().min(0).optional() .describe('Pool liquidity in USD (checked against minimum)'), }, - src/tools.js:118-156 (registration)Registration of the 'paper_buy' tool via server.tool() with description, input schema, metadata (title, hints), and handler.
server.tool( 'paper_buy', 'Execute a paper (simulated) buy order for a Solana token. Uses real-time prices from Jupiter/DexScreener. Risk manager enforces position limits, exposure caps, and minimum liquidity.', { mint: z.string().regex(MINT_REGEX) .describe('Solana token mint address'), amount_sol: z.number().positive().max(10).optional() .describe('SOL amount to spend (default: max position size, confidence-scaled)'), symbol: z.string().max(20).optional() .describe('Token symbol for display'), score: z.number().min(0).max(100).optional() .describe('Signal score (used for confidence-based position sizing)'), liquidity_usd: z.number().min(0).optional() .describe('Pool liquidity in USD (checked against minimum)'), }, { title: 'Paper Buy', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, async ({ mint, amount_sol, symbol, score, liquidity_usd }) => { try { let result; if (useRemoteTrader) { result = await traderFetch('/buy', { method: 'POST', body: JSON.stringify({ mint, amount_sol, symbol, score, liquidity_usd }), }); } else { result = await paper.buy(mint, amount_sol, { symbol, score, liquidity_usd }); } return text(JSON.stringify(result, null, 2)); } catch (err) { return error(err.message); } } ); - src/paper-engine.js:42-109 (helper)PaperEngine.buy() - the core buy logic: validates mint, checks risk (position limits, exposure caps, min liquidity), computes confidence-scaled position size, fetches real-time price from Jupiter/DexScreener, simulates slippage, records the position with stop-loss/take-profit, and returns the execution result.
async buy(mint, amountSol, signalData = {}) { this.#validateMint(mint); const liquidity = signalData.liquidity_usd || 0; const confidence = Math.max(0, Math.min(1, (signalData.score || 50) / 100)); const riskCheck = this.#checkRisk(mint, liquidity); if (!riskCheck.passed) { return { success: false, rejected: true, reason: riskCheck.reason }; } let size = Math.min(amountSol || this.#config.maxPositionSol, this.#config.maxPositionSol); const remaining = this.#config.maxExposureSol - this.#totalExposure(); size = Math.min(size, remaining); if (confidence < 1.0) size *= Math.max(confidence, 0.25); size = Math.round(size * 1000) / 1000; if (size > this.#balance) { return { success: false, rejected: true, reason: `Insufficient balance: ${this.#balance} SOL < ${size} SOL` }; } const price = await this.#fetchPrice(mint); if (!price) { return { success: false, rejected: true, reason: 'Could not fetch token price' }; } const slippage = 1 + (Math.random() * 0.004 + 0.001); const fillPrice = price * slippage; const tokensReceived = (size * LAMPORTS_PER_SOL) / (fillPrice * LAMPORTS_PER_SOL); this.#balance -= size; const txid = `PAPER_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`; const position = { mint, symbol: signalData.symbol || mint.slice(0, 8), entrySol: size, entryPrice: fillPrice, tokensHeld: tokensReceived, entryTime: Date.now(), txid, stopLoss: fillPrice * (1 - this.#config.stopLossPct / 100), takeProfit: fillPrice * (1 + this.#config.takeProfitPct / 100), highWaterMark: fillPrice, }; this.#positions.set(mint, position); this.#priceCache.set(mint, fillPrice); const entry = { type: 'buy', txid, mint, symbol: position.symbol, amountSol: size, fillPrice, tokensReceived, slippagePct: ((slippage - 1) * 100).toFixed(3), timestamp: new Date().toISOString(), balanceAfter: this.#round(this.#balance), }; this.#tradeLog.push(entry); this.#persist(); return { success: true, txid, action: 'buy', symbol: position.symbol, mint, amountSol: size, fillPrice, tokensReceived, stopLoss: position.stopLoss, takeProfit: position.takeProfit, balanceRemaining: this.#round(this.#balance), }; } - src/paper-engine.js:233-244 (helper)Risk check helper: prevents duplicate positions, enforces max exposure cap, and enforces minimum liquidity threshold (configurable via config.
#checkRisk(mint, liquidityUsd) { if (this.#positions.has(mint)) { return { passed: false, reason: `Already holding ${mint.slice(0, 8)}` }; } if (this.#totalExposure() >= this.#config.maxExposureSol) { return { passed: false, reason: `Max exposure reached (${this.#config.maxExposureSol} SOL)` }; } if (liquidityUsd > 0 && liquidityUsd < this.#config.minLiquidityUsd) { return { passed: false, reason: `Liquidity $${liquidityUsd} below min $${this.#config.minLiquidityUsd}` }; } return { passed: true }; }