Specification

VDAC v1

Verifiable Delegated Agent Credentials: scoped, attenuating, revocable authority for AI agents, rooted in a verified human. Public draft, derived from a working implementation.

Verifiable Delegated Agent Credentials (VDAC) — v1 profile

Version 0.1.16 · Public draft · 2026-10-04 (first published 2026-08-23; see the change log)

Status of this document

This is a public working draft, published for review. It describes version 1 of the VDAC credential profile (vct: urn:otcp:vdac:1) and its verification algorithm, as implemented and shipped.

The specification is derived from a working implementation, not ahead of one. Where this document and the reference implementation disagree, the implementation is correct and this document has a bug — please report it. The normative surfaces (payload schema, canonical action hashing, the eleven verification steps, and the reason-code vocabulary) are stable enough to build against. §12 lists what is explicitly not settled.

Nothing here is a standards-track document of any standards body. VDAC is a profile over existing standards — SD-JWT VC, OpenID4VP, and IETF Token Status List — not a replacement for any of them.


1. Introduction

An AI agent that acts for a person needs to answer one question at the moment it acts: on whose authority?

Bearer tokens answer it badly. An API key says "someone who had this key" — it does not say who authorized it, what they authorized, whether that authorization has been withdrawn, or whether the human is accountable for the outcome. When the actor is autonomous and the action moves money or creates an obligation, "someone had the key" is not a satisfying answer to a regulator, a merchant, or the person whose account was drained.

A VDAC is a credential in which a KYC-verified person delegates scoped, attenuating, revocable authority to a specific agent key. It is:

1.1 What a VDAC is not


2. Terminology

Term Meaning
Principal The verified human who delegates. Holds the identity credential and the key that signs VDACs.
Agent The autonomous software the authority is delegated to, identified by a non-exportable key.
Verifier The party evaluating a presentation against its own policy — typically on behalf of a relying party.
Relying party (RP) The merchant, service, or system relying on the verdict.
Root VDAC The VDAC signed by the principal, embedding the identity credential.
Sub-VDAC A VDAC signed by an agent, re-delegating a subset of its own authority.
Chain A leaf VDAC plus every ancestor up to the root. A lone VDAC is a chain of length 1.
Action The concrete operation being authorized, in canonical form (§5.2).
SUA Step-Up Assertion — the principal's signed approval of one specific action (§5.4).

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as in RFC 2119.


3. Design invariants

Seven invariants hold for every VDAC and every verification. They are the reason the format looks the way it does, and a profile that drops one is not this profile.

Invariant Enforced by
I1 Attenuation only. A child's authority is a subset of its parent's. Authority never widens on delegation. §6 steps 4b, 7, 8
I2 Non-transferable agent key. The agent key is hardware-rooted and non-exportable; it moves only by the principal re-issuing. §4.2 cnf, §6 step 5
I3 Action-bound presentation. State-changing classes bind the exact action by hash; read-only classes bind nonce and audience. §5.2, §6 step 6
I4 Bounded autonomy. Above a threshold — the principal's or the verifier's — a human approves that specific action. §5.4, §6 step 10
I5 Three-level revocation with bounded freshness. Principal root, delegation, and agent key revoke independently, each with a status-age bound. §4.4, §6 step 9
I6 Non-repudiation without identification. The principal signs the chain; the verifier learns a pseudonym, not an identity. §9
I7 Typed authority, fail-closed. Authority is typed; an unrecognized type denies. §4.3, §6 step 3

4. Credential format

4.1 Envelope

A VDAC is an SD-JWT VC (media type dc+sd-jwt). In v1 the payload uses no selective disclosure of its own — the disclosure machinery matters for the embedded identity credential, not for the authority claims, which the verifier needs in full.

The JWS protected header:

{ "alg": "ES256", "typ": "dc+sd-jwt", "crit": ["otcp_authority"] }

otcp_authority is a critical extension: a verifier that does not understand typed authority MUST reject the credential rather than ignore the claim (I7). A verifier MUST reject any crit member it has not registered (§6 step 3).

4.2 Payload

Claim Type Req. Meaning
vct "urn:otcp:vdac:1" ✔ Credential type.
jti string (UUID) ✔ Stable VDAC id. Usage is accounted per delegation line, not per jti (step 8).
iss did:jwk:… ✔ The signer. On a root VDAC, the principal's key; on a sub-VDAC, the delegating agent's key.
sub string ✔ The agent this authority is granted to (did:jwk: in v1).
cnf { jwk } ✔ The agent's non-exportable holder key (I2). The KB-JWT verifies under this key.
principal { otcp_id } root only The embedded identity credential, compact SD-JWT (§4.6).
parent string sub only The parent VDAC, embedded compact (§4.5).
otcp_authority array ✔ Typed authority grants (§4.3). Critical.
delegable false | { depth, narrowing_required } ✔ Opt-in re-delegation. Default false (§4.5).
mode "projection" | "mediated" ✔ v1 implements projection only.
linkability_path "none" ✔ Reserved escrow hook; always "none" in v1 (§9).
freshness { max_status_age_default } ✔ ISO 8601 duration bounding status age (I5).
iat, exp number ✔ Unix seconds. A sub-VDAC MUST NOT outlive its parent.
status object ✔ The three status entries (§4.4).
agent_key_attestation object ✖ Evidence about how the agent key is held (§4.7).
agent_meta object ✖ Descriptive agent metadata. Never a trust input.

4.3 Authority classes

Authority is a list of typed grants. Five classes exist in v1; an unrecognized type MUST deny (I7).

present-attribute — disclose a verified claim for a decision. No onward-use grant. Baseline binding only.

{ "type": "present-attribute", "attributes": ["age_equal_or_over.18"] }

