Skip to content

RFC: ACK-ID + ACK-Pay v2 - #179

Draft
venables wants to merge 1 commit into
mainfrom
ack-id-core-rfc
Draft

RFC: ACK-ID + ACK-Pay v2 #179
venables wants to merge 1 commit into
mainfrom
ack-id-core-rfc

Conversation

@venables

@venables venables commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Status: proposal. Nothing here has shipped. The documents live under docs/ack-id/rfc/ and docs/ack-pay/rfc/, unlisted from the docs site. We're asking for review of the design, not the prose.

Summary

ACK-ID lets an AI agent prove three things to a relying party in one HTTP request: which agent is calling, which person or company is accountable for it, and whether that owner authorized this action. The request signature proves the agent, the grant's signature proves the owner, and the grant's scope proves the authorization.

This RFC proposes v2 as one small core plus six optional extensions:

  • Core: identities (HTTPS URLs, spelled did:web in artifacts), key resolution (one fetch of did.json), grants (short-lived JWTs carrying audience, scope, and a possession pin), signed requests (RFC 9421, verifiable by Web Bot Auth infrastructure as deployed), a verification checklist, and revocation levers.
  • ext-controller: ownership for unknown counterparties: controller derivation, control grants, and ownership proofs anchored in DNS or a code host. Drafted in full; this is the line from a request back to a legal entity.
  • ext-delegation, ext-revocation, ext-web, ext-attestations, ext-audit: stubs that mark the split; normative text follows.
core-flow

ACK-Pay becomes a design language plus one normative profile: adopt the x402 offer-receipt artifacts (pinned at @x402/extensions 2.22.0), sign them with ACK-ID identities, and bind each receipt to the grant that authorized the payment. The trail runs receipt → agent → grant → owner → legal entity, and a third party can walk it with no callback to any participant.

trail

A core-only deployment (an org authenticating its own agents to its own services) is implementable with a stock JOSE library and an HTTP client in an afternoon. The whole setup, verified against jose as published:

import {
  calculateJwkThumbprint,
  exportJWK,
  generateKeyPair,
  SignJWT,
} from "jose"

// 1. Two keypairs: the owner signs grants, the agent signs requests
const owner = await generateKeyPair("EdDSA")
const agent = await generateKeyPair("EdDSA")

const agentJwk = await exportJWK(agent.publicKey)
agentJwk.kid = await calculateJwkThumbprint(agentJwk)
const ownerKid = await calculateJwkThumbprint(await exportJWK(owner.publicKey))

// 2. The agent's identity is a URL. Host this one static file
//    at https://acme.com/invoice-bot/did.json
const didDocument = {
  id: "did:web:acme.com:invoice-bot",
  verificationMethod: [
    {
      id: `did:web:acme.com:invoice-bot#${agentJwk.kid}`,
      type: "JsonWebKey2020",
      controller: "did:web:acme.com:invoice-bot",
      publicKeyJwk: agentJwk, // the only member core reads
    },
  ],
}

// 3. The owner mints a grant, pinned to the agent's key
const grant = await new SignJWT({
  scope: "invoices:read",
  cnf: { jkt: agentJwk.kid },
})
  .setProtectedHeader({ alg: "EdDSA", typ: "grant+jwt", kid: ownerKid })
  .setIssuer("did:web:acme.com")
  .setSubject("did:web:acme.com:invoice-bot")
  .setAudience("https://api.examplebank.com")
  .setIssuedAt()
  .setExpirationTime("15m")
  .setJti(crypto.randomUUID())
  .sign(owner.privateKey)

// Done. The agent signs each request (RFC 9421) and presents the
// grant in the Grant field. The verifier needs the same library
// plus one HTTPS GET of did.json.

