Skip to content

Authentication

RAMP authentication answers three questions for every request:

  1. Who are you? — Requester identity, proven by Ed25519 signature over a domain-bound key
  2. What can you access? — Scopes carried in the request (and optionally in a delegation token)
  3. Who authorized you? — Delegation chain from a principal (organization, user, upstream agent) via holder-bound JWTs that can be narrowed without round-tripping to the issuer

These three layers are independent. A public agent with no delegation still authenticates (layer 1) and declares scopes (layer 2, defaulting to public access). An enterprise agent adds a delegation token (layer 3) that binds the agent to a principal’s entitlements with time and spend limits.

Every RAMP request carries a Requester message that identifies who is making the request.

FieldTypeDescription
idstringUnique requester identifier (e.g., "agent-research-bot-001")
domainstringDomain for public key lookup — keys at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT)
typeRequesterTypeWhat kind of entity is making the request
namestring (optional)Human-readable name (e.g., "Acme Research Assistant")
billing_refstring (optional)Opaque billing reference for invoicing / cost attribution (not an entitlement; access is via scopes/delegation)
scopesrepeated stringEntitlement scopes — what the requester can access
delegationDelegation (optional)Holder-bound JWT delegation token from a principal
extStructImplementer-specific extensions
ext_criticalrepeated stringCritical extension keys (COSE crit pattern)
ValueNameDescription
0REQUESTER_TYPE_UNSPECIFIEDNot specified
1REQUESTER_TYPE_AGENTAutonomous AI agent (LLM, RAG system, research bot)
2REQUESTER_TYPE_HUMAN_TOOLHuman using an AI-powered tool (copilot, assistant)
3REQUESTER_TYPE_SERVICEEnterprise service account (automated pipeline, cron job)
4REQUESTER_TYPE_DELEGATEDAgent acting on behalf of a user (delegated identity)
5REQUESTER_TYPE_RESEARCHResearch pipeline (batch data collection, model training)

The Exchange resolves a requester’s public key by fetching {domain}/.well-known/ramp.json (a WellKnownManifest with role=ROLE_AGENT) and selecting the JsonWebKey in public_keys whose kid matches the request signature:

// GET https://research.acme.com/.well-known/ramp.json
{
"ver": "1.0",
"role": "ROLE_AGENT",
"domain": "research.acme.com",
"contact": "platform@acme.com",
"public_keys": [
{
"kid": "research-acme-2026-q2",
"kty": "OKP",
"crv": "Ed25519",
"use": "sig",
"alg": "EdDSA",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"not_before": "2026-04-01T00:00:00Z",
"not_after": "2026-10-01T00:00:00Z"
}
]
}

This ties cryptographic identity to DNS ownership — if you control the domain, you control the key.

RAMP’s authentication stack is automatically compatible with the IETF WebBotAuth working group specification (standards track, targeting April 2026). Both use:

  • RFC 9421 HTTP Message Signatures for request signing
  • Ed25519 (EdDSA) as the signature algorithm
  • JWK-formatted keys at well-known endpoints

When WebBotAuth ships as an RFC, RAMP agents will be recognised as authenticated bots by any CDN or origin server that implements it (Cloudflare is already deploying). No changes to RAMP’s authentication are required — the crypto is identical.

WebBotAuth solves identity (“this bot is who it claims to be”). RAMP adds the transaction layer on top of that identity (“this bot is authorised to access this content at this price”).

The Requester message carries identity semantics only — it has no signature field. Request authentication is an RFC 9421 HTTP Message Signature over the HTTP request itself (covering @method, @target-uri, and a Content-Digest of the body), carried in the Signature / Signature-Input HTTP headers rather than in the protobuf body. This separates identity semantics (the Requester message) from cryptographic proof (the HTTP signature). The Exchange verifies it by:

  1. Reading the keyid and covered components from the request’s Signature-Input header
  2. Extracting domain from the Requester message and fetching {domain}/.well-known/ramp.json, selecting the JsonWebKey in public_keys whose kid matches the keyid (and whose [not_before, not_after) window covers now)
  3. Reconstructing the signed content from the declared components and verifying the Ed25519 signature in the Signature header against that key
  4. Confirming the Content-Digest matches the request body, and that the manifest domain matches the requester’s domain with role=ROLE_AGENT

If verification fails, the Exchange rejects the request. No fallback, no degraded mode. See How RFC 9421 Signatures Work below for the header details.

