> ## Documentation Index
> Fetch the complete documentation index at: https://nelo.udokaam.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Relay

> Running Nelo's relayer on a Mac: it submits merchants' offline vouchers and pays the fees.

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

| Limit                            | Default                            | Why                                                                                                                |
| -------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Signature-verified vouchers only | always                             | A forged voucher costs a fee to be refused on chain; it is refused here for free                                   |
| Daily budget                     | 0.5 SOL (`RELAY_DAILY_BUDGET_SOL`) | Caps the worst day. Resets at UTC midnight                                                                         |
| Per vault per day                | 200 (`RELAY_MAX_PER_VAULT`)        | One payer cannot use the whole budget                                                                              |
| A merchant's first token account | once per merchant                  | About 0.002 SOL of rent, which is 400 fees. Funding it again for someone who closed it is how a sponsor is drained |

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

<Steps>
  <Step title="Set up every setting, once">
    ```bash theme={null}
    cd ~/nelo && git pull && pnpm install
    pnpm setup:env
    ```

    This writes the relayer's settings to `~/.config/nelo/relay.env` and the settlement service's to `~/.config/nelo/settle.env`, both owner-only, and creates `apps/merchant/.env` and `apps/payer/.env` from their examples if they are missing. It generates the tokens and copies them into the apps, so the two ends always match. Then it lists what is still blank.

    It never changes a value you have set, and it keeps the tokens it generated, so run it again whenever you like.
  </Step>

  <Step title="Fill in the keys it asks for">
    Open the files it names and paste in:

    * `RELAY_RPC_URL` in `~/.config/nelo/relay.env`: your Helius devnet URL. If one of the apps' `.env` already has one, it is copied across for you.
    * `PAJ_API_KEY` in `~/.config/nelo/settle.env`, once paj.cash sends the staging key.
  </Step>

  <Step title="A fee-payer key, funded with devnet SOL">
    ```bash theme={null}
    solana-keygen new -o ~/.config/solana/nelo/relay-fee-payer.json
    solana airdrop 2 $(solana address -k ~/.config/solana/nelo/relay-fee-payer.json) -u devnet
    ```

    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.
  </Step>

  <Step title="Start it, and put a tunnel in front of it">
    ```bash theme={null}
    cd ~/nelo/services/relay && pnpm start          # reads ~/.config/nelo/relay.env
    # in another terminal:
    brew install cloudflared                        # once
    cloudflared tunnel --url http://127.0.0.1:8787
    ```

    It listens on `127.0.0.1:8787` and needs Node 22.18 or later. Keep the ledger file it writes: it is how a retried voucher gets the same answer, so it must survive restarts. Check the tunnel from a phone's browser: `https://…trycloudflare.com/v1/health` should show the fee payer's address.
  </Step>

  <Step title="Point the apps at it">
    ```bash theme={null}
    cd ~/nelo && pnpm setup:env --relay-url https://….trycloudflare.com
    ```

    This writes the address into both apps' `.env`. A quick tunnel's address changes whenever `cloudflared` restarts, so run this again each time. Then restart Metro with `npx expo start --dev-client --clear`; no new native build is needed.
  </Step>
</Steps>

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

## Cash-outs through paj.cash

The settlement service (`services/settle`) opens payout orders with paj.cash, and the relayer pays the fee on the merchant's transfer to the order's deposit address. It runs on the same Mac, behind its own tunnel.

<Steps>
  <Step title="Start a tunnel for it, and tell the setup">
    ```bash theme={null}
    cloudflared tunnel --url http://127.0.0.1:8788
    cd ~/nelo && pnpm setup:env --settle-url https://….trycloudflare.com
    ```

    This writes the address to `SETTLE_PUBLIC_URL`, which paj.cash's webhook uses, and to the merchant app's `.env`. `PAJ_API_KEY` must be in `~/.config/nelo/settle.env` by now.
  </Step>

  <Step title="Log in to paj.cash">
    ```bash theme={null}
    cd ~/nelo/services/settle && pnpm paj:login
    ```

    paj.cash sends a code to the email or phone you give; type it in. The session is saved owner-only to `~/.config/nelo/paj-session.json`. When it expires, cash-outs pause with a message saying so; run this again.
  </Step>

  <Step title="Start it">
    ```bash theme={null}
    pnpm start      # reads ~/.config/nelo/settle.env
    ```

    Restart Metro with `--clear`. The till then shows paj.cash's naira rate, and the balance opens Cash out.
  </Step>
</Steps>

