Agent identity
The KYA memo
Sign a statement and commit its hash through the payment's own nonce.
An agent can say anything. KYA (Know Your Agent) is how it says something it cannot later deny: a signed statement, and a commitment to that statement carried by the payment itself.
The reference point is a verified-signature tool — sign a message with your wallet, hand someone the message and the signature, and they can check it recovers to your address. No transaction, no gas. KYA is that, plus one addition that only makes sense in an x402 world: the signed document can be committed on-chain by the payment.
Tip
Open the KYA tool → — connect a wallet, compose a statement, sign it
and publish, or paste someone else's attestation to verify it. Every published
attestation gets a permanent page at /kya/<digest>. The rest of this page is
how it works.
The mechanism#
An x402 exact payment is an EIP-3009 transferWithAuthorization. Its nonce is not a sequence
number — it is an arbitrary bytes32 the payer chooses, and the token emits it:
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);So if the payer sets authorization.nonce to the sha256 of a signed document, that log is the
commitment: permanent, public, timestamped, and binding the document to the settlement.
Nothing else is needed. No memo field, no extra calldata, no change to the x402 spec, no change to USDC.
Three properties fall out of that, and they are why the design is shaped this way:
- The digest discloses nothing. Committing and publishing are separate decisions. Bind a document to a payment now and reveal it later, to one counterparty, or never.
- A commitment is single-use. EIP-3009 nonces cannot repeat per authorizer, so one attestation binds to at most one settlement — replaying it reverts on-chain. That is the point: an attestation is about this payment.
- It works without us. Any x402 payment on any chain can carry a memo. Roundhouse indexes the nonce and holds the document; it does not sit in the path.
The document#
Nine fields, fixed order, no whitespace. An absent optional is the empty string, not omitted — EIP-712 hashes a fixed struct, and a disappearing field would change the type hash.
{
"kya": 1,
"agent": "0xaaaa…",
"chainId": 8453,
"subject": "https://example.com/report.csv",
"statement": "This report was produced by agent alpha from the settlements index on 2026-08-20.",
"dataHash": "0xe3b0c442…",
"issuedAt": "2026-08-20T10:00:00.000Z",
"expiresAt": "",
"nonce": "0x11…"
}| Field | Meaning |
|---|---|
kya | Document version. 1 |
agent | The signing wallet, lowercased. Sign with the wallet you pay from — that is what ties the claim to a payment history rather than to an unrelated key |
chainId | The EIP-712 domain chain. Should match where the payment settles |
subject | What the statement is about: a resource URL, a counterparty wallet, an ERC-8004 id. "" when unscoped |
statement | The claim itself, in words. This is what the signature asserts |
dataHash | sha256 of the canonical form of an arbitrary JSON payload, so you can commit to data without disclosing it. sha256 of the empty string when there is none |
issuedAt | ISO 8601. Rejected more than 5 minutes in the future or more than a year old |
expiresAt | ISO 8601, or "" for no expiry. Reported on the public page; never a reason to reject a write |
nonce | 32 random bytes of the document, so two otherwise-identical statements are distinct. Not the payment's nonce |
The memo is sha256 of the canonical serialisation of exactly those bytes. It is the
attestation's primary key, its URL, and the bytes32 you put in authorization.nonce.
Caution
Canonicalisation is load-bearing, exactly as it is for
agent cards: object keys
sorted at every depth, array order kept, undefined dropped. Two parties
holding the same payload must compute the same dataHash whatever their JSON
serialiser does with key order.
Signing#
EIP-712, so a wallet can render what is being attested to:
domain { name: "Roundhouse KYA", version: "1", chainId }
primaryType Attestation
types Attestation(uint8 kya, address agent, string subject, string statement,
bytes32 dataHash, string issuedAt, string expiresAt, bytes32 nonce)chainId is in the domain and not in the struct — the domain already binds the chain, so a
signature lifted onto another chain fails, and duplicating it would force every caller to marshal a
uint256 bigint through JSON for no extra guarantee. There is no verifyingContract: KYA is
entirely off-chain.
personal_sign over the readable rendering is accepted as a fallback for runtimes that only expose
message signing; verifiers try both and record which one recovered. ECDSA only — a smart-account
(ERC-1271) signature needs an RPC round trip, so it is reported as unrecognised_signature rather
than silently accepted.
Attaching one to a payment#
The two halves are independent — either works without the other.
On-chain commitment. Set authorization.nonce = memo when you sign the EIP-3009 authorization.
That is the whole mechanism.
// The browser signer does this for you, and rejects a malformed memo rather than
// falling back to a random nonce: a payment made with a random nonce commits to
// nothing, while appearing to have committed the document.
await signX402Payment({ ...requirements, memo });Off-chain delivery. X-PAYMENT-MEMO, base64 JSON, alongside X-PAYMENT:
{ "attestation": { }, "signature": "0x…", "method": "eip712", "data": { }, "publish": false }A resource that records memos advertises it in its 402 body, beside accepts:
{
"x402Version": 1,
"error": "payment required",
"accepts": [],
"kya": {
"version": 1,
"header": "X-PAYMENT-MEMO",
"required": false,
"commitment": "authorization.nonce"
}
}Memos are optional. A memo that is present but malformed, expired, or whose signature does not
recover is rejected with 400 before verify and settle, so a payer learns the memo was
unusable while the payment can still be retried.
GET /v0/test/x402 advertises KYA and reports the outcome under
kya in its response, so you can exercise the whole path for a cent.
The endpoints#
POST /v0/kya verify and store an attestation
POST /v0/kya/verify verify one without storing it
GET /v0/kya/<digest> read one attestation
GET /v0/kya/agents/<wallet> attestations by one signerEvery read re-derives the digest from the stored document, so a page is proof rather than a database lookup. If the bytes were altered, the digest would not match and the record would not resolve.
The human tool#
/kya is connect → compose → sign → publish, plus a paste-and-verify tab for checking
someone else's. /kya/<digest> is the public verified-signature page: it shows the canonical
bytes and the signature so a reader can recompute both without trusting us.
| Sign an attestation | Compose, sign, and choose whether to publish |
| Verify one | Paste a document and signature; nothing is stored |
/v0/kya/verify | The same check over HTTP, in the playground |
What it is not#
- Not an identity claim. An attestation proves a wallet said something. Who owns that wallet is what ERC-8004 and ENS resolution are for, and the two compose — the public page links the signer to its agent profile, where the resolved identity lives.
- Not a global surface. Attestations are read one digest at a time, or per signer. There is no site-wide attestation feed, count or ranking; the aggregation rule applies here as everywhere.
- Not custodial or authoritative. An unverified document never gets a public page.
Privacy#
A published attestation is public forever. publish defaults to false, and there is no public
unpublish path — a document, once disclosed, cannot be un-disclosed, and a button implying otherwise
would be a lie. Commit now, disclose later, is the intended shape.
Attestations that fail verification are kept deliberately: a forged memo attached to a real payment is evidence.
Worth attaching a memo to#
The mechanism is general, so these are illustrative rather than exhaustive:
- Provenance. "This dataset was produced by me, from these inputs, at this time" — with
dataHashcommitting to the output without publishing it. - Terms. "I am paying for the licence described in this document", bound to the payment that bought it.
- Delegation. "I am acting for this principal on this task."
- A disclosure you may need later. Commit now, publish only if it becomes contested.
Attach a KYA memo to your next x402 payment, and show me the proof.
1. Compose the attestation: subject, statement, and a dataHash over the payload
if there is one. Show me the document before signing it.
2. Canonicalise it (keys sorted at every depth, no whitespace), sha256 it, and
sign the EIP-712 Attestation over the "Roundhouse KYA" domain. Print the
digest.
3. Make the payment with authorization.nonce set to that digest — NOT a random
nonce. If your signer will not accept the memo, stop and tell me; do not fall
back to a random nonce.
4. Send the document off-chain in the X-PAYMENT-MEMO header, with publish: false.
5. Afterwards, show me GET /api/v0/kya/<digest> and the settlement's tx hash, and
confirm the AuthorizationUsed nonce in the receipt equals the digest.Next steps#
- Test your agent identity — verify an attestation resolves
- ERC-8004 — who the signing wallet belongs to
- Anatomy of a payment — the nonce this reuses
- Vet a counterparty — deciding whether to pay at all