Key differences from v1

  • Plain JOSE replaces Verifiable Credentials. Every artifact is a signed JWT verified with a stock JOSE library. The ideas VCs were for survive in core (self-issued identifiers, rotation that survives identity, portable signed claims, callback-free verification); the encodings (JSON-LD, the VC data model, presentation exchange) move out. A lossless grant-to-VC mapping lives in ext-attestations, so VC and eIDAS systems integrate as adapters rather than core dependencies.
  • One fetch replaces DID resolution. The did:web spelling stays, and resolution is one fixed path: <identity URL>/did.json, or /.well-known/did.json for a bare domain, read for its publicKeyJwk entries only. No fallback on 404: failing closed is what makes key removal a revocation lever. The same document serves x402's key discovery.
  • Grants carry authority. v1's controller credential asserts who controls an agent. A grant adds what the agent may do: aud, scope, constraints, exp, jti, and a required cnf.jkt possession pin. A copied grant is useless without the agent's private key; core has no bearer artifacts.
  • Requests carry the proof. RFC 9421 HTTP message signatures replace v1's challenge-response exchange. Grants ride in a signed Grant header field, and the same signatures are verifiable by Web Bot Auth infrastructure already deployed at CDNs.
  • Verification is callback-free with explicit revocation levers. RPs pin owner keys at onboarding and verify locally. Core revocation: short exp, single-use jti, agent key removal, owner key unpinning. List mechanisms move to ext-revocation, and an RP declares the longest grant lifetime it will accept.
  • Extensions replace conformance levels. Unknown grant claims are ignored, and a claim that changes what a grant means must be named in crit (the RFC 7515 pattern applied to payload claims). A verifier that cannot evaluate a crit claim rejects the grant, so an extension claim can never downgrade a verifier that lacks the extension. Extensions also give their artifacts a second rejection surface (a distinct typ, a reserved scope token, an aud shape core rejects). Each extension is adopted and versioned on its own.
  • "Mandate" is renamed to "grant." AP2 uses mandate for a human approving a purchase, and both artifacts can appear in one request. An AP2 mandate says a human approved a transaction; an ACK-ID grant says an owner authorized an agent to act.
  • Delegation chains link pairwise. The chain claim carries one hash: the immediate parent. Ancestry is pinned transitively, decoy entries are structurally impossible, and the convention matches AP2's mandate chain, so grants slot into AP2/UCP flows. ext-delegation also names OAuth 2.0 Token Exchange (RFC 8693) as the intended mint carriage for issuance services, so token-exchange-shaped issuers (a Keycloak realm, a custody provider's signing API) can mint leaves through a standard interface while verification stays offline.
  • ACK-Pay adopts x402's artifacts. Offers and receipts come from the x402 offer-receipt extension, pinned at a named version. v1's payment request token and VC receipts are superseded; the flow and role documents remain as non-normative design language. Card-network agent programs (Mastercard Agent Pay) and other rails map onto the same offer/receipt shape, as design language until a mapping is written.
  • Receipts name the accountable party. A single ack member (agent, grantId) inside the seller-signed receipt binds the payment to the grant that authorized it. A receipt without the binding stays a valid x402 receipt; it proves payment, and RPs that need the trail reject unbound receipts by policy.

What didn't change

  • The goal: a verifiable line from a request to the accountable entity behind an agent.
  • Owner and agent are separate identities, and the owner is the accountable one.
  • did:web is the spelling in artifacts; one hosted did.json serves ACK-ID, ACK-Pay, and x402.
  • Human oversight in payments stays, as design language.

Reading order

  1. docs/ack-id/rfc/README.md: the split, settled decisions with rationale, document map
  2. docs/ack-id/rfc/core.md: identities, keys, grants, signed requests, verification, revocation
  3. docs/ack-id/rfc/ext-controller.md: controllers, ownership proofs, the full checklist
  4. docs/ack-pay/rfc/core.md: the x402 profile and the receipt binding

ACK-ID: a core RFC (identities spelled did:web, one-fetch key resolution,
grants, RFC 9421 signed requests, verification checklist, revocation
levers, crit-based extension mechanism) plus six extensions, with
ext-controller drafted in full. ACK-Pay: a design language plus one
normative profile adopting the x402 offer-receipt artifacts and binding
receipts to the authorizing grant. The README records settled decisions
and the document map.
@agentcommercekit agentcommercekit deleted a comment from coderabbitai Bot Aug 27, 2026

@EfeDurmaz16 EfeDurmaz16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the full RFC set and cross-checked the payment profile against the pinned x402 source at commit 59ac597. The core + extensions split and the plain-JOSE posture feel right; comments ordered by severity.

Open questions, non-blocking:

  • ES256K (open decision 2): rejecting has a real cost (EVM sellers must provision a non-wallet key) but I still lean reject: section 3 already forbids the signing key being payTo, and WebCrypto has no secp256k1, so accepting would make receipts unverifiable with native crypto on browser and edge runtimes.
  • did:jwks: the SDK's did:jwks resolver uses the fallback chain core and ext-web forbid, and the RFC never mentions did:jwks. The README should say: superseded, folded into ext-web discovery, or profiled separately?
  • Standing authority: do we want ext-delegation to profile recurring/metered payment flows? The intermediate-plus-leaf model is promising, but caps, cadence, sibling-leaf accounting, and buyer countersignatures need their own normative treatment. I would keep it out of this profile and track it as a follow-up design issue; happy to open one.
  • Stub drafting notes: require every non-leaf ancestor to be grant-int+jwt; spell out the multi-level controller walk; name the revocation-key relationship; note that jti redemption and replay caches need coordination across an RP's acceptance domain.

