85.1.17. pepsi-stage-auto-pay¶
pay GNU Taler delivery demands automatically
- Manual section:
1
85.1.17.1.1. Name¶
pepsi-stage-auto-pay - the automatic pay-to-send settlement stage of the Pepsi pipeline.
85.1.17.1.2. Synopsis¶
pepsi-stage-auto-pay [GLOBAL-OPTIONS] worker
85.1.17.1.3. Description¶
pepsi-stage-auto-pay is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It has two roles, chosen per message:
On the inbound path it is the sending counterpart to pepsi-stage-anti-spam(1): when mail this site 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: a locally submitted (authenticated) message addressed to the wallet control address lets the sender operate their own GNU Taler wallet by e-mail — check a balance, top it up, accept a peer push payment, or initiate a peer pull request. This role is enabled only when RESPONSE_STAGE is configured (see Wallet self-service below).
The two roles share one binary and are auto-detected, so a single
PROGRAM = pepsi-stage-auto-pay may be placed on either pipeline (or both, as two
[stage-<name>] sections).
A demand is recognised by the Taler: header that pepsi-stage-anti-spam(1)
adds to its payment-request auto-reply, carrying one or more taler://pay/…
URIs. Only taler://pay/ URIs are ever settled — any other taler:// scheme
in that header (a pay-pull or withdraw URI, which would be a wallet-drain
surface) is ignored — and a URI repeated across the header(s) counts once. A
message with no Taler: header is ordinary mail and is forwarded to
NEXT_STAGE untouched, so the stage is safe to place early on the inbound path.
Pepsi only pays for mail it provably originated. The same auto-reply echoes the
original message’s Pepsi-Origin: proof-of-origin header (see
pepsi-stage-anti-spam(1) and pepsi-stage-relay-to-internet(1)). This
stage verifies it the same way a returning bounce is verified — the HMAC must
check against our secret and the nonce must still be tracked in
pepsi.origin_nonce. A demand that carries no valid proof (including every
demand when [pepsi-origin] is not configured) is never paid: it is
forwarded to NEXT_STAGE unchanged.
Warning
The header authenticates the deployment, not the individual bounce. It is
stamped on the message that is delivered, so every recipient of that message
holds a valid copy of it, and verification is an HMAC check plus an
existence check of the nonce — the nonce is not consumed, not limited to one
use, and not bound to the message it was minted for. Any recipient can
therefore replay the header on a message of their own for the rest of the
nonce’s validity window (14 days) and have this stage treat the Taler:
demands they attach as demands about mail we originated. MAX_TOTAL is the
only bound on what that costs, and it is per nonce — so set it to an amount you
are willing to lose per outbound message, not per pipeline.
The proof also names the account the original message was sent for, and that
account is the only one whose wallet may settle the demand: the demand’s envelope
recipient must be that same account, compared case-insensitively. This binding is
what makes the proof mean anything about who pays. On its own it says only that
this deployment sent some message under that nonce — and every correspondent who
has ever received Pepsi-relayed mail holds a valid, still-tracked copy of such a
header — so a demand carrying somebody else’s Pepsi-Origin and addressed to a
different local user would otherwise be settled out of that user’s wallet. A
demand whose recipient is not the account named is forwarded to NEXT_STAGE
unpaid. (Note this also means a message whose envelope sender was SRS-rewritten
before relay — forwarded third-party mail — draws no automatic payment: the proof
names the rewritten address, not the recipient the bounce comes back to.)
For a verified demand, the stage processes each taler://pay/… URI in turn,
driving the wallet through the privileged pepsi-helper-auto-pay(1) helper:
quote its price (the helper’s
previewoperation reads the order’s contract terms without paying);book it against a cumulative budget for the original message, capped by MAX_TOTAL, and against the paying wallet’s budget for the last 24 hours, capped by MAX_TOTAL_PER_DAY (when set); and
if it fits under both, settle it (the helper’s
payoperation spends wallet coins).
Every helper invocation is bounded (five minutes; the child is killed if it
runs over) and a timeout counts as a failure, so the demand goes unpaid and the
bounce is forwarded. A payment that timed out may nonetheless have gone
through, which nothing here can tell; it is logged as an error saying so. The bound exists because the merchant backend a demand names
is chosen by whoever wrote the Taler: header: an unbounded wallet command would
leave the row running with its worker unable to take another message, and
PARALLELISM such demands would stop the stage — including the wallet
self-service role, if it shares the section.
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, so they
share one budget: the per-message ledger in pepsi.origin_nonce.amount is
advanced atomically as each demand is settled (the auto_pay_try_spend function),
and a demand that would push the running total past MAX_TOTAL is refused. The
cap is single-currency: a demand whose currency differs from MAX_TOTAL is
not paid. An individual account can raise or lower its own cap with
pepsi-stage-edit-settings(1) (the override applies to mail addressed to that
account — i.e. the original sender, who receives the demand).
The daily budget. MAX_TOTAL_PER_DAY bounds what one wallet spends
on demands in any 24 hours, however many original messages they are spread
over. It is metered per wallet because the wallet is the one thing the stage
always knows: under WALLET_MODE local-user each user’s own wallet; under
shared with WALLET_SCOPE per-sender each sender’s wallet database;
under unified the one wallet everybody shares, so the limit is then the
whole deployment’s. It is deliberately not per correspondent: mailing lists
and aliases mean the stage never knows for sure who is demanding. Every booking
is recorded in pepsi.auto_pay_spend by the same auto_pay_try_spend call
that advances the per-message ledger — both checks and both writes in one
statement, serialised per wallet, so concurrent demands for different
messages cannot together pass the limit either — and a demand refused by either
budget books nothing against the other. The window is rolling, not a calendar
day (which would allow twice the limit across midnight), and rows that have left
it are pruned by the same function. The limit is single-currency and must be in
MAX_TOTAL’s currency. Unset, only the per-message budget applies.
What MAX_TOTAL does and does not bound. The Taler: header — the merchant
host, the order id, and therefore the price — is chosen entirely by the far end and
is covered by nothing: the Pepsi-Origin MAC binds the version, our hostname, the
originating account, the nonce and a timestamp, and so proves only that this
deployment sent a message under this nonce for this account. It does not bind the
demand, the merchant, or an amount. Every correspondent obtains a valid, 14-day-live
proof simply by reading the mail you sent them, so a correspondent can always
present a self-priced demand for up to MAX_TOTAL and be paid. MAX_TOTAL is
therefore a bound per original message, not per correspondent and not per period:
total exposure to one determined correspondent scales with how many messages you
have sent them. Set it to a sum you are willing to pay for a single delivery, and
set MAX_TOTAL_PER_DAY to what a wallet may lose in a day when every
correspondent holding a proof cashes it in — that is the spend limit.
When every demand on a message has been settled, the now-redundant bounce is
discarded (the row is deleted). If any demand cannot be priced, would exceed
either budget, or differs in currency, the message is instead forwarded to
NEXT_STAGE, so it proceeds as an ordinary bounce and the original sender is
informed of the delivery problem as usual. A demand that was booked under the
budget but then fails to pay is rolled back — the reservation is refunded to
the per-message ledger, and its booking deleted from the daily one, by the
auto_pay_refund function (so the failed, un-spent payment consumes neither
budget) — and the message is likewise
forwarded, with the error logged. Auto-pay
tries to pay; if it cannot, it does nothing and lets the bounce through.
The exception is a helper that cannot be started at all (a missing binary, a lost setuid bit): that is a fault of the host, not of the demand, so before any demand of the message has been paid the message is retried rather than let through, and auto-pay does not silently stop paying. Once a demand has been paid, no error on the same message is retried — a retry would quote, book and pay the settled demands a second time — so the bounce is forwarded with the error logged.
85.1.17.1.4. Wallet account¶
The wallet the helper pays from is chosen by WALLET_MODE:
- local-user
The helper drops to the paying account’s own local login — the account the verified
Pepsi-Originnames, which is also the demand’s envelope recipient (the original sender) — and uses that user’s default wallet. It must resolve to a permitted local account (the same LOCAL_DOMAINS/TARGETS/RECIPIENT_DELIMITER test as pepsi-stage-relay-to-maildir(1)); a non-local account cannot be charged and the message is forwarded. The helper refuses a uid below/etc/login.defsUID_MINin this mode, and pepsi-setup(1) warns about a TARGETS token that reaches below it.- shared (default)
The helper drops to a single dedicated account (WALLET_USER, default
pepsi-wallets) whose home holds the wallet database(s). WALLET_SCOPE selects whether each original sender gets its own wallet database (per-sender, the default) or everyone shares one (unified— e.g. a business where individual wallets make no sense).
All wallet interaction is performed by the setuid-root pepsi-helper-auto-pay(1)
helper; this stage spawns it and never touches a wallet directly. The stage binary
is therefore installed SGID pepsi-wallets so its unprivileged dispatcher
worker may exec the helper (see Installation).
85.1.17.1.5. Wallet self-service¶
When RESPONSE_STAGE is set, a locally-originated message (state.local_origin
true; the submission listener must authenticate and bind the envelope sender)
addressed to <CONTROL_LOCAL_PART>@<served-domain> (default local part
pepsi-wallet; the domain is one of [pepsi-ingress] ACCEPTED_DOMAINS) is a
wallet-control message, operating the sender’s own wallet (resolved by
WALLET_MODE/WALLET_SCOPE exactly as for the paying role, but keyed on the
sender). Any other message is forwarded to NEXT_STAGE untouched, so the stage is
safe to place anywhere on the submission path.
The command is taken from the message Subject: line — one verb plus its arguments (the verb is case-insensitive):
balanceReport the wallet balance.
withdrawCURRENCY:VALUE EXCHANGE-URL (also spelttopup/top-up)Set up a manual top-up of the given amount against the GNU Taler exchange at the given base URL (which must be an
http://orhttps://URL). The exchange is named in the request (not configured on the server) because a wallet is multi-currency and can withdraw from many exchanges. The reply carries the bankpayto://target and the wire-transfer subject to use; once the user’s bank transfer arrives, the funds appear in the wallet.pullCURRENCY:VALUE [subject]Initiate a peer pull payment; the rest of the Subject line, if any, becomes the payment subject. The reply carries a
taler://pay-pull/…URI to forward to whoever should pay.pushtaler://pay-push/… (also speltaccept)Accept an incoming peer push payment. The URI may be given in the Subject or in the message body.
helpReply with the usage instructions — as an empty Subject, an unknown verb, or a missing or malformed argument also does.
When the sender resolves to no wallet at all (WALLET_MODE = local-user and a
sender that is not a permitted local account), nothing is run and the reply says
so.
The result is mailed back to the sender as a localised auto-reply (null sender,
Auto-Submitted: auto-replied), built from the operator-customisable
wallet.<lang>.body template and injected at RESPONSE_STAGE (where it is
DKIM-signed and relayed); the original control message is then deleted. A request
that cannot be understood is answered with usage instructions. A request that
was understood but failed is answered with a fixed “could not be completed”
message: the wallet’s own diagnostic goes to the operator log only, since it
describes a wallet (shared by everyone under WALLET_SCOPE = unified) rather
than the request.
The reply template is rendered once before the wallet command runs, so a missing template is retried with the wallet untouched. Once the command has run it is never run again for the same message: a reply that then cannot be rendered or queued is logged as an error and the request deleted.
If the wallet hits a KYC (identity-verification) requirement, the reply instead carries the KYC link for the user to open — this happens only for the self-service role, never while paying inbound demands.
85.1.17.1.6. Configuration¶
Options live in the stage’s own [stage-<name>] section
(PROGRAM = pepsi-stage-auto-pay): the required MAX_TOTAL (a GNU Taler
amount CURRENCY:VALUE bounding total spend per original message), the
optional MAX_TOTAL_PER_DAY (the same currency, bounding one wallet’s spend in
any 24 hours), a required
NEXT_STAGE, and the wallet knobs WALLET_MODE, WALLET_USER,
WALLET_SCOPE, WALLET_CLI, WALLET_PAY_OPTIONS and HELPER. The wallet
self-service role adds RESPONSE_STAGE (its presence enables the role),
CONTROL_LOCAL_PART, RESPONSE_FROM and RESPONSE_SUBJECT (a withdraw
names its own exchange base URL in the Subject, so no exchange is configured).
They are documented in pepsi.conf(5). The
proof-of-origin secret is the shared [pepsi-origin] section (see
pepsi-stage-relay-to-internet(1)); without it the stage can verify no demand
and therefore pays none (it does not affect self-service).
85.1.17.1.7. Installation¶
Because the stage execs the setuid-root wallet helper, its binary is installed
standalone (not folded into the unified pepsi binary) and SGID
pepsi-wallets (mode 2550, owner pepsi:pepsi-wallets — owner-execute
and not world-execute, because the setgid bit is the gate on the helper, and
pepsi is deliberately not a member of the group, so the exec right has to
come from the owner bits); the helper is installed setuid-root,
root:pepsi-wallets, mode 4750. make install (targets
install-auto-pay-stage / install-auto-pay-helper) and the Debian package
apply these bits. The pepsi-wallets account and group are created by the
package; create them by hand otherwise.
The setgid bit being that gate, the program states the same rule itself: when the
binary actually carries that bit, then unless the real user is root or the
pepsi service account it exits with an error
before any configuration is read, and — like the setuid crypto stages — it strips
the environment variables that steer configuration loading (HOME,
XDG_CONFIG_HOME, PG*, TALER_*, PEPSI_*) and pins PATH to a
safe default. (A build installed without the setgid bit — a development tree,
make check — gained nothing and skips both steps.)
A file mode is a deployment fact and this is a program fact; neither is asked to
stand on its own.
85.1.17.1.8. State¶
Inputs: the Taler: and Pepsi-Origin: headers of the message (the body
is also scanned for an embedded Pepsi-Origin). The per-message spend ledger is
pepsi.origin_nonce.amount and the per-wallet daily one pepsi.auto_pay_spend,
not the message state.
Outputs: the stage does not modify the message; it advances or deletes it. The state layout is described in pepsi.state(7).
Transitions: finish (delete) when all demands are settled, or for a wallet
self-service control message (after injecting the reply at RESPONSE_STAGE);
otherwise advance to NEXT_STAGE (not a demand, unverifiable, unpriceable,
over budget, or a settlement failure). The stage never pauses, bounces or rewrites a
message.
85.1.17.1.9. Commands¶
- worker
Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.
85.1.17.1.10. Global Options¶
- -c FILE, –config FILE
Read the configuration from FILE instead of searching the default locations.
- -L LOGLEVEL, –log LOGLEVEL
Set the logging verbosity (default
info).- -v, –verbose
Show log messages from all sources.
- -h, –help; -V, –version
Print a usage summary / the version and exit.
85.1.17.1.11. Exit Status¶
- 0
The message was processed (paid and discarded, or forwarded).
- 1
An error occurred (message not found or not
running, or a per-address setting that breaks the stage’s section). The reason is written to the log.A fault of the host (the database, a template or helper that cannot be used) is not reported as a failure: the message is paused and retried, as Stage errors in pepsi-dispatch(1) describes. A section that does not parse makes the worker refuse to start (status 78) instead of failing each message in turn. A missing MAX_TOTAL is such a section.
85.1.17.1.12. Examples¶
A pipeline section that pays delivery demands for locally-originated mail, up to
five euro per original message and fifty a day in all, from a single shared
pepsi-wallets wallet:
[stage-auto-pay]
PROGRAM = pepsi-stage-auto-pay
MAX_TOTAL = EUR:5
MAX_TOTAL_PER_DAY = EUR:50
NEXT_STAGE = local-delivery
WALLET_MODE = shared
WALLET_SCOPE = unified
Or, paying from each original sender’s own local wallet:
[stage-auto-pay]
PROGRAM = pepsi-stage-auto-pay
MAX_TOTAL = EUR:5
NEXT_STAGE = local-delivery
WALLET_MODE = local-user
Letting one account spend more (mailed by that account to pepsi@, see pepsi-stage-edit-settings(1)):
[stage-auto-pay]
MAX_TOTAL = EUR:20
A wallet self-service section on the submission path (a message to
pepsi-wallet@example.com with a command in the Subject operates the sender’s
wallet):
[stage-wallet]
PROGRAM = pepsi-stage-auto-pay
MAX_TOTAL = EUR:0
NEXT_STAGE = dkim-sign
RESPONSE_STAGE = dkim-sign
CONTROL_LOCAL_PART = pepsi-wallet
85.1.17.1.13. See Also¶
pepsi-helper-auto-pay(1), pepsi-stage-anti-spam(1), pepsi-stage-relay-to-internet(1), pepsi-stage-edit-settings(1), pepsi-config(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)
85.1.17.1.14. Bugs¶
Report bugs to the Pepsi issue tracker.