Skip to Content
Welcome to RitoSwap's documentation!
AI SystemsMCP Stacksend_crypto_to_signed_in_user

send_crypto_to_signed_in_user

This tool (dapp/app/lib/mcp/tools/send-crypto.ts) lets the chatbot send 0.1–0.3 ETH on the active network. Registration is conditional on AI_PRIVATE_KEY being set.

Security & Gating

  • JWT injectionMCPDispatcher adds __jwt.address so the LLM can’t spoof a destination. The handler refuses to read any user-provided address and only trusts extractJwtAddress(params).
  • Manual preflight — Before sending, the tool fetches the signer’s balance, estimates gas with viem, and ensures the AI wallet can cover amountEth + fee.
  • Quotas — Successful sends call recordCryptoSpend when the quota feature flag is active; failures leave quota state untouched.
  • Chain configgetChainConfig supplies the active RPC + chain ID so viem clients point to the same network the chat session is using (RitoNet, Sepolia, Mainnet, etc.).
// dapp/app/lib/mcp/tools/send-crypto.ts const tool: Tool<{ amountEth: number }> = { name: 'send_crypto_to_signed_in_user', requiresJwt: true, inputSchema: InputSchema, async handler(params) { const priv = aiServerConfig.secrets.aiPrivateKey; if (!priv) fail('send-crypto is unavailable: AI_PRIVATE_KEY not configured.'); const amountEth = Number(params.amountEth); if (!Number.isFinite(amountEth) || amountEth < 0.1 || amountEth > 0.3) { fail('amountEth must be between 0.1 and 0.3 ETH (inclusive).'); } const to = extractJwtAddress(params as Record<string, unknown>); if (!to) fail('No JWT-bound address available.'); const { rpcUrl, chainId } = getChainConfig(); const account = privateKeyToAccount(priv); const walletClient = createWalletClient({ account, transport: http(rpcUrl) }); const publicClient = createPublicClient({ transport: http(rpcUrl) }); const value = parseEther(amountEth.toString()); const [balance, estGas, gasPrice] = await Promise.all([ publicClient.getBalance({ address: account.address }), publicClient.estimateGas({ account: account.address, to, value }), publicClient.getGasPrice(), ]); if (balance < value + estGas * gasPrice) { return errorResultShape('Insufficient balance for amount + gas'); } const hash = await walletClient.sendTransaction({ to, value }); const receipt = await publicClient.waitForTransactionReceipt({ hash }); if (isCryptoQuotaFeatureActive()) await recordCryptoSpend(to, amountEth); return { content: [ { type: 'text', text: `Sent Crypto.\n${formatEthAmount(amountEth)} ETH sent to ${shortAddr(to)}.` }, { type: 'json', data: formatPayload(hash, to, amountEth, chainId) }, ], }; }, };

Presenter Behavior

dapp/components/chatBot/ToolActivity/catalog/presenters/send_crypto.presenter.ts summarizes the outcome:

StatusLabelDetails
pendingSending CryptoNo body text to keep the chip compact.
successSent Crypto.Shows <amount> ETH sent to <short addr> on <network>, falling back to the streamed text if the JSON is missing.
errorFailed to Send Crypto.Recognizes wallet/auth errors (“Your wallet must be connected…”) so the user knows to re-authenticate.

Tips

⚠️

Because the tool estimates gas and checks the AI wallet’s balance on every call, local testing still requires funding the signer before the transaction can be broadcast.

RitoSwap Docs does not store, collect or access any of your conversations. All saved prompts are stored locally in your browser only.