Skip to main content
The relayer is why a merchant needs no SOL and never signs anything. redeem_voucher’s only signer is whoever pays the fee, and the program pays exactly the merchant the voucher names. So the relayer submits the voucher and pays, and it cannot redirect the money. It runs as one small Node process. For the week-3 demo it runs on a Mac behind a tunnel, because the phones are on mobile data and cannot reach a laptop on a home network.

What it will pay for

A refusal on budget or the per-vault cap tells the till to retry later; any other refusal is held for a person to look at.

Run it on the Mac

1

A fee-payer key, funded with devnet SOL

If the airdrop is rate-limited, use faucet.solana.com with that address. This key pays fees and nothing else: it has no authority over the program or anyone’s funds. Keep it out of the repo like every other keypair.
2

Start it

It listens on 127.0.0.1:8787. It needs Node 22.18 or later, which runs TypeScript directly. Keep the ledger file: it is how a retried voucher gets the same answer, so it must survive restarts.
3

Put a public address in front of it

It prints an https://….trycloudflare.com address. Check it from a phone’s browser: https://…trycloudflare.com/v1/health should show the fee payer’s address. The quick-tunnel address changes whenever cloudflared restarts; a named tunnel keeps one address, and is worth setting up once the demo date is fixed.
4

Point the merchant app at it

In apps/merchant/.env:
Then restart Metro with --clear. No new native build is needed for this.Put the same two lines in apps/payer/.env too. A customer paid by another customer settles through the same relayer.

Keeping it up

The Mac must be awake with both processes running whenever a till settles. caffeinate -i in a spare terminal stops it sleeping. If the relayer is down, nothing is lost: the till treats it as offline, keeps the vouchers queued, and tries again on the next round.

Why it never submits a voucher twice

The till records the signature only after the relayer answers. A till that crashes in between asks again, so the relayer answers a voucher it has already submitted with the same signature, for as long as that transaction can still land. It builds a new one only once the old one is not on chain and its blockhash has expired. Otherwise the till would see “already redeemed” on the second transaction and report its own payment as a double spend. The submission is written to the ledger before the transaction is sent, and requests are handled one at a time. services/relay/test/ pins both, with a control test that shows the duplicate send happening without them.

Double spends

Two different vouchers from one vault with the same sequence number are proof that the payer spent the same money twice. The relayer acts on that proof in two steps:
  1. Report. It sends report_conflict, which freezes the vault for good. The proof can come from two places:
    • a till that scanned both vouchers posts the pair to POST /v1/conflict with { "a": "<base64>", "b": "<base64>" };
    • the relayer sees it itself when a second till submits a different voucher at a sequence it already submitted. That till’s voucher is declined with conflict: true, and the report goes out at once.
  2. Slash. Every minute, a sweep looks at each reported vault. Once the vault is frozen and still holds stake, the sweep sends slash, which moves the stake to the reserve. The log shows slashed <vault>: <signature>.
The relayer checks both signatures before it pays for anything. It sends nothing for a vault that is already frozen, and a repeated report gets the same transaction back. Reports are kept in the ledger file with everything else.