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. Ranking uses bindings proved at method 3 or above.
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 | On-chain agentWallet metadata set by the NFT owner, later matched by the observed spender | 0.70 | Medium |
| 2 | URI JSON agentWallet matches an observed spender, URI still live | 0.45 | Easy |
| 1 | Clustering only: shared funder, same implementation, same user agent | 0.20 | Very easy |
| 0 | Name or ticker similarity (“Trading Agent #N”) | 0.00 | Spam |
Ranking rule: public L7+ rankings use bindings at bind_method ≥ 3 only. Methods 1–2 still draw in the graph, rendered dashed, and researchers can toggle them on. 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.
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 # after the vanity filter
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 = last Probe returned 2xx or a well-formed 402
L6 = MCP initialize or A2A agent-card handshake succeeded
L7 = ∃ Action(kind ∈ {x402_pay, swap, vault_deposit}, economic = true)
L8 = balance_usd > 1 at as_of_block
L9 = count(distinct cluster_of(counterparty)) ≥ 3 over 30 days
Note cluster_of() in L9. Counting raw counterparties is Sybil-cheap: three facilitator addresses you control, or three endpoints that pay each other, satisfy a naive rule. Counterparties inside one operator cluster count once.
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 (method ≥ 3) → wallet → three distinct counterparty clusters, 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 | Spec published, this page | bindings.parquet + as-of API |
| Action | Spec published | actions.parquet, typed by kind |
| Probe | Running (liveness crawler) | Append-only time series |
| Mandate | Spec published | Partner-supplied + public mandates |
| Cluster | Spec published | Recomputed daily, versioned |
| State tape | Live | /api/state · OpenAPI |
Until bindings.parquet exists, we do not sell a query API and we do not publish per-agent rankings. The panels stay what they are: live float and vault reads, plus clearly-labeled cited research. That restraint is the product.
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). Rankings use method ≥ 3.
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.