Skip to content

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

Draft
venables wants to merge 13 commits into
mainfrom
ack-id-core-rfc
Draft

venables wants to merge 13 commits 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-foundation/x402@59ac597, the source shipped in @x402/extensions 2.15.0 and unchanged through 2.25.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, grant) 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

AI usage

Claude Code drafted the review-response revisions and the thread replies; I reviewed every change.

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 Outdated
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 Outdated
Comment thread docs/ack-pay/rfc/core.md Outdated
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
Comment thread docs/ack-id/rfc/core.md Outdated
Comment thread docs/ack-id/rfc/core.md
Comment thread docs/ack-id/rfc/core.md
Comment thread docs/ack-id/rfc/core.md Outdated
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.

Comment thread docs/ack-id/rfc/ext-attestations.md
Comment thread docs/ack-id/rfc/ext-attestations.md
Comment thread docs/ack-id/rfc/ext-attestations.md
Comment thread docs/ack-pay/rfc/core.md
Comment thread docs/ack-pay/rfc/core.md
Comment thread docs/ack-pay/rfc/core.md Outdated
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

@aadopii aadopii 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.

Let a one-payment grant survive the 402 price challenge

There is an interaction between ACK-ID and ACK-Pay that I did not find in the existing reviews: when does checking a single-use payment grant become redemption of that grant?

A seller may authenticate an agent before showing a price. The agent sends a signed request, receives 402 Payment Required, then retries with payment. Core §8 says the grant is spent at first acceptance. If middleware redeems it during the initial check, the paid retry fails as already spent even though the purchase has not started.

I propose making the ordering explicit:

  1. Checking a payment grant and returning only a price challenge must not spend it.
  2. On the paid retry, recheck the request, grant, and payment payload, then atomically and durably redeem (iss, jti) before settlement submission or the protected operation starts.
  3. Only the winning request may start the authorized effects. A timeout after execution starts must not make the grant available for another operation.

This lets one purchase complete without asking the owner for another grant. It adds no token or header. A separately authorized paid quote can still consume its own grant.

The proposed changes are in #215, targeting this RFC branch: a short ACK-Pay section, an ACK-ID cross-reference, and nine executable lifecycle examples integrated into the repository checks. The examples model ordering; they do not implement cryptography, distributed storage, or settlement. The full build and check commands pass in the fork's GitHub Actions run.

This concerns the stage before payment begins and is separate from the existing comments on spent-record expiry and receipt reissuance. Follow-ups could cover a production adapter and recovery using the payment rail's idempotency support.

AI assistance: I used Claude Code, Hermes, and Codex for source review and proposal development. Codex prepared the RFC edits, executable examples, and validation workflow, and ran the checks.

@EfeDurmaz16

Copy link
Copy Markdown
Contributor

A few additional thoughts. Keeping them in one comment to avoid crowding the review. These are design suggestions, not blockers; some build on the existing discussions.

  1. Constraints
    a. Translating an ACK grant into wallet or rail-native authority must not broaden it. Required constraints that cannot be enforced should cause rejection.
    b. Could an optional profile support product/category-specific limits? For example, $100 per pair of shoes versus $400 per dataset license, alongside an aggregate budget. Classification must come from an owner-approved policy or trusted catalog, not an unchecked agent/seller label.

  2. Relationships and roles
    a. A durable agreement could capture negotiated pricing, capacity, and service terms across short-lived grants. Referencing an agreement should not itself confer spending authority.
    b. Agent identity, customer account, fund owner, payment authority, fee sponsor, and relay are distinct roles. Recognizing an agent does not authorize access to a customer’s merchant account.

  3. After purchase
    a. Refund, dispute, and order-access authority should survive the purchasing worker or grant. The authorized refund destination should not be inferred from the agent owner’s identity.
    b. Consider a separate fulfillment acknowledgment signed by an authorized recipient and bound to the receipt and delivered artifact. It would attest receipt, not quality; its absence would not prove nondelivery.

  4. Continuation and compatibility
    a. Building on the existing pending discussion: human approval should resume the same operation, bind its current terms, and require fresh authorization where needed.
    b. Discovery should identify compatible extension versions and the active feature set. Negotiation must not silently drop required constraints.

  5. Future payment lifecycles
    Building on the standing-authority discussion: subscriptions, sessions, upto, and batch settlement can remain follow-ups. Could we explicitly avoid assuming one grant = one request = one receipt = one settlement? Keeping today’s profile small should not make that relationship permanent.