act — operate a resource. verb: "write" requires action-binding; "read" is baseline. An act carries no amount, so a stepup_over on an act grant holds every write for a Step-Up Assertion, whatever its value: "0" (a wallet's "ask me for every action") and any other figure mean the same.

{ "type": "act", "verb": "write", "resource": "calendar",
  "counterparties": { "allow": ["*"], "deny": [] }, "velocity": { "count": 10, "window": "P1D" } }

spend — move funds up to caps. The rail moves the money; the VDAC is a constrained mandate.

{ "type": "spend", "currency": "EUR", "per_tx_max": "200.00", "cumulative_max": "500.00",
  "counterparties": { "allow": ["*"], "deny": [] }, "stepup_over": "100.00",
  "recurrence": "one_shot" }

commit-obligate — bind the principal to a future obligation. Carries total_liability_max. Always triggers step-up (§6 step 10).

sign-attest — a non-repudiable statement in the principal's name, bound to a document hash. Carries an optional purposes allow-list. Always triggers step-up.

Common optional fields: counterparties: {allow, deny} (matched on the canonical payee identifier, not a display name), jurisdictions, velocity: {count, window}, recurrence, stepup_over.

All monetary amounts are string decimals at the currency's scale — "200.00", never 200.0. Floats are not permitted anywhere in this profile. Arithmetic is exact (the reference implementation is BigInt-backed).

4.4 Status

"status": {
  "principal_root": { "status_list": { "uri": "https://status.example/lists/principals", "idx": 42 } },
  "delegation":     { "status_list": { "uri": "https://status.example/lists/vdacs", "idx": 7 } },
  "agent_key":      { "status_list": { "uri": "https://status.example/lists/agents", "idx": 3 } }
}

Three independent IETF Token Status List entries (I5), which is what makes revocation useful rather than nominal:

Lists MUST be large and shared, never per-principal: a list whose size or URL is unique to one person leaks that person. The principal→index map is private to the status service.

Status values. An entry holds one of the Token Status List values:

Value Meaning Reversible
0 Valid. —
1 Revoked. No. A revocation is final: the status service MUST refuse to move an entry off 1 on the holder's own credential.
2 Suspended: the principal has paused this link. Yes, back to 0 (resume) or on to 1 (revoke).
3 Reserved by this profile. —

Suspension needs a list of at least 2 bits per entry; a 1-bit list can only say valid or revoked. The delegation and agent_key lists SHOULD be 2 bits wide so a principal can pause an agent without ending its delegation. A verifier reads the width from the status list token (status_list.bits), never assumes it.

freshness.max_status_age_default bounds how old a status list may be and still be acted on. The age of a list is now − iat of the status list token itself — how long ago the status service asserted those bits, not how long this verifier has held them. Measuring from iat is what makes the bound mean anything: it is the only reading under which the bound limits revocation latency (§8), and it makes a cached list age out on its own rather than requiring a separate cache rule. A status list token that carries no iat therefore has no provable age and cannot satisfy any bound.

4.5 Delegability and sub-VDACs

delegable is false unless the principal opts in:

"delegable": { "depth": 1, "narrowing_required": true }

A sub-VDAC omits principal, carries the compact parent in parent, and is signed by the delegating agent's key — which MUST equal the parent's cnf key. Root binding is reached by walking up the chain. v1 implements same-principal projection only: mode MUST be "projection". Chains are bounded at 6 links by the reference implementation.

Every constraint in §6 step 4b applies per link: signature, delegator linkage, depth, attenuation, and expiry. A sub-VDAC that widens authority, outlives its parent, or exceeds the permitted depth is rejected at both issuance and verification — "no free chains".

4.6 Root binding by embedding

The root VDAC embeds the principal's identity credential in principal.otcp_id as a compact SD-JWT. The identity credential's cnf key MUST equal the root VDAC's iss key — which means the root VDAC's own signature is the holder proof-of-possession for the identity credential. No separate PoP is transmitted, and the agent never holds the principal key.

The embedded credential SHOULD carry no disclosures unless the verifier's policy requires a specific attribute. The wallet embeds <issuer-jwt>~ — issuer JWT only — so a delegation reveals no name, no date of birth, nothing beyond the assurance level and the binding key.

4.7 Agent-key attestation

"agent_key_attestation": { "level": "hardware_tee", "platform": "android", "verified_at": 1787000000 }

level is one of hardware_strongbox, hardware_tee, software (extensible).

This field MUST be absent unless the issuing wallet actually verified the protection of the key it is attesting to. A wallet that cannot inspect the agent key — which is the normal case for a remote agent — MUST NOT emit the field. An absent attestation is a truthful statement about missing evidence; a default-filled one is a lie a verifier's policy might rely on.

Verifiers SHOULD treat this as one input among several, never as a substitute for the status checks.


5. Presentation

5.1 The bundle

{ "vdac": "<compact SD-JWT>", "kbJwt": "<compact JWS>", "sua": "<compact JWS, when step-up fired>" }

Self-contained: the identity root travels inside the VDAC, so the verifier needs no prior relationship with the wallet.

5.2 Canonical actions and action hashing

For state-changing classes the presentation binds the exact action:

action.hash = base64url(SHA-256(JCS(action_object)))

where JCS is RFC 8785 JSON Canonicalization. Canonical action objects carry exactly these fields and no others:

Class Fields
spend type, amount, currency, payee, ref, nonce
commit-obligate type, doc_hash, total_liability, currency, counterparty, term, ref, nonce
sign-attest type, doc_hash, purpose, ref, nonce
act type, verb, resource, ref, nonce

Two properties make this reproducible across languages, which matters because the wallet that signs and the verifier that checks are usually not written in the same one:

  1. Every value is a string. No numbers appear in a canonical action, so JCS number serialization — its sharpest edge — never applies.
  2. payee and counterparty are canonical machine identifiers (IBAN, pay address, account id), never display names. A display name is spoofable; the whole point of hashing the action is defeated if the identifier can be re-pointed.

Test vector. For

{ "type": "spend", "amount": "180.00", "currency": "EUR",
  "payee": "merchant-demo", "ref": "ord-1", "nonce": "n-1" }

the canonical JSON is exactly

{"amount":"180.00","currency":"EUR","nonce":"n-1","payee":"merchant-demo","ref":"ord-1","type":"spend"}

and the hash is NnYIwGeBcVWkjYSM6tmkqHdDD_CDmnmeNQEM9NZIoYQ. An implementation that does not reproduce this string byte-for-byte is not conformant.

5.3 Key-Binding JWT

Signed by the agent key (the VDAC's cnf):

{ "nonce": "<verifier nonce>", "aud": "<verifier origin>", "iat": 1787000000,
  "action": { "type": "spend", "alg": "sha-256", "hash": "<action hash>",
              "amount": "180.00", "currency": "EUR", "payee": "merchant-demo",
              "ref": "ord-1", "nonce": "n-1" } }

action carries the hash and the cleartext fields, so the verifier reconstructs the action from its own record of the transaction and re-hashes — it never trusts the agent's claimed amount or payee. action is absent for baseline (read-only, present-attribute) presentations.

5.4 Step-Up Assertion

When step-up fires, the presentation MUST include an SUA signed by the principal key:

{ "action_hash": "<must equal the KB-JWT action hash>", "method": "biometric",
  "loa": "high", "principal": "did:jwk:…", "aud": "<verifier origin>",
  "nonce": "<verifier nonce>", "iat": 1787000000,
  "biometric": { "verified": true, "method": "biometric_strong",
                 "provider": "android_keystore", "at": 1787000000 } }

method is one of oob, biometric, fresh_mint. The verifier receives the wallet's signed assertion that liveness occurred — never biometric data, which does not leave the device.

Because the SUA is signed by the principal key and bound to action_hash, an agent that has been fully compromised — holding its own key and a valid VDAC — still cannot manufacture one. That is the property step-up exists for.

Default freshness is 300 seconds. Wallets SHOULD expire a step-up request no later than that, so an approval cannot outlive the assertion it produces.


6. Verification

A conformant verifier performs these steps in order, denying at the first failure. Each returns a machine-readable reason code (§7).

1 — Parse and walk. Parse the leaf VDAC and KB-JWT, then follow parent to the root. A lone VDAC is a chain of length 1. Malformed input, or a chain deeper than the implementation limit, denies (vdac.malformed).

2 — Algorithm policy. Every declared alg — each chain link, the KB-JWT, the embedded identity credential, and the SUA — MUST be in the accepted set (vdac.alg_not_accepted).

3 — Critical extensions. Any crit member on any link that the verifier has not registered denies (vdac.unknown_crit).

4 — Identity root. On the root link: the embedded identity credential's issuer keys MUST resolve (vdac.issuer_unresolved, followed by a second reason saying why: the issuer.* code of the verifier's trust source, such as issuer.not_authorized, or issuer.unavailable when that source could not be reached); its issuer signature MUST verify against them (vdac.root_sig_invalid); every disclosure MUST be bound to a digest in the payload (vdac.root_disclosure_unbound); the credential's cnf key MUST equal the root's iss key (vdac.iss_cnf_mismatch); the root VDAC signature MUST verify under that principal key (vdac.vdac_sig_invalid); the assurance level MUST meet the policy floor (vdac.assurance_too_low); and any attributes the policy requires MUST be disclosed (vdac.attribute_missing).

Assurance is read from kyc_assurance, assurance, or loa, and the ladder is low < substantial < high. It is fail-closed: a missing or unrecognized level never satisfies a floor.

4b — Per-link chain walk. For each non-root link: its signature MUST verify under its own iss key (vdac.link_sig_invalid); that key MUST equal the parent's cnf key (vdac.chain_link_broken); mode MUST be projection (vdac.mode_unsupported); the parent's delegable.depth MUST cover this hop (vdac.parent_not_delegable); the child's authority MUST attenuate the parent's (vdac.scope_widened); and the child MUST NOT outlive the parent (vdac.exp_exceeds_parent).

4c — Validity period. Every link, the root included, and the embedded identity credential MUST be in date: an exp at or before the verifier's clock less its skew allowance denies (vdac.expired), with a second reason naming which, delegation or otcp_id. Every link is read, not only the leaf, so a parent's date bounds a child that carries none. This is the only place a clock meets an exp; step 4b orders a child against its parent and reads no clock.

5 — Agent proof-of-possession. The KB-JWT MUST verify under the leaf's cnf key (vdac.pop_invalid), with matching nonce (vdac.nonce_mismatch) and aud (vdac.aud_mismatch), and an iat not in the future beyond clock skew (vdac.kb_not_fresh).

6 — Action binding. For state-changing classes the KB-JWT MUST carry an action (vdac.action_missing) whose nonce matches the presentation nonce (vdac.action_nonce_mismatch) and whose hash equals the verifier's own recomputation from the canonical action (vdac.action_mismatch).

7 — Attenuation. The action MUST fall inside a matching grant on the leaf: class, currency, asset, counterparty allow/deny, jurisdiction, per-transaction ceiling, granted verb, granted attributes. Failures are specific — see §7.

8 — Caps. Cumulative and velocity limits are evaluated against usage accounted per delegation line: the leaf's iss and the RFC 7638 thumbprint of its cnf key (vdac.cumulative_exceeded, vdac.velocity_exceeded, vdac.total_liability_exceeded). A replacement (§10.3), a new jti for the same agent key from the same delegator, continues the line of the delegation it replaces, so what was spent under one still counts under the next; a new agent key starts a new line. On accept the verifier commits the delta to the line, keyed on the action's ref so a retried submission is idempotent. A verifier that kept usage per jti MAY seed a line from the presented delegation's jti count the first time it sees the line.

9 — Status and freshness. The root's principal_root, plus every link's delegation and agent_key, MUST be valid (status 0, §4.4). A revoked entry denies with vdac.revoked.<level>, a suspended one with vdac.suspended.<level>, and any other value with vdac.status_unknown.<level>; all three deny every class, present-attribute included, because each is the status service's answer rather than its absence. A missing entry denies (vdac.status_missing). For state-changing actions an unavailable status list denies (vdac.status_unavailable) — fail-closed; for present-attribute and read-only actions it MAY fail open, a deliberate trade of strictness for availability where nothing changes state.

Each list MUST also be fresh. The bound is the tightest of the verifier's maxStatusAge and every chain link's own freshness.max_status_age_default: both are floors asserted by different parties — the principal in the credential, the relying party in policy — so the stricter governs, and a link may no more loosen its parent's freshness than it may outlive it (step 4b). A list whose age (§4.4) exceeds the bound is unusable and denies with vdac.status_stale, under the same fail-closed / fail-open split as unavailability — a stale list is as useless as an unreachable one. Verifiers MUST NOT widen the bound by a clock-skew allowance: quietly loosening a floor the relying party set is the same defect as not enforcing it. A duration that cannot be parsed denies (vdac.malformed); it MUST NOT be treated as "no bound". Revocation outranks everything else in this step — a revoked entry anywhere in the chain is reported as vdac.revoked.<level> even when another link is suspended or a list is stale, since it is the permanent fact and a retry will not clear it. Then an unknown value, then a suspension, then staleness: a relying party told vdac.suspended may reasonably try again later, so it MUST NOT be told that about a chain that is also revoked.

10 — Step-up. Step-up fires on the union of triggers: the verifier's route policy, the class defaults (commit-obligate and sign-attest always), the principal's own stepup_over threshold on value classes, and any stepup_over on the act grant a write matches. When it fires, a valid SUA MUST be present — signed by the principal key (not an intermediate agent), bound to this action hash, matching aud and nonce, and fresh. Absent or invalid, the verdict is review with vdac.stepup_required — not a denial. The transaction is recoverable: obtain the human's approval and re-submit the same action.

11 — Accept. Return the verdict with the assurance label, the usage delta to commit, and a chain summary (iss, sub, jti, depth, grantType).

6.1 Verifier policy inputs

acceptedAlgs, registeredCrit, minAssurance, aud, nonce, requiredAttributes, jurisdiction, maxStatusAge, stepUpRoutePolicy, stepUpMaxAgeSec (default 300), clockSkewSec (default 60).

Policy belongs to the verifier. This profile defines what can be checked; it never dictates thresholds.


7. Reason codes

Verdicts are pass, review, or block, with machine-readable reasons. RPs SHOULD branch on these codes rather than on message text.

Code Meaning
vdac.malformed Unparseable, wrong shape, or chain too deep.
vdac.alg_not_accepted A declared algorithm is outside policy.
vdac.unknown_crit Unregistered critical extension.
vdac.issuer_unresolved Identity-credential issuer keys could not be resolved; the reason after it says why: an issuer.* code of the verifier's trust source (for example issuer.not_authorized), or issuer.unavailable when the source could not be reached.
vdac.root_sig_invalid Identity credential signature invalid.
vdac.root_disclosure_unbound A disclosure is not bound to a payload digest.
vdac.iss_not_did_jwk An iss is not a resolvable did:jwk.
vdac.iss_cnf_mismatch Identity cnf ≠ root iss — the chain is not rooted in this identity.
vdac.vdac_sig_invalid Root VDAC signature invalid under the principal key.
vdac.assurance_too_low Assurance below the policy floor (fail-closed).
vdac.attribute_missing A policy-required attribute was not disclosed.
vdac.link_sig_invalid A chain link's own signature is invalid.
vdac.chain_link_broken A link is not signed by its parent's cnf holder.
vdac.mode_unsupported Non-projection mode.
vdac.parent_not_delegable Parent forbids this depth of re-delegation.
vdac.scope_widened A child claims authority its parent lacks (I1).
vdac.exp_exceeds_parent A child outlives its parent.
vdac.expired A link, or the embedded identity credential, is past its exp less the skew allowance; the reason after it says which, delegation or otcp_id.
vdac.pop_invalid KB-JWT does not verify under cnf.
vdac.dpop_key_mismatch OAuth binding (§10.1): the DPoP proof's key is not the VDAC's cnf key.
vdac.nonce_mismatch / vdac.aud_mismatch Replay or wrong audience.
vdac.kb_not_fresh KB-JWT iat outside the accepted window.
vdac.action_missing State-changing action with no bound action.
vdac.action_mismatch Action hash ≠ the verifier's recomputation.
vdac.action_nonce_mismatch Action nonce ≠ presentation nonce.
vdac.no_matching_grant No grant of the required class.
vdac.currency_mismatch / vdac.asset_mismatch Wrong denomination.
vdac.counterparty_not_allowed Payee outside allow/deny.
vdac.jurisdiction_not_allowed Outside granted jurisdictions.
vdac.verb_not_granted act verb not granted.
vdac.attribute_not_granted Attribute outside the present-attribute grant.
vdac.per_tx_exceeded Above the per-transaction ceiling.
vdac.cumulative_exceeded Above the cumulative ceiling of this delegation line (step 8).
vdac.velocity_exceeded Too many accepted actions in the window.
vdac.total_liability_exceeded Above the obligation ceiling.
vdac.status_missing A required status entry is absent.
vdac.status_unavailable Status list unreachable (denies for state-changing).
vdac.status_stale Status list older than the freshness bound, or of unprovable age (denies for state-changing).
vdac.revoked.principal_root The person's root is revoked — every delegation is dead.
vdac.revoked.delegation This delegation (and its subtree) is revoked.
vdac.revoked.agent_key This agent key is revoked.
vdac.suspended.<level> That link is paused by the principal (status 2); it may be resumed, so a retry after resumption can pass. <level> is principal_root, delegation or agent_key.
vdac.status_unknown.<level> That link's status is a value this profile does not define (3, or above on a wider list).
vdac.stepup_required review — a human must approve this action.

8. Security considerations

What a stolen VDAC gets an attacker: nothing. The credential is public-ish data. Presenting it requires signing a fresh KB-JWT under the agent key, which is non-exportable.

What a fully compromised agent gets an attacker: the granted scope, and only until revocation. This is the honest bound and the reason scoping and caps are first-class. An attacker holding the agent key can act within the grant — up to the per-transaction ceiling, up to the cumulative ceiling, only to allowed counterparties, only until any of three status bits flips. They cannot exceed the caps, re-point the payee, widen the scope, or produce an SUA.

Action re-pointing is prevented by hashing canonical machine identifiers and by the verifier reconstructing the action from its own order — never from the agent's claim. A verifier that hashes the agent's asserted amount instead of its own has implemented this profile incorrectly.

Replay is prevented by verifier-chosen single-use nonces bound into both the KB-JWT and the action object, plus iat freshness on both KB-JWT and SUA.

Delegation-chain abuse is prevented by per-link verification (§6 step 4b) — every link is signed by its delegator, attenuates, and expires no later than its parent; step 4c then refuses any link, and any identity credential, that is past its date.

Revocation latency is bounded by the tighter of freshness.max_status_age_default and the verifier's maxStatusAge, measured from the status list token's own iat (§4.4). Status is checked at every verification, and a list older than the bound is not usable — whether it came from a cache or from a service that has stopped republishing. A bound measured from fetch time instead would not bound this at all: a service issuing one list a day would satisfy any hourly refetch rule while its bits were a day stale.

Known limits, stated plainly. Agent-key attestation is only as good as the evidence behind it, and this profile explicitly permits its absence (§4.7) rather than encouraging a fabricated claim. Status availability is a liveness dependency for state-changing actions — the fail-closed choice is deliberate, and it means a status outage stops transactions. Sub-delegation is single-principal projection only; nothing here addresses delegation across principals.


9. Privacy considerations

Delegation discloses no identity attributes. The embedded identity credential carries no disclosures by default — the verifier learns a pseudonymous did:jwk, the assurance level, and nothing else. Where a grant needs an attribute, exactly that attribute is disclosed and no more.

Accountability without identification (I6). The chain is principal-signed, so an action is attributable and non-repudiable, while the verifier holds only a pseudonym. Identifying the person behind it is an out-of-band legal process against the identity issuer, not something a verifier can do from a presentation.

Status lists are herd-privacy structures. Large shared lists, never per-principal — a list whose size or URL identifies one person defeats the purpose.

Audit logs. Verifiers SHOULD record the verdict, reason codes, jti, and pseudonymous iss/sub — and SHOULD NOT record identity attributes.

Multi-show linkability is a real limitation of v1. An SD-JWT VC presented twice carries the same issuer signature, so two colluding verifiers can correlate. Batch issuance of single-use copies is the intended mitigation and is not yet part of this profile (§12).

linkability_path is always "none" in v1. The field is reserved for a future opt-in escrow construction. No implementation may treat it as an unmasking hook today.


10. Interoperability

VDAC is a profile over standards, not a parallel stack:

AP2 Intent Mandate. A VDAC spend grant is a superset of a Google AP2 Intent Mandate and exports to one losslessly: merchants ↔ counterparty allow-list, intent expiry ↔ exp, cart-confirmation ↔ step-up reachability. Descriptive AP2 fields with no authority meaning are carried verbatim. The generated field map ships with the reference implementation.

10.1 OAuth binding

A relying party that takes OAuth access tokens rather than VDACs lets the agent trade its VDAC for one, per action, at an authorisation server that runs §6. In the reference implementation that server is the OTCP verifier, one per relying party (tenant), at <verifier>/as/<tenant>. Every piece is an existing standard; this section names what VDAC adds and fixes how the pieces combine.

Standard Its job here
RFC 8414 Metadata at /.well-known/oauth-authorization-server/as/<tenant> (path insertion).
RFC 8693 Token exchange; the VDAC presentation is the subject_token.
RFC 9396 authorization_details carries the one action.
RFC 8707 resource names the relying party's API and becomes the token's aud.
RFC 9449 DPoP binds the token to the agent key. The server's DPoP-Nonce is also the KB-JWT's and the action's nonce.
RFC 9068 The access token: a JWT, typ: at+jwt.
RFC 7662 Introspection, for the relying party alone.
RFC 9470 insufficient_user_authentication when the principal must approve.
RFC 9728 The relying party's protected-resource metadata and its 401 challenge, which tell an agent where the authorisation server is.

Vocabulary (the constants in the reference implementation's profile.ts):

Name Kind Meaning
urn:otcp:params:oauth:token-type:vdac subject_token_type The subject token is the VDAC, its disclosures and the KB-JWT: <vdac-jwt>~<disclosure>~…~<kb-jwt>.
urn:otcp:vdac:action authorization_details type { "type": …, "action": <a §5.2 action object> }. Exactly one entry, with no other members.
step_up_assertion Token-request parameter The §5.4 SUA, on a retry.
step_up Error member { action_hash, nonce, expires_in } on insufficient_user_authentication.
urn:otcp:loa:<level> acr value The identity root's assurance: low, substantial or high. Not an eIDAS level of assurance.
otcp_vdac Access-token claim { jti, stepped_up }: the leaf VDAC's jti, and whether a step-up approved this action.
otcp_disclosed Introspection member The values of the attributes the relying party's policy required.
otcp_principal Introspection member How the principal was verified: { liveness, provider, verified_at }, each optional. The facts a direct verdict carries; no name, no document.
otcp_reasons Error member The §7 reason codes of a refusal.
otcp_via Request claim The agent a sign-in was asked through, { client_name } (§10.4).
pairing_code, delivery: "jwe" Request-object members The rendezvous (§10.2).
otcp_push Delivery member Where the agent asks the wallet's instance to tell the wallet an approval waits, { endpoint, handle } (§10.5).
otcp-push+jwt JWS typ The agent's signed push request (§10.5).

The exchange.

  1. The agent sends a DPoP proof (ES256; htm, htu, iat, jti) with no nonce. The server answers 400 use_dpop_nonce with a DPoP-Nonce. A nonce is single use, bound to the key it was issued to, and lives 120 seconds.
  2. The agent posts grant_type=urn:ietf:params:oauth:grant-type:token-exchange, the subject token, resource and authorization_details, with a DPoP proof that carries the nonce. The KB-JWT's aud is the authorisation server's issuer. Its nonce, and the action's nonce, are the DPoP nonce.
  3. The server checks, in this order: the proof; its jti not seen before for that key; the nonce unspent, which it then spends; one key; then §6, under the relying party's own policy (assurance floor, required attributes, status freshness, its own step-up trigger), never under anything the agent sends. One key means the DPoP key is the VDAC's cnf (else vdac.dpop_key_mismatch) and the KB-JWT verifies under it (else vdac.pop_invalid).
  4. Pass. Usage is committed (§6 step 8) when the token is issued, so a token that is never used still counts against the caps. The response carries token_type: DPoP, issued_token_type: urn:ietf:params:oauth:token-type:access_token, an expires_in of at most 300 seconds, and the authorization_details granted.
  5. Step-up (§6 step 10 fires and no SUA was sent). The server answers 400 insufficient_user_authentication with step_up and a fresh DPoP-Nonce n2 that lives 300 seconds and is bound to this one action. action_hash is the hash of the action re-presented with nonce n2. The agent asks the principal for an SUA whose aud is the issuer and whose nonce is n2, then retries with the KB-JWT, the DPoP proof and the SUA all on n2. An SUA that still does not satisfy step 10 is refused, not asked for again.
  6. Refusal. 400 access_denied with otcp_reasons.

The access token carries no personal data. Its claims:

The relying party verifies the token with the authorisation server's key set and a DPoP proof, and rejects a reused jti. It receives attribute values, and how the principal was verified, only by introspection, authenticated as itself, and only within its retention window.

The relying party's side.

  1. A request that carries no token is answered 401 with the RFC 9728 challenge: WWW-Authenticate: DPoP algs="ES256", resource_metadata="<metadata URL>" (and the same resource_metadata on a Bearer challenge). The metadata names the authorisation server, requires DPoP-bound tokens and lists urn:otcp:vdac:action.
  2. A request with a token carries Authorization: DPoP <token> and one DPoP proof. The relying party checks, in this order: the token's signature under the authorisation server's key set, typ: at+jwt, iss, aud (its own resource), exp; the proof (htm, htu, iat, ath = the token's hash) and that its key's thumbprint is the token's cnf.jkt; then the action.
  3. The action is the relying party's own. It computes the action from its own records (the order, never the request body) and compares it with the token's authorization_details action. The action's nonce is the authorisation server's (step 2 of the exchange), so the comparison is made under the token's nonce; ref is what binds the action to the relying party's record. A mismatch is refused 403 insufficient_scope.
  4. The token is spent by its first use: a second request with the same jti is refused invalid_token.
  5. A relying party that needs the disclosed values, or how the principal was verified, introspects the token with its own API key before acting.

A relying party may keep verifying VDACs itself instead (§6). An agent that signs its request (RFC 9421) may name where its keys are published with Web Bot Auth: a Signature-Agent dictionary member covered by the signature, a keyid that is the key's RFC 7638 thumbprint, tag="web-bot-auth", and a key directory at /.well-known/http-message-signatures-directory. A relying party that checks the directory still requires the key found there to be the VDAC's cnf.

10.2 Rendezvous

An agent with no public address (a command-line agent on a laptop, a service behind a firewall) cannot serve the request object the wallet fetches, nor take the wallet's answer. It meets the wallet at a rendezvous instead. In the reference implementation that is the OTCP verifier, under <verifier>/v1/agents. The rendezvous relays: it verifies no delegation, and it never holds a credential it can read.

Vocabulary (the constants in profile.ts):

Name Kind Meaning
pairing_code Request-object member The six decimal digits the agent shows, which the person types into the wallet.
delivery Request-object member How the wallet answers. "jwe": encrypted to the key in agent.jwk, never in clear.
agent.operator_uri Request-object member The HTTPS origin of the agent's operator. A wallet shows the agent as operator-verified only when the request object is served from the same registrable domain.
receipt Answer response member The wallet's credential for reading whether the agent collected its answer.

Pairing.

  1. The agent reserves a pairing. POST /v1/agents/pairings returns { id, code, request_uri, callback, poll_token, expires_in }. The code is unique among live pairings, and a pairing lives 600 seconds.
  2. The agent signs its request object (typ: otcp-vdac-req+jwt) with the key in its agent.jwk, carrying pairing_code (the code), callback (the rendezvous's) and delivery: "jwe", with an exp no later than the pairing's. It puts the object at request_uri (PUT, Authorization: Bearer <poll_token>), once. The rendezvous refuses an object not signed by its agent.jwk, or not bound to the pairing in these three members.
  3. The agent shows the code and its key's fingerprint: the first sixteen characters of the RFC 7638 thumbprint of agent.jwk, in groups of four.
  4. The person types the code into the wallet. The wallet resolves it (GET /v1/agents/pairings/code/<code> → { request_uri }), then fetches and checks the request object as it checks any other: signed by agent.jwk, callback on the request_uri's origin. It shows the code and the fingerprint beside the request. A request served by a rendezvous is not served from the agent operator's domain, so the wallet does not show the agent as operator-verified.
  5. The wallet answers at callback with { jwe } or { denied: true, reason? }. The JWE is compact, alg ECDH-ES on P-256, enc A256GCM, kid the RFC 7638 thumbprint of agent.jwk, and its plaintext is the body the wallet would otherwise post to an agent's own callback: {"vdac": "<compact VDAC>"} (with replaces when it replaces one, §10.3). The rendezvous refuses anything else, plaintext included. The first answer wins, and a code stops resolving once its pairing has one. The answer returns { receipt }, once; a later answer is refused 409 already_answered, which the wallet tells the person plainly.
  6. The agent polls the pairing (GET /v1/agents/pairings/<id>, Authorization: Bearer <poll_token>): { state, jwe?, reason?, expires_in? }, where state is pending, delivered, denied or expired. It decrypts the JWE and checks the VDAC as its own: the cnf is its key, the grant is what it asked for or less. The first poll that returns the answer marks it collected; the answer stays until exp, so a poll whose reply was lost can be repeated.
  7. The wallet reads whether the agent has its answer (GET /v1/agents/pairings/<id>/receipt, Authorization: Bearer <receipt>): { state, collected, expires_in? }, where state is delivered, denied or expired. So the person sees "your agent has it", or that the code ran out first; and a wallet replacing a credential revokes the old one only once the new one is collected.

Step-up. The same relay without a code, at /v1/agents/stepups. A step-up relay lives 300 seconds, the SUA's freshness window (§5.4). The step-up request object (typ: otcp-stepup-req+jwt) carries delivery: "jwe" and the relay's callback, and no pairing_code; the agent shows it to the principal as a link or a QR. The answer's plaintext is {"sua": "<SUA>"}: the SUA names the principal's key, which the rendezvous has no need to read. Its receipt reads at /v1/agents/stepups/<id>/receipt.

What the rendezvous keeps. The request object, which names the agent and what it asks for, not the person; the wallet's ciphertext; the poll token and the receipt as hashes only; when the agent collected the answer. All of it goes at the record's exp.

Guessing a code. A code is one in a million, lives ten minutes, and the reference implementation answers ten lookups a minute per client address. One address therefore makes at most a hundred guesses in a code's life, one chance in ten thousand of finding a given live code. A guessed code yields the agent's request object (no personal data) and the chance to answer first: with a denial, or with a delegation from the guesser's own identity, encrypted to the agent's key. It yields neither the person's credential nor their approval. The person sees the first case at once: the wallet's own answer is refused as already answered.

10.3 Replacing a delegation

A VDAC cannot be edited: its limits and its exp are signed. A person who changes an agent's limits, or renews them, gets a new VDAC for the same agent key, and the old one is revoked. An agent already connected may also ask for more (below), and the person's answer is replaced the same way. The wallet does it in one order, so the agent is never left without a working credential, nor with two.

Vocabulary (the constants in profile.ts):

Name Kind Meaning
replaces Delivery member The jti of the delegation the delivered one replaces.
replaces Request-object member The jti of the delegation an agent already connected holds, when it asks for more.
  1. The new VDAC is minted under the same rules as the first: never wider than what the agent last asked for (its ask when it connected, or a later ask for more that the person answered), its exp no later than that ask allows. A delegation paused (status 2) stays paused: the new one's delegation entry is written 2 before the VDAC leaves the wallet.
  2. The wallet delivers it the way the agent was connected, with replaces beside it: {"vdac": "<compact VDAC>", "replaces": "<old jti>"}, posted to the agent's own callback, or as the plaintext of the JWE through a fresh pairing (§10.2) when the agent was connected with a code, which the agent shows anew. A key pasted into the wallet has no address: the person copies the new VDAC to the agent.
  3. An agent takes the new VDAC only in place of the one it holds: replaces names that credential's jti, and the new one's cnf is the agent's own key. It refuses anything else (409 at a callback), and a second VDAC that names nothing to replace (the first answer wins).
  4. The wallet revokes the old delegation, both its delegation and its agent_key entries, only once the agent has the new one: its callback's 2xx, or the rendezvous receipt's collected. If the new one never arrives (a refusal, no answer, a pairing that ran out), the wallet revokes the new one instead, and the old one stands. A copied VDAC is the exception: the wallet cannot see it arrive, so the old one is revoked at once, and the agent acts again once the person has pasted the new one.

Asking for more. An agent already connected that needs more than its delegation gives (a higher cap, a grant it lacks, a later end date) asks again, on the same key: a request object as for a first connection (§10.2, or served at its own address), carrying replaces, the jti of the delegation it holds, and the whole ask, not only what grows.

  1. The wallet refuses the request, and signs nothing, unless it holds the delegation replaces names, given to the key that signed the request, active or paused, with no earlier replacement of it still waiting for the agent to collect.
  2. It shows the ask as a change to that delegation, not as a new connection: each grant as the delegation has it and as asked. The person approves the ask, gives less, or declines.
  3. Approved, the new VDAC is minted and delivered by steps 1 to 4 above, the way the request says (its callback in clear, or sealed through its pairing). The ask the person answered is the ceiling for later changes.
  4. Declined, the wallet answers { "denied": true } at the request's callback, and the agent keeps the delegation it holds: declining an ask for more ends nothing. An agent that serves its own requests keeps its key and its callback.

A request with no replaces, from a key the wallet already holds a delegation for, is a new connection. An ask for more is a new consent: it waits for the person, and an agent can only ask, never raise its own limits.

10.4 Signing in through an agent

An agent that holds no VDAC can still act for a person who is there to approve each time ("ask me every time"). The agent sends the person to the relying party's authorisation server (§10.1), which runs a presentation from the person's wallet and gives the agent a token. The wallet answers the verifier, never the agent, so the person's data does not pass through the agent or its platform. Every piece is an existing standard:

Standard Its job here
RFC 6749, RFC 7636 The authorisation code grant with PKCE (S256 only), for a client with a browser: an MCP client, a browser agent.
RFC 8628 The device grant, for a headless agent: it shows a code, and the person enters it on any screen.
RFC 7591 Registration: public clients only (token_endpoint_auth_method: none), a name and redirect URIs.
OAuth client ID metadata document Instead of registering, a client may be named by an HTTPS URL whose JSON document gives its client_id (that URL), name and redirect URIs.
RFC 9207 The redirect names the issuer (iss).
RFC 9449 DPoP is optional here: a client that proves a key gets a token bound to it (and dpop_jkt binds the code to that key); one that does not gets a Bearer token.
OpenID4VP The sign-in itself: the verifier's signed request object (or the Digital Credentials API request), and the wallet's answer to it.

Vocabulary (the constants in profile.ts):

Name Kind Meaning
otcp_via Request claim { client_name }: the client's name, as it registered it or as its metadata document gives it. Carried by the signed request object and the Digital Credentials API request of a sign-in, and by no other request. The wallet shows it as "asked through your agent". It is the client's own word, which nobody has checked.

The sign-in.

  1. The request (the authorisation request, or the device authorisation request) names the relying party's API in resource, which is required and becomes the token's aud. It may name one action in authorization_details (the type of §10.1) with no nonce: the authorisation server sets its own, as in a token exchange, and replaces any the client sent. scope is refused.
  2. The authorisation server makes a presentation request that asks for what the relying party's policy requires and nothing else: the identity credential's assurance claim, against the policy's floor, and any attribute the policy names. The request carries otcp_via. The page shows the client's name, what the wallet will share, and, for the device grant, the code to compare with the agent's.
  3. A verified answer ends the sign-in. The code flow redirects with a code that lives 60 seconds and is spent by its first redemption; a code that comes back is refused and the token it bought is revoked. The device flow marks the device code approved. The disclosed values move with the sign-in, from the presentation record to the code or device record and to the token record; the presentation record keeps the claim names only.
  4. A presentation that fails, or a person who cancels, ends the sign-in with access_denied: on the redirect for the code flow, at the next poll for the device flow.
  5. The token carries sub, the pairwise handle that a direct presentation of the same identity credential, and a token exchange by that person's agent, get at the same relying party; client_id; aud; acr; auth_time; cnf.jkt when it is DPoP-bound; and authorization_details when an action was asked for. It carries no act and no otcp_vdac: no delegation stands behind it. Like any token of §10.1 it carries no personal data, and introspection hands the relying party otcp_disclosed and otcp_principal.
  6. A device code lives 600 seconds and is polled every 5 seconds. The token endpoint answers authorization_pending, slow_down (after which the client waits 5 seconds longer), access_denied or expired_token, then the token once, then invalid_grant. A user code is eight letters from BCDFGHJKLMNPQRSTVWXZ, shown as XXXX-XXXX and compared without case, spaces or dashes.

A person can be handed a code, or a link, by someone else (RFC 8628 §5.4). The sign-in page shows the client's name and the code, and the wallet says through which agent the request came: both are the client's own words, so a person signs in only for an agent they asked to act.

10.5 Telling the wallet an approval waits

A step-up (§5.4) reaches the principal only through a link or a QR code the agent shows, and so does an ask for more (§10.3). An instance may run a push relay through which an agent holding an approval, or asking for more, tells the principal's wallet, without learning the device or reaching it itself; so does an agent that disconnects, whose delegation the wallet still lists. A push carries the link the QR code carries and authorises nothing: the wallet opens the approval or the ask from it and checks the request object as after a scan, or opens the agent, where the principal may revoke it. The link and the QR code stay; every step works without a push.

  1. A wallet whose principal turned notifications on registers with its instance's relay: its push service's device token and a P-256 push key made for this purpose, never the principal's key. It is off until the principal turns it on.
  2. With each delegation it delivers (connect, §10.3's replacements), the wallet asks the relay for a handle and delivers it beside the VDAC: {"vdac": "…", "otcp_push": {"endpoint": "<the relay's push endpoint>", "handle": "…"}}. The relay encrypts into the handle the installation, the RFC 7638 thumbprint of the VDAC's cnf key, its jti, its exp and its status entries (§4.4), under a key only the relay holds; an agent cannot read it, and two handles never match. An agent keeps otcp_push with the delegation it came with, and drops it when a replacement comes without one. An agent posts a push only to the relay of the instance it serves, never to a host an endpoint names otherwise: it keeps no otcp_push whose endpoint is not that relay's, so a delivery cannot make it sign a request to another host.
  3. An agent holding an approval posts to endpoint a JWS, alg ES256, typ otcp-push+jwt, its public key as jwk in the header, claims aud (the endpoint), iat, jti, handle, request_uri (the step-up request object's URL) and optionally exp (when the approval ends). The request may carry kind: approval (the default), raise or ended. For a raise, request_uri is the request object of an ask for more (§10.3), and exp when the ask ends. An agent sends ended once, at its own disconnect and before it destroys its key, with no request_uri: the agent no longer acts, and the delegation stays until the principal revokes it or it expires.
  4. The relay refuses unless the JWS verifies under its jwk; aud is its endpoint; iat is within 60 seconds; kind, when present, is one it knows; request_uri is present for an approval or a raise and absent for ended; the jti is unseen for that key; the handle opens and names that key's thumbprint; the delegation's exp has not passed; every status entry the handle names reads 0 (a list it cannot read or trust refuses, as step 9 does for a state-changing action), for a raise or ended as for an approval; and the key has sent fewer than ten pushes in the last hour, and for a raise fewer than three raises within them. A refused push counts toward neither.
  5. The relay builds the link from the kind, never taking one from the agent: otcpwallet://stepup?request_uri=… for an approval, otcpwallet://request?request_uri=… for a raise, otcpwallet://agent?key=… for ended, key the thumbprint the handle names. It seals { "link", "iat", "exp" } to the push key with HPKE (RFC 9180, base mode, DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, AES-128-GCM, info otcp-push-v1, aad the installation's identifier), and sends the push service { "v": "1", "enc", "ct" }, kept no longer than exp and at most five minutes. The push service reads nothing it carries; the notification names no agent, amount or payee.
  6. A wallet opens a request link from a push only as an ask for more: a request object whose replaces names a delegation it holds for the key that signed it (§10.3). Anything else it refuses, signing nothing. A wallet opens an agent link from a push only to show that agent, where the principal may revoke the delegation; it changes nothing by itself.

11. Conformance

An implementation is conformant if it:

  1. produces or accepts payloads per §4;
  2. reproduces the §5.2 canonical hash byte-for-byte, including the published test vector;
  3. performs §6 in order, denying at the first failure;
  4. emits §7 reason codes;
  5. is fail-closed on assurance, unknown crit, unknown authority type, and status availability and freshness for state-changing actions;
  6. where it implements the OAuth binding (§10.1), refuses a token request unless the DPoP key, the KB-JWT signer and the VDAC's cnf are one key, and commits usage when it issues a token;
  7. where it implements the sign-in (§10.4), requires PKCE with S256, spends a code or a device code once, and issues a token carrying no personal data;
  8. where it runs a push relay (§10.5), sends a push only for a request signed by the key its handle names, for a delegation live on every status entry the handle names, and sealed to the wallet's push key, and builds the link from the request's kind.

The reference implementation is @otcp/vdac — pure TypeScript, no service dependencies, shared by the verifier and the demo harness, and used as the executable reference for the native wallet minter.


12. Open issues for v0.2

Named rather than papered over:

  1. Batch issuance / unlinkability (§9). The mechanism is decided — N single-use copies rotated per presentation — but is not yet profiled here.
  2. Agent-key attestation format. §4.7 defines the field; it does not yet define how a remote agent conveys an attestation chain, or how a verifier validates one.
  3. Mediated mode. mode admits "mediated"; only projection is specified.
  4. Cross-principal delegation. Out of scope in v1; the chain walk assumes one principal.
  5. Post-quantum agility. The alg/crit/vct-version discipline is in place; no PQC suite is profiled.
  6. Decentralized revocation. Status lists depend on a reachable service. A principal-controlled kill switch that works when the service is down is wanted.
  7. categories counterparty scoping. v1 scopes on canonical identifiers only.

13. Change log

Version Date Notes
0.1 2026-08-23 First public draft. Payload, five authority classes, canonical action hashing with test vector, single-hop projection sub-delegation, eleven-step verification, reason-code vocabulary.
0.1.1 2026-08-25 Specified the step-9 freshness bound that §8 already relied on: age measured from the status list token's iat (§4.4), tightest-of-policy-and-credential, new vdac.status_stale reason code.
0.1.2 2026-09-20 Specified step 4c, which §4.2's required exp implied and no step performed: every link and the embedded identity credential in date, with step 5's skew allowance; new vdac.expired reason code.
0.1.3 2026-09-24 Status values (§4.4): 2 = suspended, reversible; 1 = revoked, final; 3 reserved; the delegation and agent_key lists 2 bits wide. Step 9 denies every class on 1, 2 and 3, and orders revoked, unknown, suspended, stale. New vdac.suspended.<level> and vdac.status_unknown.<level> reason codes.
0.1.4 2026-09-25 §10.1 OAuth binding: RFC 8693 token exchange with the VDAC presentation as the subject token, the action in RFC 9396 authorization_details, DPoP whose server nonce is the KB-JWT nonce, the one-key rule, step-up as insufficient_user_authentication, RFC 9068 access tokens with no personal data, and the vocabulary. New vdac.dpop_key_mismatch reason code; conformance item 6.
0.1.5 2026-09-25 §10.1: the relying party's side of the OAuth binding (the RFC 9728 challenge, the token and DPoP checks, the action compared under the token's nonce, the single-use jti), the otcp_principal introspection member, and Web Bot Auth for a signed request.
0.1.6 2026-09-25 §10.2 Rendezvous: an agent with no public address pairs through the verifier with a six-digit code, and relays its step-ups there; the wallet answers with a JWE (ECDH-ES, A256GCM) encrypted to the agent key, so the rendezvous keeps only ciphertext. pairing_code and delivery leave the reserved list.
0.1.7 2026-09-25 Any stepup_over on an act grant holds every write (§4, §6 step 10): an act has no amount, so "0" means every action. §10.2: agent.operator_uri and operator verification by registrable domain; the fingerprint is the thumbprint's first sixteen characters; the answer returns the wallet's receipt, the agent's first poll marks the answer collected, and the wallet reads that at …/receipt.
0.1.8 2026-09-25 §10.3 Replacing a delegation (Change limits, Renew): a new VDAC for the same agent key, delivered the way the agent was connected with the new replaces member naming the old jti; the agent takes it only in place of the one it holds; the wallet revokes the old one only once the agent has the new one, and a paused delegation's replacement starts paused.
0.1.9 2026-09-25 §10.4 Signing in through an agent ("ask me every time"): the authorisation code grant with PKCE and the device grant at the authorisation server of §10.1, clients registered (RFC 7591) or named by a metadata document, DPoP optional, a presentation asking only what the relying party's policy requires, otcp_via in its request, a token with the pairwise sub and no personal data. otcp_via leaves the reserved list. Conformance item 7.
0.1.10 2026-09-28 §10.5 Telling the wallet an approval waits: a push relay at the wallet's instance, a per-delegation otcp_push handle beside the delivered VDAC, the agent's otcp-push+jwt request, the relay's checks (the handle's key, the delegation live, ten an hour) and the link sealed to the wallet's push key with HPKE. Conformance item 8.
0.1.11 2026-09-28 §10.3 Asking for more: replaces is also a request-object member, naming the delegation an agent already connected holds; the wallet refuses one it does not hold, another key's, or one revoked, ended or with a replacement still waiting; it shows the ask as a change to that delegation; the ask the person answered becomes the ceiling (step 1); a declined ask ends nothing.
0.1.12 2026-09-28 §10.5 step 2: an agent posts a push only to the relay of the instance it serves, never to a host a delivery's endpoint names, so a delivery cannot make it sign a request to another host.
0.1.13 2026-09-29 §10.5: a push may also tell the wallet an agent asks for more (kind raise, §10.3); the relay builds the link from the kind, refuses an unknown kind and more than three raises an hour per key; a wallet opens a request from a push only as an ask for more.
0.1.14 2026-09-29 §6 step 8: caps are counted per delegation line (the leaf's iss and its agent key), so a replacement continues the total of the delegation it replaces; the jti is no longer the accounting key.
0.1.15 2026-09-30 §6 step 4, §7: vdac.issuer_unresolved is followed by a second reason saying why the issuer's keys did not resolve, an issuer.* code or issuer.unavailable. §7: vdac.cumulative_exceeded names the delegation line, as step 8 has counted since 0.1.14.
0.1.16 2026-10-04 §10.5: a push may also tell the wallet an agent disconnected (kind ended): sent once, at the agent's own disconnect, before its key is destroyed, with no request_uri; the relay builds otcpwallet://agent?key=… from the key the handle names; the wallet opens that agent from it and changes nothing by itself.

Feedback is wanted, particularly on §6 and §7 — the surfaces another implementation has to match. The reference implementation is the tie-breaker for any disagreement with this text.