# tool-sdk **Repository Path**: ProjectOpenSea/tool-sdk ## Basic Information - **Project Name**: tool-sdk - **Description**: SDK to help creators create tools for the tool registry (ERC-XXXX) - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-03 - **Last Updated**: 2026-05-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # @opensea/tool-sdk SDK and CLI for building [ERC-8257](https://github.com/ethereum/ERCs/pull/1723) compliant AI agent tools. Provides manifest validation, onchain registration, gating middleware, framework adapters, and project scaffolding. Pairs with the onchain reference implementation at [ProjectOpenSea/tool-registry](https://github.com/ProjectOpenSea/tool-registry) — the `ToolRegistry` contract and example access predicates this SDK reads from and writes to. ## Quick Start ```bash # 1. Scaffold a new tool project npx @opensea/tool-sdk init my-tool # 2. Implement your tool logic cd my-tool && npm install # Edit src/handler.ts # NOTE: If your project sits adjacent to a pnpm workspace, use # pnpm install --ignore-workspace to prevent pnpm from walking # up to the parent workspace. # 3. Deploy npx vercel # or wrangler deploy, etc. # 4. Register onchain npx @opensea/tool-sdk register \ --metadata https://my-tool.vercel.app/.well-known/ai-tool/my-tool.json \ --network base ``` ## CLI Reference ### `init [name]` Scaffold a new ERC-8257 tool project with interactive prompts. ```bash npx @opensea/tool-sdk init my-tool npx @opensea/tool-sdk init my-tool --no-interactive # CI mode ``` Supports Vercel, Cloudflare Workers, and Express templates. ### `validate [path]` Validate a tool manifest JSON file against the ERC-8257 schema. ```bash npx @opensea/tool-sdk validate ./manifest.json ``` ### `hash [path]` Compute the JCS keccak256 hash of a tool manifest (RFC 8785 canonicalization). ```bash npx @opensea/tool-sdk hash ./manifest.json ``` ### `export [path]` Load a TypeScript manifest and output it as JSON. Validates the manifest before printing. ```bash npx @opensea/tool-sdk export ./src/manifest.ts ``` ### `verify ` Verify a deployed well-known tool endpoint. Checks URL format, HTTP 200, schema validation, and origin binding. ```bash npx @opensea/tool-sdk verify https://my-tool.vercel.app/.well-known/ai-tool/my-tool.json ``` ### `register` Register a tool onchain via the ToolRegistry contract. ```bash PRIVATE_KEY=0x... RPC_URL=https://... npx @opensea/tool-sdk register \ --metadata \ --network base \ --nft-gate 0xCOLLECTION # optional: gate via ERC721OwnerPredicate ``` | Flag | Description | |------|-------------| | `--metadata ` | Metadata URI (required) | | `--network ` | `base` or `mainnet` (default: `base`) | | `--nft-gate
` | ERC-721 collection address; gates the tool via the canonical ERC721OwnerPredicate (version auto-detected from registry) | | `--access-predicate
` | Access predicate address (mutually exclusive with `--nft-gate`) | | `--predicate-config ` | JSON config for the access predicate (e.g. `'{"collections":["0x..."]}'`). Bundles predicate setup with registration | | `--wallet-provider ` | Wallet provider to use for signing | | `--rpc-url ` | RPC endpoint for gas estimation and tx broadcast | | `--dry-run` | Print summary without transacting | | `-y, --yes` | Skip confirmation prompt | ### `update-metadata` Update a tool's metadata URI and manifest hash onchain. ```bash npx @opensea/tool-sdk update-metadata \ --tool-id 1 \ --metadata https://my-tool.vercel.app/.well-known/ai-tool/my-tool.json \ --network base ``` | Flag | Description | |------|-------------| | `--tool-id ` | Numeric tool ID (required) | | `--metadata ` | New metadata URI (required) | | `--network ` | `base` or `mainnet` (default: `base`) | | `--wallet-provider ` | Wallet provider to use for signing | | `--rpc-url ` | RPC endpoint for gas estimation and tx broadcast | | `--dry-run` | Print summary without transacting | | `-y, --yes` | Skip confirmation prompt | ### `inspect` Read onchain tool state and cross-check against the live manifest. ```bash npx @opensea/tool-sdk inspect --tool-id 1 --network base npx @opensea/tool-sdk inspect --tool-id 1 --check-access 0xYourAddress ``` | Flag | Description | |------|-------------| | `--tool-id ` | Numeric tool ID (required) | | `--network ` | `base` or `mainnet` (default: `base`) | | `--check-access
` | Check whether an address has access to the tool | ### `deploy` Deploy a tool-sdk project to a hosting platform. ```bash npx @opensea/tool-sdk deploy --host vercel npx @opensea/tool-sdk deploy --host vercel --non-interactive -y ``` | Flag | Description | |------|-------------| | `--host ` | Hosting platform (required; currently `vercel`) | | `--non-interactive` | Read env var values from environment (for CI) | | `-y, --yes` | Auto-confirm prompts (e.g., Vercel link) | ### `pay ` Make a paid call to a tool endpoint via x402. Probes the endpoint for payment requirements, signs an EIP-3009 `transferWithAuthorization`, and replays the request with the `X-Payment` header. Optionally includes SIWE authentication for predicate-gated endpoints. ```bash npx @opensea/tool-sdk pay https://my-tool.vercel.app/api/tool \ --body '{"query":"hello"}' # Combined payment + SIWE auth (for predicate-gated paid tools): PRIVATE_KEY=0x... RPC_URL=https://mainnet.base.org \ npx @opensea/tool-sdk pay https://my-tool.vercel.app/api/tool \ --auth siwe --body '{"query":"hello"}' ``` | Flag | Description | |------|-------------| | `--body ` | JSON body (inline string or `@path/to/file.json`) | | `--auth ` | Authentication type (`siwe`). Auto-enabled when manifest declares an access block | | `--manifest ` | Path to tool manifest (JSON or TS). If it declares an access block, SIWE auth is auto-enabled | | `--chain ` | Chain for SIWE message (default: `base`) | | `--wallet-provider ` | Wallet provider to use for signing | ### `auth ` Make an authenticated call to a predicate-gated tool endpoint via SIWE. ```bash PRIVATE_KEY=0x... RPC_URL=https://mainnet.base.org npx @opensea/tool-sdk auth https://my-tool.vercel.app/api/tool \ --body '{"query":"hello"}' ``` | Flag | Description | |------|-------------| | `--body ` | JSON body (inline string or `@path/to/file.json`) | | `--wallet-provider ` | Wallet provider to use for signing | ### `dry-run-gate` Invoke a tool handler locally with no `X-Payment` header and assert a valid 402 response (x402 gate test). ```bash npx @opensea/tool-sdk dry-run-gate \ --manifest ./src/manifest.ts \ --input '{"query":"test"}' ``` | Flag | Description | |------|-------------| | `--manifest ` | Path to manifest `.ts` or `.json` file (required) | | `--input ` | JSON input body (inline or `@path`) | ### `dry-run-predicate-gate` Invoke a tool handler locally with no SIWE auth header and assert a valid 401 response (predicate gate test). ```bash npx @opensea/tool-sdk dry-run-predicate-gate \ --manifest ./src/manifest.ts \ --tool-id 1 ``` | Flag | Description | |------|-------------| | `--manifest ` | Path to manifest `.ts` or `.json` file (required) | | `--tool-id ` | Onchain tool ID to configure in the gate | | `--input ` | JSON input body (inline or `@path`) | ### `smoke` Smoke-test a live tool endpoint: SIWE-sign, send an authenticated request, and assert the HTTP status. Classifies 402 as "auth passed, payment required" for paywalled tools. ```bash PRIVATE_KEY=0x... RPC_URL=https://mainnet.base.org \ npx @opensea/tool-sdk smoke \ --endpoint https://my-tool.vercel.app/api/tool \ --tool-id 4 \ --input '{"query":"hello"}' ``` | Flag | Description | |------|-------------| | `--endpoint ` | Production endpoint URL (required) | | `--tool-id ` | Onchain tool ID (included in log output) | | `--input ` | JSON body (inline or `@path`; default: `{}`) | | `--expect ` | Expected HTTP status code | | `--chain ` | Chain for wallet client and SIWE message (default: `base`) | | `--paid` | Handle x402 payment challenge after SIWE authentication | | `--wallet-provider ` | Wallet provider to use for signing | | `--max-amount ` | Maximum payment amount in base units (default: `1000000` = 1 USDC) | ### `set-collections ` Set the ERC-721 collection gate list for an already-registered tool. ```bash PRIVATE_KEY=0x... npx @opensea/tool-sdk set-collections 4 \ 0x07152bfde079b5319e5308c43fb1dbc9c76cb4f9 \ --network base ``` | Flag | Description | |------|-------------| | `--network ` | `base` or `mainnet` (default: `base`) | | `--wallet-provider ` | Wallet provider to use for signing | | `--rpc-url ` | RPC endpoint | | `--dry-run` | Print encoded calldata without transacting | ### `get-collections ` Read the ERC-721 collection gate list for a registered tool (read-only). ```bash npx @opensea/tool-sdk get-collections 4 --network base ``` | Flag | Description | |------|-------------| | `--network ` | `base` or `mainnet` (default: `base`) | | `--rpc-url ` | RPC endpoint | ### `set-collection-tokens
` Set the ERC-1155 collection + token ID gate for an already-registered tool. ```bash PRIVATE_KEY=0x... npx @opensea/tool-sdk set-collection-tokens 4 \ 0xCOLLECTION_ADDRESS 1 2 3 \ --network base ``` | Flag | Description | |------|-------------| | `--network ` | `base` or `mainnet` (default: `base`) | | `--wallet-provider ` | Wallet provider to use for signing | | `--rpc-url ` | RPC endpoint | | `--dry-run` | Print encoded calldata without transacting | ## Wallet Configuration All commands that sign transactions (`register`, `update-metadata`, `pay`, `auth`, `smoke`, `set-collections`, `set-collection-tokens`) need a wallet. You can configure one in two ways: 1. **Environment variables** — set the env vars for your provider and the CLI auto-detects it (priority: Privy > Fireblocks > Turnkey > Bankr > PrivateKey). 2. **`--wallet-provider` flag** — explicitly select a provider by name. | Provider | `--wallet-provider` value | Required env vars | |----------|--------------------------|-------------------| | Privy | `privy` | `PRIVY_APP_ID`, `PRIVY_APP_SECRET`, `PRIVY_WALLET_ID` | | Fireblocks | `fireblocks` | `FIREBLOCKS_API_KEY`, `FIREBLOCKS_API_SECRET`, `FIREBLOCKS_VAULT_ID` | | Turnkey | `turnkey` | `TURNKEY_API_PUBLIC_KEY`, `TURNKEY_API_PRIVATE_KEY`, `TURNKEY_ORGANIZATION_ID`, `TURNKEY_WALLET_ADDRESS`, `TURNKEY_RPC_URL` | | Bankr | `bankr` | `BANKR_API_KEY` | | Private Key | `private-key` | `PRIVATE_KEY`, `RPC_URL` | See [`.env.example`](.env.example) for a full annotated template. ### Examples ```bash # Auto-detect from env vars (simplest) PRIVATE_KEY=0x... RPC_URL=https://mainnet.base.org npx @opensea/tool-sdk register \ --metadata --network base # Explicit provider selection BANKR_API_KEY=... npx @opensea/tool-sdk register \ --metadata --network base --wallet-provider bankr # Privy server wallet PRIVY_APP_ID=... PRIVY_APP_SECRET=... PRIVY_WALLET_ID=... npx @opensea/tool-sdk auth \ https://my-tool.vercel.app/api/tool --body '{"query":"hello"}' ``` ## Library API ### `defineManifest(manifest)` Type-narrowing identity function for manifest definitions. ```typescript import { defineManifest } from "@opensea/tool-sdk" export const manifest = defineManifest({ type: "https://ercs.ethereum.org/ERCS/erc-8257#tool-manifest-v1", name: "my-tool", description: "A useful tool", endpoint: "https://my-tool.vercel.app", inputs: { type: "object", properties: { query: { type: "string" } }, required: ["query"], }, outputs: { type: "object", properties: { result: { type: "string" } }, }, creatorAddress: "0x1234567890abcdef1234567890abcdef12345678", }) ``` ### `validateManifest(data)` Validates unknown data against the ERC-8257 manifest schema. ```typescript import { validateManifest } from "@opensea/tool-sdk" const result = validateManifest(jsonData) if (result.success) { console.log(result.data.name) } else { console.error(result.error.issues) } ``` ### `createToolHandler(config)` Creates a Web Request/Response handler for your tool. ```typescript import { z } from "zod/v4" import { createToolHandler } from "@opensea/tool-sdk" import { manifest } from "./manifest.js" const handler = createToolHandler({ manifest, inputSchema: z.object({ query: z.string() }), outputSchema: z.object({ result: z.string() }), gates: [], // optional: predicateGate, x402Gate handler: async (input, ctx) => { return { result: `Hello: ${input.query}` } }, }) ``` ### `createWellKnownHandler(manifest)` Creates a handler for the `/.well-known/ai-tool/.json` endpoint. ```typescript import { createWellKnownHandler } from "@opensea/tool-sdk" const wellKnown = createWellKnownHandler(manifest) // Responds at /.well-known/ai-tool/.json ``` ### `computeManifestHash(manifest)` Computes the JCS keccak256 hash of a manifest (RFC 8785 canonicalization + keccak256). ```typescript import { computeManifestHash } from "@opensea/tool-sdk" const hash = computeManifestHash(manifest) // => "0x85f160012d9fd30c7e82bc9d3959c90ec9df3c7d..." ``` ### `ToolRegistryClient` Client for interacting with the onchain ToolRegistry contract. ```typescript import { ToolRegistryClient } from "@opensea/tool-sdk" import { base } from "viem/chains" const client = new ToolRegistryClient({ chain: base, walletClient, // viem WalletClient with account }) const { toolId, txHash } = await client.registerTool({ metadataURI: "https://example.com/.well-known/ai-tool/my-tool.json", manifest, }) ``` ## Gating ### Predicate Gate (recommended) Delegates the access decision to the onchain `ToolRegistry`. The middleware verifies SIWE auth, recovers the caller's address, and staticcalls `IToolRegistry.tryHasAccess(toolId, caller, data)`. Whatever predicate the tool's creator registered (single-collection ERC-721, multi-collection, ERC-1155, subscription, composite, anything future) is the policy enforced. ```typescript import { predicateGate } from "@opensea/tool-sdk" const gate = predicateGate({ toolId: 42n, // from the ToolRegistered event rpcUrl: "https://mainnet.base.org", // optional }) const handler = createToolHandler({ manifest, inputSchema, outputSchema, gates: [gate], handler: async (input, ctx) => { // ctx.callerAddress is set on success // ctx.gates.predicate.granted === true return { result: "access granted" } }, }) ``` Status code mapping: | Outcome | Status | Body | | --- | --- | --- | | Missing or malformed SIWE | `401` | `{ error, hint }` | | `tryHasAccess` returned `(true, true)` | (passes) | n/a | | `tryHasAccess` returned `(true, false)` | `403` | `{ error, toolId, predicate }` | | `tryHasAccess` returned `(false, *)` | `502` | `{ error: "Predicate misbehaved..." }` | The `predicate` field in the 403 body is the registered access predicate's address, fetched lazily from `getToolConfig` on first denial and cached in-process. Callers can read the predicate's onchain config to learn what they need to satisfy. Authorization header format: `SIWE .` > **Note:** Stateless SIWE: does not track nonces. Callers should include a > short-lived `expirationTime` in their SIWE messages to limit replay window. > Tool operators requiring stronger replay protection should implement > server-side nonce tracking. #### Delegated agent access (delegate.xyz) An AI agent can call a predicate-gated tool **on behalf of** an NFT holder without the holder's private key. The holder delegates to the agent at [delegate.xyz](https://delegate.xyz) (onchain, one TX, revocable anytime), and the agent includes the holder's address in the request: ```typescript import { authenticatedFetch } from "@opensea/tool-sdk" const response = await authenticatedFetch(toolUrl, { method: "POST", headers: { "X-Delegate-For": holderAddress, // holder who delegated }, account: agentAccount, body: JSON.stringify({ query: "hello" }), }) ``` When `X-Delegate-For` is present, the middleware: 1. Verifies the agent's SIWE signature normally 2. Calls `checkDelegateForAll(agent, holder)` on the [delegate.xyz DelegateRegistry](https://docs.delegate.xyz) 3. If valid, runs the access predicate against the **holder** (not the agent) 4. Sets `ctx.callerAddress = holderAddress` and `ctx.agentAddress = agentAddress` | Outcome | Status | Body | | --- | --- | --- | | Invalid `X-Delegate-For` format | `400` | `{ error }` | | Delegation not found onchain | `403` | `{ error, hint }` | | Delegate registry call failed | `502` | `{ error }` | See [docs/predicate-gating-guide.md](docs/predicate-gating-guide.md) for the full delegation walkthrough. ### Client-side access preview Off-chain helper for clients that want to gate UI before invocation. Same staticcall as `predicateGate`, no SIWE required. ```typescript import { checkToolAccess } from "@opensea/tool-sdk" const { ok, granted } = await checkToolAccess({ toolId: 42n, account: "0xabc...", rpcUrl: "https://mainnet.base.org", // optional }) if (ok && granted) { // enable "Use Tool" affordance } ``` `ok === false` means the predicate misbehaved upstream and the result is indeterminate; treat it as a transient failure, not a denial. ### x402 Gate (hosted facilitator) The SDK ships two hosted-facilitator gates with the same shape: `payaiX402Gate` (PayAI hosted facilitator — free, no auth required) and `cdpX402Gate` (Coinbase Developer Platform facilitator — requires a CDP API key and JWT auth). Pick one based on the trade-offs: | Gate | Facilitator | Auth | Best for | | --- | --- | --- | --- | | `payaiX402Gate` | PayAI (`https://facilitator.payai.network`) | None | Prototyping, dogfooding, anything you want to deploy today | | `cdpX402Gate` | Coinbase Developer Platform (`https://api.cdp.coinbase.com/platform/v2/x402`) | CDP JWT (you supply via `createAuthHeaders`) | Production, when you have CDP credentials | Both emit an x402-protocol-compliant 402 response with `accepts: [PaymentRequirements]` when `X-Payment` is missing, and verify the payload against the facilitator's `/verify` endpoint when present. The manifest-side helper `x402UsdcPricing` is shared — the advertised price is identical regardless of which facilitator enforces it. **Trade-offs:** - **PayAI** is community-operated. It is free and requires no credentials, which is exactly the right fit for a first deploy. It comes with no uptime SLA and its operational maturity is whatever the community has built. For real money flowing at volume, evaluate CDP. - **CDP** is operated by Coinbase. It requires JWT auth signed with your `CDP_API_KEY_SECRET`. The SDK does not bundle a JWT signer; pass a `createAuthHeaders` callback that mints headers per request. A built-in helper that wraps `@coinbase/cdp-sdk` is a planned follow-up. #### PayAI (recommended for first deploys) ```typescript import { createToolHandler, defineManifest, payaiX402Gate, x402UsdcPricing, } from "@opensea/tool-sdk" const gate = payaiX402Gate({ recipient: "0xYourPayoutAddress", amountUsdc: "0.01", // decimal string; "10000" (base units) also accepted }) export const manifest = defineManifest({ // ... pricing: x402UsdcPricing({ recipient: "0xYourPayoutAddress", amountUsdc: "0.01", }), }) const handler = createToolHandler({ manifest, inputSchema, outputSchema, gates: [gate], handler: async (input, ctx) => { // ctx.gates.x402.paid === true return { /* ... */ } }, }) ``` #### CDP (production) ```typescript import { cdpX402Gate, x402UsdcPricing } from "@opensea/tool-sdk" import { generateCdpJwt } from "./your-cdp-auth.js" // your code, today const gate = cdpX402Gate({ recipient: "0xYourPayoutAddress", amountUsdc: "0.01", createAuthHeaders: async () => ({ Authorization: `Bearer ${await generateCdpJwt({ apiKeyId: process.env.CDP_API_KEY_ID!, apiKeySecret: process.env.CDP_API_KEY_SECRET!, method: "POST", path: "/platform/v2/x402/verify", })}`, }), }) ``` If you omit `createAuthHeaders` on `cdpX402Gate`, every verify call returns 401/403 from CDP and the gate surfaces 502. PayAI is the unauthenticated fallback for development. **Common defaults:** USDC on Base mainnet, `maxTimeoutSeconds: 60`, description `"Tool invocation"`. `network: "base-sepolia"` is supported for testing. Override any default via the config; `facilitatorUrl` is also overridable if you want to pin to a specific facilitator instance. **Settlement.** Both gates settle on chain automatically: the gate verifies the payment before your handler runs, then calls the facilitator's `/settle` endpoint after your handler succeeds and the output validates. USDC moves from payer to `recipient` once `/settle` confirms. The settled tx hash is stashed on `ctx.gates.x402.settlementTxHash` for downstream observability. **Latency.** Settlement runs synchronously: the SDK awaits `/settle` before returning the response, so a slow or unreachable facilitator adds up to 10 seconds (the per-call timeout) to the worst-case response time. Truly non-blocking settlement requires runtime-specific primitives (Cloudflare Workers and Vercel `waitUntil`) that are not portable across the runtimes this SDK supports, and fire-and-forget risks dropped settlements when a serverless process is killed after the response is sent. Blocking is the safest cross-runtime default; if you need lower-latency settlement, plumb the runtime's `waitUntil` into your handler and wrap the gate yourself. **Failure handling.** If `/settle` fails (network blip, facilitator outage, nonce already used), the failure is logged via `console.error` with prefix `[tool-sdk] gate.settle failed:` and the response still returns 200 with the handler's output. Operators replay failed settlements out-of-band using the verified payment payload from logs. ### x402 Gate (advanced: custom facilitator) The lower-level `x402Gate` accepts a `verifyPayment` callback for callers who want to run their own facilitator or verify payments without an HTTP round-trip. ```typescript import { x402Gate } from "@opensea/tool-sdk" const gate = x402Gate({ pricing: [ { amount: "20000", asset: "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", recipient: "eip155:8453:0xYourAddress", protocol: "x402", }, ], verifyPayment: async (proof) => { return validateX402ProofYourself(proof) }, }) ``` If `verifyPayment` is omitted, the gate rejects every request with an `X-Payment` header with a 501 error. Use `payaiX402Gate` (or `cdpX402Gate`) if you do not have a reason to run your own facilitator. ### Client-side x402 Two helpers for **callers** of x402-gated tools — sign EIP-3009 `TransferWithAuthorization` payments and replay requests automatically. #### `signX402Payment` Signs a USDC payment authorization and returns a base64-encoded `X-Payment` header value. Requires a viem `Account` with `signTypedData` support (e.g. `privateKeyToAccount`). ```typescript import { signX402Payment } from "@opensea/tool-sdk" import { privateKeyToAccount } from "viem/accounts" const account = privateKeyToAccount("0x...") const xPayment = await signX402Payment({ account, paymentRequirements: { scheme: "exact", network: "base", maxAmountRequired: "10000", payTo: "0xRecipient", asset: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", }, }) const res = await fetch(toolUrl, { method: "POST", headers: { "Content-Type": "application/json", "X-Payment": xPayment }, body: JSON.stringify(payload), }) ``` #### `paidFetch` Drop-in fetch wrapper that handles the 402 → sign → replay flow automatically. If the server does not return 402, the response is passed through unchanged. **Security:** `paidFetch` trusts the server's 402 response to determine the payment recipient, token, and amount. Use `maxAmount`, `allowedRecipients`, and `allowedAssets` to constrain what gets signed. By default, `asset` is validated against the known USDC contract address for the network, and `payTo` is rejected if it is the zero address or a known burn address. ```typescript import { paidFetch } from "@opensea/tool-sdk" import { privateKeyToAccount } from "viem/accounts" const account = privateKeyToAccount("0x...") const res = await paidFetch("https://tool.example.com/api", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: "what is this NFT worth?" }), account, // Optional safety caps: maxAmount: "100000", // reject if server asks for more than 0.10 USDC allowedRecipients: ["0xYourTrustedPayee"], // reject unknown payTo addresses // allowedAssets defaults to the known USDC contract per network }) const data = await res.json() ``` ### Predicate-Gated Tools Gate your tool using the onchain access predicate system. The `predicateGate` middleware verifies SIWE auth, recovers the caller's address, and delegates the access decision to `IToolRegistry.tryHasAccess` — it works with ERC721OwnerPredicate, ERC1155OwnerPredicate, SubscriptionPredicate, CompositePredicate, or any future predicate automatically. See [docs/predicate-gating-guide.md](docs/predicate-gating-guide.md) for the full setup walkthrough. ## Tips ### `ai@4` + `zod@4` type mismatch `ai@4` (Vercel AI SDK) ships its own `jsonSchema()` helper that expects a JSON Schema object, **not** a Zod schema. If you pass a `zod@4` schema to `generateObject`'s `schema` parameter it will typecheck but the return type is `unknown` because `ai@4` does not recognise Zod 4's schema brand. The working pattern is to define a hand-written JSON Schema for `ai`, then validate the result at runtime with Zod: ```typescript import { generateObject } from "ai" import { jsonSchema } from "ai/json-schema" import { z } from "zod/v4" // 1. Hand-written JSON Schema for the AI SDK const myJsonSchema = jsonSchema({ type: "object", properties: { name: { type: "string" }, score: { type: "number" }, }, required: ["name", "score"], }) // 2. Matching Zod schema for runtime validation const MySchema = z.object({ name: z.string(), score: z.number(), }) const { object } = await generateObject({ model, schema: myJsonSchema, prompt: "...", }) // 3. Validate at runtime — `object` is typed as `unknown` from ai@4 const parsed = MySchema.parse(object) // `parsed` is now fully typed as { name: string; score: number } ``` ## Framework Adapters ### Vercel ```typescript import { toVercelHandler } from "@opensea/tool-sdk" export default toVercelHandler(handler) ``` ### Cloudflare Workers ```typescript import { toCloudflareHandler } from "@opensea/tool-sdk/cloudflare" export default toCloudflareHandler(handler) ``` ### Express ```typescript import { toExpressHandler } from "@opensea/tool-sdk" app.post("/api", toExpressHandler(handler)) ``` ## ERC Spec See the full [ERC-8257 Tool Registry specification](https://github.com/ethereum/ERCs/pull/1723) for details on manifest schema, origin binding, creator binding, and consumer verification.