The relayer allows 5 cash-out transfers per merchant a day (`RELAY_CASHOUTS_PER_DAY`). If paj.cash's deposit addresses turn out to have no USDC token account, set `RELAY_SPONSOR_DEPOSIT_ACCOUNTS=true` in `relay.env` to have the relayer open one, at about 0.002 SOL each.

## Every setting

`pnpm setup:env` writes all of these; this is what each one is.

| Where                       | Setting                                                                   | What                                                                    | Set by                |
| --------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------- |
| `~/.config/nelo/relay.env`  | `RELAY_RPC_URL`                                                           | Devnet RPC with a quota, e.g. Helius                                    | you                   |
|                             | `RELAY_RPC_FALLBACK_URLS`                                                 | Other providers, tried in order when that one does not answer. Optional | you                   |
|                             | `RELAY_KEYPAIR`                                                           | The fee-payer key file                                                  | setup                 |
|                             | `RELAY_LEDGER`                                                            | What the relayer has submitted. Keep it                                 | setup                 |
|                             | `RELAY_TOKEN`                                                             | Bearer token; the apps hold a copy                                      | setup                 |
|                             | `RELAY_DAILY_BUDGET_SOL`, `RELAY_MAX_PER_VAULT`, `RELAY_CASHOUTS_PER_DAY` | Limits: 0.5, 200, 5                                                     | defaults              |
|                             | `RELAY_SPONSOR_DEPOSIT_ACCOUNTS`                                          | `true` to pay rent for a deposit account                                | off                   |
|                             | `RELAY_MINT`, `RELAY_MINT_DECIMALS`, `RELAY_PORT`, `RELAY_HOST`           | Devnet USDC, 6, 8787, 127.0.0.1                                         | defaults              |
| `~/.config/nelo/settle.env` | `PAJ_API_KEY`                                                             | Nelo's paj.cash business key. Never in an app                           | you                   |
|                             | `PAJ_ENV`                                                                 | `staging` or `production`                                               | setup: staging        |
|                             | `SETTLE_PUBLIC_URL`                                                       | This service's tunnel, for paj.cash's webhook                           | `--settle-url`        |
|                             | `SETTLE_WEBHOOK_SECRET`                                                   | Part of the webhook's path                                              | setup                 |
|                             | `SETTLE_TOKEN`                                                            | Bearer token; the merchant app holds a copy                             | setup                 |
|                             | `PAJ_LOGIN`                                                               | Where paj.cash sends the login code; skips the prompt. Optional         | you                   |
|                             | `SETTLE_MINT`, `NELO_FEE_USDC`                                            | The mint paj.cash takes; Nelo's fee per cash-out. Optional              | you                   |
|                             | `PAJ_SESSION_FILE`, `SETTLE_CASHOUTS_FILE`, `SETTLE_PORT`, `SETTLE_HOST`  | Under `~/.config/nelo`, 8788, 127.0.0.1                                 | defaults              |
| `apps/merchant/.env`        | `EXPO_PUBLIC_SOLANA_RPC_URL`, `EXPO_PUBLIC_SOLANA_RPC_FALLBACK_URLS`      | The till's RPC, and fallbacks                                           | you, or copied        |
|                             | `EXPO_PUBLIC_NELO_RELAY_URL`, `EXPO_PUBLIC_NELO_RELAY_TOKEN`              | The relayer                                                             | `--relay-url`, setup  |
|                             | `EXPO_PUBLIC_NELO_SETTLE_URL`, `EXPO_PUBLIC_NELO_SETTLE_TOKEN`            | The settlement service                                                  | `--settle-url`, setup |
|                             | `EXPO_PUBLIC_PRIVY_APP_ID`, `EXPO_PUBLIC_PRIVY_CLIENT_ID`                 | Phone-number onboarding. Optional                                       | you                   |
| `apps/payer/.env`           | `EXPO_PUBLIC_SOLANA_RPC_URL`, `EXPO_PUBLIC_SOLANA_RPC_FALLBACK_URLS`      | The payer app's RPC                                                     | you, or copied        |
|                             | `EXPO_PUBLIC_NELO_RELAY_URL`, `EXPO_PUBLIC_NELO_RELAY_TOKEN`              | The relayer, for payments received from other customers                 | `--relay-url`, setup  |

Every `EXPO_PUBLIC_` value is compiled into the app and readable by anyone holding the APK. The RPC URL and the two tokens are fine there; `PAJ_API_KEY`, the fee-payer key and the paj.cash session are not, and live only on the Mac.

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