.. This file is part of PEPSI. Copyright (C) 2026 GNUnet e.V. PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. ==================== pepsi-stage-auto-pay ==================== *Pay GNU Taler delivery demands automatically (the sending side of pay-to-send).* Role ==== ``pepsi-stage-auto-pay`` has **two** auto-detected roles. On the **inbound** path it is the **sending** counterpart to :doc:`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: :manpage:`pepsi-stage-auto-pay(1)`. A demand is recognised by the ``Taler:`` header field, one of the two private header extensions documented in :doc:`../smtp-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, :doc:`pepsi-stage-anti-spam`, authenticates to a merchant backend.) Wallet self-service (submission path) ===================================== When ``RESPONSE_STAGE`` is set, a **locally-originated** (authenticated) message to ``@`` (default local part ``pepsi-wallet``) lets the sender operate **their own** wallet by e-mail — parallel to :doc:`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 `` (accept a peer push; the URI may be in the body). The localised reply (``wallet..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``. 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 :manpage:`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. 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 :doc:`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 :doc:`../smtp-extensions` and :doc:`pepsi-stage-relay-to-internet` (which stamps ``Pepsi-Origin`` on outbound mail and records the nonce). Wallet account and the helper ============================= All wallet interaction is performed by the setuid-root :manpage:`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. Configuration ============= ``[stage-]``: ``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 :manpage:`pepsi-stage-auto-pay(1)`. 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. See also ======== :doc:`pepsi-stage-anti-spam`, :doc:`pepsi-stage-relay-to-internet`, :doc:`pepsi-stage-edit-settings`, :doc:`../smtp-extensions`, :manpage:`pepsi-stage-auto-pay(1)`.