x402_session_start
Establish a payment session for AI agents to make a single on-chain payment, then access API endpoints multiple times using a signed session token without additional payments.
Instructions
Establish an x402 V2 payment session: make a SINGLE on-chain payment and receive a cryptographically signed session token. All subsequent calls to the same endpoint within the session lifetime use x402_session_fetch — no additional payments required. Agents pay once per session rather than once per API call. Session tokens are signed locally by your wallet key (non-custodial). Returns a session_id you pass to x402_session_fetch for all future calls.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| endpoint | Yes | Base URL to establish a session for (e.g., "https://api.example.com/v1") | |
| scope | No | "prefix": covers all paths under this URL (default). "exact": single URL only. | prefix |
| ttl_seconds | No | Session TTL in seconds (default: 3600 / 1 hour). Max: 30 days. | |
| label | No | Optional label for this session (e.g., "Premium API session") | |
| max_payment_eth | No | Maximum ETH to pay for this session. Rejects if price exceeds this. | |
| method | No | HTTP method for the initial request (default: GET) | GET |
| headers | No | Additional request headers | |
| body | No | Request body for POST/PUT/PATCH session-start requests | |
| timeout_ms | No | Request timeout in milliseconds (default: 30000) |
Implementation Reference
- src/tools/session.ts:153-301 (handler)The handleX402SessionStart function processes the x402_session_start tool, including payment handling using the X402 client and creating a session record.
export async function handleX402SessionStart( input: X402SessionStartInput ): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> { try { const wallet = getWallet(); const config = getConfig(); const timeoutMs = input.timeout_ms ?? 30000; // Parse optional payment cap let maxPaymentWei: bigint | undefined; if (input.max_payment_eth) { const cap = parseFloat(input.max_payment_eth); if (isNaN(cap) || cap <= 0) { throw new Error(`Invalid max_payment_eth: "${input.max_payment_eth}"`); } maxPaymentWei = BigInt(Math.round(cap * 1e18)); } // Track payment let paymentMade = false; let paymentAmount = 0n; let paymentTxHash = ''; let paymentRecipient = ''; let paymentToken = '0x0000000000000000000000000000000000000000'; // x402 client to handle the one-time session payment const x402Client = createX402Client(wallet, { autoPay: true, maxRetries: 1, globalPerRequestMax: maxPaymentWei, onBeforePayment: (req) => { const amount = BigInt(req.amount); if (maxPaymentWei && amount > maxPaymentWei) { throw new Error( `Session payment (${amount} wei) exceeds max_payment_eth cap ` + `(${maxPaymentWei} wei = ${input.max_payment_eth} ETH). ` + `Set a higher max_payment_eth to proceed.` ); } return true; }, onPaymentComplete: (log) => { paymentMade = true; paymentAmount = log.amount; paymentTxHash = log.txHash; paymentRecipient = log.recipient; paymentToken = log.token ?? '0x0000000000000000000000000000000000000000'; }, }); const method = input.method ?? 'GET'; const reqHeaders: Record<string, string> = { 'Accept': 'application/json, text/plain, */*', ...(input.headers ?? {}), }; if (input.body && ['POST', 'PUT', 'PATCH'].includes(method)) { if (!reqHeaders['Content-Type']) { reqHeaders['Content-Type'] = 'application/json'; } } const requestInit: RequestInit = { method, headers: reqHeaders, ...(input.body ? { body: input.body } : {}), signal: AbortSignal.timeout(timeoutMs), }; // Make the payment request const response = await x402Client.fetch(input.endpoint, requestInit); const responseText = await response.text(); if (!paymentMade) { // Endpoint didn't require payment — still create a session if response was OK // This allows agents to pre-establish sessions for endpoints that may start // requiring payment, or to track usage even for free endpoints. return { content: [ textContent( `ℹ️ **No Payment Required**\n\n` + ` Endpoint: ${input.endpoint}\n` + ` Status: ${response.status} ${response.statusText}\n\n` + `No x402 payment was needed. The endpoint responded without requiring payment.\n` + `You do not need a session token — use x402_pay directly for free endpoints.\n\n` + `📄 **Response Body**\n` + '```\n' + responseText.slice(0, 4000) + (responseText.length > 4000 ? '\n... [truncated]' : '') + '\n```' ), ], }; } // Create signed session record // The signMessage function uses viem's wallet client, signing locally with the agent's key const signMessage = async (message: string): Promise<string> => { // Access the walletClient from the wallet instance for local signing const wc = (wallet as unknown as { walletClient: { signMessage: (args: { message: string }) => Promise<string> } }).walletClient; return wc.signMessage({ message }); }; const session = await createSession({ endpoint: input.endpoint, scope: input.scope ?? 'prefix', ttlSeconds: input.ttl_seconds, label: input.label, paymentTxHash, paymentAmount, paymentToken, paymentRecipient, walletAddress: config.walletAddress, signMessage, }); const ttlRemaining = session.expiresAt - Math.floor(Date.now() / 1000); const expiresAt = new Date(session.expiresAt * 1000).toISOString(); let out = `🔐 **x402 Session Established**\n\n`; out += ` Session ID: ${session.sessionId}\n`; out += ` Endpoint: ${session.endpoint}\n`; out += ` Scope: ${session.scope}\n`; if (session.label) out += ` Label: ${session.label}\n`; out += ` Network: ${chainName(config.chainId)}\n`; out += ` TTL: ${Math.ceil(ttlRemaining / 60)}m (expires ${expiresAt})\n\n`; out += `💳 **Session Payment**\n`; out += ` Amount: ${paymentAmount.toString()} (base units)\n`; out += ` Recipient: ${paymentRecipient}\n`; out += ` TX Hash: ${paymentTxHash}\n\n`; out += `✅ **Next Steps**\n`; out += ` Use \`x402_session_fetch\` with session_id="${session.sessionId}" for all subsequent\n`; out += ` requests to ${input.endpoint} — no further payments will be made during this session.\n`; out += ` Check session status with \`x402_session_status\`.\n\n`; out += `📄 **Initial Response** (${response.status})\n`; const truncated = responseText.length > 4000; out += '```\n' + responseText.slice(0, 4000) + (truncated ? '\n... [truncated]' : '') + '\n```'; return { content: [textContent(out)] }; } catch (error: unknown) { if (error instanceof Error && error.name === 'AbortError') { return { content: [textContent(`❌ x402_session_start failed: Request timed out after ${input.timeout_ms ?? 30000}ms`)], isError: true, }; } return { content: [textContent(formatError(error, 'x402_session_start'))], isError: true, }; } } - src/tools/session.ts:34-90 (schema)The X402SessionStartSchema defines the input validation rules for the x402_session_start tool.
export const X402SessionStartSchema = z.object({ endpoint: z .string() .url() .describe( 'The base URL or endpoint to establish a session for. ' + 'The agent pays once and the session covers all subsequent requests to this endpoint.' ), scope: z .enum(['prefix', 'exact']) .optional() .default('prefix') .describe( '"prefix" (default): session covers all paths under the endpoint URL. ' + '"exact": session only covers this exact URL.' ), ttl_seconds: z .number() .int() .min(60) .max(86400 * 30) .optional() .describe( 'Session lifetime in seconds (default: SESSION_TTL_SECONDS env var or 3600). ' + 'Min: 60 seconds. Max: 30 days.' ), label: z .string() .max(100) .optional() .describe('Optional human-readable label for this session (e.g. "Premium API session")'), max_payment_eth: z .string() .optional() .describe('Maximum ETH to pay for session establishment. Rejects if exceeded.'), method: z .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) .optional() .default('GET') .describe('HTTP method for the initial payment request (default: GET)'), headers: z .record(z.string()) .optional() .describe('Additional headers for the session-start request'), body: z .string() .optional() .describe('Request body for the initial session-start request (if POST/PUT/PATCH)'), timeout_ms: z .number() .int() .min(1000) .max(60000) .optional() .default(30000) .describe('Request timeout in milliseconds (default: 30000)'), }); - src/tools/session.ts:94-151 (registration)The x402SessionStartTool definition registers the MCP tool with its name, description, and input schema.
export const x402SessionStartTool = { name: 'x402_session_start', description: 'Establish an x402 V2 payment session: make a SINGLE on-chain payment and receive ' + 'a cryptographically signed session token. All subsequent calls to the same endpoint ' + 'within the session lifetime use x402_session_fetch — no additional payments required. ' + 'Agents pay once per session rather than once per API call. ' + 'Session tokens are signed locally by your wallet key (non-custodial). ' + 'Returns a session_id you pass to x402_session_fetch for all future calls.', inputSchema: { type: 'object' as const, properties: { endpoint: { type: 'string', description: 'Base URL to establish a session for (e.g., "https://api.example.com/v1")', }, scope: { type: 'string', enum: ['prefix', 'exact'], description: '"prefix": covers all paths under this URL (default). "exact": single URL only.', default: 'prefix', }, ttl_seconds: { type: 'number', description: 'Session TTL in seconds (default: 3600 / 1 hour). Max: 30 days.', }, label: { type: 'string', description: 'Optional label for this session (e.g., "Premium API session")', }, max_payment_eth: { type: 'string', description: 'Maximum ETH to pay for this session. Rejects if price exceeds this.', }, method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], description: 'HTTP method for the initial request (default: GET)', default: 'GET', }, headers: { type: 'object', additionalProperties: { type: 'string' }, description: 'Additional request headers', }, body: { type: 'string', description: 'Request body for POST/PUT/PATCH session-start requests', }, timeout_ms: { type: 'number', description: 'Request timeout in milliseconds (default: 30000)', default: 30000, }, }, required: ['endpoint'], }, };