skip to content

Issue a signup receipt

Sign a v1.2 Contract E signup_token with the RP key from the exact attestation, then record it through Contract F or identity_record_signup.

awaiting reviewupdated view markdown ->
for humans

Goal: after an account is created or resolved, optionally return an RP-signed signup_token that proves who signed up, for whom, under which terms and when.

The receipt is optional. If you issue one, the broker records it as attribution evidence. The claim set is defined in spec section 11; the field reference is Receipt and context.

A v1.2 receipt is signed by your relying-party key, not the broker. The key must be present in the exact provider attestation named by service.attestation_jti. There is no broker-key fallback.

What goes in the receipt

Claim Value
iss Your canonical RP origin, for example https://example.com. It must equal service.origin.
sub The agent id (agt_...).
aud The human broker origin, for example https://agentregistrar.dev.
jti A unique receipt id (srp_ + 26-character lowercase Crockford base32 ULID).
iat, signed_up_at Unix seconds. Both must fall inside the named attestation's [iat, exp] window.
assertion_jti The jti of the assertion presented at signup.
service.audience Your discovery audience.
service.origin Your canonical origin; must equal iss.
service.provider_name Your provider_name.
service.provider_url Optional canonical URL. It is not an identity binding.
service.account_id An opaque account reference. It is not a resource inventory.
service.attestation_jti The jti of the attestation that contained the signing key.
service.key_id The key id that signs the receipt. It must equal the JWS header kid.
agent.* id, name, email.
human.* id, name, email, verified.
tos.* version, url, sha256, optional master_tos_version.
context Optional { url, sha256 } pointer to your guide.
exp Do not add one. A v1.2 receipt has no expiry. A legacy exp is ignored.

Do not add service.instance_sku, service.endpoint_url, a free-form metadata object, resource credentials or API usage. The receipt is not a resource inventory. Vendor capability belongs in the context guide.

Sign it

Use the RP key registered with your broker and the exact attestation that contained it. If you use the shared implementation, signRpSignupReceipt builds the frozen header and payload and rejects a missing attestation key. The equivalent jose flow is below.

mkdir provider-receipt
cd provider-receipt
npm init -y
npm install jose@6.2.13

Save as sign-receipt.mjs. rp-private.jwk.json is your private key; attestation.jws is the exact attestation that was current at signup.

import { readFileSync } from "node:fs";
import { importJWK, SignJWT } from "jose";

const privateJwk = JSON.parse(readFileSync("rp-private.jwk.json", "utf8"));
const attestation = readFileSync("attestation.jws", "utf8").trim();
const payload = JSON.parse(
  Buffer.from(attestation.split(".")[1], "base64url").toString("utf8"),
);
const key = payload.keys.find((candidate) => candidate.kid === privateJwk.kid);
if (!key || !key.purposes.includes("receipt")) {
  throw new Error("the key is not a receipt key in this attestation");
}
const now = Math.floor(Date.now() / 1000);
if (now < payload.iat || now > payload.exp) {
  throw new Error("the attestation is not valid at signing time");
}

const claims = {
  iss: payload.rp.origin,
  sub: "agt_01j00000000000000000000000",
  aud: "https://agentregistrar.dev",
  jti: "srp_01j00000000000000000000000",
  iat: now,
  signed_up_at: now,
  assertion_jti: "jti_01j00000000000000000000000",
  service: {
    audience: payload.rp.audience,
    origin: payload.rp.origin,
    provider_name: "Example RP",
    provider_url: "https://example.com",
    account_id: "acct_opaque_0001",
    attestation_jti: payload.jti,
    key_id: privateJwk.kid,
  },
  agent: {
    id: "agt_01j00000000000000000000000",
    name: "opencode-agent",
    email: "opencode-agent@agents.example.com",
  },
  human: {
    id: "hum_01j00000000000000000000000",
    name: "Example Human",
    email: "human@example.com",
    verified: true,
  },
  tos: {
    version: "2026.1",
    url: "https://example.com/terms",
    sha256: "sha256:6c18126f93de8d1972589ca94633ca82e76bd9ba1f0592618b0105753f3501d6",
    master_tos_version: "2026.1",
  },
  context: {
    url: "https://example.com/docs/SKILL.md",
    sha256: "sha256:a3307e1b3904c7671f341730017f21bed656a85241b645c854b4f2887e7976ac",
  },
};