The delegation token is a JWT (the default and fully-specified format) that is holder-of-key bound: the grant is tied to a specific key via the RFC 7800 cnf confirmation claim, and the holder proves possession by signing the request with that key (the same RFC 9421 signature it already sends). The token is not a bearer credential — copying it is useless without the matching private key.

Why not a plain bearer JWT? A bearer JWT is usable by anyone who holds it, and it can’t be narrowed for a sub-agent without a round trip to the issuer. RAMP fixes both with one mechanism: cnf holder-of-key binding (theft-resistance) plus chained cnf JWTs (offline delegation). A principal narrows a grant for an agent by issuing a child JWT — no call back to the original issuer.

Each delegation JWT carries cnf.jkt — the RFC 7638 JWK thumbprint of the key the grant is bound to. At request time the verifier computes the thumbprint of the key that signed the RFC 9421 request and checks it equals cnf.jkt. A thief who steals the token but not the private signing key cannot produce a valid request, so the token grants nothing. This is the same proof-of-possession mechanism as DPoP.

Delegation is a chain of cnf-linked JWTs, content owner → principal → agent:

  • Authority JWT — signed by the resource owner (the issuer). Carries the granted scope, exp, and cnf.jkt = the principal’s key. The owner’s public key is the verifier’s only trust anchor.
  • Delegation JWT — signed by the principal, narrowing the grant for a specific agent: scope ⊆ parent, shorter exp, cnf.jkt = the agent’s key. It carries the principal’s public key in its JOSE header (jwk) so the verifier can both link it to the authority’s cnf and verify it.
  • Request — signed by the agent (RFC 9421); the verifier checks thumbprint(request key) == delegation cnf.jkt.

Chain-linkage invariant: each JWT’s signer MUST be the key the parent named in cnf. The owner verifies the authority; authority.cnf pins the principal; the delegation’s header key must match that pin (and verifies the delegation); delegation.cnf pins the agent; the request key must match that pin. A token signed by a key not named upstream is rejected.

The chain narrows only — a child’s scope must be covered by its parent’s (see Scope Matching). The principal can delegate to a sub-agent (and that sub-agent further) by issuing another child JWT, entirely offline. Verifiers need only the owner’s public key; the principal and agent keys ride inside the chain.

The token is carried in the Delegation message, nested inside Requester:

FieldTypeDescription
principal_domainstringWho granted this delegation (domain for public key lookup)
principal_idstringPrincipal’s identifier (e.g., "user@acme.com", "marketdata.example.com")
scopesrepeated stringScopes granted — convenience mirror of the token’s scope claim
expires_atTimestampWhen this delegation expires. Exchange MUST reject expired tokens
max_spend_centsint64 (optional)Maximum spend in currency minor units (e.g. cents for USD)
tokenbytesToken bytes — a JWT (base64url JWS) by default
token_formatstringToken format: "jwt" (default) or "biscuit-v3" (optional)
revocation_uristring (optional)URI for real-time revocation checking (high-value transactions)
max_accessesint32 (optional)Maximum number of accesses allowed under this delegation. Exchange tracks count and denies with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.
quota_periodDuration (optional)How often access/spend counters reset. E.g., "720h" for monthly subscriptions. When absent, quota is lifetime (bounded only by expires_at).
issuerstring (optional)Token issuer — OIDC issuer URL (for JWT signature validation via OIDC→JWKS)
extStructImplementer-specific extensions
ext_criticalrepeated stringCritical extension keys (COSE crit pattern)

The scopes field is a convenience mirror — the Exchange MAY use it for fast catalog filtering before performing full token verification. The token’s claims are authoritative.

The token’s claims are the authoritative source of the grant (the plaintext Delegation fields are only the convenience mirror). RAMP defines a small registered claim vocabulary — the same registered-tokens + optional-fields + vendor-namespace pattern used elsewhere in the protocol:

ClaimJWT claimRequired?Meaning
holdercnf (with jkt)YesBinds the grant to a key by RFC 7638 thumbprint — verified per Verification (request-signing key MUST hash to cnf.jkt). Without it the token is bearer-usable.
issuerissYesWho granted it (resource owner / principal).
scopescopeNoGranted scopes (space-delimited; segment-wise matching — see Scope Matching).
expexpNo (recommended)Expiry.
max_spend_centsramp_max_spend_centsNoSpend cap (currency minor units).
max_accessesramp_max_accessesNoAccess-count cap.
quota_periodramp_quota_periodNoCounter reset period.

Rules:

  • Everything except the holder binding (cnf) is optional. An issuer that only wants scope + expiry sets just those; absent caps mean “no cap from the delegation” (the Exchange’s own limits still apply).
  • Vendors MAY add namespaced claims (vendor:claim).
  • Constraints fail closed. A claim a verifier cannot evaluate is binding by default — it MUST reject rather than ignore (a dropped constraint would fail open, granting more than intended). This mirrors Restriction.advisory: the issuer may mark a custom claim advisory to downgrade it to ignorable. (The issuer lists the mandatory-to-understand claims; an unrecognized non-advisory claim is rejected.)
  • ramp_-prefixed claim names are reserved for this registry. Vendors MUST use their own vendor: namespace and MUST NOT define ramp_* names, so registered claims can never be shadowed.

Biscuit alternative. token_format: "biscuit-v3" remains a permitted alternative for deployments that want deep multi-hop, in-place offline attenuation across mutually-distrusting intermediaries. The same registered claim names map onto Biscuit facts, and the same holder binding applies (the request-signing key must match the holder the token is sealed to). JWT is the default and what conformant implementations must support; Biscuit is an optional profile.

MarketData (resource owner) issues an authority JWT to the Acme principal, bound to Acme’s key:

// authority JWT — signed by marketdata.example.com (the owner key)
// header
{ "alg": "EdDSA", "typ": "JWT" }
// claims
{
"iss": "marketdata.example.com",
"scope": "quote:* earnings:*",
"max_spend_cents": 50000,
"exp": 1781787345,
"cnf": { "jkt": "<thumbprint of Acme's principal key>" }
}

Acme delegates to a research agent that only needs earnings data, narrowing scope and shortening the TTL — no call back to MarketData:

// delegation JWT — signed by Acme's principal key
// header carries the principal pubkey so the verifier links it to authority.cnf
{ "alg": "EdDSA", "typ": "JWT",
"jwk": { "kty": "OKP", "crv": "Ed25519", "x": "<Acme principal pubkey>" } }
// claims
{
"iss": "acme",
"scope": "earnings:*",
"exp": 1781701545,
"cnf": { "jkt": "<thumbprint of the agent's key>" }
}

The agent then signs its request (RFC 9421) with its own key. Zero round trips to MarketData — each delegation is a locally-issued child JWT.

The Exchange/edge verifies a delegation by (offline, using only the owner’s public key):

  1. Verify the authority JWT with the resource owner’s published key (the trust anchor). Read its scope, exp, and cnf.jkt.
  2. Verify each delegation JWT with the key in its own JOSE header jwk — but only after checking thumbprint(header key) == parent cnf.jkt (the chain-linkage invariant). Reject a JWT signed by a key the parent did not name. Check each child’s scope is covered by its parent’s.
  3. Verify holder binding — the key that signed the HTTP request (RFC 9421) MUST hash to the final delegation’s cnf.jkt. If not, reject (DENIAL_REASON_DELEGATION_INVALID). This is what makes a leaked token useless to a thief: possessing the bytes is not enough — you must control the bound private key. It is not optional and not skippable; an Exchange that omits it reduces the token to a bearer credential and breaks the anti-theft guarantee.
  4. Check exp on every JWT in the chain against current time.
  5. Optionally check revocation_uri for high-value transactions.

This is entirely offline — no callback to the issuer. The verifier needs only the owner’s published public key (cacheable); the principal and agent keys arrive inside the chain (header jwk + cnf.jkt).

Binding across hops. Each delegation re-binds to the next holder’s key (cnf.jkt of the child = the next agent), so at every hop the bound key is whoever now signs the request. The chain of JWT signatures carries the binding forward — there is no point at which the token reverts to bearer form.

Which signature binds in a brokered request. A forwarded request carries more than one RFC 9421 signature (the agent’s, plus a hop signature from each intermediary). The holder-binding check always matches the terminal holder — the key named in the final delegation’s cnf.jkt. In the default broker pass-through case the broker is a pure relay: it forwards the agent’s delegation unchanged, the agent remains the terminal holder, and the agent’s request signature is the one that must match cnf.jkt (the broker’s hop signature is provenance only). Only if the agent explicitly delegated to the broker (the broker issues a child JWT bound to its own key and signs the request) does the broker become the terminal holder and its signature bind. Intermediaries never silently become the holder.

