From 451a8d3cd45a1806c4aba83aadf3e2380f75ad76 Mon Sep 17 00:00:00 2001 From: teodorofodocrispin-cmyk Date: Sat, 8 Aug 2026 13:17:28 -0500 Subject: [PATCH] docs(examples): add x402ActionProvider pre-execution safety guard example Adds a minimal, standalone example showing how to gate an on-chain transaction behind a third-party x402 safety check using the existing x402ActionProvider (no new dependencies on the framework itself). The pattern: build the candidate tx, call an x402-compatible guard service via makeHttpRequestWithX402 (auto-pays the small USDC fee), and only proceed if the verdict is SAFE. Deterministic -- does not depend on the LLM choosing to run the check. Uses SENTINEL (sentinel-agent.dev), an independent/unaffiliated x402 service, purely as a worked example since its response is easy to branch on. The pattern generalizes to any x402-compatible guard service -- swapping GUARD_URL is enough. Verified: TypeScript syntax/type-checks cleanly in isolation (no errors beyond expected unresolved workspace/external packages, which aren't installed outside the monorepo). Not run against the full monorepo build/lint pipeline from this environment -- happy to adjust to match conventions on review. --- .../x402-sentinel-preflight-guard/.env-local | 10 ++ .../.eslintrc.json | 4 + .../.prettierignore | 7 + .../x402-sentinel-preflight-guard/.prettierrc | 11 ++ .../x402-sentinel-preflight-guard/README.md | 67 +++++++ .../guard-before-transfer.ts | 168 ++++++++++++++++++ .../package.json | 22 +++ .../tsconfig.json | 9 + 8 files changed, 298 insertions(+) create mode 100644 typescript/examples/x402-sentinel-preflight-guard/.env-local create mode 100644 typescript/examples/x402-sentinel-preflight-guard/.eslintrc.json create mode 100644 typescript/examples/x402-sentinel-preflight-guard/.prettierignore create mode 100644 typescript/examples/x402-sentinel-preflight-guard/.prettierrc create mode 100644 typescript/examples/x402-sentinel-preflight-guard/README.md create mode 100644 typescript/examples/x402-sentinel-preflight-guard/guard-before-transfer.ts create mode 100644 typescript/examples/x402-sentinel-preflight-guard/package.json create mode 100644 typescript/examples/x402-sentinel-preflight-guard/tsconfig.json diff --git a/typescript/examples/x402-sentinel-preflight-guard/.env-local b/typescript/examples/x402-sentinel-preflight-guard/.env-local new file mode 100644 index 000000000..851818306 --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/.env-local @@ -0,0 +1,10 @@ +# Copy this file to .env and fill in your own values. +OPENAI_API_KEY= +CDP_API_KEY_ID= +CDP_API_KEY_SECRET= +CDP_WALLET_SECRET= +NETWORK_ID=base-mainnet + +# Max amount (whole USDC) this example will let x402ActionProvider spend +# calling the guard service before it refuses and aborts. +MAX_GUARD_PAYMENT_USDC=0.05 diff --git a/typescript/examples/x402-sentinel-preflight-guard/.eslintrc.json b/typescript/examples/x402-sentinel-preflight-guard/.eslintrc.json new file mode 100644 index 000000000..91571ba7a --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["../../.eslintrc.base.json"] +} diff --git a/typescript/examples/x402-sentinel-preflight-guard/.prettierignore b/typescript/examples/x402-sentinel-preflight-guard/.prettierignore new file mode 100644 index 000000000..20de531f4 --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/.prettierignore @@ -0,0 +1,7 @@ +docs/ +dist/ +coverage/ +.github/ +src/client +**/**/*.json +*.md diff --git a/typescript/examples/x402-sentinel-preflight-guard/.prettierrc b/typescript/examples/x402-sentinel-preflight-guard/.prettierrc new file mode 100644 index 000000000..ffb416b74 --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/.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-sentinel-preflight-guard/README.md b/typescript/examples/x402-sentinel-preflight-guard/README.md new file mode 100644 index 000000000..f7fb9588a --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/README.md @@ -0,0 +1,67 @@ +# x402ActionProvider Example: Pre-Execution Safety Guard + +This example demonstrates a pattern for gating an on-chain transaction behind +an automated, third-party safety check -- using nothing but the existing +`x402ActionProvider` and a small USDC micropayment, paid automatically. + +It is intentionally *not* a full chatbot. The safety check is deterministic +and should not depend on an LLM choosing to run it: the pattern here is +**check first, decide programmatically, only then let the agent proceed.** + +## What it does + +1. Builds a candidate transaction (`to`, `value`, `data`). +2. Calls a third-party x402 safety oracle -- [SENTINEL](https://sentinel-agent.dev), + an independent, unaffiliated x402 service -- with that transaction, via + `x402ActionProvider.makeHttpRequestWithX402`. This pays SENTINEL's small + tiered fee (from $0.005 USDC) automatically. +3. Only proceeds to sign/execute the transaction if the verdict is `SAFE`. + +This is a general pattern, not a SENTINEL-specific one: any x402-compatible +safety/compliance service can be swapped in for `GUARD_URL`. SENTINEL is used +here purely because it already returns a structured `verdict`/`risks`/`score` +payload that's easy to branch on in code, which makes for a clear example. + +## Prerequisites + +### Node version + +Requires Node.js 20+. + +```bash +node --version +``` + +### API keys + +- [CDP API Key](https://portal.cdp.coinbase.com/access/api) +- [Generate Wallet Secret](https://portal.cdp.coinbase.com/products/wallet-api) + +Rename `.env-local` to `.env` and fill in: + +- `CDP_API_KEY_ID` +- `CDP_API_KEY_SECRET` +- `CDP_WALLET_SECRET` +- `NETWORK_ID` (defaults to `base-mainnet`) +- `MAX_GUARD_PAYMENT_USDC` (defaults to `0.05`) -- caps what this example + will spend on a single safety check before refusing to pay. + +## Running the example + +From the repository root: + +```bash +pnpm install +pnpm build +cd typescript/examples/x402-sentinel-preflight-guard +pnpm start +``` + +## Adapting this pattern + +- Swap `GUARD_URL` and the request body shape for any other x402-compatible + guard/compliance service. +- The transfer itself is left commented out (`wallet.nativeTransfer`) since + this is a documentation example -- wire it into your actual action flow. +- `registeredServices` in the `x402ActionProvider` config is an allowlist; + add every guard/data service your agent is permitted to call and pay. diff --git a/typescript/examples/x402-sentinel-preflight-guard/guard-before-transfer.ts b/typescript/examples/x402-sentinel-preflight-guard/guard-before-transfer.ts new file mode 100644 index 000000000..ea340fa9c --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/guard-before-transfer.ts @@ -0,0 +1,168 @@ +import { + AgentKit, + CdpEvmWalletProvider, + walletActionProvider, + x402ActionProvider, +} from "@coinbase/agentkit"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +/** + * Example: gate an on-chain transfer behind a pre-execution safety check. + * + * This does NOT require an LLM in the loop for the safety decision itself -- + * the check is deterministic and should run every time, regardless of what + * the agent's language model decided. The pattern: + * + * 1. Build the transfer you intend to send (to, value, data). + * 2. Ask a third-party x402 safety oracle to evaluate it BEFORE signing. + * 3. Only proceed if the oracle returns a SAFE verdict. + * + * The oracle used here is SENTINEL (https://sentinel-agent.dev), an + * independent x402 service -- not a Coinbase product, not affiliated with + * AgentKit. It is used purely as a worked example of the x402ActionProvider + * "call any x402 service" pattern applied to pre-execution risk checking. + * Swap in any x402-compatible guard service that fits your use case. + * + * Cost: SENTINEL charges a small tiered USDC fee per check (from $0.005), + * paid automatically via x402ActionProvider. Set MAX_GUARD_PAYMENT_USDC to + * cap what this example is willing to spend on a single check. + */ + +/** + * Validates required environment variables are present. + * + * @throws {Error} if a required variable is missing + * @returns {void} + */ +function validateEnvironment(): void { + const required = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET", "CDP_WALLET_SECRET"]; + const missing = required.filter(name => !process.env[name]); + if (missing.length > 0) { + console.error("Missing required environment variables:", missing.join(", ")); + process.exit(1); + } +} + +validateEnvironment(); + +const GUARD_URL = "https://sentinel-agent.dev/v1/guard"; +const MAX_GUARD_PAYMENT_USDC = Number(process.env.MAX_GUARD_PAYMENT_USDC ?? "0.05"); + +/** + * Result shape returned by SENTINEL's /v1/guard endpoint. + */ +interface GuardVerdict { + verdict: "SAFE" | "UNSAFE" | "UNKNOWN"; + risks?: string[]; + score?: number; + grade?: string; + signature?: string; + [key: string]: unknown; +} + +/** + * Asks SENTINEL whether a transaction looks safe to sign, paying the small + * x402 fee automatically via the AgentKit x402ActionProvider instance. + * + * @param guard - A configured x402ActionProvider instance with sentinel-agent.dev registered + * @param walletProvider - The wallet that would sign the transaction being checked + * @param tx - The transaction under consideration + * @returns The parsed guard verdict, or null if the check itself failed (fail-closed by caller) + */ +async function checkTransactionSafety( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + guard: any, + walletProvider: CdpEvmWalletProvider, + tx: { to: string; data?: string; value?: string }, +): Promise { + const raw = await guard.makeHttpRequestWithX402(walletProvider, { + url: GUARD_URL, + method: "POST", + headers: null, + queryParams: null, + body: { + chain: "base", + from_addr: await walletProvider.getAddress(), + tx: { + to: tx.to, + data: tx.data ?? "0x", + value: tx.value ?? "0x0", + }, + }, + }); + + const parsed = JSON.parse(raw); + + if (!parsed.success) { + console.error("Guard check failed (fail-closed -- treat as not-safe):", parsed); + return null; + } + + return parsed.data as GuardVerdict; +} + +/** + * Entry point: builds a sample native transfer, checks it with SENTINEL, + * and only executes it if the verdict is SAFE. + * + * @returns {Promise} + */ +async function main(): Promise { + 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!, + networkId: process.env.NETWORK_ID ?? "base-mainnet", + }); + + const guard = x402ActionProvider({ + registeredServices: ["https://sentinel-agent.dev"], + maxPaymentUsdc: MAX_GUARD_PAYMENT_USDC, + }); + + const wallet = walletActionProvider(); + + const agentKit = await AgentKit.from({ + walletProvider, + actionProviders: [guard, wallet], + }); + void agentKit; // available for LLM-driven actions elsewhere in a real agent; unused directly here + + // Replace with the transfer your agent actually intends to make. + const intendedTx = { + to: "0x0000000000000000000000000000000000dEaD", + value: "0x0", + data: "0x", + }; + + console.log("Checking transaction safety with SENTINEL before signing..."); + const verdict = await checkTransactionSafety(guard, walletProvider, intendedTx); + + if (!verdict || verdict.verdict !== "SAFE") { + console.log( + "Refusing to sign -- verdict was", + verdict?.verdict ?? "UNAVAILABLE", + verdict?.risks ? `(risks: ${verdict.risks.join(", ")})` : "", + ); + return; + } + + console.log("Verdict SAFE (score:", verdict.score, "grade:", verdict.grade, ") -- proceeding."); + + // At this point, execute the actual transfer via the wallet action provider + // (or hand off to your agent loop). Left as a comment rather than executed + // automatically, since this is a documentation example. + // + // const result = await wallet.nativeTransfer(walletProvider, { + // to: intendedTx.to, + // value: intendedTx.value, + // }); + // console.log(result); +} + +main().catch(error => { + console.error("Example failed:", error); + process.exit(1); +}); diff --git a/typescript/examples/x402-sentinel-preflight-guard/package.json b/typescript/examples/x402-sentinel-preflight-guard/package.json new file mode 100644 index 000000000..7a3a8b941 --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/package.json @@ -0,0 +1,22 @@ +{ + "name": "@coinbase/x402-sentinel-preflight-guard-example", + "description": "Example: gate an on-chain transfer behind a pre-execution risk check using x402ActionProvider and a third-party x402 safety oracle (SENTINEL)", + "version": "1.0.0", + "private": true, + "author": "Community contribution", + "license": "Apache-2.0", + "scripts": { + "start": "NODE_OPTIONS='--no-warnings' tsx ./guard-before-transfer.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:*", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "tsx": "^4.7.1" + } +} diff --git a/typescript/examples/x402-sentinel-preflight-guard/tsconfig.json b/typescript/examples/x402-sentinel-preflight-guard/tsconfig.json new file mode 100644 index 000000000..6fee1565b --- /dev/null +++ b/typescript/examples/x402-sentinel-preflight-guard/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "preserveSymlinks": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["*.ts"] +}