Create a pending payment (requires human approval)
grip_create_paymentStage a USDC payment to a recipient without executing it on-chain. Returns an approval token requiring explicit human confirmation before settlement.
Instructions
Stages a payment from the agent's Grip wallet to a recipient. DOES NOT execute on-chain. Returns an approval_token. You MUST then show the payment details (amount, recipient, memo) to the human in plain language and ASK FOR EXPLICIT CONFIRMATION before calling grip_settle_payment. Never auto-approve. The human must say 'approve' (or equivalent) before settling. If they say 'no', call grip_settle_payment with decision='reject'.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| recipient | Yes | ||
| amount_usdc | Yes | ||
| memo | No |
Implementation Reference
- server/src/index.ts:156-225 (registration)Registration of the 'grip_create_payment' tool on the MCP server with input schema, title, description, and annotations.
server.registerTool( "grip_create_payment", { title: "Create a pending payment (requires human approval)", description: "Stages a payment from the agent's Grip wallet to a recipient. DOES NOT execute on-chain. Returns an approval_token. You MUST then show the payment details (amount, recipient, memo) to the human in plain language and ASK FOR EXPLICIT CONFIRMATION before calling grip_settle_payment. Never auto-approve. The human must say 'approve' (or equivalent) before settling. If they say 'no', call grip_settle_payment with decision='reject'.", inputSchema: { recipient: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "must be a 0x-prefixed 40-char address"), amount_usdc: z.number().positive().max(10000), memo: z.string().max(280).optional(), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ recipient, amount_usdc, memo }) => { if (amount_usdc > PER_TX_CAP) { return { content: [ { type: "text", text: `Amount ${amount_usdc} USDC exceeds per-tx cap of ${PER_TX_CAP}. Refusing to stage.` }, ], isError: true, }; } const projectedTotal = todaySpent() + amount_usdc; if (projectedTotal > DAILY_CAP) { return { content: [ { type: "text", text: `Amount ${amount_usdc} USDC would push today's total to ${projectedTotal.toFixed(2)} (daily cap ${DAILY_CAP}). Refusing to stage.` }, ], isError: true, }; } const token = newToken(); const payment: PendingPayment = { token, recipient, amountUsdc: amount_usdc, memo: memo ?? "", createdAt: new Date().toISOString(), status: "pending", }; pendingPayments.set(token, payment); const text = [ `🟡 Payment STAGED — awaiting human approval.`, ``, ` Amount: ${amount_usdc.toFixed(2)} USDC`, ` To: ${recipient}`, memo ? ` Memo: "${memo}"` : null, ` Network: Base mainnet`, ` Token: ${token}`, ``, `Show these details to the human and ask for explicit approval.`, `When they confirm: call grip_settle_payment(approval_token="${token}", decision="approve").`, `If they decline: call grip_settle_payment(approval_token="${token}", decision="reject").`, ] .filter(Boolean) .join("\n"); return { content: [{ type: "text", text }], structuredContent: { token, payment }, }; }, ); - server/src/index.ts:174-224 (handler)Handler function for grip_create_payment: validates per-tx cap and daily cap, creates a pending payment with a random token, stores it in the pendingPayments map, and returns the payment details for human approval.
async ({ recipient, amount_usdc, memo }) => { if (amount_usdc > PER_TX_CAP) { return { content: [ { type: "text", text: `Amount ${amount_usdc} USDC exceeds per-tx cap of ${PER_TX_CAP}. Refusing to stage.` }, ], isError: true, }; } const projectedTotal = todaySpent() + amount_usdc; if (projectedTotal > DAILY_CAP) { return { content: [ { type: "text", text: `Amount ${amount_usdc} USDC would push today's total to ${projectedTotal.toFixed(2)} (daily cap ${DAILY_CAP}). Refusing to stage.` }, ], isError: true, }; } const token = newToken(); const payment: PendingPayment = { token, recipient, amountUsdc: amount_usdc, memo: memo ?? "", createdAt: new Date().toISOString(), status: "pending", }; pendingPayments.set(token, payment); const text = [ `🟡 Payment STAGED — awaiting human approval.`, ``, ` Amount: ${amount_usdc.toFixed(2)} USDC`, ` To: ${recipient}`, memo ? ` Memo: "${memo}"` : null, ` Network: Base mainnet`, ` Token: ${token}`, ``, `Show these details to the human and ask for explicit approval.`, `When they confirm: call grip_settle_payment(approval_token="${token}", decision="approve").`, `If they decline: call grip_settle_payment(approval_token="${token}", decision="reject").`, ] .filter(Boolean) .join("\n"); return { content: [{ type: "text", text }], structuredContent: { token, payment }, }; }, - server/src/index.ts:162-166 (schema)Input schema for grip_create_payment: recipient (0x-prefixed ETH address), amount_usdc (positive, max 10000), memo (optional, max 280 chars).
inputSchema: { recipient: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "must be a 0x-prefixed 40-char address"), amount_usdc: z.number().positive().max(10000), memo: z.string().max(280).optional(), }, - server/src/index.ts:89-101 (helper)Helper functions todaySpent() and recordSpend() used to enforce the daily spending cap in the handler.
function todaySpent(): number { const today = new Date().toISOString().split("T")[0]; if (dailySpent.date !== today) { dailySpent.date = today; dailySpent.total = 0; } return dailySpent.total; } function recordSpend(amount: number) { todaySpent(); dailySpent.total += amount; } - server/src/index.ts:103-105 (helper)newToken() helper that generates a random approval token for each pending payment.
function newToken(): string { return `pay_${randomBytes(6).toString("hex")}`; }