# Red 8 Reward Service

Pays [MooChedda](https://moochedda.com/sdk.html) token rewards to players of games made in the Red 8 Studio Game Creator. It runs on your computer or server as a Nodofi relay worker, like UtilityServices: it connects out to a relay and needs no open ports.

Why a separate service? A token payout is a transfer from your wallet, signed with your wallet's **private key**. A game is public HTML that anyone can open and read, so the key must never be inside it. The game only *asks* for a reward. This service checks the request against your rules and limits, signs the MooChedda transfer itself, and records it.

```
Game ──reward.claim──▶ Nodofi relay ──▶ Reward Service ──signed POST /token/transfer──▶ MooChedda
     ◀──────────────── result ◀──────────────────────────────── player's wallet credited
```

## Set up

1. Install Node.js 18 or newer, then in this folder run:
   ```bash
   npm install
   ```
2. Set up the wallet rewards are paid from. Use a dedicated wallet, and keep only what you plan to give away in it:
   ```bash
   npm run create-wallet                     # a new MooChedda wallet, saved as wallet-backup.json
   npm run use-wallet -- path/to/wallet.json # or an existing wallet backup file
   ```
   Both use the same wallet backup file as MooChedda's wallet app and the BeezifiStudio App Browser (`{ address, privateKey, mnemonic }`), and set `WALLET_FILE` in `.env` (created from `.env.example`). The address is always re-derived from the private key, so a mismatched or tampered file is refused. Keep a copy of the backup file and its recovery phrase somewhere safe and offline; MooChedda can't recover them. (You can instead set `WALLET_ADDRESS` and `PRIVATE_KEY` in `.env`.) Then fund the wallet.
3. Finish `.env`:
   - `RELAY_URL` and `REGISTRY_URL`: the defaults are `https://relay.nodofi.com` and `https://registry.nodofi.com`.
   - `SERVICE_TITLE` and `OPERATOR`: how the service is listed in the registry.
   - `DRY_RUN=true` checks claims and logs what it *would* pay without moving tokens. Start this way, then set it to `false`.
4. In the studio, open your game, then **Game settings → Rewards (MooChedda)**. Set your rewards and click **Download service config**. Put the file in `rewards/`; one file per game, and one service can pay for many games.
5. Start the service:
   ```bash
   npm start
   ```
   It prints its **service address** (32 characters) and payout wallet.
6. If the relay charges for access (`relay.nodofi.com` does), the service logs *"This relay requires paid access (… USDC / … days)"* and games can't reach it yet. Pay for it from the studio: see [Paying for relay access](#paying-for-relay-access).
7. Back in the studio, enter the **service address**, click **Look up in registry** (which fills in the payout wallet), then **Test connection**. It should say it's connected, its answer is signed by the payout wallet, and list your rewards.

## Paying for relay access

Running a relay costs its operator bandwidth, so a relay can charge the services and nodes that JOIN it a USDC fee per period. Players and the studio connect as one-time requesters, and those never pay. Until the fee is paid, the relay refuses the service's JOIN.

In the studio, open **Game settings → Rewards (MooChedda) → Relay access**:

1. **Check relay access** shows whether the relay is free, paid (and until when), or unpaid and what it charges.
2. **Pay: N USDC with wallet file…** asks for a MooChedda wallet backup file holding at least that much USDC. It can be the payout wallet or any other wallet. After you confirm, the studio signs the transfer to the relay operator's wallet in the browser and sends it to MooChedda. The key is used for that one signature and is never kept or sent anywhere.
3. The studio hands the **signed transfer** to the service. The service adds proof that it holds its own `identity.json` key, which never leaves it, and sends both to the relay's `POST /api/access/pay`. The relay checks the identity against the registry and the transfer against MooChedda's ledger, then grants the period. Paying again while access is still active adds the new period to the time left. The service JOINs again straight away, with no restart.
4. If the money moved but the relay couldn't confirm it yet, **Retry confirming that payment** sends the same transfer again without paying twice. The relay accepts a transfer for 30 minutes after it was signed.

This is how a Nodofi Node pays its relay from its admin page (BeezifiStudio `desktop-node/src/server.js` `/admin/relay/pay-access`). The difference is how the studio reaches the service. **No port needs to be open:** the studio reaches the service **through the relay**, so it works on a locked-down production server.

- A paid relay refuses the service's own address until access is paid. So the service also JOINs its **pay inbox**, `<service address>.pay`. That name isn't registered, so any relay lets anyone JOIN it for free.
- Anyone can listen in the inbox, so:
  - The studio **encrypts the signed transfer to the service's identity key** (ECDH secp256k1 + AES-256-GCM). It gets that key from the registry and checks it against the service address, which is the key's sha256. Otherwise a transfer read in transit could be used to pay for someone else's node.
  - The studio accepts only **replies signed by that identity key**, so nobody else in the inbox can answer, for example with their own wallet as the fee wallet. It also checks the fee against what the relay itself publishes at `GET /`.
  - A transfer the relay has accepted is never forwarded again.
- The inbox answers only `relay.access`, `relay.pay` and `reward.ping`, never claims. The duplicate-copy check on start pings it too.

| `function` (to `<address>.pay`) | Input | Answer (signed by the identity key) |
|------------|-------|--------|
| `relay.access` | | `{ result, service, relayUrl, joined, joinError, feeAmount, feeWallet, periodDays, paidUntil, active }` |
| `relay.pay` | `sealed: { epk, iv, ct }`, the encrypted `{ paymentLeg }` | the relay's answer `{ result: true, paidUntil, active, … }`, or `{ result: false, message }` |

## The service address, and why no one can take it

The service doesn't use a name anyone could pick. It gets its own **identity** (`identity.json`, created on first run and kept private) and its address on the relay is derived from it: `sha256(identity public key)`, first 32 hex characters. This is exactly how a Nodofi Node's address works (BeezifiStudio `desktop-node/src/store.js`):

- **The address can't be faked.** Nobody can make a key whose hash is your address.
- **It's registered.** On start, and every 2 minutes after, the service registers its identity with the registry (`POST /api/nodes/register`, with a fresh signature over `sha256("<address>|<timestamp>")`), listing its title, operator, relay and payout wallet. The service shows up in the registry's directory.
- **The relay enforces it.** It JOINs with `{ uid, publicKey, signature, timestamp }`. A relay with a registry (as `relay.nodofi.com` uses `registry.nodofi.com`) then refuses anyone else who tries to JOIN your address: *"This id is registered to another node."* Messages to your address only ever reach you.
- **Answers are signed.** Every reply is signed by your payout wallet, over the request's nonce and the reply. Games and the studio accept only replies signed by the payout wallet set in the studio. So even on a relay without a registry, where anyone *can* join any address, an impostor's answers are ignored, and an old answer can't be replayed.
- **No second copy.** Before joining, the service checks whether something already answers at its address. If so (another copy with the same `identity.json`), it refuses to start, because two copies would pay every reward twice.

Keep `identity.json` with the service. Moving it to a new machine keeps the same address; deleting it gives the service a new address, which you'd then enter in the studio again. `REGISTRY_URL=` (empty) turns registration off.

## Your own token

Rewards can be paid in any MooChedda token your wallet holds, such as CHEDDA, USDC, or one you create:

```bash
npm run create-token -- GEMZ "Gemz" 1000000
```

This mints the whole supply to your wallet (MooChedda `POST /token/create`). MooChedda's mint call takes the private key in the request, so it only runs from here, over HTTPS, never from a browser. Then set the token to `GEMZ` in the studio. `npm run balance` shows what the wallet holds.

## What players do

The rewards box on the game's title, pause and end screens gives players three ways to say where rewards go, the same choices as the BeezifiStudio App Browser:

- **Create a wallet:** the game asks MooChedda (`/wallet/create`) for a new wallet, checks that its address matches its key, downloads the wallet backup file to the player, and shows the recovery phrase once.
- **Open wallet file…:** a MooChedda wallet backup file. The address is re-derived from its key, and a mismatched file is refused.
- **Paste an address:** for a wallet they already have elsewhere.

In every case the game keeps **only the address** (in the player's browser), which is all it needs to receive rewards. Private keys and recovery phrases stay in the player's wallet file. When they earn something they see "+5 GEMZ", and the game claims it.

- **One-time rewards** (win, reach a score, finish fast, reach a level) are claimed straight away.
- **Repeatable rewards** (each coin, each elimination, each cascade) are added up and claimed together at the end of a run, or every minute, so a run doesn't cause dozens of tiny transfers.
- **Without a wallet,** earned rewards wait in the game until one is entered (for that session).

## How claims are checked

The game sends only `{ game, rule, wallet, count, value, nonce }`. The amount always comes from the rules file here, never from the request. Then:

- **The rule must exist** for that game, and the wallet must be a valid MooChedda address that isn't the service's own wallet.
- **Thresholds:** score, level and cascade rewards need a reported value at or above the threshold. "Win in under N seconds" needs a time at or below N.
- **Once only:** a once-only reward is paid to a wallet one time, ever. Wallet addresses are compared case-insensitively.
- **Daily limits:** each rule's limit per player per day is enforced. A batch is trimmed to fit.
- **Daily budget:** the game's daily budget (all players together) is never exceeded. A batch is trimmed to fit, and once the budget is spent, claims are refused until tomorrow (UTC).
- **No replays, no bursts:** a repeated nonce is refused, claims run one at a time, and each wallet is paced to one claim every 2 seconds.
- **Settlement:** after MooChedda accepts a transfer, the service confirms it settled with `POST /transactions/verify`, as BeezifiStudio's relay and desktop-node do. An unconfirmed transfer is logged as `sent-unconfirmed`, and it still counts toward the limits so it can't be paid twice.
- **Ledger:** every payout is written to `ledger.jsonl` (time, game, rule, wallet, amount, token, transfer signature, status). Limits come from this ledger, so they survive restarts. Keep it.

**Know the limits of this.** A game runs on the player's computer, so a determined player can fake a win or a score. These checks cap what any one wallet, and all players together, can take each day, but they can't prove a game was really played. Keep amounts and daily limits at levels you're comfortable giving away, and watch the ledger.

## Requests (relay protocol)

The service JOINs the relay at its service address (with proof, see above). A request is a relay message to that address from a one-time sender ID, and the reply goes back to that ID. This is the same pattern as the other Nodofi workers.

| `function` | Input | Answer |
|------------|-------|--------|
| `reward.claim` | `game, rule, wallet, count?, value?, nonce?` | `{ result: true, amount, token, count, signature }` or `{ result: false, message }` |
| `reward.info` | `game?` | `{ result: true, name, dryRun, wallet, games: [{ id, name, token, dailyBudget, paidToday, rules }] }` |
| `reward.status` | `wallet, game?` | `{ result: true, total, today, payouts }` |
| `reward.ping` | | `{ result: true, name }` (the service's own duplicate check) |

Every reply also carries `proof: { signer, sig }`: the payout wallet's signature over `<request nonce>|<JSON of the reply without proof>` (sha256, secp256k1, DER).

## Files

| File | What it does |
|------|--------------|
| `src/service.js` | Relay connection, configuration, request handling and signed replies |
| `src/identity.js` | The self-certifying service address, registry registration and heartbeat, and the JOIN proof (as in desktop-node) |
| `src/relay-access.js` | The pay inbox the studio pays relay access through: opens the sealed transfer, adds the identity proof, forwards to the relay |
| `src/rewards.js` | Rules, limits, budget, pacing and the ledger |
| `src/moochedda.js` | MooChedda calls: signed transfers, settlement checks, wallets, balance and minting. It signs and verifies with `@noble/curves` secp256k1, as in BeezifiStudio (`app-browser/moochedda.js`, `relay/src/moochedda.js`): `sha256(fromAddress + toAddress + amount + tokenSymbol + timestamp)`, DER, always low S. `MOOCHEDDA_API` overrides the service for tests or self-hosting. |
| `scripts/create-wallet.js`, `scripts/use-wallet.js` | `npm run create-wallet`, `npm run use-wallet` |
| `scripts/create-token.js`, `scripts/balance.js` | `npm run create-token`, `npm run balance` |
| `test/run.js` | `npm test`, offline: relay-access payment through a fake relay that checks the identity proof (sealing, signed replies, no replays), signing (checked with noble and Node's crypto, always low S), wallet files (including a tampered one), payouts through a fake MooChedda that verifies every signature and answers `/transactions/verify`, and every limit |
