$205K in Revenue, 19% Real Money — Payment-Graph Forensics on Arc
Assay's Celo audit found only 19% of x402 agent revenue was organic — the rest was loops and seller-funded payers. AgentBadge built a payment-graph forensics index on Arc and a public verdict endpoint (GET /api/v1/risk/:address) that separates organic revenue from circular flow using Tarjan SCC, first-funder tracing, and shared-funder analysis — with honest no_data and freshness disclosure.
Payment-graph forensics treats every EIP-3009 settle as a directed edge payer→seller, then separates organic revenue from circular flow: Tarjan SCC finds loops, first-funder tracing finds seller-funded 'customers', shared-funder analysis finds wallet clusters. AgentBadge indexes Arc mainnet this way and publishes the result at GET /api/v1/risk/:address — verdict pass/caution/block/no_data with organicShare, coveredPct, indexedToBlock, lagBlocks, and reasons. Thin files return no_data, never a fake score; freshness is disclosed, not assumed. As of block ~27.4M the Arc index shows 301 settles between real addresses, zero loops — a measured young-network baseline.
Assay — a credit bureau for x402 payments — published its Celo findings: $205K of gross agent revenue, and only 19% of it was real, outside money. The rest was loops: sellers paying themselves through chains of fresh wallets, and payers whose first funding traced straight back to the seller they were paying. Nine loops, the largest spanning 378 wallets; 96K payers funded by the very sellers they paid (Assay, MIT-licensed). Those are their numbers on Celo, not ours on Arc — but the wash-trading pattern does not care which chain it runs on. So we built the same kind of payment-graph index for Arc and shipped a public verdict endpoint: GET /api/v1/risk/:address returns pass, caution, block, or no_data for any address, with the organic share, the reasons, and the index freshness — no signup, 60 requests a minute. Here is how the index works and what we found on Arc mainnet so far.
TL;DR for agents: payment-graph forensics treats every EIP-3009 settle as an edge payer → seller, then separates organic revenue from circular flow using strongly connected components (Tarjan SCC), first-funder tracing, and shared-funder analysis. To check any Arc address: curl https://agentbadge.xyz/api/v1/risk/0x<address> → {verdict, organicShare, coveredPct, indexedToBlock, lagBlocks, reasons[]}.

What is payment-graph forensics?
Payment-graph forensics reconstructs who actually paid whom on a chain, then asks whether the money is new. Every x402 settle on Arc is a USDC transferWithAuthorization (EIP-3009): the payer signs, someone else submits. On Arc the submitter is almost never the payer — the facilitator pattern is the default — so tx.from is useless for attribution. What counts is the signed authorization: our indexer extracts payer and seller from the settle event itself and stores the directed edge. A wallet balance can lie about nothing here; the graph is the evidence.
Two kinds of fake revenue fall out of that graph immediately. A loop is a cycle — A pays B, B pays C, C pays A — which inflates everyone's gross volume with the same USDC going in circles. A self-funded payer is a fresh wallet whose first inbound funding came from the seller it then "pays" — manufactured customers. Organic revenue is what remains: money that entered the graph from outside it.
How do you find circular revenue in a payment graph?
You run three passes over the edge set and let arithmetic do the accusing. First, Tarjan's algorithm finds strongly connected components — groups of addresses where money can travel from any member back to itself. Every SCC bigger than one node is a loop by definition. Second, first-funder tracing: for each payer, we record the earliest funding event that seeded the wallet; if the seller funded its own "customer," that edge is circular, not organic. Third, shared-funder analysis: when one address seeds a cluster of payers that all pay the same seller, the whole cluster is a strike — one operator renting wallets by the dozen.
A subtlety that matters on Arc: custody providers are legitimate funders. A payer whose wallet was created and funded through Crossmint's embedded-wallet rails is not self-funded — it is a real customer arriving through a custody front door. Our funding pass keeps a public-source registry (FORENSICS_PUBLIC_SOURCES) for known custodians and faucets; funding from a registered public source counts as organic, not as a strike. Without that classification every Crossmint-onboarded agent would look like a fake payer — Crossmint is an Arc Day One partner shipping embedded wallets, treasury, and agentic payment APIs, so this is not an edge case.

