Skip to content

Commit d260b2d

Browse files
committed
Add x402station action provider — pre-flight oracle for x402 endpoints
x402station is a public oracle for the x402 agentic-commerce network that probes every endpoint on agentic.market every ~10 minutes and flags decoys (price ≥ $1k USDC), zombies (100% erroring), dead endpoints, and price/latency anomalies. Calling preflight before each paid x402 request costs $0.001 USDC and is typically 20× cheaper than the request an agent would otherwise lose to a decoy. This action provider mirrors the shape of the existing `x402` provider in this repo and reuses the same EvmWalletProvider signer-wrapping pattern, so the four paid actions auto-sign their own $0.001-$0.01 USDC payments through whatever EvmWalletProvider the agent is already configured with. Two free secret-gated actions (watch_status, watch_unsubscribe) manage an existing webhook subscription. Six actions: preflight $0.001 {ok, warnings[], metadata} forensics $0.001 7-day uptime + latency p50/p90/p99 + decoy probability + concentration catalog_decoys $0.005 full known-bad blacklist, cacheable watch_subscribe $0.01 30-day webhook subscription + 100 prepaid HMAC-signed alerts watch_status free secret-gated read-back watch_unsubscribe free secret-gated soft-delete Networks: Base mainnet (eip155:8453) and Base Sepolia (eip155:84532). Tests cover the constructor's host allowlist (canonical x402station.io, http(s)://localhost dev URLs only — refuses to start otherwise so a misconfigured agent can't sign x402 payments against an unknown host), supportsNetwork (Base ✓, others ✗), every action path including the x-payment-response header decoding into paymentReceipt, and the secret-gated 404 behaviour for the free watch endpoints. 21/21 passing.
1 parent 0fe026b commit d260b2d

7 files changed

Lines changed: 1059 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@coinbase/agentkit": patch
3+
---
4+
5+
Added a new `x402station` action provider — a pre-flight oracle for the x402 agentic-commerce network. Six tools (preflight, forensics, catalog_decoys, watch_subscribe, watch_status, watch_unsubscribe) wrapping the public oracle at https://x402station.io. Four are paid via x402 ($0.001–$0.01 USDC, auto-signed via the agent's `EvmWalletProvider`); two are free + secret-gated for managing an existing webhook subscription. Networks: Base mainnet and Base Sepolia.

