Skip to content

RFC: a scheme model for ACK-Pay (up-to, sessions/batch, subscriptions, streaming, refunds) #111

@EfeDurmaz16

Description

@EfeDurmaz16

@venables would love your read on the shape.
Request for comment. Nothing is built against this yet. I want maintainer input on shape and naming before any code lands. Efe Baran Durmaz (@EfeDurmaz16).

TL;DR

ACK-Pay can express exactly one payment shape: a fixed amount, paid in full. x402 and MPP already ship the rest as first-class schemes and intents: spend caps, sessions, subscriptions, metering, refunds. This RFC proposes a small, additive scheme model for ACK-Pay plus five schemes ported from that prior art, each with a spec page, a reference implementation, and a conformance suite. ACK keeps its lead on verifiable receipts and identity, and gains the settlement shapes the rest of the ecosystem already speaks.

The gap isn't trust. It's shape.

ACK already wins the two hard things its neighbors mostly lack: verifiable receipts (W3C VCs) and an agent-to-owner identity chain (ACK-ID). What it cannot say is "authorize up to X, settle less", "open a session and settle many micro-payments in one move", "charge on a cadence", "meter per unit", or "refund against a receipt".

None of those are exotic. x402 ships an explicit scheme registry (exact, upto, deferred, escrow). MPP ships an intent model (charge, session, subscription, vouchers). ACK-Pay ships one implicit shape: exact, paid in full.

An agent that can only pay a fixed amount in full is a demo, not an economy.

What ACK-Pay expresses today

PaymentOption (packages/ack-pay/src/schemas/valibot.ts:7-16):

export const paymentOptionSchema = v.object({
  id: v.string(),
  amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]),
  decimals: v.pipe(v.number(), v.integer(), v.toMinValue(0)),
  currency: v.string(),
  recipient: v.string(),
  network: v.optional(v.string()),
  paymentService: v.optional(urlOrDidUri),
  receiptService: v.optional(urlOrDidUri),
})

There is no scheme, and no way to carry spend-cap, session, cadence, metering, or refund semantics. The receipt, though, already has a sanctioned extension point: paymentReceiptClaimSchema.metadata (valibot.ts:34-37). Each scheme can use it to record what was actually settled.

Proposal: an additive scheme discriminator

Add an optional scheme tag to PaymentOption, defaulting to "exact" so every existing request and receipt keeps working untouched. Per-scheme parameters ride in a typed shape keyed on scheme (sketch, exact names open):

// default keeps today's behaviour: a fixed amount paid in full
scheme: v.optional(v.string(), "exact")     // "exact" | "up-to" | "escrow" | "subscription" | "stream"
// params validated by a discriminated union on `scheme`; absent for "exact"
schemeParams: v.optional(/* per-scheme */)

This mirrors x402's scheme registry on purpose, so an ACK option and an x402 scheme map cleanly (see Interop). The receipt records the outcome (settledAmount, session id, subscription id, refunded-receipt id) in its existing metadata, so the receipt stays the single verifiable record.

The five schemes

Scheme Port from What it does Shape (sketch)
up-to / spend-cap x402 upto, MPP escrow cap, AP2 intent cap authorize a max, settle at or below it { maxAmount, decimals, currency }; receipt: settledAmount ≤ max
escrow (sessions + batch) x402 escrow/deferred, MPP voucher sessions open once, accrue signed vouchers, settle the aggregate in one move { maxAmount, ... } + session id; receipt(s) reference the session
subscription MPP subscription intent, AP2 renewals recurring charge on a cadence { amount, interval, anchor? }; child requests linked by subscription id
stream (metered) MPP per-unit vouchers pay-per-use over time { unitAmount, unit, maxUnits? }; receipt records units
refunds MPP native, x402 escrow refund refund against a prior receipt references a prior receipt id, issues a linked refund receipt

Refunds is the odd one out. It is not a PaymentOption shape, it is a receipt-linked operation, so it probably wants its own small request/receipt type rather than a scheme value. Flagged below.

Each scheme ships with conformance

Every scheme lands with coverage, not just code. The plan is a tools/conformance workspace package (mirroring tools/api-utils, wired into the turbo test pipeline) that drives the canonical flow end to end, createSignedPaymentRequest then 402 then verifyPaymentRequestToken then pay then createPaymentReceipt/signCredential then verifyPaymentReceipt, and asserts the cross-cutting properties (idempotency, replay, duplicate-order, approval, revocation) plus a per-scheme suite. For up-to, that means settling above the cap is rejected and the receipt's settledAmount is at or below the authorized max. demos/payments and examples/issuer are the reference flows it exercises.

(I have a thin version of this harness running locally against the existing exact flow already, as the foundation.)

Interop comes with the model

Because the scheme tags line up with x402's, an ACK Payment Service can act as an x402 Facilitator (the docs gesture at this, there is no code yet), and an AP2 interop adapter can bridge ACK-ID credentials and ACK-Pay receipts to AP2's mandate format. An interop.mdx page would map ACK, x402, MPP, and AP2 scheme by scheme, so no one is locked to one protocol. ACK's differentiator stays its verifiable receipts and identity. The schemes just let it speak everyone else's settlement.

Reach: the schemes need more than TypeScript

Agents get built in Python, Go, Rust, Ruby, not only TS. ACK is TypeScript-only today with no machine-readable spec, so the schemes would stay reachable by the JS slice only. The fix is a cross-language source of truth (JSON Schema plus a minimal OpenAPI for the Payment/Receipt service, generated from the existing valibot/zod schemas), then client+server SDK ports validated for parity by the same conformance harness. This is the model I built for solana-foundation/pay-kit (x402 and MPP across nine languages with a shared harness).

Versioning: additive, per-scheme

  • scheme defaults to "exact", so existing requests and receipts are unchanged.
  • Each scheme is independently versionable and follows ACK's date-based spec versioning.
  • New code follows ACK's dual-schema pattern (valibot plus zod v3/v4) and ships behind its own changeset.

Non-goals

  • No change to ACK-ID or the receipt format beyond the existing metadata extension point.
  • No settlement engine. Schemes describe authorization and what the receipt records. Execution stays with the Payment Service, as today.
  • Not absorbing x402 or MPP. The aim is interop, not replacement.

Open questions (where I want your call)

  1. Shape: scheme tag plus discriminated schemeParams on PaymentOption, or a new request kind per scheme?
  2. Naming: mirror x402's identifiers (exact/upto/escrow/deferred) for interop, or ACK-native names?
  3. Refunds: a dedicated request/receipt type, or a scheme?
  4. Receipt extension: is metadata the right home for settledAmount/session/subscription refs, or should these be first-class receipt fields?
  5. Conformance home: is tools/conformance right, and should it be published or internal-only?
  6. Order: if you'd take only some of these first, which schemes matter most to ACK's roadmap?

How I'd land it

Incrementally. One scheme per PR, each with a spec page, a reference implementation, and a conformance suite, reviewed before it opens, the same way I shipped the credential-verification fix in #110. I'd start with the scheme model plus up-to as a thin first PR once the shape here has your blessing.


AI-assisted (Claude Code), reviewed by me; I take responsibility for this proposal. Prior art: x402 schemes (github.com/coinbase/x402), MPP (tempo.xyz, mpp.dev), AP2 (ap2-protocol.org). I'm a core contributor to solana-foundation/pay-kit, where I built the equivalent polyglot scheme and conformance work for x402 and MPP.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions