This document is in progress and will be refined.
Installation
Network
The SDK ships with three network modes. Set the one you want once at app startup withsetNetwork().
The contract is identical across all three networks (same ABI, same functions). Only the address, chain, and fees differ. The SDK reads fees on-chain, so you never hardcode them.
Testnet
Develop and test for free on Monad testnet before touching mainnet. The contract is deployed and verified there with the same fees as mainnet, so testnet is an exact rehearsal.1. Get free testnet MON
You need testnet MON to pay fees. Grab some from the official faucet:- Go to faucet.monad.xyz
- Paste your wallet address
- Request testnet MON arrives in seconds
The faucet hands out a modest amount per request (about 20 MON at a time, with roughly 25 MON extra if you connect a social account). Testnet fees are 1/10 of mainnet (
basicFee 0.65 MON, linkedListFee 1.95 MON, tableCreationFee 1.95 MON), so one faucet claim is enough for plenty of writes.2. Point the SDK at testnet
codeIn, writeRow, connections, and encryption all behave exactly as on mainnet.
3. Wallet setup for testnet
For a Node.js signer, point your provider at the testnet RPC:Use
assertChainMatches(signer) after setNetwork('monadTestnet') to catch a signer that’s still pointed at the wrong chain before you send a transaction.4. Going to mainnet
When your app works on testnet, switch one line:dbRootIds and table names will be separate on mainnet (different chain, different contract state), so you re-create them there.
Wallet / Signer Setup
Every writer function takes anethers.Signer. Two ways to get one:
Node.js (private key)
Browser (MetaMask / injected wallet)
Add Monad to MetaMask first:
Then connect:
setRpcUrl().
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
Key related functions
codeIn(): upload data and get a transaction hashreadCodeIn(): 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 firstcodeIn() call writes both the inventory entry and advances the chain tail. Each codeIn() charges basicFee (6.5 MON mainnet / 0.65 MON testnet) for an inline payload, or linkedListFee (19.5 / 1.95 MON) 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
deriveDmSeed(userA, userB), so either party can recompute it.
Key related functions
requestConnection(): send a friend requestmanageConnection(): approve/block/unblockreadConnection(): check relationship statuswriteConnectionRow(): exchange messages/data with a connected partyfetchUserConnections(): fetch all of a user’s connections
Database Tables
Store JSON data in tables like a database, all on-chain.How are tables created?
- Call
initializeDbRoot()once perdbRootId. The caller becomes the DbRoot creator. - Call
createTable()to create a table.tableCreationFee(19.5 MON mainnet / 1.95 MON testnet) is charged here and split 31/69 betweenfeeReceiverand 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.writeRow() is called. There is no implicit creation.
Key related functions
initializeDbRoot(): claim adbRootIdcreateTable()/updateTable(): create or modify a tablewriteRow(): append a rowreadTableRows(): read rows from a tablegetTablelistFromRoot(): list all tables in a database
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
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
Fees
Fees are owner-mutable per network and read live from the contract (utils.getBasicFee / getLinkedListFee / getTableCreationFee). Defaults shipped at deploy time:
Testnet fees are exactly 10× cheaper than mainnet because the official Monad faucet drips only 0.05 MON / 12h — full mainnet pricing would make even one round trip unreachable for devs.
Where the value goes:
dbCodeIn/walletConnectionCodeIn/userInventoryCodeIn: 100% tofeeReceiver.createTable: split 31% tofeeReceiver/ 69% to the DbRoot’screator. Root creators can pin or zero out their own value viasetRootTableCreationFee().
updateTableTxChainTail, updateConnectionTxChainTail, updateUserTxChainTail, requestConnection, manageConnection, dbInstructionCodeIn. Pointer bumps and instruction edits don’t cost fee, only gas.
Function Details
Data Storage and Retrieval
codeIn()
readCodeIn()
Connection Management
requestConnection()
requestConnection is free in this version (gas only).
manageConnection()
Approve, block, or unblock a connection.
0: pending1: approved2: blocked
readConnection()
writeConnectionRow()
readConnectionRows()
fetchUserConnections()
Table Management
initializeDbRoot()
Claim a dbRootId. Only the creator can later modify table-creator allowlists or schema.
manageTableCreators()
Set who can create tables. Caller must be the DbRoot creator.
createTable()
Create a new table. Charges tableCreationFee (19.5 MON mainnet / 1.95 MON testnet by default — or whatever value the DbRoot creator pinned with setRootTableCreationFee). Split 31% to feeReceiver, 69% to DbRoot.creator.
updateTable()
Modify an existing table’s schema, gate, or writer list. Caller must be the DbRoot creator. Existing rows are preserved.
writeRow()
Append a row to an existing table. Charges basicFee (6.5 / 0.65 MON) for inline payloads or linkedListFee (19.5 / 1.95 MON) when chunked.
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.
getTablelistFromRoot()
tables = public only. globalTables = public + private.
fetchInventoryTransactions()
Walk a user’s inventory tx-chain (everything uploaded via codeIn()).
Encryption
deriveX25519Keypair()
Derive a deterministic X25519 keypair from a wallet signature. Same wallet = same keypair every time.
dhEncrypt() / dhDecrypt()
Single-recipient encryption via X25519 ECDH.
passwordEncrypt() / passwordDecrypt()
Password-based encryption via PBKDF2-SHA256.
multiEncrypt() / multiDecrypt()
Multi-recipient PGP-style hybrid encryption.
User Metadata
updateUserMetadata()
Store arbitrary metadata under the caller’s address. Overwrites any previous value.
Environment Settings
setNetwork()
Switch the active network mode. Call once at app startup.
getNetwork()
| Returns | 'sepolia' \| 'monad' \| 'monadTestnet' |
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 Monad. 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
passwordEncryptso only readers with the password see the message - User profiles: call
updateUserMetadataso each author has a name + avatar - Like counter: use
manageRowDatato annotate fortunes with reactions
