x401, explained
x401 is an open protocol that lets a server ask for proof before it serves a request. The server answers with a PROOF-REQUEST header naming the credential it wants, and the agent retries with the proof. It is the identity counterpart to x402, which asks for payment the same way. This page explains how x401 works in draft 0.2.0, what changed from 0.1.0, how it pairs with x402, and how to test a client against a live verifier.
What x401 is
x401 adds a challenge and a response for proof to plain HTTP. A website or API that needs to know something about whoever is calling, such as a verified identity, an age, a membership, an accreditation or a payment history, says so in a response header. The caller, usually an AI agent, gets a matching credential presentation from a wallet and sends it on a retry. The server checks it and serves the request.
Proof (proof.com) published x401 in June 2026 as an open, issuer-neutral protocol. The specification is a working draft under the Apache-2.0 license, at x401.proof.com (Proof also gives it as x401.id) with its source on GitHub. The current draft is 0.2.0; 0.1.0 is the previous one. Proof's announcement thanks individual contributors from Circle, OpenAI, Okta, MATTR and Lightspark, and Circle has called itself an early adopter and co-endorser. Proof has also said it will bring x401 to the FIDO Alliance's work on agent authentication. It is not a formal standard yet.
The idea behind it: an agent acting for a person or a company should be able to show what that person or company has authorized, without a sign-up form, an API key or a password. x401 carries the proof. It does not issue credentials, and each server decides which issuers it trusts. Payment stays with HTTP 402 and a payment protocol such as x402.
How a request works
- Agent to serverThe agent calls a protected route with no proof.
- Server to agentThe server answers, usually with
401 Unauthorized, and aPROOF-REQUESTheader: base64url JSON describing the credential it wants, with a fresh nonce. - Agent to walletThe agent gets a presentation that satisfies the request: from its own wallet, through the browser's Digital Credentials API, by relaying the request to a wallet or a remote service, or out of band.
- Agent to serverThe agent retries the same request with a
PROOF-RESPONSEheader carrying the result. - Server to agentIf the proof checks out, the server answers normally, often
200 OK. If not, it says why in aPROOF-RESULTheader and can ask again.
So that it does not have to prove itself on every call, the agent can trade a verified result at the server's OAuth token endpoint for a short-lived Bearer token and send that instead. The status code does not carry x401: it describes the whole response, so a server can send PROOF-REQUEST with 401, 200 or whatever fits. Payment still uses 402.
The three headers in draft 0.2.0
| Header | Direction | Carries |
|---|---|---|
PROOF-REQUEST | Server to agent | The x401 payload: what proof is needed, and how to get a token |
PROOF-RESPONSE | Agent to server | A Result Artifact (the presentation, inline or by reference) or an x401 Token Object |
PROOF-RESULT | Server to agent | The outcome, including an x401 error object when the proof fails |
Each value is a single JSON object, base64url-encoded without padding.
What the server asks for
The PROOF-REQUEST payload is flat. Its members:
| Member | What it holds |
|---|---|
scheme, version | "x401" and the payload version, "0.2.0" |
credential_requirements | A Digital Credentials API request the server wrote: digital.requests, each with a protocol (openid4vp-v1-signed, recommended, or openid4vp-v1-unsigned) and its OpenID4VP data: the DCQL query naming the credential and the claims, the nonce and the response mode |
oauth | Optional token exchange: token_endpoint, with audience or resource |
request_id, satisfied_requirements | Optional hints for reusing a token across routes |
payment | Optional and informational: whether payment is also required, and which scheme, such as x402 |
Because the request is a standard Digital Credentials API request, a browser agent can pass it to navigator.credentials.get (a signed request works only from an origin listed in its expected_origins), and an agent without a browser can relay it to a wallet or a service that handles such requests. The credential format is whatever the query names: an SD-JWT VC (dc+sd-jwt), an ISO mdoc, and so on.
How the agent answers
On the retry, PROOF-RESPONSE carries a Result Artifact:
{
"credential_result": {
"protocol": "openid4vp-v1-unsigned",
"data": { "vp_token": { "<query id>": ["<presentation>"] } }
},
"request_id": "…", // optional hint
"agent_id": "…" // optional; used only when the server binds the agent
}
Instead of credential_result it can carry credential_result_uri: an HTTPS link to a result delivered by reference, which the verifier issued for that one result, with an optional expires_at. That suits large or remotely generated presentations. For a token, the agent posts the same artifact to the token endpoint (OAuth 2.0 token exchange: grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token_type=urn:x401:params:oauth:token-type:result_artifact) and gets a Bearer token, which it then sends in Authorization or wraps in an x401 Token Object in PROOF-RESPONSE.
What the proof is bound to
A presentation has to be bound to the request it answers, or anyone could replay it. In x401 0.2.0 that binding comes from OpenID4VP:
- Signed requests name the server in
client_idand carry its signature, so the wallet can tell who is asking, andexpected_originslimits which web origins may invoke them. Over the Digital Credentials API, OpenID4VP makes the result's audience the invoking origin (origin:https://…), even for a signed request; a relayed signed request binds the audience to theclient_id. - Unsigned requests rely on the invoking web origin and the server's nonce; relayed instead of invoked from a web page, they are bound by the nonce alone. The spec warns that a result accepted on the strength of the nonce alone can be harvested and relayed, whether through a relay or from a malicious page. It asks verifiers to use fresh nonces with short expiries, and to compensate, for example by authenticating the agent or constraining the origin.
- Binding the agent itself is optional. Deployments that need it add caller authentication on top, such as HTTP Message Signatures (Web Bot Auth), mutual TLS, DPoP or a workload identity.
The nonce lives inside the OpenID4VP request. A server that keeps no state can make it self-contained, signing the route, the policy and the expiry into it, and check it when the answer comes back.
x401 and x402
The two protocols answer different questions with the same pattern. x402 asks whether the caller will pay: a 402 Payment Required response with a quote, and a retry with the payment. x401 asks whether the caller can prove something: a credential request, and a retry with the proof. They stay separate protocols, but one route can ask for both. In the draft's main flow the agent proves first, and the retry that pays carries the proof, or its token, along with the payment; other orders are still open questions in the draft. The x401 payload's payment member only tells the agent whether payment will also be needed.
Together they cover what a paid API used to need an account for: who may call (x401) and who pays (x402), with no API keys to issue or rotate.
What changed from 0.1.0 to 0.2.0
| 0.1.0 | 0.2.0 | |
|---|---|---|
| Server asks | PROOF-REQUIRED | PROOF-REQUEST |
| Agent answers | PROOF-PRESENTATION, with a VP Artifact | PROOF-RESPONSE, with a Result Artifact |
| Errors and results | PROOF-RESPONSE | PROOF-RESULT |
| Who writes the OpenID4VP request | The agent, from a proof object (presentation_protocol, dcql_query or scope, challenge, issuers) | The server, as a Digital Credentials API request in credential_requirements |
| Freshness | A challenge in the form x401:<verifier>:<nonce> | The OpenID4VP nonce inside the request |
Token exchange subject_token_type | urn:x401:params:oauth:token-type:vp_artifact | urn:x401:params:oauth:token-type:result_artifact |
| Binding | The agent was the OpenID4VP client and the audience; agent_id was required | The result is bound to the verifier or the invoking origin (a relayed unsigned result: the nonce only); binding the agent is optional and agent_id is a hint |
oauth | Required, inside proof | Optional, at the top level |
| Trusted issuers | An optional issuers hint | Trusted authorities named in the DCQL query |
Requests by scope | An alternative to a DCQL query | Removed |
| Results by reference | Not defined | credential_result_uri, issued by the verifier |
Watch out for PROOF-RESPONSE: it changed direction. In 0.1.0 the server sent it, with errors; in 0.2.0 the agent sends it, with the proof. A server that supports both drafts, as Settled does, sends PROOF-REQUEST and PROOF-REQUIRED together and reads whichever answer comes back. (Settled's 0.2.0 nonce happens to keep the 0.1.0 x401:<verifier>:<nonce> shape; to a 0.2.0 client it is just an opaque value.)
Try it against a live verifier
Settled runs an x401 verifier you can test a client against. GET https://settled.tools/x401/demo answers 401 with a PROOF-REQUEST, and a 0.1.0 PROOF-REQUIRED, asking for a Settled payer record that shows two facts. Retry with a valid PROOF-RESPONSE and it answers 200 with what it verified; send a bad one and it answers 401 with a PROOF-RESULT saying why. It counts no quota and Settled keeps nothing from the proof: Cloudflare's request records keep the IP address, the path and the user agent for up to 7 days, not the proof header. Its requests are unsigned and it accepts a key-binding aud of origin:https://settled.tools or https://settled.tools, with or without a trailing slash, so it suits clients that build the presentation themselves, as server-side agents do; it sends no CORS headers for browsers on other origins. Decoded, the request looks like this:
{
"scheme": "x401",
"version": "0.2.0",
"credential_requirements": {
"digital": {
"requests": [{
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "x401:aHR0cHM6Ly9zZXR0bGVkLnRvb2xz:…",
"exp": 1791932913,
"dcql_query": { "credentials": [{
"id": "payer_record",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["https://settled.tools/vc/payer-record/v1"] },
"claims": [
{ "path": ["active_days_30d_over_3"], "values": [true] },
{ "path": ["sanctions_clear"], "values": [true] }
]
}] },
"client_metadata": { "vp_formats_supported": { "dc+sd-jwt": {
"sd-jwt_alg_values": ["ES256"], "kb-jwt_alg_values": ["ES256"] } } }
}
}]
}
},
"oauth": {
"token_endpoint": "https://settled.tools/x401/token",
"audience": "https://settled.tools",
"resource": "https://settled.tools/v1/free/endpoints"
},
"request_id": "settled-free-search-v1",
"satisfied_requirements": ["urn:settled:x401:payer-record:free-search:v1"],
"payment": { "required": false, "scheme_hint": "x402", "notes": "Or pay instead: a day pass …" }
}
The same request guards Settled's free tier: past 50 endpoint searches a day, the refusal carries it, and an agent that answers gets 150 searches a day for its wallet. Code for getting a record and answering, step by step: Settled for agents. Every field, in plain text: llms.txt.
Payer records: what they prove and what they don't
A payer record is the credential Settled's verifier asks for, and one any x401 server can ask for. Settled signs it for a wallet's owner, as an SD-JWT VC, stating how that wallet has paid x402 sellers on Base over a 30-day window that ends shortly before it is issued (the record states the window), as Settled observed it. The holder shows each server only the facts that server asks for.
It shows
- That the wallet paid independent sellers in Settled's index: on 3, 7, or 14 or more of the 30 days; $1, $10, or $100 or more in USDC; 5, or 10 or more, sellers. A seller is independent when 10 or more other wallets paid it in the window and it is not flagged as a clone farm, on hold or sanctioned.
- That the wallet was not on Settled's copy of the OFAC SDN list (refreshed daily) when the record was issued, and that no seller it paid has paid it back, as far as Settled's ledger of USDC payments to indexed sellers shows (it sees a payment back only when the wallet is itself an indexed seller's payTo address).
- The month of the first such payment Settled saw. Settled's history starts in September 2026.
- That whoever presents it holds the key the wallet's owner named when asking for it. The wallet itself signed that request.
It does not show
- Who owns the wallet. There is no name, no country and no identity check: it is a record of payments, not an ID.
- Creditworthiness, a balance, or that the wallet will pay you.
- Payments to sellers outside Settled's index, in other tokens or on other chains, or exact amounts: each payment fact is a threshold.
- Anything that happened after it was issued, beyond what the status list shows. A record lasts 7 days. Settled revokes it overnight if the wallet turns up on the sanctions list or in a circular flow, and can revoke one by hand; the owner can revoke theirs at any time.
Every presentation reveals the wallet address, the network, the 30-day window, the holder's public key and the record's slot on the status list; only the facts are selective. So presentations of one record can be linked to each other, and the wallet is never hidden.
To check a record yourself: verify the ES256 issuer signature against the key at /.well-known/jwt-vc-issuer, each disclosure's digest, the key-binding JWT (your nonce, your audience, signed by the key in cnf), the expiry, and the record's bit on the status list. SD-JWT VC libraries check the signature, the disclosures and the key binding (check typ, iss, vct, sd_hash and a recent iat too); the status list needs a Token Status List check. Or send the presentation to POST https://settled.tools/v1/credentials/verify with the aud and nonce you expect, and Settled checks it for you; without those two it cannot tell a presentation made for another verifier, or one replayed within 5 minutes. What each claim means: /vc/payer-record/v1.
Records are free until January 11, 2027, then $0.10 for 7 days, paid by the agent over x402. Checking one is free.
Accept payer records on your own API
Put a DCQL query like this in your own PROOF-REQUEST, naming the facts you care about:
{ "credentials": [{
"id": "payer_record",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["https://settled.tools/vc/payer-record/v1"] },
"claims": [
{ "path": ["active_days_30d_over_7"], "values": [true] },
{ "path": ["sanctions_clear"], "values": [true] }
]
}] }
Then trust only records signed by the key at https://settled.tools/.well-known/jwt-vc-issuer, and check that the key-binding JWT's nonce is one you issued and that its aud names you the way OpenID4VP does for your request: over the Digital Credentials API, origin: followed by an invoking origin you accept (for a signed request, one of its expected_origins); for a relayed signed request, your client_id. A relayed unsigned request has no defined audience: rely on your nonce, and authenticate the agent if it matters.
Settled's x401 error codes
x401 leaves error codes to each verifier. Settled's come in PROOF-RESULT (0.2.0) or PROOF-RESPONSE (0.1.0), with a fresh request alongside:
| Code | Meaning |
|---|---|
invalid_result | The PROOF-RESPONSE is not a usable Result Artifact: not base64url JSON, over 22,000 characters, both or neither of credential_result and credential_result_uri, a result by reference (Settled issues none, so it takes results inline only), not OpenID4VP, or an encrypted response. |
invalid_presentation | The presentation is malformed or over 16 KB, or a 0.1.0 VP Artifact is malformed. |
challenge_mismatch | The key-binding JWT's nonce is not one Settled issued; in 0.1.0, also an artifact challenge Settled did not issue or that differs from the nonce. |
challenge_expired | The nonce is more than 5 minutes old. Take the one from a fresh PROOF-REQUEST. |
audience_mismatch | The key-binding JWT's aud does not name Settled: origin:https://settled.tools or https://settled.tools in 0.2.0, your agent_id in 0.1.0. |
key_binding_required | No key-binding JWT is appended to the presentation. |
invalid_key_binding | The key-binding JWT is malformed, not signed by the record's holder key, has the wrong sd_hash, or its iat is more than 5 minutes off. |
invalid_disclosure | A disclosure is not part of the record, or appears twice. |
untrusted_issuer, bad_signature | Not a payer record signed by settled.tools. |
unknown_record | Settled has no record of issuing this credential. |
expired, not_yet_valid | Outside the record's 7 days. |
revoked | The record's bit on the status list is set. |
requirement_not_met | The presentation does not show active_days_30d_over_3 and sanctions_clear as true. |
invalid_token | The Bearer token is not valid or has expired (tokens last an hour). |
verifier_unavailable | Settled could not check the proof just now; try again. |
At the token endpoint a failed proof comes back as OAuth invalid_grant, with the code in error_description and in PROOF-RESULT (PROOF-RESPONSE for 0.1.0), without a fresh request.
Questions
What is x401?
An open HTTP protocol, published by Proof in June 2026, that lets a server ask a caller for verifiable proof, such as an identity, an age, a membership or a payment history, before it serves a request. The server sends a PROOF-REQUEST header; the caller retries with a PROOF-RESPONSE header carrying a credential presentation.
How is x401 different from x402?
x402 asks for payment: a 402 Payment Required response with a quote, and a retry with the payment. x401 asks for proof: a credential request, and a retry with the presentation. They use the same challenge-and-retry pattern and can both apply to one route: in the draft's main flow the agent proves first, then pays, sending the proof or its token along with the payment.
Is x401 a finished standard?
No. It is a working draft, version 0.2.0 as of October 2026, published as open source under the Apache-2.0 license. Proof has said it will submit x401 to the FIDO Alliance's work on agent authentication.
Which HTTP status code does x401 use?
None is required. 401 Unauthorized is typical, but the status code describes the whole response, so a server can send PROOF-REQUEST with 200 OK or any other code that fits.
What credentials can a server ask for?
Anything a wallet can present over OpenID4VP and the Digital Credentials API, described with a DCQL query: SD-JWT VCs, such as a Settled payer record, ISO mdoc documents, and other formats the query names.
Does x401 give AI agents their own identity?
Not by itself. Its design has an agent carry proof from the person or organization that authorized it. Binding a presentation to one particular agent is optional and done with separate mechanisms, such as HTTP Message Signatures, mutual TLS or DPoP.
What changed in x401 0.2.0?
The headers were renamed (PROOF-REQUIRED became PROOF-REQUEST, PROOF-PRESENTATION became PROOF-RESPONSE, and errors moved from PROOF-RESPONSE to PROOF-RESULT), the server now writes the Digital Credentials API request itself, the agent answers with a Result Artifact, and the token exchange type became result_artifact.
Where can I test an x401 client?
Settled runs a live verifier at https://settled.tools/x401/demo. It answers with a PROOF-REQUEST asking for a Settled payer record and checks the PROOF-RESPONSE (draft 0.2.0) or PROOF-PRESENTATION (0.1.0) you send back, keeping nothing from the proof.
Sources
- x401 specification, draft 0.2.0 (Proof)
- x401 specification, draft 0.1.0
- x401 source and changelog on GitHub (Apache-2.0)
- Introducing x401: Bringing Proof of Identity to the Web (Proof, June 2026)
- Proof's x401 establishes an open protocol for AI agent identity and authorization (Help Net Security, June 26, 2026)
- OpenID for Verifiable Presentations 1.0 (OpenID Foundation)
- Digital Credentials API (W3C)
- SD-JWT-based Verifiable Credentials (IETF draft)
- Token Status List (IETF draft)
- x402, payments over HTTP 402
The GitHub changelog and README still use some earlier names, such as presentation_requirements and VP Artifact; the published specification is authoritative, and this page follows it.
Settled is an independent index of x402 endpoints. It is not affiliated with Proof, the authors of x401. This page describes the public x401 drafts as of the date above; the specification at x401.proof.com is authoritative. Also as Markdown: /x401.md.