const signupToken = await new SignJWT(claims)
  .setProtectedHeader({
    alg: "EdDSA",
    typ: "agent-signup-receipt+jwt",
    kid: privateJwk.kid,
  })
  .sign(await importJWK(privateJwk, "EdDSA"));

console.log(signupToken);

There is no exp in the payload. The receipt does not expire; its validity is the historical question "did this key and identity exist at iat?".

Return it and record it

Return the token in the signup response as signup_token, next to the credentials:

{
  "resolution": "created",
  "account_id": "acct_opaque_0001",
  "api_key": "sk_live_<show-once>",
  "signup_token": "<compact JWS>",
  "context": {
    "url": "https://example.com/docs/SKILL.md",
    "sha256": "sha256:a3307e1b3904c7671f341730017f21bed656a85241b645c854b4f2887e7976ac"
  }
}

The agent records it with identity_record_signup, or you deliver it to the broker through Contract F:

SIGNUP_TOKEN="<compact JWS from the previous step>"
RP_ATTESTATION="<the exact attestation JWS>"
curl -sS -X POST https://agentregistrar.dev/api/broker/signup-receipt \
  -H 'Content-Type: application/json' \
  -d "{\"signup_token\":\"$SIGNUP_TOKEN\",\"rp_attestation\":\"$RP_ATTESTATION\"}"

Contract F accepts exactly {signup_token, rp_attestation}. It verifies the effective RP-broker trust list before any JWKS fetch, the historical attestation, the receipt signature with the named key, the origin/audience binding, and the receipt sub against the acceptance. It records proof_source:"rp_callback". A duplicate jti with the same body returns deduplicated:true; a different body is 409 signup_conflict.

If the attestation is unavailable, Contract F returns attestation_unavailable rather than using a newer one. A transient broker metadata/JWKS outage is 503 rp_broker_unavailable and is retryable. An unauthenticated legacy body is 401 with a stable code.

Tier 2 and self-report

identity_record_signup accepts either:

  • {signup_token, rp_attestation?}: the same RP-signed receipt. If the exact attestation is omitted, the broker fetches discovery at service.origin and requires the presented attestation's jti to equal service.attestation_jti; it never substitutes a newer attestation. This records proof_source:"rp_signed".
  • {self_report}: an unsigned report for an RP that issued no receipt. It has no proof token, assertion, verified origin, or verification row, records agent_self_reported, cannot overwrite a stronger proof, and still requires a real recorded acceptance.

A pre-v1.2 broker-signed receipt is rejected for a new proof. Historical rows stay legacy_broker_signed and are never relabeled.

Resolve and retry

  • Create the account once. On a repeat, resolve it and return resolution: "resolved".
  • On resolve, omit the provider credential. Do not send duplicate welcome or legal mail.
  • You may mint a fresh receipt on resolve so a dropped-response retry can still record the proof.
  • The receipt jti identifies the receipt, not the account. Do not use it as the account key.
  • service.account_id is opaque. It may be a database id or a handle. It must not encode SKUs, regions or resource state.

Verify your work

  • The JWT header is alg: EdDSA, typ: agent-signup-receipt+jwt, and kid equals service.key_id.
  • The signing key appears in the exact attestation and has receipt in its purposes.
  • The payload has no exp, and iat/signed_up_at are inside the attestation window.
  • iss equals service.origin and the attestation rp.origin; aud is the broker origin.
  • service.attestation_jti is the exact attestation that contained the key.
  • context.url and context.sha256 match the guide you serve, and the domain matches the signup endpoint.
  • A resolve returns no credential and no duplicate mail.

Next

sections

type to search.

up/down navigateenter selectesc close