Documentation Index
Fetch the complete documentation index at: https://iqlabs.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
This document is in progress and will be refined.
The IQLabs Ethereum SDK works on Monad 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. Monad is fast and cheap, so everything just works better.
Installation
npm i @iqlabs-official/ethereum-sdk
The SDK ships as CommonJS for Node.js and works in browsers via any modern bundler.
Network
The SDK ships with three network modes. Set the one you want once at app startup with setNetwork().
| Mode | Chain ID | Currency | Contract | Default RPC |
|---|
sepolia | 11155111 | ETH | 0xB1C16271954c7238672c3666FD22Ee14C6d065Db | https://rpc.sepolia.org |
monad | 143 | MON | 0xeFd9376835076Bf8d83826F6A2277BB5362Cd893 | https://rpc.monad.xyz |
monadTestnet | 10143 | MON | 0x88af59e58C7E5DcbE7cc12972B90cff3fEEF7223 | https://testnet-rpc.monad.xyz |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad'); // mainnet call once at app startup
// or, for free testing:
iqlabs.setNetwork('monadTestnet');
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). Since fees mirror mainnet (BASIC_FEE 6.5 MON, LINKED_LIST_FEE 19.5 MON), claim a couple of times if you plan to run many writes.
Testnet MON has no real value and only works on chain ID 10143. Never send it to a mainnet address.
2. Point the SDK at testnet
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monadTestnet'); // resolves the testnet contract + RPC
That’s the only change. Every reader/writer call now targets the testnet deployment. 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:
import { Wallet, JsonRpcProvider } from 'ethers';
const provider = new JsonRpcProvider('https://testnet-rpc.monad.xyz');
const signer = new Wallet(process.env.PRIVATE_KEY!, provider);
For MetaMask, add the testnet network:
| Field | Value |
|---|
| Network Name | Monad Testnet |
| RPC URL | https://testnet-rpc.monad.xyz |
| Chain ID | 10143 |
| Currency Symbol | MON |
| Block Explorer | https://testnet.monadexplorer.com |
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:
iqlabs.setNetwork('monad'); // mainnet uses real MON
No other code changes. The same 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 an ethers.Signer. Two ways to get one:
Node.js (private key)
import { Wallet, JsonRpcProvider } from 'ethers';
const provider = new JsonRpcProvider('https://rpc.monad.xyz');
const signer = new Wallet(process.env.PRIVATE_KEY!, provider);
Add Monad to MetaMask first:
| Field | Value |
|---|
| Network Name | Monad |
| RPC URL | https://rpc.monad.xyz |
| Chain ID | 143 |
| Currency Symbol | MON |
Then connect:
import { BrowserProvider } from 'ethers';
const provider = new BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
Reader functions don’t need a signer they use the RPC configured via 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
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 tx-chain write costs a small BASIC_FEE (6.5 MON).
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. LINKED_LIST_FEE (19.5 MON) is charged here.
- 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
gate?: {
tokenAddress: string; // ERC-20 or ERC-721 contract (ZeroAddress = public)
amount: number | bigint; // min balance (raw units, ERC-20 only; ignored for ERC-721)
gateType: 0 | 1; // 0 = ERC-20, 1 = ERC-721
}
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
| Constant | Value | Charged when |
|---|
BASIC_FEE | 6.5 MON | Every codeIn() |
LINKED_LIST_FEE | 19.5 MON | createTable, requestConnection, writeRow, writeConnectionRow |
Monad is fast and cheap, so these fees are tiny.
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
// Store a short message
const txHash = await iqlabs.writer.codeIn(signer, 'Hello Monad!');
// Store large data with progress tracking
const txHash2 = await iqlabs.writer.codeIn(
signer,
longString,
'data.txt',
'text/plain',
(pct) => console.log(`upload: ${pct.toFixed(1)}%`)
);
readCodeIn()
| Parameters | txHash: transaction hash (string)
onProgress: optional progress callback (percent: number) => void |
|---|
| Returns | { metadata: { handle, typeField, offset, beforeUserTx }, data: string } |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
const result = await iqlabs.reader.readCodeIn(txHash);
console.log(result.data); // 'Hello Monad!'
console.log(result.metadata.typeField); // 'text/plain'
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
await iqlabs.writer.requestConnection(
signer,
'my-app',
friendAddress,
'dm_table',
['message', 'timestamp'],
'message_id'
);
LINKED_LIST_FEE (19.5 MON) charged here.
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
await iqlabs.writer.manageConnection(signer, friendAddress, 'my-app', 1); // approve
await iqlabs.writer.manageConnection(signer, friendAddress, 'my-app', 2); // block
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' } |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
const { status } = await iqlabs.reader.readConnection('my-app', addressA, addressB);
console.log(status); // 'pending' | 'approved' | 'blocked' | 'unknown'
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
await iqlabs.writer.writeConnectionRow(
signer,
friendAddress,
'my-app',
JSON.stringify({ message_id: '1', message: 'gm from Monad', timestamp: Date.now() })
);
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
const messages = await iqlabs.reader.readConnectionRows(
'my-app', myAddress, friendAddress, { limit: 50 }
);
messages.forEach(m => console.log(m.data));
fetchUserConnections()
| Parameters | userAddress: user address (string) |
|---|
| Returns | Array<{ connectionKey: string, partyA: string, partyB: string, status: 'pending' | 'approved' | 'blocked' | 'unknown' }> |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
const connections = await iqlabs.reader.fetchUserConnections(myAddress);
const friends = connections.filter(c => c.status === 'approved');
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
await iqlabs.writer.initializeDbRoot(signer, 'my-app');
// reverts if dbRootId already claimed
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
await iqlabs.writer.manageTableCreators(
signer, 'my-app',
[admin1, admin2], // public-table allowlist
[] // anyone can create private tables
);
// pass empty arrays to make creation open to anyone
createTable()
Create a new table. Charges LINKED_LIST_FEE (19.5 MON).
| 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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
import { parseEther } from 'ethers';
iqlabs.setNetwork('monad');
// Public table
await iqlabs.writer.createTable(
signer, 'my-app', 'posts', ['title', 'body', 'author'], 'post_id'
);
// ERC-20 gated table (must hold >= 100 tokens)
await iqlabs.writer.createTable(
signer, 'my-app', 'vip', ['name'], 'user_id', [],
{ tokenAddress: erc20Address, amount: parseEther('100'), gateType: 0 }
);
// ERC-721 gated table (must hold 1 NFT)
await iqlabs.writer.createTable(
signer, 'my-app', 'holders', ['name'], 'user_id', [],
{ tokenAddress: nftAddress, amount: 0, gateType: 1 }
);
// Private table (must know the name to access)
await iqlabs.writer.createTable(
signer, 'my-app', 'internal', ['note'], 'note_id', [],
undefined, [], true
);
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
import { parseEther } from 'ethers';
iqlabs.setNetwork('monad');
await iqlabs.writer.updateTable(
signer, 'my-app', 'vip', ['name'], 'user_id', [],
{ tokenAddress: erc20Address, amount: parseEther('500'), gateType: 0 }
);
writeRow()
Append a row to an existing table. Charges LINKED_LIST_FEE (19.5 MON).
| 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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
await iqlabs.writer.writeRow(signer, 'my-app', 'posts', JSON.stringify({
post_id: '1',
title: 'gm Monad',
body: 'first post on-chain',
author: await signer.getAddress()
}));
Table must already exist. writeRow will revert if the table was never created.
Fires two transactions internally: dbCodeIn (data) + updateTableTxChainTail (pointer + fee).
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) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
const rows = await iqlabs.reader.readTableRows('my-app', 'posts', { limit: 50 });
rows.forEach(r => console.log(r.data));
getTablelistFromRoot()
| Parameters | dbRootId: database ID (string) |
|---|
| Returns | { creator: string, tables: TableEntry[], globalTables: TableEntry[] } |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
const { creator, tables, globalTables } = await iqlabs.reader.getTablelistFromRoot('my-app');
tables.forEach(t => console.log(`${t.name} (${t.seedHex})`));
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 }> |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
const myFiles = await iqlabs.reader.fetchInventoryTransactions(myAddress, { limit: 20 });
myFiles.forEach(tx => console.log(`${tx.txHash}: ${tx.handle}`));
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 } |
import iqlabs from '@iqlabs-official/ethereum-sdk';
import { getBytes } from 'ethers';
const sign = async (msg: Uint8Array) => getBytes(await signer.signMessage(msg));
const { privKey, pubKey } = await iqlabs.crypto.deriveX25519Keypair(sign);
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 |
const enc = await iqlabs.crypto.dhEncrypt(
recipientPubHex,
new TextEncoder().encode('secret message')
);
const dec = await iqlabs.crypto.dhDecrypt(myPrivKey, enc.senderPub, enc.iv, enc.ciphertext);
console.log(new TextDecoder().decode(dec));
passwordEncrypt() / passwordDecrypt()
Password-based encryption via PBKDF2-SHA256.
const enc = await iqlabs.crypto.passwordEncrypt(
'my-password',
new TextEncoder().encode('secret data')
);
const dec = await iqlabs.crypto.passwordDecrypt('my-password', enc.salt, enc.iv, enc.ciphertext);
multiEncrypt() / multiDecrypt()
Multi-recipient PGP-style hybrid encryption.
const enc = await iqlabs.crypto.multiEncrypt(
[alicePubHex, bobPubHex, carolPubHex],
new TextEncoder().encode('group secret')
);
// each recipient decrypts with their own key
const plaintext = await iqlabs.crypto.multiDecrypt(alicePrivKey, alicePubHex, enc);
Store arbitrary metadata under the caller’s address. Overwrites any previous value.
| Parameters | signer: ethers.Signer
metadata: string | Uint8Array |
|---|
| Returns | Transaction hash (string) |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
await iqlabs.writer.updateUserMetadata(
signer,
JSON.stringify({ name: 'Alice', bio: 'building on Monad' })
);
Environment Settings
setNetwork()
Switch the active network mode. Call once at app startup.
| Parameters | mode: 'sepolia' | 'monad' | 'monadTestnet'
rpcUrl: optional override (string) |
|---|
| Returns | void |
import iqlabs from '@iqlabs-official/ethereum-sdk';
iqlabs.setNetwork('monad');
// with custom RPC
iqlabs.setNetwork('monad', 'https://your-monad-rpc');
getNetwork()
| Returns | 'sepolia' \| 'monad' \| 'monadTestnet' |
console.log(iqlabs.getNetwork()); // 'monad'
assertChainMatches()
Throws if RPC chainId doesn’t match active network mode.
iqlabs.setNetwork('monad');
await iqlabs.assertChainMatches(signer); // throws if signer's RPC isn't chainId 143
setRpcUrl() / getRpcUrl()
Override reader RPC without changing network mode.
iqlabs.setRpcUrl('https://your-monad-rpc');
console.log(iqlabs.getRpcUrl());
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
npm i @iqlabs-official/ethereum-sdk ethers
Full Code
import iqlabs from '@iqlabs-official/ethereum-sdk';
import { BrowserProvider } from 'ethers';
const DB = 'fortune-cookies';
const TABLE = 'fortunes';
// Call once to initialize (only the first caller becomes creator)
async function setupFortuneJar(signer: any) {
iqlabs.setNetwork('monad');
try {
await iqlabs.writer.initializeDbRoot(signer, DB);
await iqlabs.writer.createTable(
signer, DB, TABLE,
['fortune', 'author', 'ts'],
'id'
);
console.log('Fortune jar created on Monad!');
} catch (e) {
console.log('Jar already exists, skipping setup');
}
}
// Submit a fortune (costs 19.5 MON)
async function submitFortune(signer: any, fortune: string) {
iqlabs.setNetwork('monad');
const author = await signer.getAddress();
const txHash = await iqlabs.writer.writeRow(
signer, DB, TABLE,
JSON.stringify({
id: `${author}-${Date.now()}`,
fortune,
author,
ts: Date.now()
})
);
console.log('Fortune inscribed:', txHash);
return txHash;
}
// Draw a random fortune (free read)
async function drawFortune() {
iqlabs.setNetwork('monad');
const rows = await iqlabs.reader.readTableRows(DB, TABLE);
if (rows.length === 0) return 'The jar is empty. Be the first to add a fortune!';
const pick = rows[Math.floor(Math.random() * rows.length)];
return pick.data.fortune;
}
// Read all fortunes
async function allFortunes() {
iqlabs.setNetwork('monad');
return iqlabs.reader.readTableRows(DB, TABLE);
}
// Usage in a browser app
async function main() {
const provider = new BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
// First time only
await setupFortuneJar(signer);
// Submit
await submitFortune(signer, 'The best time to build on Monad was yesterday. The second best time is now.');
// Draw
const fortune = await drawFortune();
console.log('Your fortune:', fortune);
}
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