Services
The Inscribe Go services — indexer that mirrors Injective contract state to Postgres, API that serves joined views and proxies Cascade reads, plus the migrate runner.
Inscribe ships two Go binaries plus a migration runner, all in a single module under services/. The indexer is the source of truth for "the current state of all known markets"; the API is the read-only surface the frontend talks to. Both are independent processes; either can be restarted without losing work.
inscribe-indexer
A poll-and-mirror loop. Every tick (default 10s):
- Lists all
inscribe-market-v2instances via Injective LCD'swasm/contracts-by-code/{code_id}endpoint. - For each instance, smart-queries
GetConfig,GetMarket,ListEvidence,GetProposal,GetChallenge,GetCommittee,ListVotes,GetSettlement, andGetPool. - Mirrors the result into Postgres:
markets,evidence,bets,proposals,challenges,votes,settlements,market_pools. - Eagerly resolves Cascade CIDs in a separate worker, writing bodies to
cascade_cachewith their content-type and filename.
The indexer is stateless across restarts — it always reads the chain state of record. There is no Kafka, no offsets, no compaction. The Postgres tables are a materialized view; if you drop them and restart the indexer, they refill from chain state alone within minutes.
Running
The indexer logs one line per poll tick with the contracts seen and the state transitions written. A 24-hour soak test on testnet completes ~8600 polls without manual intervention; the only error class observed in practice is transient LCD 5xx, which the polling loop retries on the next tick.
Why polling and not events
CometBFT websocket events from a remote LCD are unreliable under network partitions and require ordered backfill on reconnect. Polling is simpler, deterministic, and recovers from any disconnect by just running another tick. The cost is a ~10-second worst-case latency from chain confirmation to indexed state — acceptable for this product (the frontend refreshes after a 12-second delay to give the indexer one full poll tick).
inscribe-api
HTTP service over the indexer's Postgres tables. CORS-permissive. Read-only except for POST /cascade/upload.
Routes
GET /markets
Lists known markets, optionally filtered by state. limit defaults to 50, capped at 500.
GET /markets/{addr}
Full per-market detail joined across all tables: market config + state, evidence list, bets list, pool totals, proposal, challenge, votes, settlement. Empty collections serialise as [], not null, so JS callers can .length / .map without defensive checks.
GET /markets/{addr}/audit
The audit viewer endpoint. Returns the same MarketDetail as above, plus an artifacts map keyed by every CID referenced (spec, evidence, proposer + challenger + voter justifications, settlement record). For each CID:
body is the parsed JSON for JSON content types, or a UTF-8 string for text. Bodies > 64 KB are truncated in the JSON response (the raw bytes remain available via GET /cascade/{cid}).
This is intentionally a single fat endpoint. A market with 20 evidence items and a full voter trail has ~30 CIDs to resolve; doing them all on the server in parallel and shipping one JSON blob beats the round-trip cost of 30 separate fetches from the browser.
GET /cascade/{cid}
Streams the raw bytes of a Cascade artifact. Resolves from cascade_cache first; on miss, pulls from sn-control and write-through caches.
The immutable cache header is correct: Cascade content is content-addressed, so once fetched the bytes are correct forever. Browsers and CDNs can cache without further negotiation.
POST /cascade/upload
Multipart upload that drives the full server-signed Cascade write (see Cascade from Injective — Pattern B). Two-step pipeline:
- Shell out to
inj-cw-upload(cascadego repo binary). This builds aMsgRequestAction, packs it for ICA dispatch via thecw-ica-controller, broadcasts on injective-888, and returns the Lumeraaction_id. - POST the file body to
sn-control /sn/uploadwith theaction_idso the Supernode stores the actual content.
Timeout is 5 minutes (overrides the surrounding chi.Timeout(60s)) because the IBC packet round-trip can take 30–60s. The endpoint has no authentication today; production deployments should put a quota-capped bearer token in front of it. See Cascade from Injective for funding requirements.
Environment
| Variable | Default | Purpose |
|---|---|---|
DATABASE_URL | required | pgx connection string |
INJECTIVE_LCD | required | LCD endpoint for the indexer's smart queries |
INSCRIBE_MARKET_V2_CODE_ID | required | Code ID the indexer scans for instances |
SN_CONTROL_URL | http://4.239.243.216:8444 | Cascade gateway for downloads |
SN_CONTROL_TOKEN | required for downloads + uploads | Bearer auth for sn-control |
CASCADEGO_DIR | /Users/kaleab/Documents/cascadego | Where inj-cw-upload lives |
INJ_CW_UPLOAD_BIN | $CASCADEGO_DIR/inj-cw-upload | Override for the binary path |
API_PORT / API_BIND | 8080 / 127.0.0.1 | HTTP listener |
Database schema (high-level)
Migrations live in services/migrations/ and run via inscribe-migrate up. Goose handles ordering and idempotency.
Production layout
The Inscribe MVP runs both processes locally during development. In production:
- API on Vercel Functions (Fluid Compute, Node.js LTS) only after the Go binary is wrapped in a Node-callable handler — the Go service runs as a standalone container otherwise.
- Indexer as a long-running container (Vercel doesn't run continuous workers natively).
- Postgres on Vercel Marketplace (Neon).
cascade_cachetable size grows with read traffic; periodicVACUUMis sufficient because every row is immutable.
See BUILD.md Phase 6 in the repo for the planned production wiring.
Local quick-check
After running migrations and starting the indexer: