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
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,2 @@
dist
node_modules
11 changes: 11 additions & 0 deletions typescript/examples/x402station-preflight-agentkit/.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"
}
55 changes: 55 additions & 0 deletions typescript/examples/x402station-preflight-agentkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# x402station Preflight + AgentKit Example

This example shows one way to call [Preflight by x402station.io](https://x402station.io/guard/recipes/agentkit) before an AgentKit-powered agent pays an x402 endpoint.

AgentKit handles wallet access and x402 settlement. x402station.io is an independent pre-payment signal layer: it measures endpoint risk before `PAYMENT-SIGNATURE` is signed.

## What the example does

1. Builds an AgentKit instance with the existing `x402ActionProvider`.
2. Calls the free x402station trial endpoint for a candidate x402 URL.
3. Blocks endpoints with hard risk signals such as `dead`, `zombie`, `decoy_price_extreme`, and `never_paid_zombie`.
4. Only then uses the x402 payment fetcher to call the paid endpoint.

Use the trial endpoint while developing. Production agents should switch `X402STATION_PREFLIGHT_URL` to `https://x402station.io/api/v1/preflight` and pay the small preflight fee or use prepaid credits.

## Prerequisites

- Node.js 20 or higher
- A Base wallet private key with enough USDC for the endpoint you plan to call
- An x402 endpoint URL to evaluate

Create `.env`:

```bash
AGENT_PRIVATE_KEY=0x...
TARGET_X402_URL=https://api.example.com/x402-endpoint
NETWORK_ID=base-mainnet
MAX_X402_PAYMENT_USDC=1
X402STATION_PREFLIGHT_URL=https://x402station.io/api/v1/preflight-trial
```

## Running the example

From the repository root:

```bash
cd typescript
pnpm install
pnpm --filter @coinbase/x402station-preflight-agentkit-example start
```

If preflight returns a blocking signal, the script exits before signing any x402 payment. If preflight passes, the script calls the target URL through AgentKit's existing `X402ActionProvider_make_http_request_with_x402` action.

## Notes

- This is not an endorsement of any merchant or endpoint. The preflight response is a machine-readable risk signal.
- Keep your own budget controls. Preflight is one guardrail, not a substitute for max-payment limits and allowlists.
- Treat softer warnings such as `proxy_markup`, `slow`, or `new_provider` as policy inputs rather than automatic failures.
- For agent workflows, put this check before tools that call `make_http_request_with_x402` or any direct `fetchWithPayment` path.

## Links

- x402station AgentKit recipe: <https://x402station.io/guard/recipes/agentkit>
- x402station OpenAPI: <https://x402station.io/api/openapi.json>
- AgentKit x402 action provider: <../../agentkit/src/action-providers/x402>
28 changes: 28 additions & 0 deletions typescript/examples/x402station-preflight-agentkit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@coinbase/x402station-preflight-agentkit-example",
"description": "Preflight x402 endpoints with x402station before AgentKit pays",
"version": "1.0.0",
"private": true,
"author": "Coinbase Inc.",
"license": "Apache-2.0",
"scripts": {
"start": "NODE_OPTIONS='--no-warnings' tsx ./preflight-agentkit.ts",
"dev": "nodemon ./preflight-agentkit.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}\"",
"test:types": "tsc --noEmit"
},
"dependencies": {
"@coinbase/agentkit": "workspace:*",
"dotenv": "^16.4.5",
"viem": "^2.27.2"
},
"devDependencies": {
"@types/node": "^20.12.11",
"nodemon": "^3.1.0",
"tsx": "^4.7.1",
"typescript": "^5.4.5"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { AgentKit, ViemWalletProvider, x402ActionProvider } from "@coinbase/agentkit";
import { createWalletClient, http, type Chain } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base, baseSepolia } from "viem/chains";
import * as dotenv from "dotenv";

dotenv.config();

type PreflightResult = {
ok?: boolean;
warnings?: string[];
risk_score?: number;
recommended_action?: string;
metadata?: Record<string, unknown>;
};

const BLOCKING_WARNINGS = new Set([
"dead",
"zombie",
"decoy_price_extreme",
"never_paid_zombie",
"dead_7d",
"mostly_dead",
]);

/**
* Reads a required environment variable.
*
* @param name - Environment variable name.
* @returns The configured value.
*/
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}

