Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions typescript/examples/x402-sentinel-preflight-guard/.env-local
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"parser": "@typescript-eslint/parser",
"extends": ["../../.eslintrc.base.json"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
docs/
dist/
coverage/
.github/
src/client
**/**/*.json
*.md
11 changes: 11 additions & 0 deletions typescript/examples/x402-sentinel-preflight-guard/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 100,
"proseWrap": "never"
}
67 changes: 67 additions & 0 deletions typescript/examples/x402-sentinel-preflight-guard/README.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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<GuardVerdict | null> {
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<void>}
*/
async function main(): Promise<void> {
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);
});
22 changes: 22 additions & 0 deletions typescript/examples/x402-sentinel-preflight-guard/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"preserveSymlinks": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["*.ts"]
}
Loading