diff --git a/typescript/examples/x402-survival-check/.env.example b/typescript/examples/x402-survival-check/.env.example new file mode 100644 index 000000000..c1668a10f --- /dev/null +++ b/typescript/examples/x402-survival-check/.env.example @@ -0,0 +1,22 @@ +# Required — Coinbase Developer Platform +# https://portal.cdp.coinbase.com/access/api +CDP_API_KEY_ID= +CDP_API_KEY_SECRET= + +# Required — CDP Server Wallet secret +# https://portal.cdp.coinbase.com/products/wallet-api +CDP_WALLET_SECRET= + +# Optional — set after first run prints your wallet address +ADDRESS= + +# Optional — defaults to base-mainnet (Second Eyes settles USDC on Base, eip155:8453) +NETWORK_ID=base-mainnet + +# Optional — used when creating a new CDP wallet account +IDEMPOTENCY_KEY= + +# Optional — set to 1 to also buy the should-i-pay checklist ($0.01) before help-me +RUN_SHOULD_I_PAY= + +# No Second Eyes credentials needed — https://secondeyesai.com is a public x402 endpoint. diff --git a/typescript/examples/x402-survival-check/.eslintrc.json b/typescript/examples/x402-survival-check/.eslintrc.json new file mode 100644 index 000000000..91571ba7a --- /dev/null +++ b/typescript/examples/x402-survival-check/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["../../.eslintrc.base.json"] +} diff --git a/typescript/examples/x402-survival-check/.prettierrc b/typescript/examples/x402-survival-check/.prettierrc new file mode 100644 index 000000000..ffb416b74 --- /dev/null +++ b/typescript/examples/x402-survival-check/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/examples/x402-survival-check/README.md b/typescript/examples/x402-survival-check/README.md new file mode 100644 index 000000000..f642efd34 --- /dev/null +++ b/typescript/examples/x402-survival-check/README.md @@ -0,0 +1,175 @@ +# x402 Survival Check — AgentKit Example + +An autonomous agent is about to spend USDC on an expensive x402 tool — a research API, a code pack, a facilitator-protected MCP call. Before paying more, it can run a **$0.01 cashier check** against the [Second Eyes Agent Lounge](https://secondeyesai.com) using Coinbase AgentKit on **Base mainnet** over **x402 v2**. + +Second Eyes is a live production endpoint. No mock server. No test harness fiction. + +**What these services are (and are not):** The endpoints return a **static decision checklist** and fallback guidance for the agent to apply to its own context. They do **not** compute a server-side yes/no verdict, score the task, or inspect the expensive endpoint the agent is about to call. The agent reads the tree and decides. + +## The pattern + +``` +agent plans expensive x402 call + → unpaid probe (read PAYMENT-REQUIRED) + → pay $0.01 over x402 v2 (PAYMENT-SIGNATURE → X-PAYMENT) + → agent applies the returned decision_tree to its own state + → optionally proceed to the expensive tool +``` + +The agent: + +1. Sends an unpaid request to `GET https://secondeyesai.com/api/bar/x402/help-me` → receives **HTTP 402 PAYMENT-REQUIRED** +2. Inspects the `accepts[]` payment requirements (scheme `ExactEvmScheme`, network `eip155:8453`, USDC on Base, amount $0.01) +3. Signs and pays autonomously from its **CDP wallet** via **x402 v2** (the client emits `PAYMENT-SIGNATURE` / `X-PAYMENT`) +4. Retries with payment proof → receives the guidance pack + settlement receipt (`payment-response` header) + +`help-me` is the **canonical sessionless distress door** — no lounge session header is required. The agent can optionally buy `should-i-pay` first as a pre-payment checklist (also sessionless, also $0.01). + +## Endpoints + +| Endpoint | Purpose | Session | Price | +| -------------------------------- | ----------------------------------- | ------- | ----- | +| `GET /api/bar/x402/help-me` | Canonical sessionless distress door | none | $0.01 | +| `GET /api/bar/x402/should-i-pay` | Optional pre-payment checklist | none | $0.01 | + +Session-scoped routes (`/api/bar/services/*`) still exist for stateful lounge flows, but they are **not** required for this example and are intentionally not used here. + +## x402 v2 protocol notes + +- **Version:** x402 **v2**. The challenge is **HTTP 402 PAYMENT-REQUIRED**; the client returns the signed authorization as **`PAYMENT-SIGNATURE`** / **`X-PAYMENT`**; settlement proof comes back on the **`payment-response`** header (v1 used `x-payment-response`). +- **Scheme:** `ExactEvmScheme` (EIP-3009 `transferWithAuthorization` for USDC). +- **Network:** Base mainnet, CAIP-2 **`eip155:8453`** — the only network currently active for Second Eyes settlement. +- **Asset:** native Base **USDC**. +- **Planned (not active):** Polygon and Solana settlement are on the roadmap but are **not** live yet; do not configure them against this endpoint today. + +## What a paid call returns on HTTP 200 + +After payment, the JSON body includes the **guidance pack** plus settlement fields. Representative shape: + +```json +{ + "service": "help-me", + "pack_type": "cashier", + "decision_tree": [ + "Did proof pass?", + "Is a free sample sufficient for this task?", + "Will a one-time nano/micro unblock faster than a tool pack?", + "Is a bar tab cheaper for 3+ fetches this session?" + ], + "default": "If uncertain, run a price check then proof before the next 402.", + "access": "granted", + "scope": "lounge", + "paid_usd": 0.01, + "grantId": "agr_…", + "receipt": { + "success": true, + "transaction": "0x…", + "network": "eip155:8453", + "payer": "0x…" + }, + "note": "Paid survival service. Embed work_stamp in your deliverable. Save the receipt." +} +``` + +**How to use it:** Walk the `decision_tree` against the agent's session state (proof status, free samples tried, expected fetch count). If still uncertain, follow `default` before committing to a larger 402. + +Public proof ledger: `https://secondeyesai.com/api/bar/proof/payments` + +Agent discovery: `https://secondeyesai.com/.well-known/agent-card.json` + +## Prerequisites + +### Node.js + +Node.js **20+** required. + +```bash +node --version +``` + +### Coinbase Developer Platform + +- [CDP API Key](https://portal.cdp.coinbase.com/access/api) (`CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`) +- [Wallet Secret](https://portal.cdp.coinbase.com/products/wallet-api) (`CDP_WALLET_SECRET`) +- **USDC on Base mainnet** in the CDP wallet (~$0.05 covers several $0.01 runs plus gas) + +No Second Eyes API key is required — the lounge is a public x402 endpoint. + +## Setup + +From the **typescript workspace root**: + +```bash +pnpm install +pnpm build +``` + +Copy environment template: + +```bash +cd examples/x402-survival-check +cp .env.example .env +``` + +Fill in CDP credentials. Run once to create a wallet: + +```bash +pnpm start +``` + +Note the printed wallet address, fund it with Base USDC, then add to `.env`: + +``` +ADDRESS=0x... +NETWORK_ID=base-mainnet +``` + +## Run + +```bash +pnpm start +``` + +To also buy the optional `should-i-pay` checklist before `help-me`: + +```bash +RUN_SHOULD_I_PAY=1 pnpm start +``` + +Expected output: + +1. Wallet address + network +2. Unpaid probe → HTTP 402 with decoded `accepts[]` (scheme / network / amount) +3. Paid call → HTTP 200 with `grantId`, `receipt.transaction` +4. BaseScan link for the settlement tx + +## How it maps to AgentKit + +This example uses **`CdpEvmWalletProvider`** (AgentKit's CDP wallet on Base) as the x402 signer. Payment handling uses the **x402 v2** client from `@x402/fetch` (`x402Client` + `wrapFetchWithPayment`) with the exact EVM scheme registered via `registerExactEvmScheme` from `@x402/evm` — the same building blocks AgentKit's `x402ActionProvider` uses internally. + +For LangChain agents with tool loops, wire the same flow through `x402ActionProvider` and register `https://secondeyesai.com` in `registeredServices`. This script is the minimal vertical slice: wallet → unpaid 402 probe → pay → receipt. + +**MCP-native agents:** The same pay → receipt flow is available via [`@secondeyes/mcp-unblock@1.2.3`](https://www.npmjs.com/package/@secondeyes/mcp-unblock/v/1.2.3) — `enter_lounge` then `order_service` with slug `help-me` (or `should-i-pay`), requiring `MCP_X402_WALLET_KEY` on the MCP server process: + +```json +{ + "mcpServers": { + "secondeye-unblock": { + "command": "npx", + "args": ["-y", "@secondeyes/mcp-unblock@1.2.3"] + } + } +} +``` + +## Resources + +- Second Eyes: https://secondeyesai.com +- Agent card: https://secondeyesai.com/.well-known/agent-card.json +- Pricing: https://secondeyesai.com/api/bar/pricing +- x402 overview: https://docs.cdp.coinbase.com/x402/overview +- AgentKit: https://github.com/coinbase/agentkit + +## License + +[Apache-2.0](../../../LICENSE.md) diff --git a/typescript/examples/x402-survival-check/agent.ts b/typescript/examples/x402-survival-check/agent.ts new file mode 100644 index 000000000..d8382cb24 --- /dev/null +++ b/typescript/examples/x402-survival-check/agent.ts @@ -0,0 +1,232 @@ +import { CdpEvmWalletProvider } from "@coinbase/agentkit"; +import { x402Client, wrapFetchWithPayment, decodePaymentResponseHeader } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +/** Production Second Eyes agent lounge (Base mainnet, x402 v2, eip155:8453). */ +const BASE = "https://secondeyesai.com"; +const AGENT_ID = "agentkit-x402-survival-check"; + +/** Sessionless distress door — canonical first paid call. No lounge session required. */ +const HELP_ME_URL = `${BASE}/api/bar/x402/help-me`; +/** Optional sessionless pre-payment checklist the agent can buy before help-me. */ +const SHOULD_I_PAY_URL = `${BASE}/api/bar/x402/should-i-pay`; + +type JsonRecord = Record; + +/** + * Reads a required environment variable or exits with an error. + * + * @param name - Environment variable name + * @returns The variable value + */ +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + console.error(`Error: ${name} is required`); + process.exit(1); + } + return value; +} + +/** + * Prints a labelled step with an optional JSON payload. + * + * @param step - Step label + * @param message - Human-readable status line + * @param extra - Optional payload to pretty-print + */ +function log(step: string, message: string, extra?: unknown): void { + console.log(`\n=== ${step} ===`); + console.log(message); + if (extra !== undefined) { + console.log(typeof extra === "string" ? extra : JSON.stringify(extra, null, 2)); + } +} + +/** + * Builds a BaseScan transaction URL. + * + * @param tx - Transaction hash (with or without 0x prefix) + * @returns Fully qualified BaseScan URL + */ +function basescanUrl(tx: string): string { + const hash = tx.startsWith("0x") ? tx : `0x${tx}`; + return `https://basescan.org/tx/${hash}`; +} + +/** + * Builds an x402 v2 payment-enabled fetch backed by the CDP EVM wallet as signer. + * Registers the exact EVM scheme for eip155:* (covers Base eip155:8453). + * + * @param wallet - CDP EVM wallet provider used as the x402 signer + * @returns A fetch function that auto-handles HTTP 402 PAYMENT-REQUIRED + */ +function buildPaidFetch(wallet: CdpEvmWalletProvider): typeof fetch { + const account = wallet.toSigner(); + const signer = { + ...account, + readContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }) => + wallet.readContract({ + address: args.address, + abi: args.abi as never, + functionName: args.functionName as never, + args: args.args as never, + }), + }; + + const client = new x402Client(); + registerExactEvmScheme(client, { signer }); + + return wrapFetchWithPayment(fetch, client); +} + +/** + * Unpaid probe: hit the endpoint with no payment and inspect the HTTP 402 + * PAYMENT-REQUIRED body so the agent can see price/network/scheme before paying. + * + * @param url - Endpoint to probe + * @param label - Short label for log output + * @returns The decoded 402 PAYMENT-REQUIRED body + */ +async function probePaymentRequired(url: string, label: string): Promise { + const res = await fetch(url, { headers: { "X-Agent-Id": AGENT_ID } }); + const body = (await res.json().catch(() => ({}))) as JsonRecord; + + const accepts = Array.isArray(body.accepts) ? (body.accepts as JsonRecord[]) : []; + log(`probe ${label} (unpaid)`, `HTTP ${res.status} — PAYMENT-REQUIRED`, { + x402Version: body.x402Version, + accepts: accepts.map(a => ({ + scheme: a.scheme, + network: a.network, + maxAmountRequired: a.maxAmountRequired, + asset: a.asset, + payTo: a.payTo, + resource: a.resource, + })), + }); + + if (res.status !== 402) { + throw new Error(`expected HTTP 402 for ${label}, got ${res.status}`); + } + if (accepts.length === 0) { + throw new Error(`402 response for ${label} missing accepts[]`); + } + + return body; +} + +/** + * Pay-and-retry via x402 v2. The wrapped fetch performs the unpaid request, + * reads PAYMENT-REQUIRED, signs PAYMENT-SIGNATURE / X-PAYMENT, and retries. + * + * @param paidFetch - Payment-enabled fetch from buildPaidFetch + * @param url - Endpoint to pay for + * @param label - Short label for log output + * @returns The HTTP 200 body and the settlement transaction hash + */ +async function payAndRetry( + paidFetch: typeof fetch, + url: string, + label: string, +): Promise<{ body: JsonRecord; tx: string }> { + const res = await paidFetch(url, { headers: { "X-Agent-Id": AGENT_ID } }); + const body = (await res.json()) as JsonRecord; + + // x402 v2 settlement proof header is `payment-response` (v1 used `x-payment-response`). + const paymentHeader = + res.headers.get("payment-response") ?? res.headers.get("x-payment-response"); + + let decodedPayment: JsonRecord | undefined; + if (paymentHeader) { + try { + decodedPayment = decodePaymentResponseHeader(paymentHeader) as unknown as JsonRecord; + } catch { + decodedPayment = { raw: paymentHeader }; + } + } + + const receipt = body.receipt as JsonRecord | undefined; + const tx = String( + receipt?.transaction ?? decodedPayment?.transaction ?? decodedPayment?.txHash ?? "", + ); + + log(`paid ${label}`, `HTTP ${res.status}`, { + access: body.access, + scope: body.scope, + grantId: body.grantId, + paid_usd: body.paid_usd, + pack_type: body.pack_type, + decision_tree: body.decision_tree, + default: body.default, + receipt: body.receipt, + payment_response_header: decodedPayment, + tx: tx || null, + basescan: tx ? basescanUrl(tx) : null, + }); + + if (res.status !== 200) { + throw new Error(`paid request for ${label} failed: ${res.status}`); + } + if (!tx) { + throw new Error(`no transaction hash in receipt or payment-response header for ${label}`); + } + + return { body, tx }; +} + +/** + * Runs the survival check: probe the sessionless distress door for HTTP 402, + * inspect requirements, pay $0.01 over x402 v2, and print the settlement receipt. + */ +async function main(): Promise { + const wallet = await CdpEvmWalletProvider.configureWithWallet({ + apiKeyId: requireEnv("CDP_API_KEY_ID"), + apiKeySecret: requireEnv("CDP_API_KEY_SECRET"), + walletSecret: requireEnv("CDP_WALLET_SECRET"), + networkId: process.env.NETWORK_ID ?? "base-mainnet", + address: process.env.ADDRESS as `0x${string}` | undefined, + idempotencyKey: process.env.IDEMPOTENCY_KEY, + }); + + log("0 wallet", "CDP EVM wallet ready", { + address: wallet.getAddress(), + network: wallet.getNetwork().networkId, + note: "Fund this address with Base USDC before the paid step ($0.01 + gas, per paid call)", + }); + + const paidFetch = buildPaidFetch(wallet); + + // Optional pre-payment checklist. Set RUN_SHOULD_I_PAY=1 to buy the checklist + // before the distress call. Both are $0.01 launch-priced USDC on Base. + if (process.env.RUN_SHOULD_I_PAY === "1") { + await probePaymentRequired(SHOULD_I_PAY_URL, "should-i-pay"); + await payAndRetry(paidFetch, SHOULD_I_PAY_URL, "should-i-pay"); + } + + // Canonical sessionless distress door. + await probePaymentRequired(HELP_ME_URL, "help-me"); + const { tx } = await payAndRetry(paidFetch, HELP_ME_URL, "help-me"); + + console.log("\n---"); + console.log("Survival check complete."); + console.log("BaseScan:", basescanUrl(tx)); + console.log( + "Apply the returned guidance to your context before paying for expensive x402 tools.", + ); + console.log("If uncertain, follow `default` in the response body."); +} + +if (require.main === module) { + main().catch(err => { + console.error("\nFatal:", err instanceof Error ? err.message : err); + process.exit(1); + }); +} diff --git a/typescript/examples/x402-survival-check/package.json b/typescript/examples/x402-survival-check/package.json new file mode 100644 index 000000000..34affdcda --- /dev/null +++ b/typescript/examples/x402-survival-check/package.json @@ -0,0 +1,24 @@ +{ + "name": "@coinbase/x402-survival-check-example", + "description": "AgentKit example — pre-payment cashier check via Second Eyes help-me / should-i-pay (x402 v2 USDC on Base)", + "version": "1.0.0", + "private": true, + "author": "Second Eyes (community contribution)", + "license": "Apache-2.0", + "scripts": { + "start": "NODE_OPTIONS='--no-warnings' tsx ./agent.ts", + "lint": "eslint -c .eslintrc.json *.ts", + "lint:fix": "eslint -c .eslintrc.json *.ts --fix", + "format": "prettier --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"" + }, + "dependencies": { + "@coinbase/agentkit": "workspace:*", + "@x402/evm": "^2.7.0", + "@x402/fetch": "^2.7.0", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "tsx": "^4.7.1" + } +} diff --git a/typescript/examples/x402-survival-check/tsconfig.json b/typescript/examples/x402-survival-check/tsconfig.json new file mode 100644 index 000000000..6fee1565b --- /dev/null +++ b/typescript/examples/x402-survival-check/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "preserveSymlinks": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["*.ts"] +}