/**
* Parses the maximum x402 payment limit.
*
* @returns Maximum payment limit in USDC.
*/
function parseMaxPayment(): number {
const raw = process.env.MAX_X402_PAYMENT_USDC ?? "1";
const value = Number(raw);
if (!Number.isFinite(value) || value <= 0) {
throw new Error("MAX_X402_PAYMENT_USDC must be a positive number");
}
return value;
}

/**
* Resolves the viem chain used by the local wallet provider.
*
* @param networkId - Coinbase network ID.
* @returns The matching viem chain.
*/
function getChain(networkId: string): Chain {
if (networkId === "base-mainnet") {
return base;
}

if (networkId === "base-sepolia") {
return baseSepolia;
}

throw new Error("NETWORK_ID must be base-mainnet or base-sepolia for this example");
}

/**
* Calls x402station before any x402 payment is signed.
*
* @param url - Candidate x402 endpoint.
* @returns Machine-readable preflight result.
*/
async function preflightEndpoint(url: string): Promise<PreflightResult> {
const preflightUrl =
process.env.X402STATION_PREFLIGHT_URL ?? "https://x402station.io/api/v1/preflight-trial";

const response = await fetch(preflightUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ url }),
});

if (!response.ok) {
throw new Error(`x402station preflight failed with HTTP ${response.status}`);
}

return (await response.json()) as PreflightResult;
}

/**
* Fails closed for hard risk signals.
*
* @param result - x402station preflight response.
*/
function assertPreflightAllowed(result: PreflightResult): void {
const warnings = result.warnings ?? [];
const blocking = warnings.filter(warning => BLOCKING_WARNINGS.has(warning));

if (blocking.length > 0 || result.ok === false || result.recommended_action === "block") {
throw new Error(
[
"Blocked before PAYMENT-SIGNATURE.",
`warnings=${JSON.stringify(warnings)}`,
`risk_score=${result.risk_score ?? "unknown"}`,
`recommended_action=${result.recommended_action ?? "unknown"}`,
].join(" "),
);
}
}

/**
* Runs the end-to-end preflight-before-payment flow.
*/
async function main() {
const targetUrl = requiredEnv("TARGET_X402_URL");
const privateKey = requiredEnv("AGENT_PRIVATE_KEY") as `0x${string}`;
const networkId = process.env.NETWORK_ID ?? "base-mainnet";
const maxPaymentUsdc = parseMaxPayment();

const account = privateKeyToAccount(privateKey);
const chain = getChain(networkId);
const walletClient = createWalletClient({
account,
chain,
transport: http(process.env.RPC_URL),
});
const walletProvider = new ViemWalletProvider(walletClient, { rpcUrl: process.env.RPC_URL });

const agentkit = await AgentKit.from({
walletProvider,
actionProviders: [
x402ActionProvider({
registeredServices: [targetUrl],
maxPaymentUsdc,
}),
],
});

console.log(`AgentKit initialized with ${agentkit.getActions().length} actions.`);
console.log(`Preflighting ${targetUrl}`);

const preflight = await preflightEndpoint(targetUrl);
assertPreflightAllowed(preflight);

console.log("Preflight passed. Signing is now allowed for this request.");

const paidRequest = agentkit
.getActions()
.find(action => action.name === "X402ActionProvider_make_http_request_with_x402");

if (!paidRequest) {
throw new Error("AgentKit x402 payment action is unavailable for this wallet network");
}

const result = await paidRequest.invoke({
url: targetUrl,
method: "GET",
headers: {
accept: "application/json",
"x-agentkit-example": "x402station-preflight",
"x-agentkit-network": networkId,
},
queryParams: null,
body: null,
});

console.log(result);
}

main().catch(error => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "Node16",
"preserveSymlinks": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["*.ts"]
}
Loading
Loading