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 · Public draft · 2026-08-23

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. The usage-accounting key — cumulative and velocity caps are counted per jti.
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.

{ "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.

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 signature MUST verify against resolved issuer keys (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).

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 jti (vdac.cumulative_exceeded, vdac.velocity_exceeded, vdac.total_liability_exceeded). On accept the verifier commits the delta, keyed on the action's ref so a retried submission is idempotent.

9 — Status and freshness. The root's principal_root, plus every link's delegation and agent_key, MUST be unrevoked (vdac.revoked.<level>). 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.

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), and the principal's own stepup_over threshold on value classes. 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.
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.pop_invalid KB-JWT does not verify under cnf.
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 for this jti.
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.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.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.

Revocation latency is bounded by freshness.max_status_age_default and by the verifier's maxStatusAge. Status is checked at every verification; a cached list older than the bound is not usable.

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.


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 for state-changing actions.

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.

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.