RFC: ACK-ID + ACK-Pay v2 - #179
Conversation
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.
EfeDurmaz16
left a comment
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| - **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 |
There was a problem hiding this comment.
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.
| of the grant presented: | ||
|
|
||
| ```json | ||
| "ack": { "agent": "did:web:acme.com:shopper", "grantId": "grn_4kq8" } |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
|
|
||
| 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`. |
There was a problem hiding this comment.
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.
| 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). |
There was a problem hiding this comment.
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).
| 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 |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
SkalorAI
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| reject an offer past it. Sellers SHOULD keep offer validity short (the | ||
| x402 default of 300 seconds is a reasonable ceiling). | ||
|
|
||
| ## 4. Identity binding |
There was a problem hiding this comment.
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:
- Confirm attestation is the intended home, versus a
constraintsmember on the grant. Core §9 reads as though the latter would also be legal: a clearance requirement narrows, narrowing semantics ship asconstraints, and an unrecognized member is a rejection — which is the correct failure mode for a control requirement. - A design-language note that a receipt's
ackMAY 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.
| 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 |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
|
|
||
| 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 |
There was a problem hiding this comment.
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.
Status: proposal. Nothing here has shipped. The documents live under
docs/ack-id/rfc/anddocs/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:
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.ACK-Pay becomes a design language plus one normative profile: adopt the x402 offer-receipt artifacts (pinned at
@x402/extensions2.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.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
joseas published:Key differences from v1
<identity URL>/did.json, or/.well-known/did.jsonfor a bare domain, read for itspublicKeyJwkentries only. No fallback on 404: failing closed is what makes key removal a revocation lever. The same document serves x402's key discovery.aud,scope,constraints,exp,jti, and a requiredcnf.jktpossession pin. A copied grant is useless without the agent's private key; core has no bearer artifacts.Grantheader field, and the same signatures are verifiable by Web Bot Auth infrastructure already deployed at CDNs.exp, single-usejti, agent key removal, owner key unpinning. List mechanisms move to ext-revocation, and an RP declares the longest grant lifetime it will accept.crit(the RFC 7515 pattern applied to payload claims). A verifier that cannot evaluate acritclaim 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 distincttyp, a reserved scope token, anaudshape core rejects). Each extension is adopted and versioned on its own.chainclaim 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.ackmember (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
did.jsonserves ACK-ID, ACK-Pay, and x402.Reading order
docs/ack-id/rfc/README.md: the split, settled decisions with rationale, document mapdocs/ack-id/rfc/core.md: identities, keys, grants, signed requests, verification, revocationdocs/ack-id/rfc/ext-controller.md: controllers, ownership proofs, the full checklistdocs/ack-pay/rfc/core.md: the x402 profile and the receipt binding