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:

solidity
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.

json
{
  "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…"
}
FieldMeaning
kyaDocument version. 1
agentThe 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
chainIdThe EIP-712 domain chain. Should match where the payment settles
subjectWhat the statement is about: a resource URL, a counterparty wallet, an ERC-8004 id. "" when unscoped
statementThe claim itself, in words. This is what the signature asserts
dataHashsha256 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
issuedAtISO 8601. Rejected more than 5 minutes in the future or more than a year old
expiresAtISO 8601, or "" for no expiry. Reported on the public page; never a reason to reject a write
nonce32 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:

text
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.

javascript
// 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:

json
{ "attestation": { }, "signature": "0x…", "method": "eip712", "data": { }, "publish": false }

A resource that records memos advertises it in its 402 body, beside accepts:

json
{
  "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#

http
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 signer

Every 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 attestationCompose, sign, and choose whether to publish
Verify onePaste a document and signature; nothing is stored
/v0/kya/verifyThe 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 dataHash committing 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.
Agent prompt
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#