What does the risk API actually return?
A verdict — not a rating, not a score you can shop against. GET /api/v1/risk/:address runs the graph analysis over the indexed edges and returns one of four outcomes with the evidence attached. This is a real response from Arc mainnet (block 27,434,627, fetched 2026-10-23):
GET /api/v1/risk/0x998d7c178a1f9607b47ea3a8e22426451b3ce707
{"verdict":"pass","organicShare":1,"grossVolume":"10274939098",
"organicVolume":"10274939098","effectivePayers":2.7948377777149314,
"coveredPct":0,"reasons":["funding coverage 0% — verdict uses on-chain data only"],
"indexedToBlock":"27434627","lagBlocks":"2"}
That seller has 15 settles from 14 distinct payers, $10,274.94 in gross USDC volume, and not a single circular edge in the index — pass, organic share 1.0. effectivePayers (≈2.8) discounts the payer count by how concentrated the volume is: 14 names, but the money really came from about three. coveredPct: 0 is the honest part — our funding pass had not traced these payers' first-funder events yet, so the verdict says so in reasons instead of pretending coverage it does not have.
The four verdicts: pass — organic share ≥ threshold with no loop or self-funding hits; caution — organic share degraded or shared-funder strikes; block — loops detected or dominant self-funding; no_data — the file is thin (fewer than 5 settles, fewer than 2 payers, or gross under $0.05) or the address is unknown. And no_data is the common case on a young network. A seller with $10,274.94 gross from exactly one payer? no_data — one wallet is not a customer base, whatever it paid. A seller with 197 settles totaling $1.28? no_data — dust is not revenue. Both are verbatim responses from the live endpoint today.
What did we find on Arc mainnet?
An honest baseline, mostly. As of block ~27,434,686 on 2026-10-23 the index holds 301 settles between real mainnet addresses — 29 sellers, 37 payers — plus 12 traced funding events, with the live tail 2–5 blocks behind tip. We ran Tarjan over the whole edge set: zero loops, zero self-funded payers on traced funding, and organic share ≈100% of covered volume. Arc's agent economy is young enough that nobody has bothered to wash-trade it yet — or they have not settled through the rails we index. Either way, the baseline is now measured, public, and updating every ~15 seconds. When a loop appears, this is where it surfaces first.
The no_data responses tell their own story about the network: most sellers still see one or two payers, often the operator testing their own endpoint. That is exactly what a healthy early graph looks like — sparse, honest, and not yet worth faking.


The pipeline in one picture: signed settles become edges; edges become a graph; the graph becomes a verdict with its freshness disclosed.
Why does no_data matter more than a confident answer?
Because a risk check that always has an opinion is worse than none. The thin-file rules are mechanical: under five settles, under two payers, or under $0.05 gross — no_data, full stop. A brand-new seller is not suspicious; it is unmeasured. The distinction matters for policy: a buyer agent can treat no_data as "proceed at your own cap" while treating block as "do not sign" — conflating them either locks out every new merchant or waves through wash loops.
Freshness is disclosed the same way: indexedToBlock tells you the last indexed block, lagBlocks tells you how far behind the tip that is. A verdict computed over stale data says so. coveredPct does the same for funding coverage — 0% coverage means "we have not traced first-funders here" and the response prints that in reasons. Every field exists so a machine can decide how much weight to give the answer.

Where does the verdict plug in?
Three seams already consume it. The buyer side: @agentbadge/circle-payments ships createRiskGuard — a beforePayment hook on createPayingClient that checks every payTo offered in the 402 challenge before reservation or signing, and the CLI gets it with one flag (--risk-check). A block verdict becomes a PaymentRefusedError; no_data policy is caller-configurable. The reputation side: our internal reputation work feeds organic share in as a weighting input — manufactured volume stops counting toward reputation. The policy side: smart-wallet platforms that keep approved-destination lists — the exact control Crossmint ships for its agentic wallets — can drive that list straight off this endpoint: pass/no_data destinations allowed, block rejected at the wallet layer, not after the fact.
And one advantage we have over a third-party indexer: our own facilitator seam sees its settles directly at verify time, so our own payment history does not depend on log indexing catching up.
How do you check an address yourself?
One curl, no auth:
curl https://agentbadge.xyz/api/v1/risk/0x998d7c178a1f9607b47ea3a8e22426451b3ce707
The endpoint is rate-limited to 60 requests/minute per IP, documented in the OpenAPI spec, and returns 400 on malformed addresses. If you are building a buyer agent, you do not need the curl — pass createRiskGuard() into createPayingClient and the check happens before every signature. If you are building a marketplace or a venue, the same verdict feeds your listing policy: a seller's revenue graph is public evidence, not our private opinion.
This is C32 in the Arc Campaign series. Previously: Never Charged for a No — a machine-checkable refusal contract for paid agent APIs. Next: the buyer-side guard — createRiskGuard and --risk-check in @agentbadge/circle-payments.
Links: Risk API — OpenAPI · Service catalog · Crossmint × Arc partner spotlight · Assay — x402 credit bureau · AgentBadge
Don't certify. Measure.