typescript/agentkit/src/action-providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export * from "./flaunch";
3636
export * from "./onramp";
3737
export * from "./vaultsfyi";
3838
export * from "./x402";
39+
export * from "./x402station";
3940
export * from "./yelay";
4041
export * from "./zerion";
4142
export * from "./zerodev";
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# X402station Action Provider
2+
3+
The `x402station` action provider gives any AgentKit agent a pre-flight oracle for x402 endpoints. Four paid tools (`preflight`, `forensics`, `catalog_decoys`, `watch_subscribe`) auto-sign their own $0.001–$0.01 USDC payments through the agent's configured `EvmWalletProvider`; two free secret-gated tools (`watch_status`, `watch_unsubscribe`) manage an existing webhook subscription.
4+
5+
This is a wrapper around the public oracle at [x402station.io](https://x402station.io) — same API + identical signal vocabulary used by the official `x402station-mcp` package on npm.
6+
7+
## Why pre-flight?
8+
9+
The agentic.market catalog has 25,000+ x402 endpoints. A non-trivial fraction are honeypots:
10+
11+
- **Decoys** priced ≥ $1,000 USDC per call. An agent that pays one drains its wallet.
12+
- **Zombies** that 402-handshake fine but always 4xx after settlement (the call-side payment goes through, the agent gets nothing).
13+
- **Dead** endpoints that return network errors or 5xx every probe.
14+
- **Price-jacked** endpoints whose listed price drifted 10× past the provider's group median.
15+
16+
x402station independently probes every endpoint every ~10 minutes (not facilitator-reported) so it catches what facilitator-only monitors miss. Calling `preflight` before each paid x402 request costs $0.001 — typically 20× cheaper than the request the agent would otherwise lose to a decoy.
17+
18+
## Networks
19+
20+
- Base mainnet (`base-mainnet` / `eip155:8453`) — production
21+
- Base Sepolia (`base-sepolia` / `eip155:84532`) — testing
22+
23+
The oracle accepts USDC payments on both networks via Coinbase's CDP facilitator; the action provider's `supportsNetwork` returns `false` for any other network.
24+
25+
## Actions
26+
27+
| Action | Cost | Description |
28+
|---|---|---|
29+
| `preflight` | $0.001 | `{ok, warnings[], metadata}` for any URL — fast safety check |
30+
| `forensics` | $0.001 | 7-day uptime + latency p50/p90/p99 + decoy probability + concentration stats |
31+
| `catalog_decoys` | $0.005 | Every URL flagged dangerous, in one cacheable blob |
32+
| `watch_subscribe` | $0.01 | 30-day webhook subscription + 100 prepaid HMAC-signed alerts |
33+
| `watch_status` | free* | Read-back: active/expired, alerts remaining, recent deliveries |
34+
| `watch_unsubscribe` | free* | Soft-delete a watch |
35+
36+
\* Free actions are secret-gated by the 64-char hex secret returned from `watch_subscribe`. Constant-time compare on the server; mismatched secret returns 404 (not 401) so an attacker scraping IDs can't distinguish "exists but wrong secret" from "doesn't exist".
37+
38+
## Signal vocabulary
39+
40+
Strings returned in `warnings[]` from `preflight` / `forensics`. **Bold** signals flip `ok` to `false` and an agent should refuse the target call:
41+
42+
- **`dead`** — ≥3 unhealthy probes in the last 30 min
43+
- **`zombie`** — ≥3 probes in the last hour, zero healthy
44+
- **`decoy_price_extreme`** — listed price ≥ $1,000 USDC
45+
- **`dead_7d`** — ≥20 probes over 7 days, zero healthy (forensics-only)
46+
- **`mostly_dead`** — ≥20 probes over 7 days, uptime < 50% (forensics-only)
47+
- `unknown_endpoint` — URL not in the catalog (informational; still billed)
48+
- `no_history` — in catalog but no probes in the last hour
49+
- `suspicious_high_price` — price $10–$1,000 USDC
50+
- `slow` — avg latency ≥ 2,000 ms in the last hour
51+
- `new_provider` — service first seen < 24h ago
52+
- `slow_p99` — latency p99 ≥ 5,000 ms (forensics-only)
53+
- `price_outlier_high` — current price > 10× provider-group median
54+
- `high_concentration` — endpoint's provider owns ≥ 5% of the catalog
55+
56+
The watch endpoint accepts a subset of these in its `signals` array — the worker fires when subscribed signals appear or clear vs the last computed state.
57+
58+
## Example
59+
60+
```typescript
61+
import {
62+
AgentKit,
63+
CdpEvmServerWalletProvider,
64+
x402stationActionProvider,
65+
} from "@coinbase/agentkit";
66+
67+
const walletProvider = await CdpEvmServerWalletProvider.configureWithWallet({
68+
apiKeyId: process.env.CDP_API_KEY_ID!,
69+
apiKeySecret: process.env.CDP_API_KEY_SECRET!,
70+
walletSecret: process.env.CDP_WALLET_SECRET!,
71+
networkId: "base-mainnet",
72+
});
73+
74+
const agentKit = await AgentKit.from({
75+
walletProvider,
76+
actionProviders: [x402stationActionProvider()],
77+
});
78+
79+
// The LLM can now call preflight, forensics, etc. via getActions().
80+
// Pre-flight a target before the agent commits a paid call to it:
81+
const actions = agentKit.getActions();
82+
const preflight = actions.find((a) => a.name.endsWith("_preflight"))!;
83+
const result = await preflight.invoke({
84+
url: "https://api.venice.ai/api/v1/chat/completions",
85+
});
86+
console.log(JSON.parse(result));
87+
// { result: { ok: false, warnings: ["dead", "zombie"], metadata: {...} },
88+
// paymentReceipt: { transaction: "0x…" } }
89+
```
90+
91+
## Configuration
92+
93+
```typescript
94+
x402stationActionProvider({
95+
// Defaults to https://x402station.io. Only the canonical host or a
96+
// localhost dev URL is accepted — refuses to start otherwise so a
97+
// misconfigured agent can't sign x402 payments against an unknown host.
98+
baseUrl: "https://x402station.io",
99+
});
100+
```
101+
102+
## Links
103+
104+
- Service: <https://x402station.io>
105+
- Manifest: <https://x402station.io/.well-known/x402>
106+
- OpenAPI: <https://x402station.io/api/openapi.json>
107+
- Agent skills (v0.2.0): <https://x402station.io/.well-known/agent-skills>
108+
- Skill description: <https://x402station.io/skill.md>
109+
- Source: <https://github.com/sF1nX/x402station>
110+
- npm (MCP adapter for non-AgentKit agents): <https://www.npmjs.com/package/x402station-mcp>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {
2+
X402stationActionProvider,
3+
x402stationActionProvider,
4+
} from "./x402stationActionProvider";
5+
export type { X402stationConfig } from "./schemas";
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { z } from "zod";
2+
3+
/**
4+
* Configuration options for X402stationActionProvider.
5+
*/
6+
export interface X402stationConfig {
7+
/**
8+
* Override the default oracle base URL.
9+
*
10+
* Allowed values: `https://x402station.io` (canonical, default) or any
11+
* `http(s)://localhost*` for development. Any other host is rejected at
12+
* construction time so a misconfigured agent can't sign x402 payments
13+
* against an attacker-controlled URL.
14+
*/
15+
baseUrl?: string;
16+
}
17+
18+
/**
19+
* Signal vocabulary returned by the oracle. Whitelisted at the schema
20+
* level so a typo in the agent's `signals` array doesn't silently never
21+
* fire (the route would 400, but catching it earlier saves a wallet
22+
* round-trip).
23+
*
24+
* Critical signals (those that flip preflight `ok` to `false`):
25+
* `dead`, `zombie`, `decoy_price_extreme`, `dead_7d`, `mostly_dead`
26+
*/
27+
export const SignalEnum = z.enum([
28+
"unknown_endpoint",
29+
"no_history",
30+
"dead",
31+
"zombie",
32+
"decoy_price_extreme",
33+
"suspicious_high_price",
34+
"slow",
35+
"new_provider",
36+
"dead_7d",
37+
"mostly_dead",
38+
"slow_p99",
39+
"price_outlier_high",
40+
"high_concentration",
41+
]);
42+
43+
/**
44+
* Input schema for the `preflight` and `forensics` actions.
45+
*/
46+
export const PreflightSchema = z.object({
47+
url: z
48+
.string()
49+
.url()
50+
.describe(
51+
"Full URL of the x402 endpoint the agent is about to pay (must be http(s)://, max 2048 chars).",
52+
),
53+
});
54+
55+
export const ForensicsSchema = PreflightSchema;
56+
57+
/**
58+
* Empty input — no parameters needed.
59+
*/
60+
export const CatalogDecoysSchema = z.object({}).describe("No parameters required");
61+
62+
/**
63+
* Input for `watch_subscribe`. Pays $0.01 USDC, returns a watchId + a 64-char
64+
* hex secret. The secret is the HMAC seed for verifying delivery payloads
65+
* and is only returned once — store it.
66+
*/
67+
export const WatchSubscribeSchema = z.object({
68+
url: z
69+
.string()
70+
.url()
71+
.describe("The x402 endpoint URL to watch."),
72+
webhookUrl: z
73+
.string()
74+
.url()
75+
.refine((u) => u.startsWith("https://"), {
76+
message:
77+
"webhookUrl must use HTTPS — HMAC-signed alert payloads must not travel in clear text",
78+
})
79+
.describe(
80+
"Where x402station will POST alert payloads. Must be HTTPS (HMAC-signed payloads must travel encrypted) and reachable from the public internet.",
81+
),
82+
signals: z
83+
.array(SignalEnum)
84+
.min(1)
85+
.max(20)
86+
.optional()
87+
.describe(
88+
"Signal names to alert on. Defaults to ['dead', 'zombie', 'decoy_price_extreme'].",
89+
),
90+
});
91+
92+
/**
93+
* Input for `watch_status` and `watch_unsubscribe`. Both are free + secret-gated.
94+
*/
95+
export const WatchStatusSchema = z.object({
96+
watchId: z.string().uuid().describe("The watchId UUID returned by watch_subscribe."),
97+
secret: z
98+
.string()
99+
.length(64)
100+
.regex(/^[0-9a-f]{64}$/i, "secret must be 64 hex chars")
101+
.describe("The 64-char hex secret returned by watch_subscribe (store it; not retrievable later)."),
102+
});
103+
104+
export const WatchUnsubscribeSchema = WatchStatusSchema;

0 commit comments

Comments
 (0)