Skip to main content
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:
Every row carries three gateway-stamped fields: __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

Minimum .env for Solana mainnet:
Start it:
Your gateway is live at 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-agnostic Dockerfile. Build once, configure with env.
That is the entire deployment contract: Bare VPS, Compose, Kubernetes, Akash: all fine. The gateway does not care.
Mount the cache on storage whose lifecycle is independent of the container (retain on delete). A redeploy that only swaps the image should keep the same volume, otherwise every restart refetches from chain.

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:
  1. ?network= wins. solana, sepolia, monad, monadTestnet, or robinhood. An unknown value returns 400.
  2. Otherwise the id shape decides. A base58 id (Solana pubkey or signature) goes to Solana; anything else (a 0x hash or address, or an EVM dbRootId) goes to the EVM default network.
  3. Id-less routes such as /health and /dbroots use the default chain unless ?network= says otherwise.
Identifier shapes differ per chain, so a few path segments change: 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

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 when ADMIN_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:
The stream emits 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:
Only 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 one getSignaturesForAddress 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:
  1. Live reads. Every row served through the walk path is recorded as a side effect.
  2. POST /table/.../notify. Posting clients inject their own writes, which land in the index immediately.
  3. Log backfill. The contract emits DbCodeInEvent with the dbRoot and table ids as indexed topics, so a filtered eth_getLogs scan 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.
Once a table’s backfill is complete, deep 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 streamed tar.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 on HELIUS_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:
That single record lights up three surfaces at once, because the SNS web-resolution spec checks URL first:
Include the file path in the record, not just the signature. Many manifests use a non-standard index (gameboy.html, not index.html), and the gateway uses the path you bake in as the default file when someone visits with no path.
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:
The URL record format is operator-agnostic, so a domain can point at any gateway. The regex extracts the signature from whichever gateway URL was set, and the on-chain content is identical either way. Pick the operator you trust.

How it fits together

The only real divergence between chains lives in 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.