When delegation verification fails, the Exchange returns a specific denial reason in the response:

Denial ReasonMeaning
DENIAL_REASON_QUOTA_EXCEEDEDSubscription access count exhausted for this period
DENIAL_REASON_DELEGATION_INVALIDDelegation token missing, unverifiable (broken chain linkage or holder-binding mismatch), expired, or it widens scope beyond its parent
LanguageLibraryNotes
Gogolang-jwt/jwtEdDSA (Ed25519) signing/verification, JOSE headers
TypeScriptjoseEdDSA, runs in Node and the browser
PythonPyJWT (+ cryptography)EdDSA

All use Ed25519 natively — the same key type as RAMP identity signatures — so the delegation token and the RFC 9421 request signature share one key type and one library surface.

Scopes are the mechanism by which the Exchange decides what resources to show a requester. The flow is:

  1. Agent sends ResourceQuery with Requester.scopes (and optionally a Delegation that also carries scopes)
  2. Exchange filters its catalog — only resources matching the declared scopes are returned
  3. Resources outside the scopes are not returned. The requester never learns they exist.

Scopes follow the pattern {domain}:{permission} or {profile}:{permission}:

ScopeMeaning
"credit:read"Can access credit reports (read-only)
"subscription:marketdata-2026"Has active MarketData subscription
"revshare:publisher-x"Party to an off-protocol revenue-share agreement (gates the FREE revshare term — see Licensing Terms)
"academic:*"Full access to academic resources
"internal:reports"Can access internal reports
"dist:US:CA"Multi-segment scope (colon-separated; matched segment-wise, see below)
"*"Unrestricted (public Exchange default)

Matching is segment-wise (:-separated). A granted scope G covers a required scope R iff, comparing segment by segment, each G segment equals the corresponding R segment or is *; a terminal * matches all remaining segments. There is no implicit prefix match. A grant that is more specific than what is required does not cover it — coverage requires G to be equal-to-or-broader-than R, never narrower (e.g. granted dist:US:CA does not cover required dist:US).

GrantedCoversDoes not cover
dist:*dist:US, dist:US:CA
dist:US:*dist:US:CAdist:EU
distdistdist:US
dist:US:CAdist:US:CAdist:US (grant narrower than required)
*everything

No scopes (empty): Exchange applies its default access policy — typically all publicly available resources. This is the common case for public agents.

Subscription scopes: A scope like subscription:nyt-2026 signals that the requester has a subscription entitlement. The Exchange returns subscription pricing (zero marginal cost) instead of per-request pricing.

Enterprise scopes: Specific {domain}:{permission} scopes grant access to restricted resources. The Exchange enforces RBAC by filtering the catalog to matching resources.

When a resource exists but the requester’s scopes are insufficient, the Exchange decides how to respond based on its disclosure policy:

  • Hide existence: Silent omission. The resource is simply not returned — no hint that it exists. Appropriate when the resource’s existence is itself sensitive.
  • Reveal it’s gated: Returns OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT in the OfferGroup.absence_reason field, telling the requester the resource exists but their scopes/subscription don’t cover it. Standard wherever access is subscription- or scope-gated — enterprise RBAC and public marketplace subscriptions alike.

This distinction prevents information leakage on public Exchanges while allowing enterprise environments to provide actionable diagnostics.

When a request passes through forwarding parties (e.g., Agent to Broker to Exchange), the forwarding chain is not carried in the message body at all. It lives entirely in the HTTP headers as a stack of RFC 9421 HTTP Message Signatures.

Each forwarding party adds one labeled signature to the Signature / Signature-Input headers:

  1. The agent signs the request first.
  2. Each subsequent broker adds its own labeled signature, covering the request plus the prior hop’s signature.

Because every signature commits to the signature before it, the ordered set of signatures is the forwarding chain: it is tamper-evident and order-bound. You cannot reorder, remove, or insert a hop without breaking the chain. There is no separate per-hop timestamp or payload-signature field — the RFC 9421 signature itself carries that proof.

The Exchange verifies the full chain by:

  1. Reading each labeled signature and its keyid from Signature / Signature-Input
  2. Resolving each keyid at {domain}/.well-known/ramp.json (by kid)
  3. Verifying every signature, confirming each later hop covers the prior hop’s signature
  4. Enforcing depth caps — RequestConstraints.max_hops and WellKnownManifest.max_intermediary_hops — by counting the signatures in the stack

