You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
1. Verifies the agent's EIP-3009 signature normally
598
+
1. Verifies the agent's identity (via `X-Payment` or `Authorization: EIP-3009`) normally
594
599
2. Calls `checkDelegateForAll(agent, holder)` on the [delegate.xyz DelegateRegistry](https://docs.delegate.xyz)
595
600
3. If valid, runs the access predicate against the **holder** (not the agent)
596
601
4. Sets `ctx.callerAddress = holderAddress` and `ctx.agentAddress = agentAddress`
@@ -835,7 +840,7 @@ const data = await res.json()
835
840
836
841
### Predicate-Gated Tools
837
842
838
-
Gate your tool using the onchain access predicate system. The `predicateGate`middleware verifies an EIP-3009 zero-value authorization, recovers the caller's address via `ecrecover`, and delegates the access decision to `IToolRegistry.tryHasAccess` — it works with ERC721OwnerPredicate, ERC1155OwnerPredicate, SubscriptionPredicate, ERC20BalancePredicate, CompositePredicate, or any future predicate automatically.
843
+
Gate your tool using the onchain access predicate system. When `operatorAddress` is configured, `predicateGate`uses a unified 402 challenge flow: it returns `PaymentRequirements` with `maxAmountRequired: "0"`, the caller signs a zero-value `X-Payment`, and the middleware recovers the caller's address via `ecrecover`. Access is then delegated to `IToolRegistry.tryHasAccess` — it works with ERC721OwnerPredicate, ERC1155OwnerPredicate, SubscriptionPredicate, ERC20BalancePredicate, CompositePredicate, or any future predicate automatically.
839
844
840
845
See [docs/predicate-gating-guide.md](docs/predicate-gating-guide.md) for the full setup walkthrough.
Copy file name to clipboardExpand all lines: docs/predicate-gating-guide.md
+35-49Lines changed: 35 additions & 49 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,14 +1,14 @@
1
1
# Predicate-Gated Tools Guide
2
2
3
-
Gate your tool using the onchain access predicate system. Callers prove their identity by signing an EIP-3009 zero-value `TransferWithAuthorization` ([EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)), and the SDK delegates the access decision to the `ToolRegistry` contract — whatever predicate the tool's creator registered is the policy enforced.
3
+
Gate your tool using the onchain access predicate system. When `operatorAddress` is configured, the gate uses a unified 402 challenge flow: it returns `PaymentRequirements` with `maxAmountRequired: "0"`, and the caller signs a zero-value `X-Payment` header (an EIP-3009`TransferWithAuthorization` with `value=0`). The SDK recovers the caller's address and delegates the access decision to the `ToolRegistry` contract — whatever predicate the tool's creator registered is the policy enforced.
4
4
5
5
## Overview
6
6
7
7
The tool-sdk supports two independent gating mechanisms:
8
8
9
9
| Gate | Purpose | How it works |
10
10
|------|---------|--------------|
11
-
|**Predicate gate**| Identity-based access control |Caller signs an EIP-3009 zero-value authorization; the middleware recovers the address via `ecrecover` and staticcalls `IToolRegistry.tryHasAccess(toolId, caller, data)` to check the registered predicate. Supports [delegated agent access](#delegated-agent-access-delegatexyz) via `X-Delegate-For` header. |
11
+
|**Predicate gate**| Identity-based access control |With `operatorAddress` configured, the gate returns 402 with `PaymentRequirements` (`maxAmountRequired: "0"`). The caller signs a zero-value `X-Payment` and retries. The middleware recovers the address via `ecrecover` and staticcalls `IToolRegistry.tryHasAccess(toolId, caller, data)` to check the registered predicate. Supports [delegated agent access](#delegated-agent-access-delegatexyz) via `X-Delegate-For` header. |
12
12
|**x402 gate**| Payment-based access control | Caller includes an `X-Payment` header with a signed USDC transfer authorization; a facilitator verifies and settles the payment |
13
13
14
14
Use predicate gating when access should be tied to **who the caller is**. Use x402 when access should be tied to **per-call payment**. You can [combine both](#combining-predicate-gating-with-x402-payment).
1.Checks for an `X-Payment` header (preferred) or`Authorization: EIP-3009 <token>` header (also accepts deprecated `Authorization: SIWE <token>` for backward compatibility)
87
+
2.If no auth is present and `operatorAddress` is configured, returns 402 with `PaymentRequirements` (`payTo`=operator, `maxAmountRequired`=`"0"`, `scheme`=`"exact"`)
88
+
3.Decodes and validates the authorization (from`X-Payment` or `Authorization` header)
89
89
4. Checks `validBefore` (must be in the future) and `validAfter` (must be in the past)
90
90
5. Recovers the signer via `ecrecover` on the EIP-712 typed data — no RPC call needed
91
91
6. Calls `registry.tryHasAccess(toolId, recoveredAddress, data)` — a staticcall to the onchain `ToolRegistry`
@@ -95,7 +95,9 @@ Status code mapping:
95
95
96
96
| Outcome | Status | Body |
97
97
|---------|--------|------|
98
-
| Missing or malformed authorization |`401`|`{ error, hint }`|
| No auth, no `operatorAddress`|`401`|`{ error, hint }`|
100
+
| Malformed `X-Payment` or `Authorization`|`401`|`{ error }`|
99
101
|`tryHasAccess` returned `(true, true)`| (passes) | n/a |
100
102
|`tryHasAccess` returned `(true, false)`|`403`|`{ error, toolId, predicate }`|
101
103
|`tryHasAccess` returned `(false, *)`|`502`|`{ error: "predicate misbehaved..." }`|
@@ -166,40 +168,23 @@ if (ok && granted) {
166
168
167
169
## Step 4: Client-side authentication
168
170
169
-
Callers authenticate by signing an EIP-3009 zero-value `TransferWithAuthorization` and including it in the `Authorization` header.
171
+
When `operatorAddress` is configured, the gate returns a 402 challenge. The client handles this automatically via `eip3009AuthenticatedFetch`: it sends a bare request, reads the `PaymentRequirements` from the 402, signs a zero-value `X-Payment`, and retries.
170
172
171
-
### Header format
173
+
### Preferred flow (402 + X-Payment)
172
174
173
-
```
174
-
Authorization: EIP-3009 <base64url(json)>
175
-
```
176
-
177
-
The token is a base64url-encoded JSON object containing the EIP-3009 authorization fields:
178
-
179
-
```json
180
-
{
181
-
"from": "0xCallerAddress",
182
-
"to": "0xOperatorOrZeroAddress",
183
-
"value": "0",
184
-
"validAfter": "0",
185
-
"validBefore": "1735689900",
186
-
"nonce": "0xrandom32bytes",
187
-
"signature": "0x..."
188
-
}
189
-
```
175
+
The `X-Payment` header carries a base64-encoded JSON payload containing an EIP-3009 `TransferWithAuthorization` (value=0) signed against the operator address from the 402 challenge.
190
176
191
177
Key constraints enforced by the middleware:
192
178
193
179
-**`validBefore`** must be in the future (the SDK defaults to `now + 5 minutes`)
194
180
-**`validAfter`** must be in the past (typically `0`)
195
181
-**`value`** must be `"0"` (zero-value transfer — used for identity proof, not payment)
196
182
-**`from`** is recovered via `ecrecover` and used as the caller address
197
-
198
-
> **Tip:** The `to` field can be the tool operator's address for domain binding, or `0x0` as a fallback. The SDK's `eip3009AuthenticatedFetch` accepts an optional `to` parameter.
183
+
-**`to`** must match the gate's `operatorAddress` (the 402 challenge advertises this as `payTo`)
199
184
200
185
### Example client code (SDK)
201
186
202
-
The simplest approach is `eip3009AuthenticatedFetch`:
187
+
The simplest approach is `eip3009AuthenticatedFetch`, which handles the 402 challenge automatically:
Run your tool locally and send a request with a valid EIP-3009 authorization header to verify the full flow. Use the client code from Step 4 against your local or deployed endpoint.
250
234
251
-
For a quick smoke test of the gate rejecting unauthenticated requests, `curl` the endpoint without the `Authorization` header:
235
+
For a quick smoke test of the gate rejecting unauthenticated requests, `curl` the endpoint without any auth headers:
252
236
253
237
```bash
254
238
curl -X POST https://my-tool.vercel.app/api \
255
239
-H "Content-Type: application/json" \
256
240
-d '{"query": "test"}'
257
241
```
258
242
259
-
Expected response:
243
+
Expected response (when `operatorAddress` is configured):
HTTP status: `402`. If `operatorAddress` is not configured, the gate returns `401` with `{ error, hint }` (legacy behavior).
260
+
```
261
+
268
262
## Delegated agent access (delegate.xyz)
269
263
270
264
An AI agent can call a predicate-gated tool **on behalf of** an NFT holder without the holder sharing their private key. The holder sets up a delegation at [delegate.xyz](https://delegate.xyz), and the agent presents the holder's address alongside its own EIP-3009 authentication.
Gates run in array order (see `src/lib/handler/index.ts`). Put `predicateGate`**first**:
378
372
379
-
1.**Predicate gate** runs first — verifies the EIP-3009 signature and establishes `ctx.callerAddress`. Returns `401` if the signature is invalid or`403` if the predicate denies access.
380
-
2.**x402 gate** runs second — checks the `X-Payment`header and verifies the payment. Returns `402` if no payment is provided.
373
+
1.**Predicate gate** runs first — returns 402 with zero-value `PaymentRequirements`. The client signs a zero-value `X-Payment` and retries. Once verified, the predicate gate establishes `ctx.callerAddress`. Returns`403` if the predicate denies access.
374
+
2.**x402 gate** runs second — returns 402 with real payment `PaymentRequirements`. The client signs a real `X-Payment` and retries. Returns `402` if no payment is provided.
381
375
382
-
This ordering ensures identity is established before payment is processed.
376
+
The client handles multiple 402 challenges in a retry loop — `paidAuthenticatedFetch` does this automatically.
383
377
384
378
### Client requirements
385
379
386
-
Callers must include **both** headers:
387
-
388
-
```
389
-
Authorization: EIP-3009 <base64url(json)>
390
-
X-Payment: <base64-encoded-payment-payload>
391
-
```
392
-
393
-
The easiest way is `paidAuthenticatedFetch`, which handles both headers automatically:
380
+
`paidAuthenticatedFetch` handles the multi-402 flow automatically: it sends a bare request, then on each 402 reads `PaymentRequirements`, signs an `X-Payment`, and retries. No separate `Authorization` header is needed.
maxAmount: "100000", // safety cap for the x402 payment
403
391
})
404
392
```
405
-
406
-
Alternatively, use the EIP-3009 client code from [Step 4](#step-4-client-side-authentication) for the `Authorization` header and `signX402Payment` or `paidFetch` from `@opensea/tool-sdk` for the `X-Payment` header.
| 200 | Success | Parse the JSON body per the manifest's `outputs` schema |
341
341
| 400 | Invalid input | Fix request body to match the manifest's `inputs` schema |
342
-
| 401 | Missing/invalid auth | Sign an EIP-3009 zero-value authorization and include `Authorization: EIP-3009 <token>`|
343
-
| 402 | Payment required | Read `body.accepts[0]` for payment requirements, sign and retry with `X-Payment`|
342
+
| 401 | Missing/invalid auth (no `operatorAddress`) | Sign an EIP-3009 zero-value authorization and include `Authorization: EIP-3009 <token>` (legacy)|
343
+
| 402 | Payment / identity required | Read `body.accepts[0]` for `PaymentRequirements`. For predicate gates (`maxAmountRequired: "0"`), sign a zero-value `X-Payment` and retry. For x402 paywalls, sign and pay the requested amount.|
344
344
| 403 | Access denied | Inspect `body.predicate` to discover what's needed; acquire the required token/subscription |
345
345
| 405 | Method not allowed | Use POST |
346
346
| 500 | Internal tool error | Retry or contact the tool creator |
0 commit comments