# Solidarity: processor mode (informative)

Status: draft 2 (2026-10-22), with the AP2 part rewritten as a Solidarity Mandate ("AP2: a Solidarity Mandate"). Part of the `solidarity` proposal, whose x402 extension is [solidarity.md](solidarity.md). Informative: processor mode puts nothing on the x402 wire, and nothing here is part of x402, AP2, ACP or any other protocol, or reviewed or endorsed by their maintainers or communities. Its log follows [attestation.md](attestation.md), which is normative. Author: Craig Merry, craigmerry.com, written with AI assistance (Claude Code). No fund exists and no money moves.

This document covers card and bank rails, notes for AP2 and ACP, the `net` refund rule, fees and `minCollection`, and fund policy. The owner policy and the rules for both modes are in [solidarity.md](solidarity.md) ("Modes", "Owner policy").

## Processor mode

For card and bank rails (for example under AP2 or ACP) and custodial agent wallets. The owner consents once, for example as a constraint in a mandate shown on a trusted surface. A buyer-side party that holds the owner's credential or funds, and is already permitted to collect and remit charitable funds where it operates, holds the policy and applies it. It:

- records one line per finalized purchase in its own log;
- collects shares in aggregate, from a prefunded balance or with one charge per period under its own descriptor. Amounts below its published `minCollection` carry forward to the next period. A line's period is the month the processor collected it;
- pays each fund once per period;
- never puts the share on the merchant-facing payment record: not in the amount, line items, settlement or receipt the merchant sees.

A second card authorization per purchase would pay a fixed fee larger than most shares and double the points of failure. Putting the share inside the merchant's charge would mix two beneficiaries and drag the share into the merchant's refunds and chargebacks. Processor mode gives one consent, separate records and a monthly aggregate.

The cap bounds what the processor commits in each calendar month. A share committed late in one month can be collected in a later one, so a period's lines can include shares committed in earlier months, and one owner's lines in a period can add up to more than the cap.

## The processor's log

It publishes exactly what [attestation.md](attestation.md) specifies, the same as a facilitator's log, with these particulars:

