This is the shortest conformant path. You write one terms file, hash its exact
bytes, generate one JSON document, serve it over HTTP and validate it. The last
step returns a stable error from a stub signup route. example.com and
example-rp are synthetic values; replace them with your own origin and
audience.
The base flow is free and needs no broker account, no SDK and no network call to the broker to accept an assertion. A provider account is optional and is required only to become a verified provider, issue authenticated receipts, or report abuse with standing.
Before you start
- Node 22 or newer, with
npmorpnpm. - An HTTPS origin for production.
http://localhostandhttp://127.0.0.1are the documented development exception. - A public page where a human can read the same terms you will hash.
- No broker account, no SDK and no network call to the broker are required for the conformant path.
1. Write the terms as one text file
The complete agreement the agent accepts lives in terms.txt. This example is synthetic. The trailing newline is part of the file and therefore part of the hash.
mkdir provider-quickstart
cd provider-quickstart
cat > terms.txt <<'EOF'
Example RP Terms of Service
Version: 2026.1
These terms govern accounts created through a brokered agent signup. The agent
acts for a verified human owner, and that human owner is bound by these terms.
1. The service is provided as-is, with no uptime guarantee.
2. The human owner is responsible for the agent's actions.
3. Either party may terminate the account at any time.
EOFThe content rules are in ToS binding. Do not trim, re-wrap or extract the text from HTML when you hash it.
2. Generate the document and its hash
Save this as build-discovery.mjs. It reads the exact bytes of terms.txt, computes the digest and writes the document, so the text and hash cannot drift.
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
const text = readFileSync("terms.txt", "utf8");
const sha256 = "sha256:" + createHash("sha256").update(text, "utf8").digest("hex");
const document = {
protocol: "agent-identity-v1",
audience: "example-rp",
provider_name: "Example RP",
provider_url: "https://example.com",
contact: "security@example.com",
issuer_allowlist: ["https://agentregistrar.dev"],
required_scopes: ["tos:accept"],
terms: {
version: "2026.1",
url: "https://example.com/terms",
sha256,
text,
},
endpoints: {
agent_signup: "https://example.com/api/signup/agent",
},
};
writeFileSync("agent-signup.json", JSON.stringify(document, null, 2) + "\n");
console.log(sha256);node build-discovery.mjsExpected output:
sha256:6c18126f93de8d1972589ca94633ca82e76bd9ba1f0592618b0105753f3501d6The generated document:
{
"protocol": "agent-identity-v1",
"audience": "example-rp",
"provider_name": "Example RP",
"provider_url": "https://example.com",
"contact": "security@example.com",
"issuer_allowlist": [
"https://agentregistrar.dev"
],
"required_scopes": [
"tos:accept"
],
"terms": {
"version": "2026.1",
"url": "https://example.com/terms",
"sha256": "sha256:6c18126f93de8d1972589ca94633ca82e76bd9ba1f0592618b0105753f3501d6",
"text": "Example RP Terms of Service\nVersion: 2026.1\n\nThese terms govern accounts created through a brokered agent signup. The agent\nacts for a verified human owner, and that human owner is bound by these terms.\n\n1. The service is provided as-is, with no uptime guarantee.\n2. The human owner is responsible for the agent's actions.\n3. Either party may terminate the account at any time.\n"
},
"endpoints": {
"agent_signup": "https://example.com/api/signup/agent"
}
}Every field above is identity-only. The allowed and forbidden fields are in Discovery fields. Do not add product catalogs, SKUs, provisioning endpoints or OTP endpoints.
3. Serve it
Save this as serve.mjs. The discovery route returns JSON with no authentication and no redirect; the signup route is a stub that returns one stable error.
import { createServer } from "node:http";
import { readFileSync } from "node:fs";
const document = readFileSync("agent-signup.json", "utf8");
const server = createServer((req, res) => {
if (req.url === "/.well-known/agent-signup.json" && req.method !== "GET") {
res.writeHead(405, { allow: "GET" });
return res.end();
}
if (req.url === "/.well-known/agent-signup.json") {
res.writeHead(200, {
"content-type": "application/json",
"cache-control": "public, max-age=60",
"access-control-allow-origin": "*",
});
return res.end(document);
}
if (req.url === "/api/signup/agent" && req.method !== "POST") {
res.writeHead(405, { allow: "POST" });
return res.end();
}
if (req.url === "/api/signup/agent") {
// Stub: real verification is in the generic HTTP guide.
res.writeHead(401, { "content-type": "application/json" });
return res.end(JSON.stringify({
error: "invalid_assertion",
error_description: "Signature or JOSE header verification failed",
retryable: false,
}));
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not_found" }));
});
server.listen(3001, "127.0.0.1", () => console.log("listening on http://127.0.0.1:3001"));node serve.mjsExpected output:
listening on http://127.0.0.1:3001In another terminal, check the transport:
curl -sS -D - -o /dev/null http://127.0.0.1:3001/.well-known/agent-signup.jsonExpected, in part:
HTTP/1.1 200 OK
content-type: application/json
cache-control: public, max-age=60
access-control-allow-origin: *4. Validate the document
Two paths are supported. Use the one that fits your project.
Path A, from a clone of the spec repository:
git clone https://github.com/agentregistrar/spec
cd spec
pnpm install
pnpm --silent validate:discovery http://127.0.0.1:3001/.well-known/agent-signup.json --json --probe-jwksPath B, with the published package in any project. The package has no bin entry, so run its TypeScript source with tsx:
npm install @agentregistrar/discovery@1.2.0 tsx@4.23.15
npx tsx node_modules/@agentregistrar/discovery/src/validate-discovery.ts \
http://127.0.0.1:3001/.well-known/agent-signup.json --json --probe-jwksBoth paths use --probe-jwks, so both report one probe. Expected from either path, shortened here:
{
"validator": "agent-identity-v1",
"validatorVersion": "1.2.0",
"mode": "url",
"valid": true,
"exitCode": 0,
"protocol": "agent-identity-v1",
"audience": "example-rp",
"providerName": "Example RP",
"errors": [],
"warnings": []
}The v1.2 result also carries separate conformance and attestation objects; a
document with no rp_attestation reports attestation.status: "unverified" and
still exits 0. The exact field reference is in the conformance
validator.
Exit code 0 means valid. 1 means the document was read and rejected; 2
means it could not be fetched. --probe-jwks additionally checks that every
issuer publishes a usable JWKS and does not touch your signup endpoint.
If the validator fails on terms.sha256, hash the exact file bytes again; see Compute the ToS hash.
5. Return one stable error
Send a request with a malformed assertion:
curl -sS -X POST http://127.0.0.1:3001/api/signup/agent \
-H 'Authorization: Bearer test' \
-w '\nHTTP %{http_code}\n'Expected output:
{"error":"invalid_assertion","error_description":"Signature or JOSE header verification failed","retryable":false}
HTTP 401The envelope and the fixed code table are in Stable error codes. error_description is non-normative; agents branch on error.
6. Verify a real assertion
Replace the stub with a verifier. Start with Verify assertions, which includes a runnable local fixture and the mapping from each failed check to a stable code.
7. Register
Once the document passes live HTTPS validation, submit it with the evidence. The criteria and pull-request shape are in Validate and register.
Optional: become a verified provider
The base flow above is complete without an account. To add the optional broker-issued provider verification credential and the authenticated paths:
- Create a provider account with your RP broker and verify the organization
contact. A new account starts
pendingandfree. - Prove the origin and audience. Create an origin registration, then satisfy
either the HTTPS well-known challenge or the DNS TXT challenge. The first
verified origin promotes the account to
active. - Register a key. Generate an Ed25519 keypair locally, request a
proof-of-possession challenge, and register the public key with the required
receipt/abusepurposes. The broker never sees the private key. - Issue and renew the attestation. With a verified plan entitlement, an
active key, and a verified origin, issue the attestation and publish it as
rp_attestationin discovery. Renew on a 12-hour target against a 24-hour expiry. - Sign receipts and verify their history. Sign Contract E receipts with the
RP key named in the attestation, keep the exact attestation that contained
that key, and send
{signup_token, rp_attestation}to Contract F. - Report abuse with standing. Sign the
{rp_attestation, report}JWS and post it to the broker's advertisedabuse_endpoint. A receipt-linked report can apply a protective suspension pending review; an unlinked report opens a case without suspending.
This release ships plan and entitlement hooks only. There is no checkout,
pricing page, invoice, DPA, or provider terms text here. A free provider is
conformant and cannot issue an attestation; a verified plan requires a live
subscription set by the operator.
See Trust and limits for why verification is identity, not safety, and Validate and register for the registry's five-field verification snapshot.
Optional: receipt and context
A signup response may include an RP-signed signup_token and a hash-pinned
context pointer to your guide. Both are optional. The v1.2 receipt has no
exp; a legacy exp is ignored. See Issue a signup
receipt and Publish a context
guide.
Verify your work
-
terms.sha256equals the SHA-256 of the exactterms.textbytes. - The discovery route returns
200,application/json, no redirect and no authentication. - The validator exits
0with zero errors. - One failing signup request returns a fixed v1 error code.
- No forbidden vendor field appears at any depth, including under
extensions.
Next
- Publish discovery: framework routes, caching and terms changes.
- Hosting: generic HTTP: replace the stub with a working handler.
- Production checklist: keys, replay, logs and incidents.