Payouts¶
Paying hosts their earned credits in EURC on Solana, and taking top-ups the same way.
This build runs on devnet only, where the coins are worthless. There is no mainnet setting:
the list of networks in shared/payouts.py has only devnet. Whether this business may move coins
for others is a legal question, and going live needs that answer first, then a reviewed code
change. Don't change that list to try something out.
How it fits together¶
| Part | Where | Holds |
|---|---|---|
| The leader | leader/billing/payouts.py |
The books. No key, and no Solana library |
| The payout service | silent_swarm/payouts/, a separate process |
The only key |
The service comes to the leader for work and reports back. The leader cannot reach in, and a breached leader can only ask. The signer keeps its own limits and database, and reads each message before signing it. Run the service on another machine than the leader.
Setting it up¶
On the leader:
SWARM_LEDGER=record # or enforce; payouts pay out what the ledger has earned
SWARM_PAYOUTS=devnet
SWARM_PAYOUT_SERVICE_TOKEN=... # long and random: without it, the service's routes do not exist
SWARM_PAYOUT_TREASURY=... # where top-ups are paid
On the service's machine, with the package installed with its payouts extra:
swarm-payouts keygen /secure/hot.json # readable by you alone; it refuses to overwrite a key
export SWARM_PAYOUTS=devnet
export SWARM_PAYOUT_KEY_FILE=/secure/hot.json
export SWARM_PAYOUT_DB=/secure/payouts.sqlite3
export SWARM_PAYOUT_SERVICE_TOKEN=...
export SWARM_LEADER_URL=https://your-leader
export SWARM_PAYOUT_RPC_URLS=https://first-provider,https://second-provider
swarm-payouts run
Back up SWARM_PAYOUT_DB. It is what stops a double payment, because it holds every signature
ever made. Losing it mid-payment means checking the chain by hand before restarting.
The service refuses to start if an RPC endpoint is on another network, checked by genesis hash, because the EURC mint has the same address on mainnet and devnet.
Limits¶
The signer enforces these whatever the leader asks. A payment over a daily limit waits, and one over the per-payment limit is cancelled, which puts the earnings back.
| Setting | Default |
|---|---|
SWARM_PAYOUT_MAX_PER_PAYMENT_MICRO_EUR |
100 EUR |
SWARM_PAYOUT_MAX_PER_ADDRESS_DAY_MICRO_EUR |
200 EUR |
SWARM_PAYOUT_MAX_TOTAL_DAY_MICRO_EUR |
1,000 EUR |
SWARM_PAYOUT_SANCTIONS_FILE |
None: nothing is screened without it |
Keep about a day of payouts in the hot wallet, and refill it by hand from a reserve that this
code never touches. SWARM_PAYOUT_RESERVE_MICRO_EUR on the leader tells reconciliation what that
reserve holds, and it is your own figure.
Linking a wallet¶
A wallet belongs to an account, never to a machine. No key ever goes near a fellow.
Linking happens on the profile page. It needs 2FA turned on, a code from it, and a signature from the wallet over a message that names the account. API tokens and MCP are refused on purpose, so a leaked token cannot redirect anyone's money.
Any change freezes payouts for 72 hours and emails the account's address, naming the old and new wallets.
For a wallet with no signing screen, save the message to a file and run:
swarm-payouts sign-message key.json message.txt
How a payment stays single¶
planned -> signed -> sent -> finalized -> reconciled
^ |
+- expired
- Record first. A signed payment and its signature are on disk before the first send.
- Ask before anything new. Every signature a payout ever had is checked first. Sending the same bytes again is harmless, because a chain runs one signature once.
- A new signature only once the old one is provably dead: past its blockhash's last valid height by finalized blocks, and then unknown to every provider.
- No answer is not "no". A provider that cannot answer stops that payout from moving on.
A payout that keeps expiring stops as stuck after SWARM_PAYOUT_MAX_ATTEMPTS (8). Its earnings
stay pending. Look at it on the chain before doing anything.
On the leader, earnings move to pending before an order is offered, so what is on its way cannot
be spent or paid again.
Top-ups¶
swarm me topup 5000000 gives an invoice with a unique reference and a payment link. Credits arrive
only when the service finds a finalized payment naming that reference, with the exact mint and
destination, by what the balances really did. A client's claim is never read.
The wrong amount credits nothing, marks the invoice amount_mismatch and writes an audit entry, for
a person to sort out. Top-ups land in a spendable wallet, which can never become payable.
Reconciliation¶
Daily, the service compares the chain with its own record, and the leader compares that with the ledger. Any of these stops payouts, until an admin has looked and resumed in the browser:
- a payment left the hot wallet that nothing here signed, which means a stolen key or a bug;
- the service's finalized total differs from the ledger's paid total;
- providers disagree on the balance;
- more is owed than the hot wallet and the reserve cover.
Anchors¶
Each closed month, the service publishes a memo holding a Merkle root over that month's statements, and the ledger's chain head. Leaves are salted and the memo holds nothing personal, because a public chain cannot erase. A host can check their statement without trusting this server:
swarm me credits-proof 2026-10
That gives the statement, its salt, the proof and the transaction that published the root.
For the books¶
swarm admin payouts export gives every coin movement as CSV, with its EUR amount, coin amount, rate
and where the rate came from. EURC is booked at a fixed 1 EUR, and each line says so: ask your tax
advisor whether that is good enough.
What has been checked, and what has not¶
- Against a simulated chain: the whole loop, and crashes at every step, including 300 randomized runs of crashes and provider faults.
- Against real devnet programs:
exp/spikes/devnet_simulate.pyruns a real payout transaction on the real mint with a real holder's account. Nothing moves, and it needs no funds. - Not yet with real signatures and funds on devnet. Devnet's RPC faucet is closed. To run it, fund the key at https://faucet.solana.com and run:
python exp/spikes/devnet_payout.py /some/dir
It prints the wallet to fund, and fails until it has SOL. It uses a test token of its own, because Circle's EURC faucet is a web page too.
What is not built¶
Separate signer process, KMS or HSM, the multisig reserve, KYC, DAC7 and DAC8 data, USDC, card and SEPA top-ups, and a web page for top-ups (use the CLI or MCP).