39. pepsi-stage-auto-pay

Pay GNU Taler delivery demands automatically (the sending side of pay-to-send).

39.1. Role

pepsi-stage-auto-pay has two auto-detected roles. On the inbound path it is the sending counterpart to pepsi-stage-anti-spam: when mail this deployment relayed is held at the far end behind a pay-to-send gate, that gate e-mails the original sender a payment demand, and this stage detects such demands and settles them automatically, so the held message is released and delivered. On the submission path it is a wallet self-service endpoint (enabled by RESPONSE_STAGE; see below). Reference: pepsi-stage-auto-pay(1).

A demand is recognised by the Taler: header field, one of the two private header extensions documented in SMTP Protocol Extensions; several URIs in one field and several fields are both accepted. Detection is not authorisation: the stage pays only for mail this deployment provably originated, which it establishes from the Pepsi-Origin: header recovered out of the returned message — an RFC 2104 HMAC over the sending host, sender and a nonce that must still be tracked in pepsi.origin_nonce.

Neither header is registered under RFC 3864, and neither carries an X- prefix, per RFC 6648. The taler:// URI they carry is GNU Taler’s own scheme rather than an IANA-registered one (RFC 7595 sets that procedure); the wire-transfer instructions the self-service withdraw command returns use the payto: URI scheme of RFC 8905, and the merchant credential the stage authenticates with is an RFC 8959 secret-token: URI.

39.2. Wallet self-service (submission path)

When RESPONSE_STAGE is set, a locally-originated (authenticated) message to <CONTROL_LOCAL_PART>@<served-domain> (default local part pepsi-wallet) lets the sender operate their own wallet by e-mail — parallel to pepsi-stage-edit-settings, but keyed on the envelope sender. The command is the message Subject:: balance, withdraw CUR:VAL EXCHANGE-URL (a manual top-up from the named GNU Taler exchange — chosen per request, since a wallet is multi-currency and multi-exchange — the reply gives bank wire instructions), pull CUR:VAL [subject] (a peer pull — the reply gives a taler://pay-pull/… URI to forward) or push <taler://pay-push/…> (accept a peer push; the URI may be in the body). The localised reply (wallet.<lang>.body template, null sender) is injected at RESPONSE_STAGE and the original consumed; a malformed request gets usage instructions. If the wallet hits a KYC requirement, the reply carries the KYC link instead (self-service only — never while paying inbound demands). Any other message passes through to NEXT_STAGE.

39.3. Decision

Driven by the message’s headers (not its state):

  • no Taler: header → not a demand; forward to NEXT_STAGE untouched (so the stage is safe to place early on the inbound path).

  • a Taler: header, but the demand is not provably ours — no valid Pepsi-Origin: proof (forged/expired, or any demand when [pepsi-origin] is unconfigured) → never pay; forward to NEXT_STAGE.

  • provably ours → for each taler://pay/… URI, driving the privileged pepsi-helper-auto-pay(1) wallet helper: quote its price (the helper’s preview), book it against the per-original-message budget (MAX_TOTAL), and if it fits, settle (the helper’s pay) it.

    • every demand settled → discard the now-redundant bounce (delete the row);

    • a demand that cannot be priced, exceeds the budget, or differs in currency → forward to NEXT_STAGE so the message proceeds as an ordinary bounce and the sender is informed;

    • a demand booked under the budget that then fails to pay → the reservation is refunded to the ledger (the un-spent payment must not consume the budget) and the message is forwarded. Auto-pay tries to pay; if it cannot, it does nothing (logs the error) and lets the bounce through.

39.4. Cumulative budget

The budget is keyed on the Pepsi-Origin nonce, which identifies the original outbound message. A message fanned out to a mailing list draws several demands — one per member whose site charges — that all carry the same nonce echoed from the original, so they share one budget. The per-message ledger lives in pepsi.origin_nonce.amount; each settlement advances it atomically (the auto_pay_try_spend function locks the row), and a demand that would push the running total past MAX_TOTAL is refused. The cap is single-currency. An account can override its own MAX_TOTAL by e-mail through pepsi-stage-edit-settings (the override is keyed on the account that receives the demand — the original sender).

Verification reuses the proof-of-origin machinery: see SMTP Protocol Extensions and pepsi-stage-relay-to-internet (which stamps Pepsi-Origin on outbound mail and records the nonce).

39.5. Wallet account and the helper

All wallet interaction is performed by the setuid-root pepsi-helper-auto-pay(1) helper, which drops to the right user before running taler-wallet-cli. WALLET_MODE chooses the account: local-user pays from the bounce recipient’s own local wallet (the original sender; the recipient must resolve to a permitted local account), while shared (the default) pays from a single dedicated WALLET_USER account (default pepsi-wallets) whose home holds the wallet database(s) — one per sender (WALLET_SCOPE = per-sender) or a single unified wallet shared by everyone (WALLET_SCOPE = unified, e.g. a business). Because the stage execs the helper, its binary is installed standalone and SGID pepsi-wallets (the helper is setuid-root, root:pepsi-wallets, mode 4750).

Note

The one wallet-version-dependent point is how pepsi-helper-auto-pay reads a demand’s price from taler-wallet-cli’s preview output; it is isolated in a single function so a future CLI change is a one-line fix.

39.6. Configuration

[stage-<name>]: PROGRAM = pepsi-stage-auto-pay, MAX_TOTAL (a Taler amount CURRENCY:VALUE bounding total spend per original message, required), NEXT_STAGE (required — where a non-demand or unpaid message is forwarded), and the wallet knobs WALLET_MODE, WALLET_USER, WALLET_SCOPE, WALLET_CLI, WALLET_PAY_OPTIONS and HELPER; the self-service role adds RESPONSE_STAGE (enables it), CONTROL_LOCAL_PART, RESPONSE_FROM and RESPONSE_SUBJECT (a withdraw names its exchange in the request, so none is configured). The proof-of-origin secret is the shared [pepsi-origin] section. See pepsi-stage-auto-pay(1).

39.7. State

  • Inputs: the message’s Taler: and Pepsi-Origin: headers (the body is also scanned for an embedded Pepsi-Origin). The spend ledger is pepsi.origin_nonce.amount, not the message state.

  • Outputs: none on the message; it is advanced to NEXT_STAGE or deleted.

39.8. See also

pepsi-stage-anti-spam, pepsi-stage-relay-to-internet, pepsi-stage-edit-settings, SMTP Protocol Extensions, pepsi-stage-auto-pay(1).