## The rules, in order

1. **Never handle a private key, a seed phrase, or a signature in Ruby.** The browser signs. If a
   task seems to require a server-held key, stop and say so: it is a different product with a
   different threat model, and this kit is not it.
2. **Never call an RPC or an aggregator from a controller or a model.** Go through `Chain.for` and
   `Swap.for`, so the fake path keeps working and a fresh clone stays hermetic.
3. **All amounts are base-unit integers, serialized as decimal strings.** Never a Ruby Float, never
   a JavaScript Number.
4. **Interactive wallet UI is a React island** reading `data-*` values and a JSON data island, never
   an inline `<script>`. The CSP blocks inline script silently.
5. **New model gets `account_id`**, a Pundit policy, and a tenant-isolation spec proving a foreign
   id 404s.
6. **Mainnet writes need `ONCHAIN_ALLOW_MAINNET_WRITE` and an explicit confirmation.**

## Adding a new chain

Copy an entry in `Chain::REGISTRY`, then update `.env.example` and
[Bring Your Own Keys](/docs/bring-your-own-keys/optional-integrations). Touch the CSP **only if the
browser talks to it directly**, which it should not: reads go through `/onchain/rpc`.

If the chain is a mainnet an aggregator supports, add it to `Swap::SUPPORTED_CHAIN_IDS` too. A spec
asserts that list matches the registry's mainnets, so it will tell you.

## Adding an action that sends a transaction

The shape, end to end:

1. **A Rails endpoint that builds the calldata** and returns `{ to, data, value }`. It holds
   whatever key is needed. It never signs.
2. **A function in `app/javascript/onchain/api.js`.** That file is the *only* place `fetch` appears,
   which is what keeps the server contract readable in one screen. A spec enforces it.
3. **An island** that calls `sendTransaction` with exactly what the server returned, then posts the
   hash to `/onchain/transactions` and stops.
4. **A row in `islands.js`.** The registry is an explicit map, not a glob: an agent should see every
   island in one screen.
5. **Status is server-rendered.** `ConfirmOnchainTransactionJob` watches the receipt and the
   `onchain/_transaction` partial refreshes itself in a turbo-frame. React never subscribes to
   anything.
6. **A CSP allowance** in `app/controllers/concerns/onchain_csp.rb` if a new host is involved, and a
   spec pinning it. CSP failures are silent.

## Watch for these

**Never auto-fail a transaction still in the mempool.** It is slow, not lost. Telling a user their
money is gone and then having it arrive is worse than saying nothing. `ConfirmOnchainTransactionJob`
only marks something dropped when the node has never heard of it.

**A receipt is not settlement.** On a chain that can reorg, a block can be re-mined. The job waits
for confirmations, five on a mainnet.

**Never fabricate calldata in a fake.** `Swap::Local` returns `transaction: nil` for exactly this
reason. A fake that looks executable will eventually be executed.

## The boundary is a test

`spec/onchain/no_client_side_chain_logic_spec.rb` greps every file in the island for
`encodeFunctionData`, `parseUnits`, `createSiweMessage`, `Number(`, and `readContract`, each with a
message naming the Rails owner of that concern.

It exists because the boundary is invisible in any single file, and because the easy wrong fix for
almost every future bug is "just build the message in JS". Two files are allowed to construct a
transport, and only because of *where* they point it; a further spec asserts both read the proxy path
and name no RPC provider.

If you find yourself adding to that allowlist, that is the moment to ask whether the work belongs in
Ruby instead.