venables and others added 12 commits September 14, 2026 12:49
The profile named @x402/extensions 2.22.0 next to commit 59ac597, but
that commit ships 2.15.0, and the field names were copied from the
extension's documentation page, which disagrees with types.ts. Pin the
source at the commit (the last change to offer-receipt, unchanged
through 2.25.0), make the code the source of truth, and use `scheme`
and `transaction`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Platform HTTP clients follow redirects without exposing the hops, so
per-hop SSRF checks never run unless the fetcher walks the chain
itself. Make refusal the default and following an explicit opt-in for
fetchers that implement the checks, matching the SDK resolver (#133).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three drafting requirements from review: a request field for
attestations since Grant rejects non-grant typs, attestation subjects
that name a verified chain ancestor so did:key leaves can inherit
them, and the SD-JWT VC status claim as the revocation lever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the open item with the intended shape: Signature-Agent absent,
identity from the leaf sub, key decoded from the identifier, keyid
equal to its thumbprint and to cnf.jkt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A colon inside a path segment made the URL-to-DID mapping lossy:
https://acme.com/a:b and https://acme.com/a/b both spelled
did:web:acme.com:a:b. Reject any segment character outside the did:web
idchar set (percent-encoding is already rejected), which closes that
hole and any other separator collision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Close open decision 2 as reject. WebCrypto has no secp256k1, so an
ES256K receipt would not verify with native crypto on browser and edge
runtimes, and key separation already forbids the signing key being
payTo, so accepting it would buy no wallet-key reuse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The SDK resolver's jwks.json-then-OIDC fallback is the chain core
forbids. Name the disposition in the settled decisions so the RFC
answers what happens to the method.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Covering content-digest binds the header value only; a verifier must
recompute the digest over the received body or a middlebox can swap
the body under a valid signature. created had a maximum age but no
future bound; apply the Section 5 skew, the same bound iat carries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The receipt carries no amount, so the profile covers fixed-price flows
only; say so in Section 5 and fold a settled-amount field into open
decision 1 so attribution and outcome upstream together. Fold buyer
side receipt recovery (pay, lose the 200, hold no proof) into open
decision 2, since idempotent re-issue needs a request surface the
profile does not define.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Acceptance runs to exp + skew but the single-use record was dropped at
exp, leaving a skew-sized window in which a spent grant passes again.
Retain through exp + skew in core Section 8 and for registration grants
in ext-delegation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Name the receipt binding as a standalone attribution shape (agent,
grant reference, settlement reference) so other rail mappings carry
the same members; state that translation into rail-native authority
never widens a grant; and say the profile's one-grant, one-request,
one-receipt, one-settlement shape is not a permanent relationship.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jti is unique per issuer only and the ack member names no issuer, so
a second owner could mint a grant with the same sub and jti and walk
the trail to the wrong party. Define the artifact reference (base64url
SHA-256 over the compact serialization) once in core Section 4.2, have
chain reuse it, and replace ack.grantId with ack.grant carrying the
reference of the presented grant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@venables

Copy link
Copy Markdown
Contributor Author

@EfeDurmaz16, on the open questions in your review:

  • ES256K: closed as reject. Your WebCrypto point is now the stated reason in Section 3, plus the observation that key separation already forbids the signing key being payTo, so accepting it wouldn't buy wallet-key reuse anyway. 0a95426
  • did:jwks: superseded. README decision 10 records it: the SDK resolver's jwks.json-then-OIDC fallback is exactly the chain core forbids, and ext-web's opt-in discovery profile covers the need. 3c06bdf
  • Standing authority: agreed, keep it out of ext-delegation. Please open the follow-up issue; the intermediate-plus-leaf sketch is the right starting point.

On your follow-up list, two landed as one-liners in 9ade080. Section 1 says the profile describes one grant, one request, one receipt, one settlement, and doesn't fix that relationship (your 5). Section 6 says translation into rail-native authority never widens a grant, and an unenforceable constraint refuses the payment (your 1a). The rest (category limits, agreements, refund authority, fulfillment acks, discovery versions) are follow-up issues rather than profile text; happy to have you file them.

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.

4 participants