x401

Protect your endpoints from agentic traffic with Digital Credentials.

AI agents now browse, buy, sign, and call APIs on behalf of people. Bot detection can only tell you a request is not human. x401 tells you which person stands behind it, through Digital Credentials.

x401 is an open protocol that extends the HTTP 401 flow. When a user agent requests a protected resource without identity, the endpoint responds 401 with an x401 PROOF-REQUEST header: a Digital Credentials Presentation request.

The user agent delegates the request to the user's Wallet. The user authenticates to their Wallet and approves the credential presentation. The user agent retries the protected resource with a PROOF-RESPONSE header: a Digital Credentials Presentation response.

After cryptographic verification of the presented Verifiable Credential, the user gets access to the protected resource. x401 lets you seamlessly harness the power of Proof's Digital Credentials through Verify Identity and Sign Transactions.

x401 is built on top of Digital Credentials and is wallet-agnostic; see the specification. Proof's Cloud Wallet supports x401 through OpenID for Verifiable Presentations (OID4VP).

Proof Cloud Wallet is also available as a Web Component for your website!

User agentYour endpointProof Cloud WalletGET /protected1401 + PROOF-REQUEST2Presentation request3User authenticates and approves4vp_token5GET /protected + PROOF-RESPONSE6Verify vp_token7200 + protected resource8

Getting started

Implement x401 with Proof's Cloud Wallet and start verifying identities immediately.

Ask your agent to protect your endpoints behind x401 using Proof's Cloud Wallet:

Protect my API endpoints behind the x401 protocol, using Proof's Cloud Wallet, following the instructions at https://api.proof.com/.well-known/agent-skills/setup-x401-verifier/SKILL.md

Or follow the step-by-step guide.

1. Install dependencies

npm install express @proof.com/x401-node @proof.com/proof-vc-server

2. Generate a key pair

The key pair signs your OID4VP requests and your enrollment. It uses P-256.

openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem

3. Host an OAuth Client ID Metadata Document

Create an OAuth Client ID Metadata Document (CIMD) and host it at an endpoint you own, publicly accessible over HTTPS. The CLIENT_ID is the exact URL of the document.

import express from "express";
import { readFileSync } from "node:fs";
import { createPrivateKey, createPublicKey } from "node:crypto";
import { agentsTrustListUrl, createClientIdMetadataDocument } from "@proof.com/proof-vc-server";

// Your domain host
const domain = "";
// The exact URL at which the Client ID Metadata Document is hosted.
const CLIENT_ID = `https://${domain}/.well-known/proof-client.json`;
const privateKey = createPrivateKey(readFileSync("private-key.pem"));
const publicJwk = createPublicKey(privateKey).export({ format: "jwk" });
// "sandbox" for testing, "production" for production.
const environment = "sandbox";

const app = express();

const document = await createClientIdMetadataDocument({
  environment,
  clientId: CLIENT_ID,
  // Your business name, shown to users when they approve
  // a credential presentation for your endpoints.
  clientName: "Acme Corporation",
  // The agents allowed to use x401 with Proof's Cloud Wallet at your endpoints:
  // your own agents (by URL) or Proof's trusted list with `agentsTrustListUrl`.
  redirectUris: [agentsTrustListUrl(environment)],
  jwks: [publicJwk],
});

app.get("/.well-known/proof-client.json", (req, res) => {
  res.json(document);
});

export { app, CLIENT_ID };
📘

Agents trust list

agentsTrustListUrl defines which trusted agents use x401 and Proof's Cloud Wallet at your endpoints. Proof publishes its list at https://api.proof.com/.well-known/agents-trust-list. See the OID4VP over the Digital Credentials API expected_origins parameter for details.

Proof's Cloud Wallet accepts an OID4VP request only when its expected_origins are listed in the CIMD's redirectUris.

4. Register your OAuth Client ID Metadata Document

Enroll on Proof as an x401 Verifier by registering your CIMD, then follow the instructions received by email to complete your enrollment.

npx @proof.com/proof-vc-server enroll https://example.com/.well-known/proof-client.json --email [email protected] --key private-key.pem --environment sandbox
📘

Email address

The email address must be on the registrable domain of your CIMD's host. www. is treated as the apex domain. For example, [email protected] works for https://example.com, https://www.example.com and https://app.example.com.

5. Protect your endpoint

Create an x401Middleware that inspects the request's PROOF-RESPONSE header and cryptographically verifies the presented vp_token containing the Verifiable Credential.

import { app, CLIENT_ID } from "./client-metadata.mjs";
import { readFileSync } from "node:fs";
import { createPrivateKey, randomUUID } from "node:crypto";
import { verifier as x401, HEADER, DC_API_PROTOCOL } from "@proof.com/x401-node";
import { agentsTrustListUrl, createClient, createVerifier } from "@proof.com/proof-vc-server";

const privateKey = createPrivateKey(readFileSync("private-key.pem"));
const environment = "sandbox";

const proofClient = createClient({
  environment,
  clientId: CLIENT_ID,
  // Signs your OID4VP requests.
  useSecuredAuthorizationRequest: true,
  privateKeyFactory: () => privateKey,
});
const proofVerifier = createVerifier({
  environment,
  clientId: CLIENT_ID,
  // Fetches the detached signatures of the vp_token from Proof. See step 6.
  privateKeyFactory: () => privateKey,
});
// Nonces of pending OID4VP requests. Use a shared store in production.
const nonces = new Set();

async function x401Middleware(req, res, next) {
  const response = req.get(HEADER.PROOF_RESPONSE);

  if (response === undefined) {
    // Prevents replay attacks: each nonce is accepted once.
    const nonce = randomUUID();
    nonces.add(nonce);
    // A signed OID4VP request over the Digital Credentials API.
    const request = await proofClient.signedDcApiRequest({
      // See https://dev.proof.com/docs/verify-a-credential#scopes-and-claims
      scope: "urn:proof:params:scope:verifiable-credentials:basic",
      nonce,
      // A subset of the redirectUris of your CIMD.
      expectedOrigins: [agentsTrustListUrl(environment)],
    });
    // The PROOF-REQUEST header, following the x401 protocol.
    const payload = x401.buildPayload({
      credentialRequirements: {
        digital: {
          requests: [{ protocol: DC_API_PROTOCOL.SIGNED, data: { request } }],
        },
      },
    });
    return res
      .status(401)
      .set(HEADER.PROOF_REQUEST, x401.encodePayload(payload))
      .send("x401 PROOF-RESPONSE required");
  }

  try {
    const artifact = x401.decodeResultArtifact(response);
    // The credential result is inline, or behind a URL your endpoint fetches.
    let credentialResult = null;
    if (artifact.credential_result_uri === undefined) {
      credentialResult = artifact.credential_result;
    } else {
      const response = await fetch(artifact.credential_result_uri);
      credentialResult = await response.json();
    }
    // Verifies the vp_token, issued by Proof's Certificate Authority.
    // See https://dev.proof.com/docs/proof-certificate-authority
    const presentation = await proofVerifier.verifyVPToken({
      encodedVPToken: credentialResult.data.vp_token,
      aud: CLIENT_ID,
    });
    req.credential = presentation.proof_id_default[0];
    if (!nonces.delete(req.credential.getNonce())) {
      return res.status(401).send("x401 PROOF-RESPONSE replayed");
    }
  } catch {
    return res.status(401).send("x401 PROOF-RESPONSE invalid");
  }
  next();
}

app.get("/protected", x401Middleware, (req, res) => {
  // Reachable by user agents presenting a valid Verifiable Credential.
  res.json({ message: "Access granted", claims: req.credential.toJSON() });
});

app.listen(3000);
⚠️

Prevent replay attacks

Verifiers MUST compare req.credential.getNonce() with the nonce of their OID4VP request and accept each nonce only once.

📘

Claims and scopes

Learn about the claims supported by Proof's Verifiable Credentials and how to request them with scopes.

6. (Optional) Use with x402

You can decide to pay for x401 transactions with x402. When paying with x402, Proof delivers x401 presentations with detached signatures: the vp_token is readable, but not verifiable until your endpoint fetches its signatures from Proof, authenticated with the key pair of step 2:

POST https://api.proof.com/verifiable-credentials/v1/x401-signatures

verifyVPToken fetches the signatures for you. The endpoint responds 402 Payment Required when the presentation is not paid yet, and accepts x402 payments in USDC on Base (version 2, scheme exact). Pass an x402-capable fetch to the verifier to pay automatically:

npm install @x402/fetch @x402/evm viem
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

// Your wallet, funded with USDC on Base (Base Sepolia in sandbox).
const wallet = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);

// Replaces proofVerifier from step 5.
const proofVerifier = createVerifier({
  environment,
  clientId: CLIENT_ID,
  privateKeyFactory: () => privateKey,
  // Pays 402 Payment Required responses with x402.
  fetch: wrapFetchWithPaymentFromConfig(fetch, {
    schemes: [{ network: "eip155:*", client: new ExactEvmScheme(wallet) }],
  }),
});
📘

Paying without x402

Verifiers who do not use x402 can add a credit card or link their bank account to their Proof account. Presentations are delivered with their signatures and can be verified immediately. They are charged to the default payment method.

Learn how the signatures are fetched and paid in Use with x402.

7. Test your endpoint

The endpoint returns HTTP 401 with a proof-request header. Inspect the Digital Credentials Presentation request, then the OID4VP request it carries:

curl -so /dev/null -w '%header{proof-request}' https://example.com/protected \
  | jq -R 'gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'
curl -so /dev/null -w '%header{proof-request}' https://example.com/protected \
  | jq -R 'gsub("-";"+") | gsub("_";"/") | @base64d | fromjson
    | .credential_requirements.digital.requests[0].data.request
    | split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'

Then ask your agent to fetch the protected resource with Proof's Cloud Wallet:

Please install the Proof MCP https://mcp.proof.com/mcp, follow its instructions, then fetch <your protected URL>

With a valid PROOF-RESPONSE, the endpoint returns 200 and the verified claims.

Go to production

Set environment to "production" and enroll:

npx @proof.com/proof-vc-server enroll https://example.com/.well-known/proof-client.json --email [email protected] --key private-key.pem

Cloudflare Workers

x401 also ships as a Cloudflare Workers library, @proof.com/x401-workers. Install it and plug x401 on top of your routes, with Hono or a plain Worker.

npm install @proof.com/x401-workers
import { Hono } from "hono";
import { agentsTrustListUrl, d1NonceStore } from "@proof.com/x401-workers";
import { x401, type X401Variables } from "@proof.com/x401-workers/hono";

type Env = { DB: D1Database; SIGNING_JWK: string };

const app = new Hono<{ Bindings: Env; Variables: X401Variables }>();

// Every route under /protected now requires a Verifiable Credential.
app.use(
  "/protected/*",
  x401<{ Bindings: Env }>((c) => ({
    clientId: CLIENT_ID,
    privateKeyFactory: () => JSON.parse(c.env.SIGNING_JWK),
    nonceStore: d1NonceStore(c.env.DB),
    scope: "urn:proof:params:scope:verifiable-credentials:basic",
    expectedOrigins: [agentsTrustListUrl(environment)],
  })),
);

app.get("/protected", (c) => c.json(c.var.x401.proof_id_default[0]?.toJSON()));

export default app;

Find out more in Use on Cloudflare Workers.

Resources


Did this page help you?