From b6969f2cdfe8ee5d77d7c33fed03e6bd08a86911 Mon Sep 17 00:00:00 2001 From: jywy78kgrf-create Date: Tue, 25 Aug 2026 03:42:06 +0000 Subject: [PATCH] examples: x402 buy loop with an opt-in merona pre-payment seller check Self-contained example (no changes to agentkit core). Composes the existing x402ActionProvider actions with a small example-local action provider: make_http_request -> check_x402_seller -> retry_http_request_with_x402. check_x402_seller calls merona's free, keyless trust API via the published merona-guard npm package and returns a recorded-fact PASS/FAIL/UNCERTAIN verdict; the agent does not pay on FAIL. Fail-soft (unreachable => UNCERTAIN, never blocks). No dependency added to agentkit core. --- .../langchain-x402-guard-chatbot/README.md | 59 ++++++ .../langchain-x402-guard-chatbot/chatbot.ts | 170 ++++++++++++++++++ .../meronaGuard.ts | 57 ++++++ .../langchain-x402-guard-chatbot/package.json | 28 +++ .../tsconfig.json | 9 + 5 files changed, 323 insertions(+) create mode 100644 typescript/examples/langchain-x402-guard-chatbot/README.md create mode 100644 typescript/examples/langchain-x402-guard-chatbot/chatbot.ts create mode 100644 typescript/examples/langchain-x402-guard-chatbot/meronaGuard.ts create mode 100644 typescript/examples/langchain-x402-guard-chatbot/package.json create mode 100644 typescript/examples/langchain-x402-guard-chatbot/tsconfig.json diff --git a/typescript/examples/langchain-x402-guard-chatbot/README.md b/typescript/examples/langchain-x402-guard-chatbot/README.md new file mode 100644 index 000000000..d69bcfcc1 --- /dev/null +++ b/typescript/examples/langchain-x402-guard-chatbot/README.md @@ -0,0 +1,59 @@ +# AgentKit x402 buy loop with an opt-in pre-payment guard + +A self-contained example: an AgentKit agent that pays x402 APIs, but **checks the +seller wallet before it pays**. It adds nothing to agentkit core — it composes +the existing `x402ActionProvider` actions with one small, example-local action +provider. + +## The buy loop: `inspect → check → decide → pay` + +`x402ActionProvider` already separates inspecting a 402 from paying it: + +- `make_http_request` — returns the 402 payment requirements (including the + seller `payTo` and `network`) **without paying**. +- `retry_http_request_with_x402` — **pays**. + +This example drops a check in between: + +``` +make_http_request → check_x402_seller → retry_http_request_with_x402 +``` + +`check_x402_seller` (in [`meronaGuard.ts`](./meronaGuard.ts)) calls +[merona](https://merona.io)'s free, keyless trust API and returns a recorded-fact +**PASS / FAIL / UNCERTAIN** verdict. The agent is instructed not to pay on a +recorded FAIL, and to ask the user on UNCERTAIN. + +merona is an independent x402 settlement index; its verdicts are recomputable +from public on-chain data and it holds no seller relationships. The check is +**fail-soft** — if merona is unreachable it returns UNCERTAIN rather than +throwing, so it never blocks the agent's loop. It uses the published +[`merona-guard`](https://www.npmjs.com/package/merona-guard) package (zero +runtime dependencies) and adds no dependency to agentkit itself. + +## Run it + +```bash +npm install +export OPENAI_API_KEY=... # the model +export CDP_API_KEY_ID=... # CDP wallet +export CDP_API_KEY_SECRET=... +export CDP_WALLET_SECRET=... +export NETWORK_ID=base-sepolia # or base +npm start +``` + +Then ask it to fetch a paid x402 URL. On a 402 it will read the requirements, +run `check_x402_seller` on the seller, and only pay if the seller isn't a +recorded FAIL. + +> Coverage note: merona's delivery-probe evidence is strongest on Base. On +> networks it doesn't index yet the verdict is UNCERTAIN — which, being +> fail-soft, means "proceed, but I have no evidence either way." + +## What this demonstrates + +A pre-payment counterparty check is a natural, opt-in extension of the x402 buy +loop. The guard here is provider-agnostic in spirit — `meronaGuard.ts` is ~40 +lines and could point at any trust source; it defaults to merona because it's +free and needs no key. diff --git a/typescript/examples/langchain-x402-guard-chatbot/chatbot.ts b/typescript/examples/langchain-x402-guard-chatbot/chatbot.ts new file mode 100644 index 000000000..391d32a9c --- /dev/null +++ b/typescript/examples/langchain-x402-guard-chatbot/chatbot.ts @@ -0,0 +1,170 @@ +import { + AgentKit, + CdpEvmWalletProvider, + walletActionProvider, + cdpApiActionProvider, + cdpEvmWalletActionProvider, + erc20ActionProvider, + x402ActionProvider, +} from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { HumanMessage } from "@langchain/core/messages"; +import { MemorySaver } from "@langchain/langgraph"; +import { createAgent } from "langchain"; +import { ChatOpenAI } from "@langchain/openai"; +import * as dotenv from "dotenv"; +import * as readline from "readline"; +import { meronaGuardActionProvider } from "./meronaGuard"; + +dotenv.config(); + +/** + * Validates that required environment variables are set. + * + * @throws {Error} If required environment variables are missing. + * @returns {void} + */ +function validateEnvironment(): void { + const missingVars: string[] = []; + const requiredVars = ["OPENAI_API_KEY", "CDP_API_KEY_ID", "CDP_API_KEY_SECRET", "CDP_WALLET_SECRET"]; + requiredVars.forEach(varName => { + if (!process.env[varName]) missingVars.push(varName); + }); + if (missingVars.length > 0) { + console.error("Error: Required environment variables are not set"); + missingVars.forEach(varName => console.error(`${varName}=your_${varName.toLowerCase()}_here`)); + process.exit(1); + } + if (!process.env.NETWORK_ID) { + console.warn("Warning: NETWORK_ID not set, defaulting to base-sepolia testnet"); + } +} + +validateEnvironment(); + +/** + * Initialize the agent with AgentKit, the x402 action provider, and the opt-in + * merona pre-payment guard. + * + * @returns {Promise<{ agent: unknown; config: unknown }>} The agent and its config. + */ +async function initializeAgent() { + const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + + const networkId = process.env.NETWORK_ID || "base-sepolia"; + + const walletProvider = await CdpEvmWalletProvider.configureWithWallet({ + apiKeyId: process.env.CDP_API_KEY_ID, + apiKeySecret: process.env.CDP_API_KEY_SECRET, + walletSecret: process.env.CDP_WALLET_SECRET, + idempotencyKey: process.env.IDEMPOTENCY_KEY, + address: process.env.ADDRESS as `0x${string}` | undefined, + networkId, + rpcUrl: process.env.RPC_URL, + }); + + // x402 configuration + const x402Config = { + registeredServices: networkId === "base-sepolia" ? ["https://www.x402.org/protected"] : [], + allowDynamicServiceRegistration: false, + maxPaymentUsdc: 1.0, + registeredFacilitators: {}, + }; + + const agentkit = await AgentKit.from({ + walletProvider, + actionProviders: [ + walletActionProvider(), + cdpApiActionProvider(), + cdpEvmWalletActionProvider(), + erc20ActionProvider(), + x402ActionProvider(x402Config), // make_http_request, retry_http_request_with_x402, ... + meronaGuardActionProvider(), // check_x402_seller (this example) + ], + }); + + const tools = await getLangChainTools(agentkit); + const memory = new MemorySaver(); + const agentConfig = { configurable: { thread_id: "x402 buy-loop with merona guard" } }; + + const agent = createAgent({ + model: llm, + tools, + checkpointer: memory, + systemPrompt: ` + You are an agent that can pay for x402 APIs onchain using Coinbase AgentKit. + + The buy loop is: inspect -> check -> decide -> pay. + When a request returns HTTP 402: + 1. Call make_http_request to read the payment requirements. + 2. Take the seller 'payTo' and 'network' from those requirements and call + check_x402_seller with them. + 3. Only call retry_http_request_with_x402 if the check is NOT a FAIL. + If the check is UNCERTAIN, tell the user what it says and ask before paying. + If it is a FAIL, do not pay; report the recorded reason. + + The check is free and fail-soft: if it is unavailable it returns UNCERTAIN, + which is a signal, not an error. Before your first action, get the wallet + details to see what network you're on. Be concise. + `, + }); + + return { agent, config: agentConfig }; +} + +/** + * Run the agent interactively based on user input. + * + * @param {unknown} agent - The agent executor. + * @param {unknown} config - Agent configuration. + * @returns {Promise} + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function runChatMode(agent: any, config: any) { + console.log("Starting chat mode... Type 'exit' to end."); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const question = (prompt: string): Promise => + new Promise(resolve => rl.question(prompt, resolve)); + + try { + // eslint-disable-next-line no-constant-condition + while (true) { + const userInput = await question("\nPrompt: "); + if (userInput.toLowerCase() === "exit") break; + + const stream = await agent.stream({ messages: [new HumanMessage(userInput)] }, config); + for await (const chunk of stream) { + if ("model_request" in chunk) { + const response = chunk.model_request.messages[0].content; + if (response !== "") console.log("\n" + response); + } + if ("tools" in chunk) { + for (const tool of chunk.tools.messages) { + console.log("Tool " + tool.name + ": " + tool.content); + } + } + } + console.log("-------------------"); + } + } catch (error) { + if (error instanceof Error) console.error("Error:", error.message); + process.exit(1); + } finally { + rl.close(); + } +} + +/** + * Start the chatbot agent. + * + * @returns {Promise} + */ +async function main() { + const { agent, config } = await initializeAgent(); + await runChatMode(agent, config); +} + +main().catch(error => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/typescript/examples/langchain-x402-guard-chatbot/meronaGuard.ts b/typescript/examples/langchain-x402-guard-chatbot/meronaGuard.ts new file mode 100644 index 000000000..18dfe0cd6 --- /dev/null +++ b/typescript/examples/langchain-x402-guard-chatbot/meronaGuard.ts @@ -0,0 +1,57 @@ +import { customActionProvider } from "@coinbase/agentkit"; +import { Guard } from "merona-guard"; +import { z } from "zod"; + +/** + * Map an x402 network id to a chain merona indexes. x402 uses CAIP-2 + * (`eip155:8453`, `solana:...`); merona covers base / polygon / solana. Anything + * else is passed through — merona then returns an UNCERTAIN the model can act on. + */ +function meronaChain(network: string): string { + const n = network.toLowerCase(); + if (n === "eip155:8453" || n === "base") return "base"; + if (n === "eip155:137" || n === "polygon") return "polygon"; + if (n.startsWith("solana")) return "solana"; + return n; +} + +// Free, no key. Verdicts are cached for their TTL and fail soft: if merona is +// unreachable the result is an UNCERTAIN, never a thrown error into the loop. +const guard = new Guard(); + +/** + * A self-contained, opt-in action provider that checks an x402 seller wallet + * with merona BEFORE the agent pays it. It slots between x402ActionProvider's + * two existing actions: + * + * make_http_request -> check_x402_seller -> retry_http_request_with_x402 + * + * `make_http_request` returns the 402 payment requirements (including the seller + * `payTo` and `network`); this action checks that seller; the agent only calls + * `retry_http_request_with_x402` if the verdict is not a recorded FAIL. + * + * merona (https://merona.io) is a free, keyless, independent x402 settlement + * index. Verdicts are PASS / FAIL / UNCERTAIN and recomputable from public + * on-chain data. The returned reason is recorded-fact only — a model reasoning + * over it reads "a paid probe recorded a settled payment with no content + * returned", never a slur about the seller. + */ +export const meronaGuardActionProvider = () => + customActionProvider({ + name: "check_x402_seller", + description: + "Check an x402 seller wallet with merona BEFORE paying it. Returns a recorded-fact " + + "PASS / FAIL / UNCERTAIN verdict. Call this after make_http_request returns the 402 " + + "payment requirements, using their payTo and network, and BEFORE calling " + + "retry_http_request_with_x402. Do not pay on FAIL; on UNCERTAIN, ask the user first.", + schema: z.object({ + payTo: z.string().describe("The seller wallet address from the 402 payment requirements"), + network: z + .string() + .describe("The network from the 402 requirements, e.g. 'base' or a CAIP-2 id like 'eip155:8453'"), + }), + invoke: async (args: { payTo: string; network: string }) => { + const r = await guard.check(args.payTo, meronaChain(args.network)); + return r.explain(); + }, + }); diff --git a/typescript/examples/langchain-x402-guard-chatbot/package.json b/typescript/examples/langchain-x402-guard-chatbot/package.json new file mode 100644 index 000000000..fce1402ce --- /dev/null +++ b/typescript/examples/langchain-x402-guard-chatbot/package.json @@ -0,0 +1,28 @@ +{ + "name": "@coinbase/langchain-x402-guard-chatbot-example", + "description": "AgentKit x402 buy-loop example with an opt-in merona pre-payment seller check", + "version": "1.0.0", + "private": true, + "author": "merona ", + "license": "Apache-2.0", + "scripts": { + "start": "NODE_OPTIONS='--no-warnings' tsx ./chatbot.ts", + "dev": "nodemon ./chatbot.ts" + }, + "dependencies": { + "@coinbase/agentkit": "workspace:*", + "@coinbase/agentkit-langchain": "workspace:*", + "@langchain/core": "^1.1.0", + "@langchain/langgraph": "^1.2.0", + "@langchain/openai": "^1.2.0", + "dotenv": "^16.4.5", + "langchain": "^1.1.0", + "merona-guard": "^0.1.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.0", + "tsx": "^4.7.1", + "typescript": "^5.6.0" + } +} diff --git a/typescript/examples/langchain-x402-guard-chatbot/tsconfig.json b/typescript/examples/langchain-x402-guard-chatbot/tsconfig.json new file mode 100644 index 000000000..6fee1565b --- /dev/null +++ b/typescript/examples/langchain-x402-guard-chatbot/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "preserveSymlinks": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["*.ts"] +}