When only the agent’s signature is present, the agent is querying the Exchange directly — no broker was involved.

See Multi-Hop Example (Broker) below for the concrete header layout.

Request signatures are RFC 9421 HTTP Message Signatures, carried in HTTP headers — not fields on any protobuf message. This separates identity semantics (the Requester message: who is making the request, scopes, delegation) from cryptographic proof (the HTTP signature attesting the request is authentic). No RAMP message has ever carried a request-signature field; the protobuf body carries identity only, and the forwarding chain is likewise a stack of labeled RFC 9421 signatures rather than an in-message hop list (see Multi-hop forwarding).

RFC 9421 defines a standard way to sign HTTP messages. Three headers work together:

HeaderPurpose
Content-DigestHash of the request body (binds the signature to the payload)
Signature-InputDeclares which components are signed, the key ID, and the algorithm
SignatureThe actual cryptographic signature

The Exchange verifies the signature by reconstructing the signed content from the declared components, fetching the signer’s public key via {domain}/.well-known/ramp.json (selected by kid), and verifying the Ed25519 signature.

An agent sends a signed request directly to the Exchange:

POST /ramp.v1.ExchangeService/DiscoverResources HTTP/1.1
Content-Type: application/json
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: agent=("@method" "@target-uri" "content-digest");keyid="agent-key";alg="ed25519"
Signature: agent=:dGVzdC1zaWduYXR1cmUtYWdlbnQ=:

The Signature-Input header declares:

  • Label: agent — identifies this signature in multi-signature scenarios
  • Covered components: @method (POST), @target-uri (the RPC path), content-digest (body hash)
  • keyid: agent-key — resolved via the agent’s .well-known/ramp.json (public_keys[].kid)
  • alg: ed25519 — same key type used throughout RAMP

When a request passes through a Broker, each hop adds its own signature. The Broker signs over the same components plus the agent’s existing signature, creating a verifiable chain:

POST /ramp.v1.ExchangeService/DiscoverResources HTTP/1.1
Content-Type: application/json
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: agent=("@method" "@target-uri" "content-digest");keyid="agent-key";alg="ed25519", broker=("@method" "@target-uri" "content-digest");keyid="broker-key";alg="ed25519"
Signature: agent=:dGVzdC1zaWduYXR1cmUtYWdlbnQ=:, broker=:dGVzdC1zaWduYXR1cmUtYnJva2Vy:

Each labeled signature (agent, broker) is independently verifiable. The Exchange checks every signature in the Signature header against the corresponding keyid in Signature-Input.

RFC 9421 is the only request-authentication mechanism. A request without a valid RFC 9421 signature (Signature + Signature-Input, with a Content-Digest over the body) is rejected — there is no proto-field fallback and no degraded mode. There were never any in-message signature fields to fall back to.

A signed retrieval_endpoint returned by a transaction is, by default, a bearer token: anyone holding it before expires_at can fetch. RAMP optionally binds the URL to the purchasing agent, DPoP-style (RFC 9449):

  • The Exchange embeds agent_identity_hash — the RFC 7638 JWK Thumbprint (SHA-256) of the agent’s Ed25519 request-signing key — inside the HMAC-signed URL, and echoes it on the response (TransactionResponse.agent_identity_hash, forwarded unchanged by the Broker on RAMPResponse.agent_identity_hash).
  • A capable delivery endpoint (edge function) verifies the binding fully offline: confirm the URL HMAC (proves the hash is Exchange-issued and untampered), then require the fetcher to present its public key and an RFC 9421 signature over the retrieval request, and check thumbprint(presented key) == agent_identity_hash. No JWKS fetch required.
  • Bound to the agent’s request-signing key, never to the principal/delegation — the agent is the fetcher, and is exactly one key per transaction.
  • Enforcement is NOT mandatory: a bearer-only signed-URL CDN that cannot run code falls back to HMAC + short TTL + TLS. RAMP reference implementations run on edge functions and do enforce it.

See Signed URL Verification for the full edge-function verification flow and the stolen-URL threat analysis.

A research bot queries a public Exchange. No delegation token, no special scopes — it gets whatever is publicly available.

{
"ver": "1.0",
"id": "sq-research-001",
"requester": {
"id": "research-bot-42",
"domain": "research.acme.com",
"type": "REQUESTER_TYPE_AGENT",
"name": "Acme Research Assistant",
"scopes": []
},
"uris": [
"https://cdn.ramp-protocol.org/premium/ai-funding-roundup"
],
"acceptable_restrictions": [
{ "axis": "FUNCTION", "values": ["SUMMARIZE"] }
]
}

