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 injection —
MCPDispatcheradds__jwt.addressso the LLM can’t spoof a destination. The handler refuses to read any user-provided address and only trustsextractJwtAddress(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
recordCryptoSpendwhen the quota feature flag is active; failures leave quota state untouched. - Chain config —
getChainConfigsupplies 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:
| Status | Label | Details |
|---|---|---|
| pending | Sending Crypto | No body text to keep the chip compact. |
| success | Sent Crypto. | Shows <amount> ETH sent to <short addr> on <network>, falling back to the streamed text if the JSON is missing. |
| error | Failed 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.