45. pepsi-stage-auto-pay

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

45.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. (The stage holds no merchant credential: it pays through a wallet, and only the charging side, pepsi-stage-anti-spam, authenticates to a merchant backend.)

45.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.

45.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 the paying wallet’s daily budget (MAX_TOTAL_PER_DAY), and if it fits both, settle (the helper’s pay) it.

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

    • a demand that cannot be priced, exceeds either 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 both ledgers (the un-spent payment must not consume either 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.

45.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).

MAX_TOTAL alone bounds one message, not a correspondent: everybody who received one of our messages holds a valid proof, so somebody who received k of them can demand MAX_TOTAL k times. MAX_TOTAL_PER_DAY is the aggregate: what one wallet may spend in any 24 hours — the user’s own under WALLET_MODE = local-user, the sender’s under shared/per-sender, the deployment’s under shared/unified. It is never per correspondent, whom mailing lists and aliases make unknowable. The same auto_pay_try_spend call checks both budgets and records the booking in pepsi.auto_pay_spend — serialised per wallet, so concurrent demands for different messages cannot overrun it — and auto_pay_refund deletes the booking again when the payment fails to settle. The window is a rolling 24 hours; the limit is single-currency, in MAX_TOTAL’s currency; unset, there is none.

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).

45.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

Everything that depends on the taler-wallet-cli version is confined to pepsi-helper-auto-pay: the subcommand and flag spelling each operation uses (build_wallet_args), and the small parsers that read an amount, a payto: URI, a taler:// URI and a KYC link out of its output. The stage itself speaks only to the helper, so a future CLI change is confined there.

45.6. Configuration

[stage-<name>]: PROGRAM = pepsi-stage-auto-pay, MAX_TOTAL (a Taler amount CURRENCY:VALUE bounding total spend per original message, required), MAX_TOTAL_PER_DAY (optional, same currency, bounding one wallet’s spend in any 24 hours), 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).

45.7. State

  • Inputs: the message’s Taler: and Pepsi-Origin: headers (the body is also scanned for an embedded Pepsi-Origin). The spend ledgers are pepsi.origin_nonce.amount (per message) and pepsi.auto_pay_spend (per wallet, last 24 hours), not the message state.

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

45.8. See also

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