The gateway turns on-chain IQ Labs data into plain HTTP. Point a browser, a
curl, or a fetch() at it and you get JSON, images, or rendered HTML back,
with no RPC key and no SDK install. Everything it serves lives on chain, so any
gateway can serve the same bytes.The public gateway
A first real read, no auth, no setup:
__txSignature (the tx that
wrote it), __signer (fee payer), and __blockTime (chain-truth timestamp).
Why run your own
- No single point of failure. If one gateway is down, spin up another or point at a peer.
- Your own cache, your own region. Faster reads for your users.
- Nothing to lose. All data is on chain, so a wiped cache just refills.
- No lock-in. Same URL shapes everywhere, so switching operators is a base-URL change.
Run your own
The gateway runs on Bun. It is read-only by design: it never
writes to chain and exposes no write API, so a public deployment cannot be
made to spend or mutate anything.
Local in four commands
.env for Solana mainnet:
http://localhost:3000, with Swagger UI at
http://localhost:3000/docs. Confirm it is up:
Boot modes
IQ_CHAIN picks how the process boots. Leave it unset for the default
multi-chain mode, which is what gateway.iqlabs.dev runs.
Locked modes exist for fault isolation: one bad RPC cannot affect a chain it
was never wired to. Multi mode exists so one host can answer for everything.
Configuration
Shared
Solana
EVM
EVM networks
Deploy with Docker
The repo ships a chain-agnosticDockerfile. Build once, configure with env.
Bare VPS, Compose, Kubernetes, Akash: all fine. The gateway does not care.
Multi-chain routing
In multi mode the chain is resolved per request, so there is no/solana/...
or /monad/... prefix to remember. Rules, in order:
?network=wins.solana,sepolia,monad,monadTestnet, orrobinhood. An unknown value returns400.- Otherwise the id shape decides. A base58 id (Solana pubkey or signature)
goes to Solana; anything else (a
0xhash or address, or an EVM dbRootId) goes to the EVM default network. - Id-less routes such as
/healthand/dbrootsuse the default chain unless?network=says otherwise.
A network that the running gateway has no RPC configured for returns
503
instead of guessing.
Endpoints
The live/openapi.json is always the source of truth for the gateway you are
talking to. This is the working set.
Assets and metadata
Tables (the on-chain database)
Pagination is cursor-based, not offset-based:
Users
Gate verification
Discovery and search
Name services
ENS uses a dedicated Ethereum mainnet RPC (
ENS_RPC_ENDPOINT) and is cached
30 min. SNS lookups are cached 5 min, positive and negative both, and
concurrent cold lookups for one domain collapse into a single RPC call.
System
Admin (operators only)
Mounted only whenADMIN_TOKEN is set. Bearer auth.
GET /admin/queue returns RPC queue stats; POST patches concurrency,
minTimeMs, and maxDepth at runtime. No restart needed.
Live updates without polling
Two endpoints turn the gateway into a push channel for a table. Subscribe from the client:hello on connect, row on every notify for that table, and
ping every 30s to keep proxies from closing the connection.
Notify right after you write a tx, so readers see it before the RPC has
even indexed the signature:
txSignature is required. Including row lets the gateway skip the
chain read and inject your row immediately; including signer also drops that
wallet’s cached asset list so their next /user/.../assets call is fresh. If
the row cannot be fetched, the gateway invalidates the cached pages instead, so
the next read is correct either way.
Caching
Three tiers, with the chain as the floor:
Individual rows are immutable once written, so they cache for 24 hours. Head
pages cache for 60 seconds with a throttled background refresh, gated on the
table account’s
lastTimestamp: if the timestamp has not moved, the gateway
keeps serving the cached page instead of rescanning signatures.
Use If-None-Match on /table/{pda}/rows to get 304 Not Modified and skip
the body entirely.
Durable EVM row index
On Solana, enumerating a table’s rows is onegetSignaturesForAddress call:
the RPC keeps a per-account transaction index for free. EVM chains have no
equivalent, so the SDK walks beforeDataTx pointers one transaction at a
time. That walk is strictly sequential, is redone on every cold cache, and
silently loses rows when two wallets write the same table at once (the
two-transaction writeRow can orphan the loser’s row off the pointer chain).
The gateway fixes this with a durable index in cache.db that, unlike the
LRU tiers above, is never pruned: once a transaction is known to belong to a
table, that fact is chain-truth and is kept forever.
Three sources feed it:
- Live reads. Every row served through the walk path is recorded as a side effect.
POST /table/.../notify. Posting clients inject their own writes, which land in the index immediately.- Log backfill. The contract emits
DbCodeInEventwith the dbRoot and table ids as indexed topics, so a filteredeth_getLogsscan enumerates every row-write for one table in block order, including rows the pointer walk can never reach. Scans run at background RPC priority, persist progress, and resume where they left off.
before cursors are answered
straight from SQLite instead of re-walking the chain past the cursor, and
row payloads missing from the index are hydrated in parallel. Index size is
visible at /table/cache/stats under rowIndex.
Bootstrap from a peer
A cold gateway can inherit a hot one’s cache instead of refetching everything from chain. The snapshot is a streamedtar.gz of cache.db plus blobs, made
consistent with VACUUM INTO.
There is deliberately no
POST /cache/restore or peer-sync endpoint. Operators
write to their own cache directory as a local filesystem operation; nobody
writes across the network. That is what keeps a public gateway read-only.Helius (optional, but fast)
With a paid Helius plan onHELIUS_API_KEY, the Solana path automatically
uses getTransactionsForAddress (100 full txs per call, roughly 100x faster
for session files), batched JSON-RPC for row reads, and startup backfill when
BACKFILL_FROM_SLOT is set. Without it everything still works on plain RPC,
just slower for large files.
Hosting a site on Solana
Solana-only. The gateway serves any file from an on-chain manifest, so a static site can live entirely on chain.
Both Iqoogle and gateway manifest formats are supported, with SPA fallback for
unknown paths and root-relative asset resolution.
Deploy a directory:
Point a .sol domain at it
On your .sol domain via sns.id, set one record:
URL first:
For the prettier
<name>.sol.site URL, also set a CNAME through the
“Configure Sol.site” UI on sns.id:
sns.iqlabs.dev is a direct A record to the gateway origin with no CDN proxy
in front. If the origin IP ever changes, that one A record is updated and every
CNAME user keeps working.
Test it:
How it fits together
src/chain/. Cache, RPC
queue, ETag handling, SSE, search, and the server shell are shared. Importing
the inactive adapter is side-effect-free, so a misconfigured EVM env can never
take down a Solana-only gateway.
Read-heavy tooling in the IQ Labs stack already routes through gateways by
default with raw RPC as the final fallback. See the
on-chain Git CLI for how the
GATEWAY_URL
fallback chain works, including pointing it at your own instance.