This document is in progress and will be refined.
Robinhood Chain mainnet is live and supported. Switch with setNetwork('robinhood'). Requires SDK v0.3.0 or later. The Robinhood Chain testnet contract is not deployed yet.
The IQLabs Ethereum SDK works on Robinhood Chain out of the box. Same API, one line to switch networks. Store data on-chain, build databases, send encrypted DMs, gate content by token ownership. Robinhood Chain is an Ethereum-compatible Layer 2 with ETH as the gas token, so fees stay a fraction of a cent while everything behaves exactly like Ethereum.
Installation
The SDK ships as CommonJS for Node.js and works in browsers via any modern bundler.
Network
The SDK ships with four network modes. Set the one you want once at app startup with setNetwork().
| Mode | Chain ID | Currency | Contract | Default RPC |
|---|
sepolia | 11155111 | ETH | 0x246A08D9fdD9b3990A88eD1f2DF1A87239839F07 | https://rpc.sepolia.org |
monad | 143 | MON | 0x7ae06f87Cf93606DA2BD6A281afB28028cAE233D | https://rpc.monad.xyz |
robinhood | 4663 | ETH | 0x88af59e58C7E5DcbE7cc12972B90cff3fEEF7223 | https://rpc.mainnet.chain.robinhood.com |
The contract is identical across all networks (same ABI, same functions). Only the address, chain, and fees differ. The SDK reads fees on-chain, so you never hardcode them. The Robinhood deployment is verified on Blockscout.
Getting ETH on Robinhood Chain
Robinhood Chain uses ETH for gas and fees. Bridge ETH (or supported ERC-20s) in via the canonical Arbitrum bridge or partner bridging routes — see the official Robinhood Chain docs for current options.
Fees are tiny: a basic write costs 0.00012 ETH (about $0.22). Bridging a few dollars of ETH is enough for many writes.
Robinhood Chain is chain ID 4663. It is a separate network from Ethereum mainnet and Arbitrum One — ETH must be bridged in before it can be spent there.
Wallet / Signer Setup
Every writer function takes an ethers.Signer. Two ways to get one:
Node.js (private key)
Add Robinhood Chain to MetaMask first:
| Field | Value |
|---|
| Network Name | Robinhood Chain |
| RPC URL | https://rpc.mainnet.chain.robinhood.com |
| Chain ID | 4663 |
| Currency Symbol | ETH |
| Block Explorer | https://robinhoodchain.blockscout.com |
Then connect:
Reader functions don’t need a signer they use the RPC configured via setRpcUrl().
Use assertChainMatches(signer) after setNetwork('robinhood') to catch a signer that’s still pointed at the wrong chain before you send a transaction.
Core Concepts
Data Storage (Code In)
Store any data (files, text, JSON) directly on-chain. Data lives in transaction calldata, not on a centralized server. Reads reconstruct data by walking a linked list of transactions.
How is it stored?
Depending on data size, the SDK picks the optimal method:
- Inline (small): data fits in a single transaction’s metadata field, no chunking
- Linked list (large): data is split into chunks, uploaded via
sendCode() calls in batches up to ~96 KB each, and the tail tx hash is recorded
codeIn(): upload data and get a transaction hash
readCodeIn(): read data back from a transaction hash
User State
Each address has an on-chain record managed by the contract. No separate account to initialize.
What gets stored?
- User-set metadata (name, profile, bio, anything you serialize)
userTxChainTail: the most recent inventory write, used as the head of the user’s tx-chain
When is it created?
No explicit “create user” step. The first codeIn() call writes both the inventory entry and advances the chain tail. Each codeIn() charges basicFee (0.00012 ETH) for an inline payload, or linkedListFee (0.00036 ETH) when the data is chunked.
Connection State
An on-chain relationship between two addresses (friends, DM channels, etc.).
What states can it have?
- pending (
0): request sent but not accepted yet
- approved (
1): request accepted, users are connected
- blocked (
2): one side blocked the other
A blocked connection can only be unblocked by the blocker.
The connection seed is derived deterministically from the two addresses via deriveDmSeed(userA, userB), so either party can recompute it.
Database Tables
Store JSON data in tables like a database, all on-chain.
How are tables created?
- Call
initializeDbRoot() once per dbRootId. The caller becomes the DbRoot creator.
- Call
createTable() to create a table. tableCreationFee (0.00036 ETH) is charged here and split 31/69 between feeReceiver and the DbRoot’s creator.
- Call
writeRow() to append rows.
A table is uniquely identified by dbRootId + tableName. Both are hashed with keccak256 internally, but raw names are also stored on-chain so the SDK can list them without a hardcoded lookup.
Tables must exist before writeRow() is called. There is no implicit creation.
Token & Collection Gating
Tables can be gated so only users holding a specific ERC-20 token or ERC-721 NFT can write data.
Gate Types
| Type | gateType | Description |
|---|
| Token (ERC-20) | 0 | User must hold >= amount of the specified token |
| Collection (ERC-721) | 1 | User must hold any NFT from the specified collection |
ERC-20 amount is in raw token units (wei-style). For an 18-decimal token, “100 tokens” = parseEther("100"), not 100.
ERC-1155 is not supported.
Gate parameter
Encryption (Crypto)
Built-in encryption module (iqlabs.crypto) for encrypting data before storing on-chain. Primitives are identical to the Solana SDK, so the same plaintext flows across chains.
Three encryption modes
- DH Encryption (single recipient): Ephemeral X25519 ECDH → HKDF-SHA256 → AES-256-GCM
- Password Encryption: PBKDF2-SHA256 (250k iterations) → AES-256-GCM
- Multi-recipient Encryption: PGP-style hybrid, one CEK wrapped per recipient via ECDH
Users derive a deterministic X25519 keypair from their wallet signature. The wallet is the key, no separate keystore.
Fees
Fees are owner-mutable per network and read live from the contract (utils.getBasicFee / getLinkedListFee / getTableCreationFee). Defaults shipped at deploy time:
| Fee | Default | ~USD | Charged on |
|---|
basicFee | 0.00012 ETH | ~$0.22 | dbCodeIn / walletConnectionCodeIn / userInventoryCodeIn when the payload is inline (≤ 700 bytes) |
linkedListFee | 0.00036 ETH | ~$0.65 | the same three functions, when the payload is chunked via sendCode |
tableCreationFee | 0.00036 ETH | ~$0.65 | createTable / createPrivateTable |
discountFee | 0.00006 ETH | ~$0.11 | replaces basicFee on the inline path for IQ-token holders |
Defaults target the same dollar values as Monad mainnet (priced at ETH ~$1,795, July 2026) and are retunable on-chain as price moves.
Where the value goes:
dbCodeIn / walletConnectionCodeIn / userInventoryCodeIn: 100% to feeReceiver.
createTable: split 31% to feeReceiver / 69% to the DbRoot’s creator. Root creators can pin or zero out their own value via setRootTableCreationFee().
Free: updateTableTxChainTail, updateConnectionTxChainTail, updateUserTxChainTail, requestConnection, manageConnection, dbInstructionCodeIn. Pointer bumps and instruction edits don’t cost fee, only gas.
Function Details
Data Storage and Retrieval
codeIn()
| Parameters | signer: ethers.Signer
data: data to upload (string or string[])
filename: optional filename (string, default: "")
filetype: file type hint (string, default: "")
onProgress: optional progress callback (percent: number) => void |
|---|
| Returns | Transaction hash (string) |
readCodeIn()
| Parameters | txHash: transaction hash (string)
onProgress: optional progress callback (percent: number) => void |
|---|
| Returns | { metadata: { handle, typeField, offset, beforeUserTx }, data: string } |
Connection Management
requestConnection()
| Parameters | signer: ethers.Signer
dbRootId: database ID (string)
receiver: counterparty address (string)
tableName: connection table name (string)
columns: column list (string[])
idCol: ID column (string)
extKeys: extension keys (string[], default: []) |
|---|
| Returns | Transaction hash (string) |
requestConnection is free in this version (gas only).
manageConnection()
Approve, block, or unblock a connection.
0: pending
1: approved
2: blocked
| Parameters | signer: ethers.Signer
otherParty: counterparty address (string)
dbRootId: database ID (string)
newStatus: 0 | 1 | 2 |
|---|
| Returns | Transaction hash (string) |
readConnection()
| Parameters | dbRootId: database ID (string)
partyA: first wallet (string)
partyB: second wallet (string) |
|---|
| Returns | { status: 'pending' | 'approved' | 'blocked' | 'unknown', requester: 'a' | 'b', blocker: 'a' | 'b' | 'none' } |
writeConnectionRow()
| Parameters | signer: ethers.Signer
otherParty: counterparty address (string)
dbRootId: database ID (string)
rowJson: JSON data (string)
onProgress: optional progress callback (percent: number) => void |
|---|
| Returns | Transaction hash (string) |
readConnectionRows()
| Parameters | dbRootId: database ID (string)
partyA: first wallet (string)
partyB: second wallet (string)
options: { limit?: number } (optional) |
|---|
| Returns | Array<{ txHash: string, data: any }> (most recent first) |
fetchUserConnections()
| Parameters | userAddress: user address (string) |
|---|
| Returns | Array<{ connectionKey: string, partyA: string, partyB: string, status: 'pending' | 'approved' | 'blocked' | 'unknown' }> |
Table Management
initializeDbRoot()
Claim a dbRootId. Only the creator can later modify table-creator allowlists or schema.
| Parameters | signer: ethers.Signer
dbRootId: database ID (string) |
|---|
| Returns | Transaction hash (string) |
manageTableCreators()
Set who can create tables. Caller must be the DbRoot creator.
| Parameters | signer: ethers.Signer
dbRootId: database ID (string)
tableCreators: addresses allowed to create public tables (string[])
extCreators: addresses allowed to create private tables (string[]) |
|---|
| Returns | Transaction hash (string) |
createTable()
Create a new table. Charges tableCreationFee (0.00036 ETH by default — or whatever value the DbRoot creator pinned with setRootTableCreationFee). Split 31% to feeReceiver, 69% to DbRoot.creator.
| Parameters | signer: ethers.Signer
dbRootId: database ID (string)
tableName: table name (string)
columns: column names (string[])
idCol: ID column (string)
extKeys: extension keys (string[], default: [])
gate: optional access gate
writers: optional writer whitelist (string[], default: [])
isPrivate: create private table (boolean, default: false) |
|---|
| Returns | Transaction hash (string) |
updateTable()
Modify an existing table’s schema, gate, or writer list. Caller must be the DbRoot creator. Existing rows are preserved.
| Parameters | Same as createTable() minus isPrivate |
|---|
| Returns | Transaction hash (string) |
writeRow()
Append a row to an existing table. Charges basicFee (0.00012 ETH) for inline payloads or linkedListFee (0.00036 ETH) when chunked.
| Parameters | signer: ethers.Signer
dbRootId: database ID (string)
tableName: table name (string)
rowJson: JSON row data (string)
onProgress: optional progress callback (percent: number) => void |
|---|
| Returns | Transaction hash (string) |
Table must already exist. writeRow will revert if the table was never created.
Fires two transactions internally: dbCodeIn (data — fee charged here, basic or linkedList by payload size) + updateTableTxChainTail (free pointer bump).
readTableRows()
Walk the table’s tx-chain and reconstruct rows.
| Parameters | dbRootId: database ID (string)
tableName: table name (string)
options: { limit?: number } (optional) |
|---|
| Returns | Array<{ txHash: string, data: any }> (most recent first) |
getTablelistFromRoot()
| Parameters | dbRootId: database ID (string) |
|---|
| Returns | { creator: string, tables: TableEntry[], globalTables: TableEntry[] } |
tables = public only. globalTables = public + private.
fetchInventoryTransactions()
Walk a user’s inventory tx-chain (everything uploaded via codeIn()).
| Parameters | userAddress: user address (string)
options: { limit?: number } (optional) |
|---|
| Returns | Array<{ txHash: string, handle: string, tailTx: string, typeField: string, offset: string }> |
Encryption
deriveX25519Keypair()
Derive a deterministic X25519 keypair from a wallet signature. Same wallet = same keypair every time.
| Parameters | signMessage: (msg: Uint8Array) => Promise<Uint8Array> |
|---|
| Returns | { privKey: Uint8Array, pubKey: Uint8Array } |
dhEncrypt() / dhDecrypt()
Single-recipient encryption via X25519 ECDH.
dhEncrypt | recipientPubHex: hex string
plaintext: Uint8Array → { senderPub, iv, ciphertext } (all hex) |
|---|
dhDecrypt | privKey: Uint8Array
senderPubHex, ivHex, ciphertextHex: hex strings → Uint8Array |
passwordEncrypt() / passwordDecrypt()
Password-based encryption via PBKDF2-SHA256.
multiEncrypt() / multiDecrypt()
Multi-recipient PGP-style hybrid encryption.
Store arbitrary metadata under the caller’s address. Overwrites any previous value.
| Parameters | signer: ethers.Signer
metadata: string | Uint8Array |
|---|
| Returns | Transaction hash (string) |
Environment Settings
setNetwork()
Switch the active network mode. Call once at app startup.
| Parameters | mode: 'sepolia' | 'monad' | 'monadTestnet' | 'robinhood'
rpcUrl: optional override (string) |
|---|
| Returns | void |
getNetwork()
| Returns | 'sepolia' \| 'monad' \| 'monadTestnet' \| 'robinhood' |
assertChainMatches()
Throws if RPC chainId doesn’t match active network mode.
setRpcUrl() / getRpcUrl()
Override reader RPC without changing network mode.
Tutorial: On-Chain Fortune Cookies 🥠
Build a permanent on-chain fortune cookie machine on Robinhood Chain. Anyone can submit a fortune. Anyone can draw a random one. All fortunes live on-chain forever.
What this teaches: initializeDbRoot → createTable → writeRow → readTableRows
Setup
Full Code
How to extend
- NFT gate: only holders of a specific collection can submit fortunes
- Encrypted fortunes: use
passwordEncrypt so only readers with the password see the message
- User profiles: call
updateUserMetadata so each author has a name + avatar
- Like counter: use
manageRowData to annotate fortunes with reactions