Skip to content

Commit 786c2bb

Browse files
CodySearsOSryanio
andcommitted
Release v0.26.0
Origin-SHA: ba6c08327287ef3e140b9681456d8aac33e1b07f Co-authored-by: Ryan Ghods <ryan@ryanio.com>
1 parent 6a76e1d commit 786c2bb

12 files changed

Lines changed: 279 additions & 884 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pnpm run type-check # TypeScript type checking
3333
| `src/lib/handler/` | `createToolHandler` — Web Request/Response handler factory |
3434
| `src/lib/middleware/` | Gating middleware (predicate gate, x402, x402 facilitators, well-known endpoint) |
3535
| `src/lib/wallet/` | Re-exports from `@opensea/wallet-adapters` (adapters, types, and viem bridge) |
36-
| `src/lib/client/` | Authenticated HTTP clients: EIP-3009 auth, x402 payment, external signer, paid authenticated fetch |
36+
| `src/lib/client/` | Authenticated HTTP clients: EIP-3009 auth, SIWE auth, x402 payment, external signer, paid authenticated fetch |
3737
| `src/lib/usage/` | Usage reporters — report tool invocations / x402 settlements to the OpenSea usage endpoint, plus EIP-3009 zero-value auth helpers |
3838
| `src/lib/adapters/` | Framework adapters (Vercel, Cloudflare, Express) |
3939
| `src/lib/utils.ts` | Shared utilities used across `lib/` |

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# @opensea/tool-sdk
22

3+
## 0.26.0
4+
5+
### Minor Changes
6+
7+
- 1699f42: **BREAKING**: Unify `predicateGate` to use the standard 402 + X-Payment (EIP-3009) auth flow only.
8+
9+
- `PredicateGateConfig.operatorAddress` is now **required** (was optional)
10+
- `Authorization: EIP-3009 <token>` header is no longer accepted: use the X-Payment header via the 402 challenge flow
11+
- `Authorization: SIWE <msg>.<sig>` header is no longer accepted (was already deprecated)
12+
- CLI `dry-run-predicate-gate` now requires `--operator-address` and asserts a 402 response
13+
314
## 0.25.0
415

