Build your own integration
Reusable patterns for any Injective × Cascade product — the action_id pointer pattern, contract-side conventions, indexer responsibilities, and what to copy from Inscribe.
Inscribe is one shape of Injective × Cascade product (prediction markets with permanent justifications). It is not the only one. This page is a recipe book for building a different one: where to copy from Inscribe, where to diverge, and the failure modes to design around.
The shape every integration takes
Regardless of what your dApp does, the architecture is the same three-layer split:
The thing you customise per product is what the bytes are: trade receipts, NFT media + metadata, governance proposals, oracle inputs, audit logs, archival timeline data, off-chain order signatures, encrypted messages, attestations, votes, you name it. Everything else is plumbing.
Decision: who signs the Cascade write?
The most important decision in the design phase. The options are spelled out in Cascade from Injective; the short version:
| Choose Pattern A (user-signed) if... | Choose Pattern B (server-signed) if... | Choose Pattern C (contract-driven) if... |
|---|---|---|
| Provenance matters — the user must be the on-chain author | UX matters — one signature, faster path | The artifact must exist whenever the state transition happens, with no user in the loop |
| Users already need a Lumera address for other reasons (faucet, staking) | Users may not have a funded Lumera address | Indexers and trustless readers need the artifact guaranteed-present |
| You want zero server-side custody | You're OK funding ulume at the operator level | You're prepared for the IBC packet round-trip latency and operational complexity |
Most production integrations end up using A for user-authored content and B or C for derived/canonical content. Inscribe uses B for everything in the MVP because that ships faster; it migrates user-content paths to A as wallet onboarding matures.
Contract-side conventions
These three patterns are what make an Injective contract Cascade-friendly. Adopt them and your indexer and frontend become trivial.
Convention 1: Every Cascade-bound field is a String
Not a Vec<u8>, not a custom type — a plain UTF-8 String that the contract treats as opaque. The contract checks !cid.trim().is_empty() and that's the entire validation. Resolution and integrity are off-chain concerns.
Don't store base32 / base58 prefixes ("bafy…"). Cascade action_ids are numeric strings; storing them as-is keeps your indexer's queries simple.
Convention 2: Emit the CID as a wasm event attribute
Indexers reconstruct state from wasm events. Every state transition that introduces a new CID should emit it as a discoverable attribute alongside action, submitter, and the rest.
Indexers can then either (a) decode events live and avoid smart queries entirely, or (b) use events as triggers and smart-query for full state. The Inscribe indexer uses (b) for simplicity.
Convention 3: Expose CIDs in queries
A read-only client that doesn't run an indexer should be able to drive a viewer from contract queries alone. That means at minimum:
This makes the contract self-describing: a developer with no indexer can query the chain, get every CID, fetch each from a Cascade gateway, and render the full state.
Convention 4: State machines, not free-form
The Inscribe market is a strict state machine: Open → Proposed → Challenged → Voting → Final. Every execute message checks the current state and either advances it or rejects. This is the cleanest way to model Cascade-bound workflows because each state defines exactly which CIDs may be added:
| State | What CIDs are added |
|---|---|
Open | evidence |
Proposed | evidence, proposal justification |
Challenged | evidence, challenge justification |
Voting | evidence, vote justifications |
Final | settlement record |
An attacker cannot add a "vote justification" in the Open state and confuse the indexer; the contract rejects it.
Convention 5: Atomic cross-contract calls for funds + CIDs
If your CID submission also moves funds (a bond, a fee, a deposit), use the standard CosmWasm pattern: emit a CosmosMsg::Wasm(WasmMsg::Execute) in the same response. The Inscribe market does this for proposer / challenger bond locking against bond-vault. The whole tx either succeeds or reverts — the indexer never sees a half-state where the CID was registered but the bond was not.
Indexer responsibilities
Three jobs, in order of priority:
- Mirror on-chain state into a queryable store. Smart-query each contract instance every N seconds. Postgres is more than enough; the data volume is bounded by the number of markets/objects, not by user traffic.
- Eagerly resolve every new CID. When you see a new event with a CID attribute, kick off a background fetch from a Cascade gateway and cache the body locally. Frontend reads should hit your cache, not Lumera.
- Serve joined views for the frontend.
GET /things/{id}should return on-chain state and inlined Cascade content in one response.
The third one is what makes the UX feel fast. A naive frontend doing 20 round trips per page (one for state, 19 for CIDs) is painful; the same data served as one JSON blob is instant.
Frontend conventions
The Inscribe web app is ~1500 lines of Next.js + Keplr. Patterns worth copying:
getOfflineSignerOnlyAminofor Injective signing. Direct-mode signing does not work witheth_secp256k1. AlwaysAmino. See Injective primer.- Server Components for read paths. The Inscribe market detail page is a Server Component that fetches
/markets/{addr}and renders. No client-side data fetching on initial load. - Client Components for write actions. A single
<MarketActions>Client Component renders only the buttons appropriate to the current state (Open→ bet + evidence + propose;Proposed→ evidence + challenge + finalize; etc.). Drives the user through the state machine without showing actions that would be rejected on chain. - Refresh after a delay. After a successful execute, schedule a
router.refresh()10–12 seconds out so the indexer has one full poll-tick to catch up before the UI re-reads. - Surface contract errors verbatim. Don't try to translate
wasm execute failed: WrongState(...)into something prettier — the verbatim error is usually the most actionable thing the user can see.
When to skip Cascade entirely
Cascade is the right home for content that needs to be permanent, verifiable, and large. It is not the right home for everything an Injective dApp produces.
| Use Cascade if... | Use plain on-chain state if... |
|---|---|
| The artifact is > a few hundred bytes | The artifact is a small enum or numeric value |
| The artifact needs to outlive the dApp's lifecycle | The artifact is intermediate / cache state |
| The artifact will be cited or audited in the future | The artifact is purely operational |
| You want pay-once economics | You don't need permanence at all |
| The artifact is user-content (proof, evidence, justification) | The artifact is metadata about contract state |
A 32-byte hash, an enum, a counter — these belong in CosmWasm state. A 500-byte JSON blob with a claim and citations, a 5-MB PDF, a 3D NFT model — these belong on Cascade with a String pointer in CosmWasm state.
Things to design around
Pitfalls the Inscribe build surfaced. None are blockers, but each deserves a deliberate decision.
IBC packet latency
If you use Pattern C (contract-driven Cascade writes), the round-trip is 30–60 seconds in the happy path and can stall on relayer hiccups. Don't put it on the user's hot path. Defer it to canonical artifacts that don't gate user actions, or use Pattern B with a fast server.
sn-control as a SPOF
sn-control is a single HTTP service holding a bearer token. If it's down, your POST /cascade/upload is down. Either run your own sn-control (Inscribe inherits one from the cascadego deployment) or use a Cascade gateway with HA. The download path can use multiple gateways via a URL resolver; the upload path is currently single-homed.
Quota and ulume balance
Every Cascade write spends ~10–15k ulume. A funded address with 1M ulume covers ~70 uploads. Monitor your service balance via /healthz (cascade-api) or whatever your gateway exposes; alert before exhaustion. The Inscribe API does not yet enforce per-user quotas; production deployments should.
eth_secp256k1 vs secp256k1
Injective uses eth_secp256k1; Lumera uses standard secp256k1. If you derive both from the same Keplr mnemonic, the user has two different addresses (different coinType paths). UI flows must surface both addresses and not confuse them.
Indexer lag
A 10-second poll interval means UI sees a stale state for up to 10 seconds after a tx confirms. Two mitigations:
- After a successful tx, optimistically update the local UI state.
- Use shorter intervals (3–5s) at the cost of more LCD load.
Inscribe chose the simpler refreshSoon() pattern: wait 12 seconds, then router.refresh() and let the Server Component re-fetch. Good enough for the testnet UX, fragile under heavy load.
Re-litigation needs gateway redundancy
The "anyone can fetch the full record forever" property depends on at least one Cascade gateway being reachable. The bytes are permanent on the Supernode network, but a reader who only knows an HTTPS URL needs that URL to resolve. Plan for at least two gateways in production (your own + Lumera-operated) and consider exposing a "Cascade-native" reader that talks to Supernodes directly for very long-horizon use cases.
What to copy from Inscribe
Concrete artifacts worth lifting if you're building in a similar shape:
| Inscribe file | What to copy / adapt |
|---|---|
contracts/bond-vault/ | The custody-by-market pattern with a whitelist of caller contracts |
contracts/voter-registry/src/sampling.rs | Deterministic weighted-without-replacement committee sampling |
contracts/inscribe-market-v2/src/contract.rs::compute_payout | Pari-mutuel payout formula with Invalid refund path |
services/internal/indexer/indexer.go | Poll-and-mirror loop with eager Cascade resolution |
services/internal/api/server.go::auditMarket | The fat audit endpoint pattern |
services/internal/api/upload.go | Pattern B server-signed upload pipeline |
web/lib/inj.ts + web/lib/keplr.ts | Injective + Keplr signing with Amino-only |
web/components/market-actions.tsx | State-machine-aware action UI |
scripts/deploy-testnet.sh + smoke-test-testnet.sh | Reproducible deploy + bring-up |
Other product shapes that fit
Beyond Inscribe-style prediction markets, the same Injective × Cascade pattern is a natural fit for:
- NFT marketplaces where metadata permanence matters (parallel to the Lukso LSP-4 / LSP-8 integration): contract holds the token + a Cascade
action_id; metadata + media live on Cascade. - DAOs where proposals, debate threads, and voting rationale need to be archival: Snapshot's "proposals on IPFS" pattern, but with permanence.
- DeFi protocols publishing oracle inputs with a permanent receipts trail (think Chainlink data feeds where the input set is part of the audit).
- Audit / compliance logs that need decade-scale verifiability, signed by the on-chain contract.
- Encrypted messaging layered on Cascade's encrypted storage with Injective contracts handling access control.
- Long-horizon prediction markets with settlement dates years out — Cascade's permanence guarantee outlives the underlying market.
In each case the integration shape is what's in the diagram at the top of this page; only the schema of "what the bytes are" changes.