Review assistance: Codex and Claude Code were used to cross-check the RFC against the pinned protocol sources and draft these notes. I reviewed the conclusions.

Comment thread docs/ack-pay/rfc/core.md
settlement mechanism, and no new artifact formats. The artifacts are the
x402 [offer-receipt extension](https://docs.x402.org/extensions/offer-receipt),
adopted as published and profiled here. This profile pins the extension as
shipped in `@x402/extensions` 2.22.0 (x402 repo, offer-receipt source at

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At commit 59ac597 the implementation and docs disagree: types.ts defines version, resourceUrl, scheme, network, asset, payTo, amount and transaction, while the docs page says offerType and txHash, which Section 3 copies. The version is also off: package metadata at that commit says 2.15.0, not 2.22.0. Pick one pinned source of truth, fix the version, and state intentional deviations explicitly.

Comment thread docs/ack-pay/rfc/core.md
- **Signature scheme.** ACK-Pay conformance requires the JWS scheme. An
EIP-712/did:pkh signature MAY additionally be present; it carries no
ACK-Pay semantics and is passed through unevaluated.
- **Signer identity.** The JWS signer MUST be a did:web identity per ACK-ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving the signer's keys proves who signed, not that they were entitled to. Nothing binds the signer DID to resourceUrl: anyone can host a did.json and sign offers for someone else's resource, and checks 1-4 pass. Needs an authorization rule: signer host matches the resourceUrl origin, or the resource names its accepted signer DIDs.

Comment thread docs/ack-pay/rfc/core.md
of the grant presented:

```json
"ack": { "agent": "did:web:acme.com:shopper", "grantId": "grn_4kq8" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jti is only unique per issuer, and ack does not name the issuer, so a colluding owner can mint a grant with the same sub and jti and the trail walks to the wrong party. Bind by content: put the grant's hash in ack (base64url SHA-256 over the compact serialization, same construction as chain), or at least add the owner DID.

Comment thread docs/ack-pay/rfc/core.md
offer's `validUntil` had not passed at `issuedAt`.
4. **Freshness**: `issuedAt` is sane for the claimed transaction; where
`txHash` is present, it MAY be checked against the named network.
5. **The trail**: when the `ack` member is present and the named grant is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check 5 runs ACK-ID section 7 rules, but those are time-of-request checks. When a third party walks the trail later, the grant has expired and the seller may have rotated keys away, so every honest historical receipt fails. The profile needs a historical-verification mode: which checks run against the artifact's own timestamps, and the seller's key-publication obligation through the dispute horizon.

Comment thread docs/ack-pay/rfc/core.md

What a passing verification proves: the named seller quoted these terms,
acknowledged payment for this resource, and attributed the payment to this
agent under this owner's grant. The receipt carries no amount or `payTo`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without an amount, a receipt proves payment happened but not what was settled, so the artifact caps out at fixed-price flows; upto/session/metered need "authorized X, settled Y <= X" and have nowhere to put Y. Open decision 1 already upstreams ack; propose attribution and outcome together: add settledAmount. x402's own scheme registry already defines upto and deferred, so these shapes are in scope for the pinned rail, not hypothetical.

Comment thread docs/ack-id/rfc/core.md
Resolution rules:

- All fetches MUST use HTTPS, MUST time out, and MUST cap response size.
Fetchers MAY follow redirects to a bounded depth (RECOMMENDED limit 3).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automatic redirect following in browser fetch, undici, and Workers never exposes hops; the per-hop checks only happen if the fetcher runs redirect: "manual" and walks the chain itself, which most will not do. Flip the default: refuse redirects, following as explicit opt-in for fetchers that implement the checks. We just made this change in the SDK resolver (#133).

Comment thread docs/ack-id/rfc/core.md
deployment that must choose between subdomain-per-agent and path-per-agent
layouts should read that document's Section 2.

Before a verifier uses an identity for comparison or URL construction, it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rejection list does not ban : inside a path segment, and colons are legal in URL paths, so https://acme.com/a:b and https://acme.com/a/b both map to did:web:acme.com:a:b. The mapping stops being bijective and one key document answers for two identities. Add path-segment : to the rejection list.

Comment thread docs/ack-id/rfc/core.md
credential. `Signature-Agent` comes from Web Bot Auth. The `Grant` field
is defined by this document.

- The signature MUST cover `@method`, `@target-uri`, `content-digest` when a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two enforcement gaps: covering content-digest binds the header, but nothing requires recomputing the digest over the received body, so a middlebox can swap the body under a valid signature. And created has a maximum age but no future bound; reject far-future values with the section 5 skew, like iat.

Comment thread docs/ack-id/rfc/core.md
authorization, a one-shot registration); core supplies the mechanism.
Redemption MUST be atomic: a check-and-set keyed by (`iss`, `jti`). A
separate read-then-write lets two concurrent presentations both pass.
The RP keeps the record until the grant's `exp` has passed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acceptance allows a grant until exp + skew, but the redemption record is kept only until exp. That leaves a skew-sized window where a spent grant passes again. Keep the record until exp + skew.

endpoint (`https://raw.githubusercontent.com/<owner>/.ack-id/HEAD/ack-id.json`).
Profiles for other hosts pin their equivalents. The shape is the same as
the well-known file, with `anchor` = `github:<owner>` or the host's
equivalent. The proof record MUST store the host's stable numeric owner ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three lifecycle gaps in ownership proofs. The expected numeric owner ID has no defined home: in the committed file it belongs to the rename-squatter it defends against, so it only works as trust-on-first-use, which should be stated. Proof claims have no exp and "whoever publishes proof status" is an undefined actor; either verifiers re-check anchors each time (say so) or a callback is back. And the DNS jkt pin is not wired into the Section 6 checklist: a verifier holding the pin can still accept artifacts signed by any other assertion key, defeating the pin against a compromised origin.

@venables venables added the documentation Improvements or additions to documentation label Aug 28, 2026

@SkalorAI SkalorAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design review from the Skalor side. We implement against ACK-ID as an attestation issuer and run a pre-transaction control layer above the rails, so these are the places the RFC touches us directly.

Nothing here overlaps @EfeDurmaz16's comments, which I agree with — particularly content-binding ack and the need for a historical-verification mode.

Three of the seven are ext-attestations (carriage, subject binding through delegation chains, revocation); four are ACK-Pay (one reference rule, where a per-transaction control decision lives, issuer key obligations, rail-neutrality). Happy to go deeper on any of these in the thread.


## Adds

- **Carriage.** JWTs (`typ: "jwt"`) or SD-JWTs (`typ: "dc+sd-jwt"`), issued by

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attestations have no wire slot in a signed request.

Core §6 makes the Grant field a List of grants, and core §7 rejects any token whose typ is not grant+jwt. An attestation (typ: "jwt" / dc+sd-jwt) therefore has nowhere to ride in the request itself. An RP that wants, say, an agent's bureau rating at decision time has to fetch it out of band — which reintroduces exactly the callback core removed.

Proposal: an Attestation structured field, same RFC 9651 List-of-Tokens grammar as Grant, covered by the request signature under the same coverage rule, each token evaluated independently under this extension. Core-only verifiers ignore the field entirely (it carries evidence, never authority), so it needs no crit entry and raises no downgrade concern.

If you'd rather not add a field, the alternative is to let Grant carry non-grant typs and have core §7 skip rather than reject them — but that weakens the "every token in this field is a grant" invariant core currently relies on.

- **Carriage.** JWTs (`typ: "jwt"`) or SD-JWTs (`typ: "dc+sd-jwt"`), issued by
any party under its own keys, typed by issuer-controlled `vct` per SD-JWT
VC. No central attestation-type registry.
- **Subject binding** (the one normative rule): the attestation issuer MUST

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subject binding excludes exactly the deployment shape delegation exists for.

Attestations bind to a subject identity, and ext-delegation's ephemeral did:key workers "die with their grant". Any attestation that is behavioural — a rating built from an agent's decision and settlement history — can never attach to a did:key leaf: the leaf has no history, and is gone before it earns any. Under the current text every orchestrated worker starts unattested.

Proposal: an attestation MAY name as sub any verified ancestor in the presented chain — typically the intermediate's sub, i.e. the durable agent or owner DID. A verifier that has walked the chain per ext-delegation then accepts an attestation whose sub equals any ancestor's sub, with cnf compared against that ancestor's key rather than the leaf's. The attestation follows the accountable identity rather than the throwaway key.

- **Selective disclosure.** SD-JWT only, and only here. Guidance: assert
results ("verified at level 2") rather than raw attributes (a date of
birth).
- **Freshness.** Attestations SHOULD include `cnf` (an RFC 7638 thumbprint

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attestations have no revocation lever.

ext-revocation gives grants a status claim under crit. Attestations have no equivalent, yet they are the artifact most likely to need pulling back inside their lifetime — a rating downgrade after an incident, a compliance status withdrawn.

SD-JWT VC already defines a status claim over the IETF Token Status List. Proposal: ext-attestations references it, and a verifier implementing this extension MUST check it when present, mirroring ext-revocation's obligation. Without that, a downgraded attestation stays good until exp, which pushes issuers toward minute-scale expiries and makes attestations useless as durable evidence.

Comment thread docs/ack-pay/rfc/core.md
reject an offer past it. Sellers SHOULD keep offer validity short (the
x402 default of 300 seconds is a reasonable ceiling).

## 4. Identity binding

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does a per-transaction control decision live?

The RFC has three artifact classes: a grant authorizes (owner), an attestation asserts (third party), a receipt acknowledges (seller). A per-transaction control decision from a third party — allow / refuse / pending, made against a specific offer under a specific grant, valid for seconds — fits none of them cleanly. We currently model it as an attestation carrying cnf, references to the offer and the grant, and a short exp.

Two asks:

  1. Confirm attestation is the intended home, versus a constraints member on the grant. Core §9 reads as though the latter would also be legal: a clearance requirement narrows, narrowing semantics ship as constraints, and an unrecognized member is a rejection — which is the correct failure mode for a control requirement.
  2. A design-language note that a receipt's ack MAY carry a reference to the clearance the seller relied on, so the walkable trail includes the control decision and not only the authorization.

Related: nothing in ACK-Pay records what was refused. Disputes ask "why was this paid" and "why was this blocked" in equal measure, and ext-audit's evidence bundles should admit control-decision artifacts for both outcomes.

Comment thread docs/ack-pay/rfc/core.md
artifact:

- When the paid request was ACK-verified (signed request plus grant, ACK-ID
core Sections 6-7), the seller SHOULD include in the receipt payload a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reference rule for every cross-artifact link.

+1 to @EfeDurmaz16 on binding ack by content rather than jti. Widening the point: the trail is about to grow several references of the same kind — receipt→grant (ack), grant→offer (the proposed constraints.offer pin), and any third-party artifact→{offer, grant, receipt} (an attestation citing what it evaluated).

Please define the reference construction once, in core, and reuse it: base64url SHA-256 over the compact serialization — the same construction as chain. For EIP-712 artifacts, define it over the EIP-712 typed-data digest, so one rule covers both formats the x402 extension admits.

One rule, one test vector, and every artifact that points at another verifies the same way.

Comment thread docs/ack-pay/rfc/core.md
offer's `validUntil` had not passed at `issuedAt`.
4. **Freshness**: `issuedAt` is sane for the claimed transaction; where
`txHash` is present, it MAY be checked against the named network.
5. **The trail**: when the `ack` member is present and the named grant is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historical verification needs an issuer-side obligation, not just a verifier mode.

+1 to the historical-verification mode @EfeDurmaz16 raised. Adding the other half: whoever signs receipts, attestations or clearances that will be re-verified after key rotation needs a stated obligation to keep retired keys resolvable — or to be log-backed per ext-audit — through a declared dispute horizon.

We would commit to that as an attestation issuer and would rather it were a rule than a courtesy. Suggest the profile names the horizon explicitly, and makes ext-audit's event log the conformant way to satisfy it.

Comment thread docs/ack-pay/rfc/core.md

The offer/receipt pattern is rail-independent: an offer is a signed quote
before payment, a receipt a signed acknowledgment after, whatever moved the
money between them. Mappings for l402, card-network agent programs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the attribution binding rail-neutral in shape.

ack is defined as a member inside the x402 receipt payload. We clear the same policy across several rails, and the accountability trail matters identically on each.

Even as design language, it would help to name the binding as a standalone shape — an attribution object of {agent, grant reference, rail, settlement reference}, signed by whoever issues that rail's receipt — with the x402 profile being the case where it embeds in the receipt payload. Naming it now prevents four incompatible bindings later, and it is the shape a rail-neutral verifier actually consumes.

Small related question on ext-delegation's open item: for did:key subjects, is the intent that Signature-Agent is absent and the did:key is carried in the chain's leaf sub only, with the request key matched via cnf.jkt? We have implemented it that way and would like to confirm before it hardens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants