Agentic Finance Graph

Home/Research/BindingRecord spec

Specification · v0.2 · the join key · updated 27 September 2026

BindingRecord: the missing link between an agent and its money.

Every explorer in this sector draws the same arrow — NFT → wallet → money — and none of them specify it. This is our specification: a versioned, evidence-ranked bind, with money stored as typed actions underneath it. Without this object, a ledger of machine money is a status page.

Agentic Finance Graph · Published · updated 2026-09-27 to what the collector does today · Layer 1 schema · comments to agenticfinancegraph@proton.me

Direct answer A BindingRecord is the versioned, evidence-ranked join from an agent identity to the key that spends for it — 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:

Bind methods and default confidence
RankMethodDefault confidenceForgeability
5Wallet signs a challenge over agent_gid + nonce (on-chain or EIP-712)0.95Hard
4A EIP-7702 authorization or session policy whose signed payload embeds the agent id0.90Hard
3Live. 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 field0.70Medium
2Reserved: a URI-only agentWallet whose file can change after the fact (not used for ranking)0.45Easy
1Live. 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 it0.55Medium
0Name or ticker similarity (“Trading Agent #N”), and clustering on shared funders or implementations: never a bind, only a Cluster (below)0.00Spam

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:

Evidence sources behind a binding and the money under it
FieldRead fromHow fresh
Registration, holder, transferThe ERC-8004 registry on Base: mint and Transfer events, ownerOfRegistry watcher, minutes
Declared wallet, services, agent filetokenURI 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 landsSeconds for ranked wallets; the scan re-reads each with the receipt check before anything is counted
Routing versus spendingThe 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 totalsWith 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 purchaseEvery 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 flaggedEvery 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 soWith 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 agentURI host 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.parquet and probes.parquet for 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

ObjectStatusShips as
BindingRecordLive (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)
ActionLive 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
ProbeLive (sandboxed prober, a uniform sample of 1,024 registrations every six hours, on-demand revaluation)Rungs L1–L6 on the tape; check.reval
MandateSpec; the Desk's allow-list and alert rules are a local mandate the operator keepsPartner-supplied and signed mandates next
ClusterSpec; the peer pack approximates it by shared top counterpartyRecomputed daily, versioned, next
State tapeLive/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.