ERC-82578257
Try the registry

Live on Ethereum + Base.
No signup, no key.§

Reference deployment of the v0.2 contracts at the same address on both chains. Read it from any RPC. The SDK and CLI handle the rest.

Registry · v0.2
ToolRegistry
Same address on Ethereum and Base.
0x265BB2DBFC0A8165C9A1941Eb1372F349baD2cf1
cast
# read total tools cast call 0x265BB2DBFC0A8165C9A1941Eb1372F349baD2cf1 \ "toolCount()(uint256)" \ --rpc-url https://mainnet.base.org # read tool #1 cast call 0x265BB2DBFC0A8165C9A1941Eb1372F349baD2cf1 \ "getToolConfig(uint256)((address,string,bytes32,address))" 1 \ --rpc-url https://mainnet.base.org
§01 · Walkthrough

One tool,
end to end.§

An NFT appraisal tool gated by holding a Chonk on Base. Three moves: register, gate, invoke.

01 · Publisher
Register with an NFT-gate predicate
Manifest is served at the well-known path; the SDK computes the JCS hash and submits the registration tx.
bash
npx @opensea/tool-sdk register \ --metadata https://nft-appraisal-tool.vercel.app/.well-known/ai-tool/nft-appraiser.json \ --network base \ --nft-gate 0x8...Chonks # → ToolRegistered(toolId: 42, accessPredicate: ERC721OwnerPredicate)
02 · Agent
Discover, verify, check access
Read the registry entry, validate the manifest (hash, origin, creator), then a single staticcall to the predicate. Fail-closed.
ts
const cfg = await registry.getToolConfig(42n); const manifest = await validateManifest(cfg); // hash + origin + creator const { granted } = await checkToolAccess({ toolId: 42n, account, chain: base }); // granted: false → ask the predicate what it wants const { requirements } = await predicate.getRequirements(42n); // requirements[0].kind === IERC721Holding(0x8...Chonks)
03 · Agent
Acquire, then invoke
Decode the requirement kind, satisfy it (buy, mint, subscribe, prove), re-check, then hit the endpoint with a SIWE header.
ts
// satisfy requirements[0] via your marketplace / mint / subscription of choice const retry = await checkToolAccess({ toolId: 42n, account, chain: base }); // { granted: true } const res = await authenticatedFetch(`${manifest.endpoint}/api/tool`, { body: JSON.stringify({ chain: 'base', contractAddress: '0x...Chonks', tokenId: '1234' }), signer: agentWallet, });
§02 · Verified access

From wallet to response,
in one command.§

The SDK collapses the agent side of the flow into a single command: resolve the tool, check the predicate, satisfy whatever it requires (mint, subscribe, x402 pay), and invoke. The tool itself independently re-checks access on every request — the SDK just handles your side of the dance.

pay
Stateless calls
Resolve, gate, satisfy, invoke. The tool sees the call land with its access conditions already met — it doesn’t need to know which wallet specifically made it.
bash
npx @opensea/tool-sdk pay \ --tool-id 42 \ --network base \ --body '{"chain":"base","contractAddress":"0x...Chonks","tokenId":"1234"}' # → predicate.hasAccess: false # → requirement: IERC721Holding(0x8...Chonks) # → satisfied via marketplace mint, re-checked, granted # → POST /api/tool → 200 { value: 1.2 ETH, ... }
pay ——auth siwe
Bound to the caller
Same flow with a Sign-In-With-Ethereum signature attached to the request. The tool verifies both the predicate and the signed message before it serves a response.
bash
npx @opensea/tool-sdk pay \ --tool-id 42 \ --network base \ --auth siwe \ --body '{ ... }' # → predicate satisfied + SIWE message signed # → POST /api/tool with Authorization: SIWE <sig> # → tool re-verifies signature + onchain access → 200 OK
Trust note
The SDK is a convenience layer. The tool itself is the source of truth for access — every endpoint independently re-checks the predicate (and, when --auth siwe is used, the SIWE signature) before serving a response. The SDK doesn’t grant access; it satisfies whatever the predicate already requires.
§03 · Composability

Anyone can write a predicate.§

The predicate is just an address. This is the same pattern as Seaport zones and Uniswap v4 hooks: a pluggable contract the protocol staticcalls into without caring about the implementation. The registry never changes; the policy space is open-ended.

ERC721OwnerPredicate.sol
// gate by holding any NFT in a collection contract NFTGate is IAccessPredicate { mapping(uint256 => address) public collectionFor; function setCollection(uint256 id, address c) external { require(REGISTRY.getToolConfig(id).creator == msg.sender); collectionFor[id] = c; } function hasAccess(uint256 id, address a, bytes calldata) external view returns (bool) { return IERC721(collectionFor[id]).balanceOf(a) > 0; } function getRequirements(uint256 id) external view returns (AccessRequirement[] memory r, RequirementLogic l) { r = new AccessRequirement[](1); r[0] = AccessRequirement({ kind: type(IERC721Holding).interfaceId, data: abi.encode(collectionFor[id]), label: "" }); l = RequirementLogic.OR; } }
The pattern
Deploy a contract implementing IAccessPredicate. Point any tool’s accessPredicate at its address. Done. No allowlist, no governance, no version bump.
Three required functions
  • hasAccess — yes / no
  • getRequirements — what would unlock it
  • name — diagnostic identity
Already deployed on Ethereum + Base
ERC721OwnerPredicate v0.2
0xc8721c9A776958FfFfEb602DA1b708bf1D318379
ERC1155OwnerPredicate v0.2
0x77373Dc3c1AE9A1e937eF3e5E08F4807D47c7c11
NFT gate
balanceOf(collection) > 0
Subscription
ERC-5643 time-bound tier
Composite
AND/OR over leaf predicates
ZK proof
verifyProof(witness)
Allowlist
merkleProof(leaf, root)
Stake-weighted
staked(token) >= threshold
DAO vote
proposal.passed(toolId)
Your idea
implement IAccessPredicate
§
ERC-8257 · Draft · CC0-1.0tool-registry ↗tool-sdk ↗community ↗