Skip to content

Commit 94f2f2f

Browse files
committed
Sync from opensea-devtools
Origin-SHA: 1427c0d67267db48311d921c5badb57f9c6e1848
1 parent 85447ac commit 94f2f2f

17 files changed

Lines changed: 145 additions & 45 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,10 @@ const { toolId, txHash } = await client.registerTool({
516516
})
517517
```
518518

519+
## Invocation Flow
520+
521+
For a visual walkthrough of the full 402 challenge-response lifecycle (identity verification, access check, payment settlement, and usage reporting), see [docs/tool-invocation-flow.md](docs/tool-invocation-flow.md).
522+
519523
## Gating
520524

521525
### Predicate Gate (recommended)

docs/tool-invocation-flow.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Tool Invocation Flow
2+
3+
Unified 402 challenge-response for both free and paid tools.
4+
5+
![Unified 402 Tool Usage Flow](tool-invocation-flow.png)
6+
7+
## Sequence Diagram
8+
9+
```
10+
Caller Tool Server ToolRegistry (onchain) Facilitator OpenSea API
11+
│ │ │ │ │
12+
│ 1. POST /tool (no auth) │ │ │ │
13+
│ ────────────────────────> │ │ │ │
14+
│ │ │ │ │
15+
│ 2. 402 PaymentRequirements│ │ │ │
16+
│ maxAmountRequired: "0" │ (free) │ │ │
17+
│ maxAmountRequired: amt │ (paid) │ │ │
18+
│ <──────────────────────── │ │ │ │
19+
│ │ │ │ │
20+
│ 3. Sign X-Payment │ │ │ │
21+
│ (EIP-3009 │ │ │ │
22+
│ TransferWithAuthorization│ │ │ │
23+
│ from: caller │ │ │ │
24+
│ to: payTo from 402 │ │ │ │
25+
│ value: 0 or amount │ │ │ │
26+
│ nonce: random 32 bytes) │ │ │ │
27+
│ │ │ │ │
28+
│ 4. POST /tool │ │ │ │
29+
│ X-Payment: base64(...) │ │ │ │
30+
│ ────────────────────────> │ │ │ │
31+
│ │ │ │ │
32+
│ │ 5. ecrecover X-Payment │ │ │
33+
│ │ recover caller address │ │ │
34+
│ │ verify to == operator │ │ │
35+
│ │ check TTL │ │ │
36+
│ │ │ │ │
37+
│ │ 6. tryHasAccess(toolId, │ │ │
38+
│ │ callerAddr, data) │ │ │
39+
│ │ ────────────────────────> │ │ │
40+
│ │ granted: true/false │ │ │
41+
│ │ <──────────────────────── │ │ │
42+
│ │ false → 403 │ │ │
43+
│ │ │ │ │
44+
│ │ 7. handler(input, ctx) │ │ │
45+
│ │ validate input/output │ │ │
46+
│ │ │ │ │
47+
│ │ 8. gate.settle() ─────────────────────────────> │ │
48+
│ │ (paid only: POST │ │ │
49+
│ │ /settle with payload) │ { tx: "0x..." } │ │
50+
│ │ <───────────────────────────────────────────────── │ │
51+
│ │ │ │ │
52+
│ │ 9. POST /api/v2/tools/usage ──────────────────────────────────────> │
53+
│ │ (awaited before response) │ │
54+
│ │ free: eip3009_authorization (forward signature)│ │
55+
│ │ paid: x402_settlement (forward tx hash) │ │
56+
│ │ <────────────────────────────────────────────────────────────────── │
57+
│ │ │ │ │
58+
│ 10. 200 OK (tool output) │ │ │ │
59+
│ <──────────────────────── │ │ │ │
60+
```
61+
62+
### Key details
63+
64+
- **Unified 402 pattern** — free and paid tools use the same challenge-response. The only difference is `maxAmountRequired` (`"0"` vs actual USDC price).
65+
- **X-Payment = EIP-3009 `TransferWithAuthorization`** — signed EIP-712 typed data in the USDC domain. `value=0` for identity proof only; `value=price` for paid tools.
66+
- **Pure ecrecover** — server-side identity verification is `recoverTypedDataAddress()`, no RPC needed.
67+
- **Combined predicate + payment**`paidPredicateGate` resolves identity and payment in a single 402 round trip. The real-value signature proves identity (`from`) and authorizes payment simultaneously. Predicate is checked before settlement — if access denied, 403 and no funds move.
68+
- **Usage reporting is awaited** — the report completes before the 200 is returned (survives serverless freeze). 5-second timeout; errors never fail the tool response.
69+
- **10-minute TTL**`validBefore = now + 600s`, `validAfter = 0`.
70+
- **Single signature** — the caller's original EIP-3009 auth is forwarded to the usage API as-is; the tool server never re-signs.

docs/tool-invocation-flow.png

445 KB
Loading

examples/nft-appraisal-tool/api/holder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ setAnthropicConfig({
3232
const baseEndpoint =
3333
process.env.TOOL_ENDPOINT ?? "https://nft-appraisal-tool.vercel.app"
3434
const paywall = buildHolderPaywall({ recipient })
35-
const gates = buildHolderGates(paywall, {
35+
const gates = buildHolderGates({
3636
toolId: BigInt(holderToolId),
3737
rpcUrl: process.env.BASE_RPC_URL,
38+
recipient,
3839
})
3940
const manifest = buildHolderManifest({
4041
creator,

examples/nft-appraisal-tool/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@ai-sdk/anthropic": "^1.2.0",
21-
"@opensea/tool-sdk": "^0.14.2",
21+
"@opensea/tool-sdk": "^0.16.0",
2222
"ai": "^4.3.0",
2323
"zod": "4.3.6"
2424
},

examples/nft-appraisal-tool/pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/nft-appraisal-tool/scripts/paid-holder-call.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ console.log("\n--- paidAuthenticatedFetch (EIP-3009 + x402) ---")
8282
const startedAt = Date.now()
8383
const res = await paidAuthenticatedFetch(toolUrl, {
8484
account,
85-
chainId: 8453,
8685
// Reject if the server asks for more than 0.01 USDC (the holder rate).
8786
// Prevents a compromised server from inflating the price.
8887
maxAmount: "10000",

examples/nft-appraisal-tool/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ function initHandlers(env: Env) {
7474
const holderHandler = env.HOLDER_TOOL_ID
7575
? buildToolHandler({
7676
manifest: holderManifest,
77-
gates: buildHolderGates(holderPaywall, {
77+
gates: buildHolderGates({
7878
toolId: BigInt(env.HOLDER_TOOL_ID),
7979
rpcUrl: env.BASE_RPC_URL,
80+
recipient,
8081
}),
8182
usageReporting: buildUsageReporting({
8283
apiKey: env.OPENSEA_API_KEY,

examples/nft-appraisal-tool/src/paywall.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import {
22
defineToolPaywall,
33
type GateMiddleware,
4-
predicateGate,
4+
paidPredicateGate,
55
} from "@opensea/tool-sdk"
66

7+
/** Holder-tier price. Shared by the manifest pricing and the gate so they
8+
* can never drift. */
9+
const HOLDER_AMOUNT_USDC = "0.01"
10+
const FACILITATOR = "payai" as const
11+
712
/**
813
* Single source of truth for the public x402 paywall config. Returns both
914
* the manifest pricing entry and the gate, so they can never drift apart.
@@ -42,28 +47,40 @@ export function buildHolderPaywall({
4247
}) {
4348
return defineToolPaywall({
4449
recipient,
45-
amountUsdc: "0.01",
46-
facilitator: "payai",
50+
amountUsdc: HOLDER_AMOUNT_USDC,
51+
facilitator: FACILITATOR,
4752
})
4853
}
4954

5055
/**
51-
* Compose the holder gate chain: `predicateGate` first (so non-holders
52-
* get a clean 403 before being asked to pay), then the x402 paywall gate.
53-
* `predicateGate` verifies EIP-3009 auth and asks the onchain `ToolRegistry`
54-
* whether the caller satisfies the tool's registered access predicate.
56+
* Holder gate: a single `paidPredicateGate` that resolves the onchain
57+
* CHONK-ownership predicate and the $0.01 x402 payment in one 402 round trip
58+
* (2 requests instead of 3). The predicate is checked before the facilitator
59+
* settles, so non-holders get a 403 and no funds move.
60+
*
61+
* `operatorAddress` here is the payment recipient: `paidPredicateGate` uses it
62+
* as both the 402 `payTo` and the identity binding (the caller's `X-Payment`
63+
* `to` must match it), so it must equal the manifest pricing's `recipient`.
5564
*/
56-
export function buildHolderGates(
57-
paywall: ReturnType<typeof buildHolderPaywall>,
58-
{
59-
toolId,
60-
rpcUrl,
61-
}: {
62-
/** On-chain ToolRegistry tool ID (from the `ToolRegistered` event). */
63-
toolId: bigint
64-
/** Optional Base RPC override; defaults to viem's public RPC. */
65-
rpcUrl?: string
66-
},
67-
): GateMiddleware[] {
68-
return [predicateGate({ toolId, rpcUrl }), paywall.gate]
65+
export function buildHolderGates({
66+
toolId,
67+
rpcUrl,
68+
recipient,
69+
}: {
70+
/** On-chain ToolRegistry tool ID (from the `ToolRegistered` event). */
71+
toolId: bigint
72+
/** Optional Base RPC override; defaults to viem's public RPC. */
73+
rpcUrl?: string
74+
/** Payment recipient; also the identity binding and 402 `payTo`. */
75+
recipient: `0x${string}`
76+
}): GateMiddleware[] {
77+
return [
78+
paidPredicateGate({
79+
toolId,
80+
rpcUrl,
81+
operatorAddress: recipient,
82+
amountUsdc: HOLDER_AMOUNT_USDC,
83+
facilitator: FACILITATOR,
84+
}),
85+
]
6986
}

examples/token-nft-overlap-tool/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"test": "vitest run"
1818
},
1919
"dependencies": {
20-
"@opensea/tool-sdk": "^0.15.0",
20+
"@opensea/tool-sdk": "^0.16.0",
2121
"zod": "4.3.6"
2222
},
2323
"devDependencies": {

0 commit comments

Comments
 (0)