516
### Minor Changes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/tool-sdk",
3-
"version": "0.25.0",
3+
"version": "0.26.0",
44
"type": "module",
55
"description": "SDK and CLI for building ERC-8257 compliant AI agent tools",
66
"repository": {

skill/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ const account = await createBankrAccount("your-bankr-api-key")
350350
|------|---------|--------|
351351
| 200 | Success | Parse the JSON body per the manifest's `outputs` schema |
352352
| 400 | Invalid input | Fix request body to match the manifest's `inputs` schema |
353-
| 401 | Missing/invalid auth (no `operatorAddress`) | Sign an EIP-3009 zero-value authorization and include `Authorization: EIP-3009 <token>` (legacy) |
353+
| 401 | Invalid or expired X-Payment signature | Re-sign a fresh zero-value EIP-3009 authorization and retry with the `X-Payment` header |
354354
| 402 | Payment / identity required | The challenge is in `body.accepts[0]` (x402 v1) or the `PAYMENT-REQUIRED` response header (v2). For predicate gates (amount `"0"`), sign a zero-value authorization; for x402 paywalls, sign the requested amount. Send it back in `X-PAYMENT` (v1) or `PAYMENT-SIGNATURE` (v2) — `pay`/`paidFetch` choose the right header automatically. |
355355
| 403 | Access denied | Inspect `body.predicate` to discover what's needed; acquire the required token/subscription |
356356
| 405 | Method not allowed | Use the verb the tool expects. `pay` auto-retries as GET when an unspecified-method POST probe returns 404/405; otherwise pass `--method <verb>`. |

skill/references/predicate-gating.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ Agent Tool Server ToolRegistry (onchain
3838
8. If access is denied: return 403 with predicate address for self-diagnosis
3939
9. If predicate misbehaved: return 502
4040

41-
> **Backward compatibility:** The gate still accepts `Authorization: EIP-3009 <base64url(json)>` and deprecated `Authorization: SIWE <token>` headers. However, the 402 + `X-Payment` flow is preferred for new integrations because it mirrors the x402 pattern and lets the client discover the correct `to` address from the challenge.
4241

4342
## Build a predicate-gated tool (server side)
4443

@@ -74,6 +73,7 @@ export const toolHandler = createToolHandler({
7473
gates: [
7574
predicateGate({
7675
toolId: 1n, // your onchain tool ID from registration
76+
operatorAddress: "0xYOUR_OPERATOR_ADDRESS", // receives the zero-value authorization
7777
// chain: base,
7878
// rpcUrl: "https://mainnet.base.org",
7979
}),

src/__tests__/auth.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ describe("auth command", () => {
220220

221221
function stubBankrFetch() {
222222
return vi.fn(async (url: string, init?: RequestInit) => {
223-
if (typeof url === "string" && url.includes("/wallet/info")) {
223+
if (typeof url === "string" && url.includes("/wallet/me")) {
224224
return new Response(JSON.stringify({ address: BANKR_ADDRESS }), {
225225
status: 200,
226226
})
@@ -258,8 +258,8 @@ describe("auth command with --bankr-key", () => {
258258
"{}",
259259
])
260260

261-
// Should have called /wallet/info first
262-
expect(fetchMock.mock.calls[0][0]).toContain("/wallet/info")
261+
// Should have called /wallet/me first
262+
expect(fetchMock.mock.calls[0][0]).toContain("/wallet/me")
263263

264264
// Should print the Bankr address
265265
const output = logSpy.mock.calls.map(c => c.join(" ")).join("\n")
@@ -287,8 +287,8 @@ describe("auth command with --bankr-key", () => {
287287
"{}",
288288
])
289289

290-
// Bankr path: first call should be /wallet/info
291-
expect(fetchMock.mock.calls[0][0]).toContain("/wallet/info")
290+
// Bankr path: first call should be /wallet/me
291+
expect(fetchMock.mock.calls[0][0]).toContain("/wallet/me")
292292

293293
logSpy.mockRestore()
294294
})

src/__tests__/delegation-gate.test.ts

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const HOLDER_ADDRESS =
88
"0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" as `0x${string}`
99
const TEST_PREDICATE =
1010
"0xpredicatepredicatepredicatepredicatepredi" as `0x${string}`
11+
const TEST_OPERATOR =
12+
"0x5ECA0441311643608a8c9Ab8B250f695Dd32E2a8" as `0x${string}`
1113

1214
const mockTryHasAccess = vi.fn(async () => ({ ok: true, granted: true }))
1315
const mockGetToolConfig = vi.fn(async () => ({
@@ -31,24 +33,12 @@ vi.mock("viem", async importOriginal => {
3133
return {
3234
...actual,
3335
createPublicClient: () => ({
34-
verifySiweMessage: vi.fn().mockResolvedValue(true),
3536
readContract: mockReadContract,
3637
}),
38+
recoverTypedDataAddress: () => Promise.resolve(AGENT_ADDRESS),
3739
}
3840
})
3941

40-
vi.mock("viem/siwe", () => ({
41-
parseSiweMessage: vi.fn().mockReturnValue({
42-
address: AGENT_ADDRESS,
43-
domain: "example.com",
44-
uri: "https://example.com",
45-
version: "1",
46-
chainId: 8453,
47-
nonce: "testnonce",
48-
issuedAt: new Date(),
49-
}),
50-
}))
51-
5242
beforeEach(() => {
5343
mockTryHasAccess.mockReset()
5444
mockTryHasAccess.mockResolvedValue({ ok: true, granted: true })
@@ -60,19 +50,33 @@ afterEach(() => {
6050
vi.clearAllMocks()
6151
})
6252

63-
function makeAuthHeader(): string {
64-
const siweB64 = btoa("mock-siwe-message")
65-
.replace(/\+/g, "-")
66-
.replace(/\//g, "_")
67-
.replace(/=/g, "")
68-
return `SIWE ${siweB64}.0xmocksignature`
53+
function makeXPaymentHeader(overrides: Record<string, unknown> = {}): string {
54+
const authorization = {
55+
from: AGENT_ADDRESS,
56+
to: TEST_OPERATOR,
57+
value: "0",
58+
validAfter: "0",
59+
validBefore: String(Math.floor(Date.now() / 1000) + 300),
60+
nonce: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
61+
...overrides,
62+
}
63+
const payload = {
64+
x402Version: 1,
65+
scheme: "exact",
66+
network: "base",
67+
payload: {
68+
signature: "0xabcd",
69+
authorization,
70+
},
71+
}
72+
return Buffer.from(JSON.stringify(payload)).toString("base64")
6973
}
7074

7175
function makeRequest(headers?: Record<string, string>): Request {
7276
return new Request("https://example.com/api", {
7377
method: "POST",
7478
headers: {
75-
Authorization: makeAuthHeader(),
79+
"X-Payment": makeXPaymentHeader(),
7680
...headers,
7781
},
7882
})
@@ -83,7 +87,10 @@ describe("predicateGate with delegate.xyz delegation", () => {
8387
const { predicateGate } = await import(
8488
"../lib/middleware/predicate-gate.js"
8589
)
86-
const gate = predicateGate({ toolId: TEST_TOOL_ID })
90+
const gate = predicateGate({
91+
toolId: TEST_TOOL_ID,
92+
operatorAddress: TEST_OPERATOR,
93+
})
8794
const ctx: Partial<ToolContext> = { gates: {} }
8895

8996
const response = await gate.check(
@@ -118,7 +125,10 @@ describe("predicateGate with delegate.xyz delegation", () => {
118125
const { predicateGate } = await import(
119126
"../lib/middleware/predicate-gate.js"
120127
)
121-
const gate = predicateGate({ toolId: TEST_TOOL_ID })
128+
const gate = predicateGate({
129+
toolId: TEST_TOOL_ID,
130+
operatorAddress: TEST_OPERATOR,
131+
})
122132
const ctx: Partial<ToolContext> = { gates: {} }
123133

124134
const response = await gate.check(
@@ -136,7 +146,10 @@ describe("predicateGate with delegate.xyz delegation", () => {
136146
const { predicateGate } = await import(
137147
"../lib/middleware/predicate-gate.js"
138148
)
139-
const gate = predicateGate({ toolId: TEST_TOOL_ID })
149+
const gate = predicateGate({
150+
toolId: TEST_TOOL_ID,
151+
operatorAddress: TEST_OPERATOR,
152+
})
140153
const ctx: Partial<ToolContext> = { gates: {} }
141154

142155
const response = await gate.check(
@@ -155,7 +168,10 @@ describe("predicateGate with delegate.xyz delegation", () => {
155168
const { predicateGate } = await import(
156169
"../lib/middleware/predicate-gate.js"
157170
)
158-
const gate = predicateGate({ toolId: TEST_TOOL_ID })
171+
const gate = predicateGate({
172+
toolId: TEST_TOOL_ID,
173+
operatorAddress: TEST_OPERATOR,
174+
})
159175
const ctx: Partial<ToolContext> = { gates: {} }
160176

161177
const response = await gate.check(
@@ -174,7 +190,10 @@ describe("predicateGate with delegate.xyz delegation", () => {
174190
const { predicateGate } = await import(
175191
"../lib/middleware/predicate-gate.js"
176192
)
177-
const gate = predicateGate({ toolId: TEST_TOOL_ID })
193+
const gate = predicateGate({
194+
toolId: TEST_TOOL_ID,
195+
operatorAddress: TEST_OPERATOR,
196+
})
178197
const ctx: Partial<ToolContext> = { gates: {} }
179198

180199
const response = await gate.check(
@@ -196,9 +215,11 @@ describe("predicateGate with delegate.xyz delegation", () => {
196215
const { predicateGate } = await import(
197216
"../lib/middleware/predicate-gate.js"
198217
)
199-
const gate = predicateGate({ toolId: TEST_TOOL_ID })
218+
const gate = predicateGate({
219+
toolId: TEST_TOOL_ID,
220+
operatorAddress: TEST_OPERATOR,
221+
})
200222

201-
// Override parseSiweMessage to return AGENT_ADDRESS as caller for this test
202223
mockTryHasAccess.mockResolvedValue({ ok: true, granted: true })
203224

204225
const ctx: Partial<ToolContext> = { gates: {} }
@@ -218,6 +239,7 @@ describe("predicateGate with delegate.xyz delegation", () => {
218239
)
219240
const gate = predicateGate({
220241
toolId: TEST_TOOL_ID,
242+
operatorAddress: TEST_OPERATOR,
221243
delegateRegistryAddress: customAddress,
222244
})
223245
const ctx: Partial<ToolContext> = { gates: {} }

src/__tests__/external-signer.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ describe("external signer + authenticatedFetch round-trip", () => {
218218
const BANKR_ADDRESS = "0x8b8e1C20E0630De8C60f0e0D5C3e9C7c20F0c20e"
219219

220220
describe("createBankrAccount", () => {
221-
it("fetches address from /wallet/info and creates a signing account", async () => {
221+
it("fetches address from /wallet/me and creates a signing account", async () => {
222222
const fetchMock = vi.fn(async (url: string, _init?: RequestInit) => {
223-
if (url.includes("/wallet/info")) {
223+
if (url.includes("/wallet/me")) {
224224
return new Response(JSON.stringify({ address: BANKR_ADDRESS }), {
225225
status: 200,
226226
})
@@ -241,9 +241,9 @@ describe("createBankrAccount", () => {
241241
const sig = await account.signMessage!({ message: "hello" })
242242
expect(sig).toBe("0xdeadbeef")
243243

244-
// Verify /wallet/info was called with API key header
244+
// Verify /wallet/me was called with API key header
245245
const infoCall = fetchMock.mock.calls[0]
246-
expect(infoCall[0]).toContain("/wallet/info")
246+
expect(infoCall[0]).toContain("/wallet/me")
247247
expect(infoCall[1]?.headers).toEqual(
248248
expect.objectContaining({ "X-API-Key": "test-api-key" }),
249249
)
@@ -256,22 +256,22 @@ describe("createBankrAccount", () => {
256256
expect(signBody.message).toBe("hello")
257257
})
258258

259-
it("throws when /wallet/info returns an error", async () => {
259+
it("throws when /wallet/me returns an error", async () => {
260260
vi.stubGlobal(
261261
"fetch",
262262
vi.fn(async () => new Response("Unauthorized", { status: 401 })),
263263
)
264264

265265
await expect(createBankrAccount("bad-key")).rejects.toThrow(
266-
"Bankr /wallet/info failed (401)",
266+
"Bankr /wallet/me failed (401)",
267267
)
268268
})
269269

270270
it("throws when /wallet/sign returns an error", async () => {
271271
vi.stubGlobal(
272272
"fetch",
273273
vi.fn(async (url: string) => {
274-
if (typeof url === "string" && url.includes("/wallet/info")) {
274+
if (typeof url === "string" && url.includes("/wallet/me")) {
275275
return new Response(JSON.stringify({ address: BANKR_ADDRESS }), {
276276
status: 200,
277277
})

0 commit comments

Comments
 (0)