agent_gid → wallet → controller, stamped with the method that proved it, a deterministic confidence, and the block range in which it was valid. Every payment, deposit, delegation and incident in the graph hangs off a binding. Today the public rank uses two live methods, the registration's holder (ownerOf) and a wallet the registration declares, each named on the record; a signed challenge from the Desk will lift a binding to the top rank.
Why an index without the bind is not a ledger
The sector has four public facts about any given agent and no published way to glue them together: an ERC-8004 identity NFT, the controller code behind a wallet, the key that actually signs, and the money observed moving. Explorers show the first. Payment scanners show the last. Nobody publishes the join.
That gap is not cosmetic. Every claim above L7 — “this agent paid”, “this agent is funded”, “this agent has repeat counterparties” — is a claim about a wallet, not about an NFT. If the bind is wrong, the funnel is fiction: the right wallet can be attached to a dead identity, or the wrong wallet to a live one, and every downstream number (vault maps, incident attribution, any paid API) inherits the error silently.
So we are publishing the join before we publish the graph that depends on it. Argue with the schema; that is the point of shipping it as a document.
The object
BindingRecord
binding_id ulid
agent_gid eip155:{chainId}:{registry}:{agentId}
wallet chain-qualified address
controller 7702_impl | safe_module | eoa | 4337 | unknown
policy_id nullable → Mandate
bind_method 0..5 (ranked, see below)
confidence 0..1 (deterministic function of evidence)
valid_from_block uint
valid_to_block uint | null
superseded_by binding_id | null
evidence[] {type, uri, tx, body_hash, observed_at}
cluster_id operator cluster
Two rules make this a ledger rather than a snapshot. First, a binding is born, dies and can be superseded — never edited in place. Second, every row that spends money references a binding_id, so attribution is replayable at any block height.
bind_method: ranked, never averaged
A confidence score that blends a cryptographic signature with a guess is worse than no score, because it hides which one you have. We rank the evidence and publish the table:
| Rank | Method | Default confidence | Forgeability |
|---|---|---|---|
| 5 | Wallet signs a challenge over agent_gid + nonce (on-chain or EIP-712) | 0.95 | Hard |
| 4 | A EIP-7702 authorization or session policy whose signed payload embeds the agent id | 0.90 | Hard |
| 3 | Live. The registration declares its wallet (agentWallet in the on-chain metadata or the registration file) and that wallet is the observed spender. Recorded as method 3 · registration agentWallet field | 0.70 | Medium |
| 2 | Reserved: a URI-only agentWallet whose file can change after the fact (not used for ranking) | 0.45 | Easy |
| 1 | Live. The registration's holder, read with ownerOf(tokenId), is the observed spender. Most bindings today; recorded as method 1 · ERC-721 ownerOf. Weaker than a declared wallet because a holder can pay for many things, so the record says which method bound it | 0.55 | Medium |
| 0 | Name or ticker similarity (“Trading Agent #N”), and clustering on shared funders or implementations: never a bind, only a Cluster (below) | 0.00 | Spam |
Ranking rule, as shipped: public L7+ rankings use bindings at bind_method 1 (ownerOf) or 3 (declared wallet), and every record, evidence pack and API answer names the method (bindings[].method, method_text, evidence). A rank never averages two methods; the strongest open binding decides. Methods 5 and 4 are what an operator can add from the Desk (sign a challenge with the agent's wallet) and what a session-policy rail can add; either lifts the binding to the top of the table. One rule keeps the table honest: one open binding per (registration, wallet). The collector once wrote a fresh open row on every sweep, which doubled floats in any sum over the join; that was fixed on 27 September 2026 and the duplicates removed, keeping the earliest row. Confidence itself is a published function, not a model:
c = 1 − Π(1 − wᵢ) # wᵢ = fixed weight of evidence i, from the table above
If we later train a model, it may only propose a binding that a rule still has to accept.
Transfer closes the bind
An ERC-721 transfer moves the identity, not the history. On Transfer we close valid_to_block and require a fresh proof before any new attribution:
on identity.Transfer(from, to, tokenId):
close binding.valid_to_block = tx.block
open new BindingRecord (unproved) for the new owner
keep all prior Actions attached to the closed binding
This is the rule that stops a bought or stolen badge from inheriting somebody else’s volume — the single most common way an agent leaderboard lies. It runs today: the registry watcher sees the transfer within minutes, closes the binding, and logs the detection bind_closed_after_nft_transfer, which the Desk shows as a major flag on that agent.
Where the evidence comes from now
The spec above is only as good as the reads behind it. This is what feeds each field on 27 September 2026, and how fresh it is:
| Field | Read from | How fresh |
|---|---|---|
| Registration, holder, transfer | The ERC-8004 registry on Base: mint and Transfer events, ownerOf | Registry watcher, minutes |
| Declared wallet, services, agent file | tokenURI and the registration file, fetched from our own sandbox (private address space refused, nothing executed) | Daily sample; on demand through check.reval |
| Payments (Action, kind PAY) | USDC transfers out of the bound wallet: a scan of every ranked wallet on a tiered schedule (paid in the last 30 days: daily; funded, never paid: weekly; the rest: monthly), and since 26 September an Alchemy Address Activity push for every ranked wallet the moment a transfer lands | Seconds for ranked wallets; the scan re-reads each with the receipt check before anything is counted |
| Routing versus spending | The receipt of each transfer: money that leaves the counterparty again within the same hop is pass_through, money moved before the registration existed is pre_registration; both are published as their own metrics and excluded from totals | With each scan |
| Float (L8) | balanceOf on USDC, one multicall per 50 ranked wallets over public Base nodes; a paid evidence pack re-reads the wallet at the moment of purchase | Every 6 hours for every ranked wallet (before 27 September only a random sample was re-read, and L8 was decided on balances up to three weeks old) |
| Controller (EIP-7702) | eth_getCode on the bound wallet; a delegation is compared with a curated allowlist that ships empty on purpose, and the Desk names the two common published implementations (Coinbase's EIP7702Proxy, MetaMask's EIP7702StatelessDeleGator) so only unknown code or a change of code is flagged | Every few days per wallet |
| Counterparties (L9) | Distinct to addresses in the 30-day window of counted payments. cluster_of() below is the planned tightening; today the count is of addresses, and the record says so | With each scan |
Every read carries its own as_of on the record, and the Desk's pulse clock shows the age of each feed. A number without a time is a number we did not measure.
Action: the atom of machine money
Payments, swaps, deposits and mints are not the same object and must never share a column. One typed table fixes that:
Action
action_id
binding_id nullable (unbound spend is still recorded, never ranked)
kind x402_pay | vault_deposit | vault_withdraw | swap
| bridge | gas_sponsor | mint | validation | review | incident
counterparty
notional_usd at scrape fx
asset
tx_or_receipt
policy_id_in_force
economic bool # counted: receipt read, not a routing hop, after registration
as_of_block
producer self | x402scan | keyrock | 8004scan | …
The producer field is not bookkeeping politeness. It is what stops imported research from being mistaken for our own crawl — a distinction the data panels already make visually, and which the machine-readable state API carries as kind: live | cited.
The ladder becomes SQL
Once bindings and actions exist, the liveness ladder stops being a second definition maintained by hand. It is a set of queries over the same two tables, so a tile and the graph cannot disagree:
L5 = a Probe of a declared web endpoint (not a hosting sink) returned 2xx or 402 within 8 s, or L6
L6 = MCP initialize, A2A agent card, or x402 requirements in a 402
L7 = ∃ counted Action(kind = pay) that left control:
¬own_position ∧ to ∉ own_addresses(actor) ∧ ¬bridged_back_to_payer
L8 = L7 ∧ max(stable_float over bound wallets) ≥ 10 # USDC, USDT, EURC, DAI
L9 = L8 ∧ count(distinct to where independent(to)) ≥ 3 over 30 days
independent(to) = has_code(to) ∨ distinct_paying_agents(to) ≥ 2
Note independent() in L9. Counting raw counterparties is Sybil-cheap: three addresses you control satisfy a naive rule. An address counts only if it has contract code or another agent pays it too. Shipped 13 October 2026 (ladder v2): L5 to L9 are computed exactly as above, from one file the prober, the rank job and the publish gate all read. A full operator cluster is still not computed: two addresses one operator controls, each paid by two of its agents, still count as independent, and the ladder page says so.
Mandate: the object that makes an incident definable
“Policy-bounded spend” is a slogan until somebody writes down who authorised what:
Mandate
mandate_id
principal HumanOperator | org DID | unknown
binding_id
caps {per_tx, per_day, assets[], targets[]}
expires
kill_switch address or session expiry
attestation EIP-712 | TEE quote | KYC vendor | none
With a Mandate, an incident is a predicate rather than a blog category — action outside mandate — and detectors become queries:
retry_loop := same payload_hash, ≥8 failures, within 10 min
god_key_spend := L8 wallet, no delegation, transfer to a first-seen target
policy_bypass := action.asset ∉ mandate.assets
bind_hijack := identity.Transfer, then URI wallet rewrite < 100 blocks
Each fires an Action(kind = incident), so incidents live on the same tape as the money that caused them. See the security checklist for the god-key case in practice.
Cluster: computed, not asserted
- Common funding parent across the first N inbound transfers.
- Same delegated implementation plus an overlapping session-key factory.
- Same
agentURIhost or TLS leaf certificate. - Same Safe owner set.
Clusters are recomputed, versioned and published alongside the dumps. One operator running fifty identities is one economic actor with fifty badges, and the graph should say so.
Two clocks, two graphs
Two clocks: wall-clock time for probes, block height for money. Never mixed in one column — a probe at 14:02 UTC and a payment at block 34,882,109 are different kinds of fact.
Two graphs: the declared graph is what registration files claim (services, self-reported wallets); the observed graph is probes, transactions and code state. We rank only the observed graph and render the declared one as an overlay. That single separation is the difference between an index of intentions and a ledger of behaviour.
The test that proves the bind exists
A stranger, using only the public dumps, must be able to:
- Take
bindings.parquet,actions.parquetandprobes.parquetfor a given day. - Recompute the Base L5 rate and land inside the published interval — 6.8% (95% CI 5.5–8.4%) for the 2026-08-16 crawl.
- Pick one L9 agent, follow identity → binding (NFT holder or declared agentWallet) → wallet → three independent counterparties, and find every transaction on a block explorer.
- See a transfer appear as a bind close and a bind open — never as inherited volume.
If step three fails, we do not have a graph of machine money. We have a liveness blog with DeFi wallpaper. That test is the acceptance criterion for Layer 1, and we will publish the dumps that let anyone run it.
Status and what ships next
| Object | Status | Ships as |
|---|---|---|
| BindingRecord | Live (methods 1 and 3; one open row per pair) | /api/agent bindings[], the liveness pack, the public agent pages; every ranked agent's bindings as one file at /api/bindings (since 13 Oct 2026, rebuilt every three hours, with a SHA-256 of the rows) |
| Action | Live for payments (kind PAY, with pass_through and pre_registration) | money_path[] on the record; deposits, swaps and bridges are read as rail metrics, not yet as typed actions |
| Probe | Live (sandboxed prober, a uniform sample of 1,024 registrations every six hours, on-demand revaluation) | Rungs L1–L6 on the tape; check.reval |
| Mandate | Spec; the Desk's allow-list and alert rules are a local mandate the operator keeps | Partner-supplied and signed mandates next |
| Cluster | Spec; the peer pack approximates it by shared top counterparty | Recomputed daily, versioned, next |
| State tape | Live | /api/state · OpenAPI · x402 manifest |
Since 26 September 2026 the rank is public (every agent at L7 or above, each with its binding method on the record), evidence packs are sold per call over x402 and included in Desk plans, and the Desk keeps an operator's own agents against this same join. The restraint moved, it did not go away: nothing is ranked without an open binding, nothing is counted without a receipt, and the dumps that let a stranger recompute it have started: the bindings of every ranked agent are one file at /api/bindings since 13 October 2026. The counted payments behind each rank are on each agent's record (the newest 25) and in the evidence packs; a full payments file is not published yet.
FAQ
What is a BindingRecord?
The versioned, evidence-ranked join from an agent identity to the wallet that spends for it, carrying the method that proved it, a deterministic confidence, and the block range in which it was valid. Every payment, deposit and incident in the graph hangs off a binding.
How do you prove a wallet belongs to an AI agent?
By ranked methods, never by averaging: a wallet signature over the agent id (5), a delegation or session policy embedding the agent id (4), on-chain agentWallet metadata later matched by the spender (3), a URI JSON claim (2), clustering alone (1), name similarity (0 — proves nothing). The public rank today uses the NFT holder (ownerOf) or an agentWallet the registration declares; the status table shows which.
What happens when an agent NFT is transferred?
The binding closes at the transfer block and a new one must be proved. Old receipts stay on the old binding, so a purchased identity cannot inherit someone else’s payment history.
Why does the liveness ladder need the bind?
Because L7 and above are claims about money, and money belongs to wallets, not NFTs. With bindings in place the ladder becomes queries over bindings and actions, so the tile and the graph cannot disagree.
Can I use this schema in my own indexer?
Yes — it is published to be copied, and it is better for the sector if several indexers converge on one join. If you implement it, tell us what breaks; the version number on this page exists so disagreements can be dated.