The settlement primitive, documented
Everything the Deploier practice desk does is available to read and call. Build against it now; the contract interface will follow the same shapes.
Read how a commitment is formed, how a wallet signs an action, and which endpoints serve the public tape and the private records.
Compute a commitment in a few lines
The commitment is the only thing about a deal that becomes public. It is deterministic, so any party can recompute it.
- Canonical terms
- Keys sorted at every depth, lowercase addresses, amounts as decimal strings with at most six places.
- Salt
- 16 random bytes, hex encoded, kept with the private record and never published.
- Hash
- keccak256 over the UTF-8 of canonical JSON, a pipe and the salt. Issuances use the same rule over their own fields.
terms = { maker, taker, give: { asset, amount }, get: { asset, amount }, memo }salt = randomBytes(16).hex() input = canonicalJSON(terms) + "|" + saltcommitment = "0x" + keccak256(utf8(input)) $ verify(record, tapeEntry)✓ recomputed hash matches the public tape✓ status settled at a Robinhood Chain blockDeveloper endpoints
Two calls per action, then reads. What the server applies is what the wallet signed, never what the browser sends afterwards. Public reads are rate limited per network and never cached.
POST /api/practice/ticket
Body: { address, action }. The server validates the action, writes the exact message to sign with a one-time nonce (5 minute expiry) and returns { nonce, message }.
personal_sign
The wallet signs the message. It names the domain, the action in plain words, the wallet, chain id 4663 and the nonce.
POST /api/practice/redeem
Body: { nonce, signature }. The nonce is burned atomically, the signer is recovered and must match, then the stored action runs. New records return their view key once.
GET /api/practice/tape
The public tape: id, kind, commitment, status, time and block for the latest records. No parties, assets or amounts.
POST /api/practice/record
Body: { id, key }. Returns the full private record when the view key matches. A wrong key and an unknown id give the same 404.
GET /api/tape
The on-chain tape once contracts are deployed: commitment, status, block and time for the newest escrow settlements, issuances and registry records, read from contract storage.
GET /api/chain
Latest block, block time, gas price, ETH/USD and the USDG token's symbol, decimals and supply.
GET /api/stocks
Tokenized stocks on the chain with their Chainlink feed price and last update time.
POST /api/rpc
Read-only JSON-RPC relay for the browser: a short method allowlist, a contract allowlist for eth_call and a bounded eth_getLogs range.
Settlement contracts
Three contracts, Solidity 0.8.24, source and tests in the repository. None has an owner, an admin key, a pause switch, a fee or an upgrade path. An empty address means not deployed yet: that part of the desk runs in practice mode. Each deployed address can be checked byte for byte against the build with npm run verify-deployment -- <address> --tx <creation tx>, and its verified source is on Blockscout.
SettlementEscrow
Not deployed yet
Delivery versus payment between two named wallets. Any ERC-20 or native ETH (token 0x0…0) on either leg; incoming amounts are measured by balance difference and must arrive in full.
Functions
- propose(taker, (token, amount) legA, (token, amount) legB, bytes32 commitment, uint64 expiry) payable → id: escrows leg A; expiry within 30 days
- accept(id) payable: named taker only, before expiry; pulls leg B and releases both legs, or reverts as a whole
- cancel(id): maker only, any time before acceptance; returns leg A
- refund(id): anyone, after expiry; returns leg A to the maker
- getSettlement(id), getSettlements(ids), idOf(maker, commitment), settlementCount(), isExpired(id)
Events
- Proposed(id, maker, taker, commitment, tokenA, amountA, tokenB, amountB, expiry)
- Settled(id, commitment)
- Cancelled(id, commitment)
- Refunded(id, commitment, caller)
CommitmentRegistry
Not deployed yet
A public tape of commitments. Anyone records a bytes32; author and block are stored and readable by sequence number. Kind and status labels are self-declared and prove nothing: settlement and issuance status come only from the escrow and the tokens.
Functions
- record(bytes32 commitment) → seq
- recordWith(bytes32 commitment, uint8 kind, uint8 status) → seq
- getRecord(seq), latest(seq, limit), count()
Events
- Recorded(author, commitment, kind, status, seq)
AllowlistTokenFactory
Not deployed yet
Issues AllowlistTokens. The calling wallet becomes the issuer, receives the whole supply (whole units, 0 decimals) and alone manages the holder allowlist. A maturity stops transfers and opens redemption. The issuer cannot move anyone's units.
Functions
- issue(name, symbol, uint256 supply, address[] holders, uint64 maturity, bytes32 commitment) → token
- token: setAllowed(address[] accounts, bool allowed), issuer only
- token: redeem(amount) after maturity; isAllowed(account), issuer(), maturity(), commitment()
- tokenOf(issuer, commitment), isToken(token), latest(limit), tokenCount()
Events
- Issued(token, issuer, commitment, name, symbol, supply, maturity, holders)
- token: AllowlistUpdated(account, allowed), Redeemed(holder, amount)
Chain values
Constants the site and the contracts use.
- Chain id
- 4663 (0x1237)
- RPC
- https://rpc.mainnet.chain.robinhood.com
- Explorer
- https://robinhoodchain.blockscout.com
- USDG
- 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 · 6 decimals
- Signing
- EIP-191 personal_sign, recovered with secp256k1
Building something on the layer?
Share what you are building on the escrow, the registry or the issuance factory, and what your integration needs from the tape.