The shape of it
A watch rule is the plainest kind of rule: something to read, a line, and a side. It is stored sealed in your browser and evaluated there. The chain never learns the rule exists; all it ever sees is a read.
The batch hook is a Uniswap v4 hook. A pool under it refuses direct swaps: orders are cast into a batch, and the batch settles as one swap of whatever is left after buys and sells cancel out. The ledger records the net, not the parts.
The undertow is a pool of notes: commitments in a Poseidon Merkle tree, spent by zero-knowledge proof. A cast can be paid from a note, and its output goes to a one-time key. The batch then settles against the pool, and the pool is nobody in particular.
Watch rules: where a rule lives
On first use the browser generates an AES-GCM-256 key and marks it non-extractable. It is stored in IndexedDB, where the page can use it but no script can read it out. Your rules and the last two hundred crossings are kept as one ciphertext in localStorage. Copy the profile folder, or read localStorage from a script, and you get ciphertext.
Every thirty seconds, while the tab is visible, every enabled rule becomes one call in a single Multicall3 aggregate3: ETH balances, token balanceOf, and PoolManager extsload words for a pool's slot0 and liquidity. The RPC sees addresses and storage slots. It does not see a name, a line, a side, or that anything fired.
A rule fires on a crossing, not on a level: the read on which the value steps over the line. The first read after a rule is set only records where things stand, so a line drawn below today's balance does not fire the moment it is made.
// one read calls = rules.map(toCall) // target + calldata, nothing else values = multicall3.aggregate3(calls) // one eth_call for rule in rules: if crossed(rule.last, values[rule], rule.line, rule.side): fire(rule) rule.last = values[rule] seal(rules) // AES-GCM, key never leaves IndexedDB
To move rules to another browser, export them under a passphrase: PBKDF2-SHA-256 at 310,000 rounds derives the key, and the file is ciphertext without it. There is no account to recover from. That is the point, and also the cost.
The batch hook: one swap per batch
A pool under the hook rejects any swap in beforeSwap unless the caller is the hook itself. Orders are cast into the open batch with a size and a side. When the window closes, anyone may settle: buys and sells are matched against each other at the pool's current price, with no LP fee on the matched part, and only the residual is swapped through the pool. Each side receives one blended price.
What it hides
- Your footprint on the pool: its price, its Swap event and its storage only ever see the residual.
- What each order paid: a cast is not a price point, each side gets one blended price.
- Who traded against whom.
What it still shows
- The cast itself (size, side, recipient), unless it comes from the undertow.
- The batch total, the residual and its output, on the ledger.
Two orders that match exactly leave the pool's price and liquidity untouched. The batches page runs that settlement against a live pool today; once the hook is on chain it reads the hook's events instead.
The undertow: where the order comes from
Sink. Deposit a token into the note pool. You now hold a commitment in a Merkle tree, encrypted to your own viewing key, that only you can spend. Cast. Prove you own a note and cast its value into the open batch; the batch sees the pool, not you, and the output is already addressed to a fresh one-time key. Net. Anyone closes the batch. Rise. Spend the output note to any address, or leave it below the line and cast again.
Hidden: who cast, since the spender is a nullifier; who received, since the output goes to a one-time key; balances and transfers inside the pool. Still visible: amounts going in and out of the pool, the size of a shielded cast, and the set of addresses that have ever deposited.
Wallets and reads
Reading a chain needs no wallet, so Sotto never asks for one just to look. Pools, hooks and batch inputs are read by your browser straight from the public Robinhood Chain RPC; there is no server of ours in between, and nothing is logged by us. When an action needs a wallet, it will be discovered through EIP-6963, connected for that one transaction and not remembered.
What is real today
A roadmap that doesn't say what has shipped is worse than none. Each row says which it is.
| Piece | Status | Where |
|---|---|---|
| Watch rules | Running in your browser | /hooks. AES-GCM vault, one Multicall3 read every 30 s against the public RPC. |
| Pools and hook permissions | Live | /pools. Swap and Initialize events read from the PoolManager by your browser; permissions decoded from each hook address. |
| Batch settlement | Simulated on live pools | /batches nets the orders in your browser against live slot0 and liquidity. The hook contract is designed; its address will be listed here when deployed. |
| The undertow | In design | Groth16 circuit and note pool specified. Nothing deployed, no proving key. |
| Audit | None | Nothing here has been audited. Treat every contract as experimental. |
| $SOTTO | Launching | Robinhood Chain via Pons. The only contract address is the one on /token. |
Roadmap
Now. Watch rules live; pools and hook permissions live; batch settlement simulated on live pools; token on Pons.
Next. The batch hook and its gate deployed on Robinhood Chain with a first market under it; the batches page reading its events; a relayer so a shielded cast never links your wallet.
Then. The undertow: a public Groth16 ceremony anyone can contribute to, the note pool, one-time keys; private liquidity; and rules that act, so a watch can fire a cast.
Dates are targets, not promises. Anything touching the token (fees, buybacks, governance) is written here as a plan and only becomes real when a contract for it is deployed and listed in the table above.
Constants
| Chain | Robinhood Chain · 4663 |
| RPC | rpc.mainnet.chain.robinhood.com |
| PoolManager | 0x8366a39c…e40951 |
| Multicall3 | 0xcA11bde0…76CA11 |
| Batch hook | not deployed |