Three weeks ago an x402 rating site gave our agent API a CCC. The reason line said organic 1/8: only one in eight payers was a stranger. The other seven were us.
That was true. Here is what happened, what we changed, and the tool that fell out of it.
What an x402 service is
agent.connskill.com sells market data and small real-world actions to AI agents: keyword volume by city, SERP snapshots, SMS verification numbers, receive-only inboxes, EU-hosted LLM calls. No signup, no API key. An agent calls the endpoint, gets an HTTP 402 with a price, pays USDC on Base, retries with the payment header, and receives the data. 65 endpoints, one price each.
Why our own wallets were paying
Every endpoint has an end-to-end test that runs through the real payment path: an agent with its own wallet buys the product, and we compare the delivered file with what the provider charged us. That is the only test we trust, because anything cheaper has lied to us before. It also means our own wallets show up on-chain as buyers.
A rating crawler cannot tell a test wallet from a customer. It counted payers and noticed that seven of eight belonged to the same operator. Self-dealing, from the outside. It was right to flag it.
What we did instead of hiding it
-
Named our wallets. A single
wallets.jsonlists every address we own. Every report that counts revenue reads that file and splits payments into own, dust (address-poisoning transfers, another story) and external. Real numbers as of this week: 53 own payments, 38 external from 3 wallets, 7 dust. - Stopped counting ourselves. Our daily status reports external revenue only. The first version of that report proudly listed a 15 USDC "sale" that was a transfer between two of our own wallets.
- Turned the detection into an endpoint. If a crawler can spot self-dealing from the outside, so can an agent deciding which service to trust.
The endpoint
POST https://agent.connskill.com/v1/trust-check
{ "origin": "https://some-x402-service.example" }
For $0.05 it fetches the service's discovery documents and finds up to three paid endpoints across the dialects we have seen in the wild (services[], endpoints[], resources[], plain OpenAPI); probes one and parses the 402 challenge (is there a payTo address, does the network match, does the price parse); reads USDC transfers to that address from Base; and counts payers, how many are the operator's own addresses or circular, and returns a share.
Run against ourselves while writing this:
{
"origin": "https://agent.connskill.com",
"probe": { "x402_valid": true, "network": "eip155:8453", "payTo": "0x43B8…63dE",
"amountRaw": "100000", "http_ms": 146, "probedPath": "/v1/serp-report" },
"chain": { "windowDays": 3, "source": "rpc-fallback", "inflowUsdc": 0.09,
"payers": 1, "payersSelfDealing": 0, "selfDealingShare": 0 },
"verdict": "green",
"reasons": []
}
Now look closer at that green. source: rpc-fallback, windowDays: 3: the block explorer was down, so the check read logs from an RPC node over a three-day window. The one payer in that window is our own daily end-to-end test wallet, paying 0.03 USDC every morning. The check did not catch it, because that wallet never received anything from payTo — the circularity test only sees round trips, and a test wallet funded once from somewhere else looks exactly like a customer.
So two limits, stated plainly: a verdict is only as wide as its window (the response tells you which one it used), and self-dealing is only detectable when the money comes back. The rating site had the same blind spot in reverse: it flagged us for wallets that were ours, and would have missed a fresh one. Naming your wallets is the only thing that closes that gap, and no crawler can do it for you.
Why publish this
Because the rating was right, and the fix is more useful public than private. If you run an x402 service, name your wallets and stop counting them. If you run an agent that buys from x402 services, check the payer mix before you trust a price list. The endpoint is there for that; so is the OpenAPI.
And if you want something we do not sell yet, there is a request board. It is empty. That is the honest number too.
Top comments (0)