This is the quickstart told as one story: what the provider publishes, what the broker signs, what the provider returns and what lands in the inbox. Every value below is either a live document or a synthetic identifier in the shape the tools produce.
The relying party is VaporCloud, a mock infrastructure provider at skdl.dev. It is a real implementation of agent-identity-v1 with fictional appliances, which makes it useful for a first run: nothing you do there can cost money.
The discovery document, field by field
curl -sS https://skdl.dev/.well-known/agent-signup.json -o agent-signup.json| Field | Live value | Why it is there |
|---|---|---|
protocol |
agent-identity-v1 |
The contract version. Anything else means the service does not support this protocol. |
audience |
vaporcloud |
The stable service identifier. It becomes the assertion aud. |
provider_name |
VaporCloud |
Human-readable name, for display only. |
spec |
https://github.com/agentregistrar/spec/blob/v1.2.0/agent-identity-v1.md |
Optional pointer to the exact specification the provider implements. |
provider_url |
https://skdl.dev |
The provider origin, used to bind the context pointer to the provider's domain. |
contact |
security@skdl.dev |
Where to report a security issue or abuse. |
issuer_allowlist |
["https://agentregistrar.dev"] |
The brokers this provider accepts. Exact match, no wildcards. |
required_scopes |
["tos:accept", "signup:free_tier"] |
The scopes the assertion must carry. Your agent holds both by default. |
terms.version |
2026.1 |
The terms revision, for humans. |
terms.url |
https://skdl.dev/terms |
The URL the human can read. It becomes accept_tos_url. |
terms.sha256 |
sha256:e854d9c2...fec5 |
The hash of the exact terms bytes. Recompute it; do not trust it. |
terms.text |
the complete terms | The text the agent passes back to prove it read what it accepted. |
endpoints.agent_signup |
https://skdl.dev/api/signup/agent |
The only endpoint in the protocol. |
extensions.dev.vaporcloud |
{ "support_url": "..." } |
Namespaced vendor metadata. Not a capability field. |
What is absent matters as much: no SKU list, no provisioning endpoint, no pricing, no quota, no API documentation. Those belong in the post-signup guide, because a discovery document is fetched before any account exists and must not advertise capability.
Recompute the terms hash from the bytes you received:
node -e 'const d=require("./agent-signup.json");const h=require("node:crypto").createHash("sha256").update(Buffer.from(d.terms.text,"utf8")).digest("hex");console.log("sha256:"+h);console.log("matches:",("sha256:"+h)===d.terms.sha256)'sha256:e854d9c2e7446cc132861235fa8e1c14c062c32c176b4f34d6845065cef5fec5
matches: trueWhat the broker signs
The default call is identity_signup, which fetches discovery, applies the
provider verification and human policy, and takes the service plus the complete
terms text. The advanced identity_prove below takes the discovery values
directly and is useful when you need the raw claims. In both, the request
includes the complete terms text:
{
"audience": "vaporcloud",
"accept_tos_url": "https://skdl.dev/terms",
"accept_tos_hash": "sha256:e854d9c2e7446cc132861235fa8e1c14c062c32c176b4f34d6845065cef5fec5",
"accepted_tos_text": "<the complete terms.text as received>",
"requested_scopes": ["tos:accept", "signup:free_tier"]
}requested_scopes is optional and defaults to ["tos:accept", "signup:free_tier"]; every entry must already be delegated to the agent. force_new is optional and creates a second consent event for the same terms, which is rarely what you want.
The returned claims are the exact payload that was signed:
{
"iss": "https://agentregistrar.dev",
"sub": "agt_01JAB2C3D4E5F6G7H8J9K0MN1P",
"aud": "vaporcloud",
"jti": "jti_01JAB2C3D4E5F6G7H8J9K0MN1P",
"iat": 1792605731,
"exp": 1792606031,
"agent": {
"id": "agt_01JAB2C3D4E5F6G7H8J9K0MN1P",
"name": "opencode-macbook",
"email": "opencode-macbook-1a2b3c@agents.agentregistrar.dev",
"harness": "opencode-v2"
},
"human": {
"id": "6d0f...",
"name": "You",
"email": "you@example.com",
"verified": true
},
"delegation": {
"master_tos_version": "2026-10-01",
"master_tos_hash": "sha256:...",
"accepted_rp_tos_url": "https://skdl.dev/terms",
"accepted_rp_tos_hash": "sha256:e854d9c2...fec5",
"accepted_at": 1792605731,
"scopes": ["tos:accept", "signup:free_tier"]
}
}Three details are easy to get wrong.
- The scopes are at
delegation.scopes. There is no top-levelscopeorscopesclaim. The tool also returns a flatscopesarray so you never have to reach into the claims for the common case. delegation.accepted_atis the consent time, not the mint time. A fresh assertion minted later against the same acceptance keeps the original timestamp.expis five minutes afteriat. An assertion is a short-lived bearer token, so treat it like a password for those five minutes.
The result also contains assertion.jwt, which is the only thing you transmit, and assertion.jwt_sha256, which is useful for correlating logs without logging the token.
What the provider does with it
export ASSERTION_JWT="<assertion.jwt from identity_signup or identity_prove>"
curl -sS -X POST https://skdl.dev/api/signup/agent \
-H "Authorization: Bearer $ASSERTION_JWT" \
-H "Content-Type: application/json" \
-d '{}'VaporCloud verifies the signature against https://agentregistrar.dev/.well-known/jwks.json, checks iss against its allowlist, aud, exp, the scopes, human.verified and the accepted terms hash. Then it creates or resolves the account.
A created account returns 201:
{
"account_id": "7b0c9e2a-4f1d-4c8a-9e37-2d5b6a1c8f04",
"resolution": "created",
"vapor_api_key": "vc_live_<32 hex>",
"signup_token": "<signed receipt>",
"signup_token_jti": "srp_01jab...",
"context": {
"url": "https://skdl.dev/docs/SKILL.md",
"sha256": "sha256:93691b1998f6d6c0fdddf1ec8b65c9a735ae7fb5c48abca3e157b8e00dfc84c1"
},
"credential_recovery": {
"available": true,
"method": "rotate",
"endpoint": "https://skdl.dev/api/account/rotate-key",
"guide_url": "https://skdl.dev/docs/SKILL.md"
},
"attribution": {
"agent_id": "agt_01JAB2C3D4E5F6G7H8J9K0MN1P",
"agent_email": "opencode-macbook-1a2b3c@agents.agentregistrar.dev",
"human_owner": "You (you@example.com)",
"human_email": "you@example.com",
"accepted_tos_hash": "sha256:e854d9c2...fec5"
}
}The attribution block is the provider echoing back what it verified. It is useful for a human reading a receipt; it is not a second proof.
Created versus resolved
resolution is the provider telling you which path ran.
"created": a new account. The response carries the show-once credential."resolved": an account already existed for this agent and service. No credential is returned, and the provider suppresses the welcome and legal mail because nothing changed.
A resolve is a success. It is the correct outcome for a dropped-response retry, for a second run after a crash, and for an agent that forgot whether it had signed up. What it is not is a credential channel: if you lost the key, a resolve will not give it back.
The broker treats the repeated signup the same way: identity_record_signup refreshes the existing proof row for the same agent, service and account instead of inserting a second one.
The receipt and the context pointer
signup_token is the provider's RP-signed receipt. Send it to identity_record_signup (optionally with the exact rp_attestation that contained the key) and the broker verifies the historical attestation and receipt signature, links it to the acceptance, and stores the proof. The v1.2 receipt has no exp; a legacy exp is ignored. The result repeats proof_source, deduplicated, the context pointer and the handoff reminder:
{
"context_url": "https://skdl.dev/docs/SKILL.md",
"context_sha256": "sha256:93691b1998f6d6c0fdddf1ec8b65c9a735ae7fb5c48abca3e157b8e00dfc84c1",
"handoff": {
"broker_stores_credentials": false,
"store_credentials_now": true,
"recovery": "provider-owned",
"context_url": "https://skdl.dev/docs/SKILL.md",
"note": "Persist any provider credential in a secure store before redacting, summarizing, or ending the task. The Broker never stores provider credentials. If a credential is lost, use the provider's recovery path (see its guide); do not re-run signup to recover it."
}
}Verify the guide before you act on it. The context URL must stay on the provider's domain, use HTTPS, and hash to the value in the response:
curl -sS https://skdl.dev/docs/SKILL.md -o SKILL.md
sha256sum SKILL.md93691b1998f6d6c0fdddf1ec8b65c9a735ae7fb5c48abca3e157b8e00dfc84c1 SKILL.mdIf the digest differs, do not follow the guide. Re-fetch, and if it still differs, stop and ask the human: the guide is hash-pinned precisely because it is vendor-authored data.
The guide is where the vendor's capabilities live. VaporCloud documents a provisioning call there, which is a vendor API and not part of the protocol:
export VAPOR_API_KEY="<the vapor_api_key you stored>"
curl -sS -X POST https://skdl.dev/api/provision \
-H "Authorization: Bearer $VAPOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sku": "quantum-toaster-x1", "name": "holiday-bagel-rig"}'A successful call returns 201 with an instance in READY, including an endpoint_url and a specs block. That response is VaporCloud's, and it can change without the protocol changing. The broker never sees it and never records it.
What lands in the inbox
Four messages exist after a completed signup: three land in the agent inbox, and the fourth is the same ledger receipt forwarded to the human owner's email.
| Message | Sender | Flagged legal | What it is |
|---|---|---|---|
[ToS Ledger] Agent "<name>" accepted Terms of Service for vaporcloud (#<jti>) |
Agent Registrar <alerts@agents.agentregistrar.dev> |
yes | The broker's acceptance receipt. Headers carry x-ledger-type, x-assertion-jti, x-service-audience, x-service-tos-hash and x-forwarded-to-human. |
[Fwd: Agent ToS Ledger] ... |
Agent Registrar <alerts@...> |
yes | The same receipt forwarded to the verified human email. It does not appear in the agent inbox. |
[VaporCloud] Welcome & Legal ToS Receipt (#<jti>) |
VaporCloud <noreply@agents.agentregistrar.dev> |
yes | The provider's welcome and legal receipt, sent only when the account was created. |
[VaporCloud] Instance Ready: <name> (<sku>) |
VaporCloud <noreply@...> |
no | Provisioning confirmation, sent after a vendor provisioning call, not after signup. |
Read the acceptance receipt to confirm what was recorded:
{ "name": "inbox_read", "arguments": { "message_id": "5c1f..." } }The body is a fixed ledger receipt: assertion JTI, timestamp, target service, accepted ToS URL and hash, master agreement version. It is the human-readable copy of the ledger row; the portal is the system of record.
What went wrong in the first live run
The first end-to-end run on 2026-10-18 worked at the protocol level and still produced three mints, two signup posts and one provision. The failures were interface failures, and each one now has a specific defense.
The orphan acceptance. The agent minted an assertion, then decoded the JWT and guessed claims.scope instead of claims.delegation.scopes. It aborted after the mint. The acceptance and the ledger mail existed, but no signup followed. Today the result carries assertion.claims and a flat scopes array, so there is no reason to decode or guess. The acceptance remains a real, recorded consent event; it is not a duplicate obligation.
The duplicate mail. The agent minted again for the same terms on its next attempt, and each mint recorded another acceptance and sent another ledger mail. Today an identical call while the assertion is unexpired returns the same JWT with reuse.reused: true, creates no acceptance and sends no mail. After expiry a fresh assertion references the same acceptance.
The lost credential. The agent redacted the provider key from its transcript, then re-ran signup to "re-resolve" the account. Today signup resolves the account and returns no key, so that retry cannot work: rotation is the documented recovery path.
Cleanup
Revoke the agent in the portal when you are done with the demo. That stops new assertions and further identity and inbox calls for that identity. It does not delete the VaporCloud account, invalidate the key you stored, or undo the accepted terms; those remain the provider's and the ledger's record.
If you want the credential gone, delete it from your secret store and, if the account matters, rotate it with the provider. Revoking the broker identity is not a credential revocation.
Next
- Identity lifecycle for the state model behind these steps.
- Credentials and recovery for the recovery path in detail.
- ToS and ledger for what the human is accountable for.