- A batch includes only shares the processor has actually collected from the owner, and a line's period is the month of collection.
- Each payment to a fund is a `payout:<reference>` settlement reference, with a reference that no other log paying that fund uses (for example, one that starts with the log's host) and that the fund can match in its own records.
- Its descriptor omits `accepts` and may publish `minCollection`: when an owner's shares not yet collected add up to less, they carry forward to the next period.
- It returns share ids to the owner, who fetches proofs with them.

## Refunds

`keep` is the default, as in [solidarity.md](solidarity.md) ("Refunds"). Processor mode can also offer `net`.

**Net.** An owner policy may elect `refunds: "net"` instead. When a purchase that carried a share is refunded in whole or in part, the processor records a credit. For each purchase, the credits never add up to more than `min(floor(total refunded × rate), share)`, where the total counts every refund of that purchase so far and `share` is the amount recorded for it. Each refund's credit is that bound minus the credits already given for the purchase, so several partial refunds credit exactly what one refund of the same total would. The credit only lowers that owner's later shares. It can be used in the period of the refund and the next one, wherever the purchase's batch was, and lapses at the end of the next period. It is never paid out, never changes a closed batch and never produces a zero or negative line.

**Chargebacks.** The share is never on the merchant-facing payment record, so a chargeback or dispute of the purchase cannot touch it. If the processor's collection from the owner is later reversed (for example, a chargeback of its own monthly charge), the loss is the processor's. It must not reduce a published batch or ask the fund for anything, and it may suspend the owner's policy.

## Fees

A 0.1% share of a small purchase is smaller than one on-chain transfer fee or one card charge fee, so aggregation is what makes small shares possible at all.

- In separate mode, `batch-settlement` moves fees from per share to per owner per period ([attestation.md](attestation.md), "Optional share kind: `batch-settlement`"). `exact` is used only at or above the log's `minAmount`.
- The log operator pays the gas for `exact` transfers, claims and sweeps, because totals are gross. The owner funds its own `batch-settlement` channel ([attestation.md](attestation.md), "Optional share kind: `batch-settlement`").
- In processor mode, one charge per period carries the card's fixed per-charge fee. At a 0.1% rate that fee is larger than the monthly share of an owner whose agent spends less than a few hundred dollars a month, which is why `minCollection` carries small amounts forward.

Whether a log may instead disclose and deduct a fee (gross, fee and net per totals row, shown to the owner at consent) is an open question.

## AP2 and ACP notes

These are suggestions to those communities, not part of either protocol. AP2 names follow its v0.2 specification and Payment Mandate documents; AP2's core specification now lives with FIDO, so they may change. Nothing here is endorsed by either community.

**AP2.** The owner policy can be an AP2 mandate of its own, a Solidarity Mandate, which the owner signs once on the Trusted Surface and the agent presents only to the party that applies it. "AP2: a Solidarity Mandate", below, defines it and says why a Payment Mandate Constraint, as draft 2 first suggested, cannot work.

**ACP.** Processor mode, run by the buyer-side party that holds the delegated credential. The policy is a standing buyer consent, not a merchant-advertised extension, which matches the Extensions Framework's merchant-side scope. Inputs are completed checkouts, plus refund events when `net` is elected. Collection is a separate periodic charge, outside any per-checkout allowance. Only the receipt format needs a spec surface; custody stays with payment service providers.

| This proposal | AP2 (suggested) | ACP (suggested) |
| --- | --- | --- |
| Owner policy | A Solidarity Mandate (`com.craigmerry.solidarity.1`), signed on the Trusted Surface and presented only to the holder | Standing buyer consent held with the delegated credential |
| Policy holder (`holder`) | A buyer-side party, normally the Credential Provider | The party behind Delegate Payment |
| Share on the merchant's record | Not in the Checkout Mandate or the Payment Mandate | Not in the checkout or its extensions |
| Collection | Aggregate, once per period | A separate periodic charge |
| `net` refunds | Refund events the processor sees | Refund events for completed checkouts |
| Receipt | Enrollment in the holder's signed Mandate Receipt; share ids from the holder; proofs from the log | Share ids to the owner; proofs from the log |

### AP2: a Solidarity Mandate

AP2 names here follow its v0.2 specification, its Payment Mandate and Agent Authorization documents, its schemas and its Python SDK (google-agentic-commerce/AP2 at commit `e1ea56db`). Suggestions only: none of this is part of AP2.

**Why not a Payment Mandate Constraint.** Draft 2 first suggested carrying the policy as a constraint in an open Payment Mandate. As AP2 stands, that cannot work:

- Constraints are evaluated against the closed Payment Mandate, and an agent MUST present only the disclosures from its open mandates that are needed for that evaluation. A share never limits the payment, so its constraint is never needed, and an agent following AP2 would never present it.
- If it were presented, it would travel with the Payment Mandate. The Credential Provider forwards the mandate to the Network, the Merchant Payment Processor verifies it in the payment token, and the agent's key binding covers exactly the disclosures it presented, so no one along the way can drop one. Each of them MUST treat a constraint type it does not know as failing (`unresolved_constraint`), which would block the purchase.
- AP2's published open Payment Mandate schema lists its eight constraint types in a closed `oneOf`, and its Python SDK rejects any other type.

AP2 also lets a specifier define new mandate types, so the policy gets one of its own. The Checkout Mandate and the Payment Mandate carry nothing, and no party but the holder ever sees the policy.

**Type.** `vct` `com.craigmerry.solidarity.1`. AP2 recommends a collision-resistant name for a new type, such as a reverse-DNS prefix the specifier controls, and craigmerry.com specifies this one. The `.1` suffix follows AP2's mandate versioning.

**Content.** An SD-JWT VC like AP2's other mandates. The owner approves it on the Trusted Surface, where it shows the rate, cap, fund and refund rule ([solidarity.md](solidarity.md), "Owner policy"), and it is signed as AP2 mandates are. It is a standing consent given to the holder, not an authorization for an agent to pay, so it delegates nothing and carries no `cnf`. Other SD-JWT VC claims, such as `iss`, may appear.

- `holder`: the one party that applies the share, written exactly as its origin: lowercase `https`, no path, no trailing slash, no default port. It is normally the Credential Provider, which provides the payment credential for the owner's card and bank payments.
- `rate`, `fund`, `logs` and `refunds`: the owner policy's fields. `logs` names the holder's log.
- `cap`: an AP2 Amount, an integer in minor units of an ISO 4217 currency, so `{"amount": 500, "currency": "USD"}` is $5.00 a month.
- `iat`, required in this mandate type: when the owner approved it. It stands in for the policy's `since`.
- `exp`, optional: when the policy ends. AP2 defines no way to revoke a mandate, so an owner who wants to stop sooner tells the holder, which must then stop at once ([solidarity.md](solidarity.md), "Owner policy"). A newer mandate starts it again.

None of its claims is selectively disclosable: the holder needs all of them, and the mandate goes to no one else.

<!-- example:ap2-mandate -->
```json
{
  "vct": "com.craigmerry.solidarity.1",
  "holder": "https://processor.example",
  "rate": "0.001",
  "cap": { "amount": 500, "currency": "USD" },
  "fund": "https://fund.example/solidarity",
  "logs": ["https://processor.example/solidarity"],
  "refunds": "keep",
  "iat": 1792574400,
  "exp": 1824110400
}
```

<!-- example:ap2-mandate-schema -->
```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "vct": { "const": "com.craigmerry.solidarity.1" },
    "holder": { "type": "string", "pattern": "^https://[^/?#\\sA-Z]+$" },
    "rate": { "type": "string", "pattern": "^0\\.(01|00[0-9]{0,3}[1-9])$" },
    "cap": {
      "type": "object",
      "properties": { "amount": { "type": "integer" }, "currency": { "type": "string", "pattern": "^[A-Z]{3}$" } },
      "required": ["amount", "currency"], "additionalProperties": false
    },
    "fund": { "type": "string", "pattern": "^\\S{1,256}$" },
    "logs": { "type": "array", "items": { "type": "string", "pattern": "^https://" } },
    "refunds": { "enum": ["keep", "net"] },
    "iat": { "type": "integer" },
    "exp": { "type": "integer" }
  },
  "required": ["vct", "holder", "rate", "cap", "fund", "logs", "iat"]
}
```

**Presentation.** The Shopping Agent presents the mandate to the holder, and only to the holder. This reuses AP2's mandate format, its Verification and Processing Rules and its Mandate Receipt, but not its authorization model: AP2 mandates authorize an agent to act, and this one authorizes nothing. With no `cnf`, nothing binds it to a key, so it works like a bearer credential. The holder takes it only as the policy of the owner whose approval it carries, and the agent shows it to no one else.

The holder returns a signed Mandate Receipt, whose `reference` is the hash of the mandate it received. AP2 lets a Mandate Receipt carry use-case properties, and the holder adds `solidarity`. When it enrolls the policy, `result` is `success` and `solidarity` is `enrolled`. When it refuses, `result` is `error` with AP2's `invalid_mandate` (the mandate fails to approve the requested action), and `solidarity` is `refused` with a reason:

- `bad-policy`: not a valid processor-mode policy; `holder` names this holder but not exactly as its origin; `iat` missing or later than the holder's clock allows (a few minutes of skew); or an `exp` that has already passed;
- `wrong-log`: `logs` does not name the holder's log;
- `fund-not-allowed`: the fund is not on the holder's allow-list;
- `superseded`: the holder already holds a policy for this owner and currency with a later or equal `iat`;
- `stopped`: a retry of a mandate the owner has since stopped. Only a newer mandate starts it again;
- `suspended`: the holder suspended this owner, in every currency, after its own collection was reversed ("Refunds"). No mandate lifts that.

A retry of the mandate the holder already enrolled gets the same answer again.

<!-- example:ap2-receipt -->
```json
{
  "iss": "https://processor.example",
  "result": "success",
  "reference": "WJYWALFsU0xZ5khqJSQWWlIoT502O9YGFoE3xITNmsw",
  "solidarity": { "status": "enrolled", "log": "https://processor.example/solidarity" }
}
```

<!-- example:ap2-enrollment-schema -->
```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "status": { "enum": ["enrolled", "refused"] },
    "reason": { "enum": ["bad-policy", "wrong-log", "fund-not-allowed", "superseded", "stopped", "suspended"] },
    "log": { "type": "string", "pattern": "^https://" }
  },
  "required": ["status", "log"],
  "oneOf": [
    { "properties": { "status": { "const": "enrolled" }, "reason": false } },
    { "properties": { "status": { "const": "refused" } }, "required": ["reason"] }
  ],
  "additionalProperties": false
}
```

**Applying it.** The holder applies the policy in processor mode ("Processor mode", above) to every payment it provides the credential for, for that owner, in the cap's currency, from `iat` until `exp`: one line once the payment is final, under the policy in force at that moment. It returns share ids to the owner through its own channel ("The processor's log"), and the owner fetches proofs with them. Payments in any other currency carry no share. An owner who pays in several currencies signs one mandate per currency, as [solidarity.md](solidarity.md) requires one policy per asset.

**Replacing it.** A mandate with a later `iat` replaces the policy for its currency, and starts it again if the owner had stopped it. What the owner already committed that month in that currency still counts against the new cap, and each share already recorded keeps the rate, fund and refund rule it was recorded under.

**One holder.** A client MUST NOT add `extensions.solidarity` to a payment whose credential the holder provides ([solidarity.md](solidarity.md), "Modes"). An x402 payment that the client signs with its own key is not one of those, so an owner can hold a Solidarity Mandate for card payments and an x402 policy for stablecoin payments, and no payment carries two shares.

The reference implementation's `ProcessorHolder.enrollAp2` follows these steps, and its `conformance/ap2.json` covers each reason.

## Fund policy

The proposal leaves allocation to each fund. A fund that wants to be trusted with shares would:

- publish its allocation rule, its exact inputs (pinned by hash) and its results, so anyone can recompute them;
- report what it raises alongside official aid levels, so it cannot quietly replace them;
- cosign the statements that name it ([attestation.md](attestation.md), "Witnesses and the fund").

One possible policy is a window of a UN pooled fund, such as the Central Emergency Response Fund, allocated by a published rule like the one in [`allocate.js`](../allocate.js). That is a suggestion for discussion, not a requirement, and nothing here is affiliated with or endorsed by the UN.

The only normative fund rules are in [attestation.md](attestation.md): each log publishes its own allow-list (`funds` in its descriptor) and settles only to those funds, and a fund's cosignature attests receipt.

## Open questions

1. **AP2.** Is a separate Solidarity Mandate the right shape, given that a Payment Mandate Constraint cannot work as AP2 stands ("AP2: a Solidarity Mandate")? It reuses AP2's format and receipts but authorizes no agent action and carries no `cnf`: is a mandate type like that acceptable, or should consent to a holder be its own kind of credential? Do the names hold now that AP2's core specification lives with FIDO? Should the Credential Provider be the `holder` and run processor mode? Is there a route for a non-member to propose a mandate type?
2. **ACP.** Are delegated credentials scoped to a single checkout with a maximum amount? If so, processor mode needs a standing buyer consent outside Delegate Payment's allowance.
3. **Regulation.** Collecting owners' money and passing it to a third-party fund can be money transmission or remittance, can need charitable-solicitation registration in many US states, and a monthly charge falls under card-network rules for recurring merchant-initiated charges. Who can run processor mode, and where?
4. **Allow-lists and tax receipts.** Who curates a shared fund allow-list beyond each log's own? Who issues tax receipts, and in which countries can a share be deductible?
5. **`net` at all.** Is `net` worth keeping, given that `keep` is simpler and final?