The Exchange verifies the signature by fetching the public key from research.acme.com/.well-known/ramp.json, then returns all publicly available offers for the requested URI.

A financial analysis agent carries a MarketData-issued delegation JWT that grants access to subscription-tier data.

{
"ver": "1.0",
"id": "sq-finance-001",
"requester": {
"id": "finbot-alpha",
"domain": "fintech.example.com",
"type": "REQUESTER_TYPE_AGENT",
"name": "FinBot Alpha",
"billing_ref": "lic-marketdata-fintech-2026",
"scopes": [
"subscription:marketdata-2026",
"quote:*",
"earnings:*"
],
"delegation": {
"principal_domain": "marketdata.example.com",
"principal_id": "marketdata.example.com",
"scopes": ["quote:*", "earnings:*"],
"expires_at": "2026-03-20T18:00:00Z",
"max_spend_cents": 50000,
"token": "eyJhbGciOiJFZERTQSJ9.<claims: iss, scope, exp, cnf.jkt>.<sig>",
"token_format": "jwt"
}
},
"uris": [
"marketdata://quote/NVDA",
"marketdata://earnings/NVDA/2025-Q4"
],
"acceptable_restrictions": [
{ "axis": "FUNCTION", "values": ["RETRIEVE"] }
]
}

The Exchange:

  1. Verifies the request’s RFC 9421 HTTP Message Signature against fintech.example.com’s published key
  2. Verifies the delegation JWT — signed by marketdata.example.com’s key — and checks its cnf.jkt equals the thumbprint of the request signer (holder binding)
  3. Reads scopes from the token’s scope claim (quote:*, earnings:*)
  4. Returns subscription-tier pricing (zero marginal cost) for matching resources

An internal agent at a law firm carries a delegation JWT issued by the firm’s IAM system, granting access to legal and credit resources.

{
"ver": "1.0",
"id": "sq-legal-001",
"requester": {
"id": "legal-research-agent",
"domain": "agents.kirkland.com",
"type": "REQUESTER_TYPE_SERVICE",
"name": "Kirkland Legal Research",
"billing_ref": "lic-kirkland-enterprise-2026",
"scopes": [
"legal:case-law",
"credit:read"
],
"delegation": {
"principal_domain": "iam.kirkland.com",
"principal_id": "user@kirkland.com",
"scopes": ["legal:case-law", "legal:statutes", "credit:read"],
"expires_at": "2026-03-20T20:00:00Z",
"max_spend_cents": 100000,
"token": "eyJhbGciOiJFZERTQSJ9.<claims: iss, scope, exp, cnf.jkt>.<sig>",
"token_format": "jwt"
}
},
"uris": [
"westlaw://case/2025-us-456",
"duns://credit/123456789"
],
"acceptable_restrictions": [
{ "axis": "FUNCTION", "values": ["RETRIEVE", "SUMMARIZE"] }
]
}

The internal Exchange verifies the delegation JWT against iam.kirkland.com’s published key. Because the resource’s existence isn’t secret here, if the agent requests one outside its scopes the Exchange returns OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT rather than silently omitting it.

Delegated Sub-Agent (cnf-chained delegation)

Section titled “Delegated Sub-Agent (cnf-chained delegation)”

The Kirkland legal agent from the previous example delegates to a sub-agent that only needs credit data. It issues a child delegation JWT (bound to the sub-agent’s key) that removes legal access and lowers the spend cap.

{
"ver": "1.0",
"id": "sq-credit-001",
"requester": {
"id": "credit-checker-sub",
"domain": "agents.kirkland.com",
"type": "REQUESTER_TYPE_DELEGATED",
"name": "Kirkland Credit Sub-Agent",
"billing_ref": "lic-kirkland-enterprise-2026",
"scopes": [
"credit:read"
],
"delegation": {
"principal_domain": "iam.kirkland.com",
"principal_id": "user@kirkland.com",
"scopes": ["credit:read"],
"expires_at": "2026-03-20T19:00:00Z",
"max_spend_cents": 20000,
"token": "eyJhbGciOiJFZERTQSJ9.<child claims: iss, scope, exp, cnf.jkt>.<sig>",
"token_format": "jwt"
}
},
"uris": [
"duns://credit/123456789"
],
"acceptable_restrictions": [
{ "axis": "FUNCTION", "values": ["RETRIEVE"] }
]
}

Key differences from the parent agent’s request:

  • type is REQUESTER_TYPE_DELEGATED — signals this agent acts on behalf of another
  • delegation.scopes is narrowed to ["credit:read"] (legal scopes removed)
  • delegation.max_spend_cents is reduced from 100000 ($1,000) to 20000 ($200)
  • delegation.expires_at is shortened (1 hour earlier)
  • The parent agent’s forwarding is recorded in the HTTP headers, not the message: the parent adds a labeled RFC 9421 signature, and the sub-agent adds its own signature over the request plus the parent’s (see Multi-hop forwarding)
  • The delegation is a JWT chain: the authority JWT (signed by IAM) plus a child JWT (signed by the parent agent) whose cnf.jkt binds the grant to the sub-agent’s key and whose scope is narrowed

The Exchange verifies the full JWT chain: IAM signed the authority JWT; the parent agent signed the child JWT (its signing key matches the authority’s cnf — chain linkage); the sub-agent’s request key matches the child’s cnf.jkt. No call to IAM was required for the delegation.

Every party publishes its keys in one place — {domain}/.well-known/ramp.json (WellKnownManifest.public_keys, role-tagged). There are no per-role key files and no separate keys endpoint; keys are inline JWKs.

PartyEndpointRole
Agent / Requester{domain}/.well-known/ramp.jsonROLE_AGENT
Intermediary{domain}/.well-known/ramp.jsonROLE_AGENT
Exchange{domain}/.well-known/ramp.jsonROLE_EXCHANGE
Delegation Issuer (principal / resource owner){domain}/.well-known/ramp.jsonROLE_AGENT (key signing delegation JWTs)
// GET https://agents.kirkland.com/.well-known/ramp.json
{
"ver": "1.0",
"role": "ROLE_AGENT",
"domain": "agents.kirkland.com",
"contact": "platform@kirkland.com",
"public_keys": [
{
"kid": "kirkland-2026-q2",
"kty": "OKP",
"crv": "Ed25519",
"use": "sig",
"alg": "EdDSA",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"not_before": "2026-04-01T00:00:00Z",
"not_after": "2026-10-01T00:00:00Z"
}
]
}

Key rotation follows a standard overlap pattern:

  1. Publish the new key alongside the old key (both valid)
  2. Start signing new requests with the new key
  3. After a grace period (Exchange-defined, typically 24-48 hours), remove the old key
  4. Exchanges SHOULD cache keys with a TTL and re-fetch periodically

For delegation JWTs, key rotation at the issuer does not invalidate existing tokens — the authority JWT was signed with the key that was current at issuance time. Exchanges SHOULD accept tokens signed by recently-rotated keys within a configurable grace period.

Each JsonWebKey carries RFC3339 not_before / not_after bounds. The window is half-open [not_before, not_after) — a key is valid at not_before and invalid at and after not_after. At least one key in public_keys MUST be valid at serve time, and verifiers MUST reject signatures whose key falls outside its window.

/.well-known/ramp.json MAY be cached by consumers and CDNs for routine periods (minutes to hours) — keys rotate slowly and carry explicit bounds. The invalidation_url body, by contrast, SHOULD be served with a short or zero freshness lifetime (e.g. Cache-Control: max-age=60, or no-store) so the 300s revocation poll is not defeated by an intermediary cache.

Enforcement is operational, not protocol-guaranteed: a consumer that grants access against a stale revocation set bears that as its own business risk — it is not a protocol deficiency.

When a manifest sets invalidation_url, consumers poll it on a 300s cadence (±10% jitter) and replace their local revoked set with the response — a KeyInvalidationList (kid-only snapshot: as_of + revoked[]). A revoked kid stays revoked; reject any signature whose kid is in the set. When invalidation_url is unset, consumers rely on routine rotation and not_after expiry.

An individual agent with no public domain of its own is accommodated without a protocol change: a registry (operated by an Exchange/Broker or a third party) hosts the agent’s WellKnownManifest, and the agent sets Requester.domain to that registry host (e.g. agent-7.registry.example). The agent generates its own keypair; the registry publishes the public half at {registry}/.well-known/ramp.json. The domain + well-known paradigm is preserved end to end. Relaxing the discovery anchor (an explicit per-requester keys URL) is a deliberate deferral.