Builders

Build apps on STRATO

Write Solidity, sign users in with email, read every contract as a database table, and pay one cent a transaction. This guide walks from your first query to a production app, using two STRATO apps as worked examples.

Language
Solidity (SolidVM)
Fee per tx
0.01 USDST
Reads
Cirrus REST, free
Auth
OIDC or wallet

Why build on STRATO

STRATO is a Layer-1 for real-world-asset DeFi. The parts most chains leave to you, such as indexing, custody and fee tokens, ship with every node.

Write Solidity

Contracts run on SolidVM, a Solidity interpreter. Upload source, not bytecode, and call functions by name with JSON.

Flat one-cent fees

Every transaction costs 0.01 USDST. No gas auctions, and your users never need to buy a native coin.

Email sign-in, no seed phrases

Users log in with OIDC and get a key held in Vault. MetaMask and the STRATO Wallet work too.

Indexer included

Cirrus turns every contract's state, mappings and events into Postgres tables you query over REST.

Real assets to compose

USDST, tokenized gold and silver, swap pools, lending, CDPs and a price oracle are already live.

Fast finality

PBFT consensus commits blocks with no reorgs, so a confirmed result is final.

The STRATO stack

Every node serves five APIs from one host. You don't need to run a node, deploy an indexer or manage keys to get started; the public endpoints cover it.

What you build
Web / mobile UI
React, Vite, Next… signs in with STRATO or a wallet
Backend gateway
Builds txs, reads Cirrus, runs bots, holds no user keys
Every STRATO node serves
app.strato.nexus
/rpc
JSON-RPC
viem, ethers, wallets
/bloc/v2.2
Bloc
Call any function by name
/cirrus/search
Cirrus
Indexed state & events
/strato-api
Core API
Blocks, accounts, txs
/api
App API
Swaps, CDP, lending, bridge
Execution
SolidVM
Runs Solidity source directly. No bytecode step.
Consensus
PBFT validators
Fast finality, no reorgs to wait out.
Indexing
Cirrus (Postgres)
Every contract's state, mappings and events, as tables.
Identity
Keycloak OIDC + Vault

Users sign in with email. Their signing key is created and held in Vault, so no seed phrases. Or bring your own key.

Fees
0.01 USDST per tx

Flat, paid in the stablecoin (or one voucher). No gas price, no native coin to buy.

Built-in assets
USDSTGOLDSTSILVSTSTRATOETHWBTC

Plus a price oracle, swap pools, lending and CDPs.

The STRATO stack. You build the dashed box; everything below it is already running.

Choose your integration path

The main question is who signs the transaction. Answer it and the API follows.

Only reading data?
Start with Cirrus (D)
Users sign in with email?
Bloc (B) or App API (A), signed in Vault
You or users hold the key?
JSON-RPC (C) or a wallet (E)
OptionEndpointWho signsBest for
AApp REST API
/apiYour STRATO account (Vault)Use STRATO's DeFi (swap, lend, mint, bridge) without writing contracts
BBloc
/bloc/v2.2Your STRATO account (Vault)Your own contracts, called by name with JSON args. What Odds and Play use.
CJSON-RPC
/rpcYour own private keyBots, scripts and self-custody tools with viem or ethers
DCirrus
/cirrus/searchNobody: read-onlyDashboards, leaderboards, order books, any indexed read
EExternal wallet
STRATO Wallet · MetaMaskThe user's walletBrowser dApps where users bring their own keys
Most apps combine two: Bloc or JSON-RPC to write, Cirrus to read.

Quick start

Build against testnet (helium) at app.testnet.strato.nexus. Swap the host for app.strato.nexus when you go live. Contract addresses for the core tokens are the same on both.

1

Read data, no credentials

Start with Cirrus. Contract state is a table named <creator>-<Contract>, each mapping is <creator>-<Contract>-<mapping>, and each event is <creator>-<Contract>-<Event>. Platform contracts use the creator BlockApps.

Cirrus is a read-only PostgREST API over every contract on chain. No key, no login.

H=https://app.testnet.strato.nexus/cirrus/search

# Token metadata for USDST
curl -s "$H/BlockApps-Token?address=eq.937efa7e3a77e20bbdbd7c0d32b6514f368c1010&select=_name,_symbol,customDecimals"

# A user's USDST balance (a mapping is its own table: address, key, value)
curl -s "$H/BlockApps-Token-_balances?address=eq.937efa7e3a77e20bbdbd7c0d32b6514f368c1010&key=eq.<user>&select=value::text"

# Gold and silver prices from the platform oracle (USD x 1e18)
curl -s "$H/BlockApps-PriceOracle-prices?address=eq.0000000000000000000000000000000000001002&key=in.(cdc93d30182125e05eec985b631c7c61b3f63ff0,2c59ef92d08efde71fe1a1cb5b45f4f6d48fcc94)&select=asset:key,price:value::text"
2

Get an identity

Writes need a signer. Your backend gets an OIDC access token from Keycloak (realm mercata), and the node signs with that identity's Vault key. Fund the address with testnet USDST or vouchers before you send anything.

For scripts, bots and backend jobs: a client-credentials token from Keycloak. Request a client at support.blockapps.net.

import axios from "axios"

const DISCOVERY =
  "https://keycloak.blockapps.net/auth/realms/mercata/.well-known/openid-configuration"

export async function getAccessToken(): Promise<string> {
  const { data: oidc } = await axios.get(DISCOVERY)
  const { data } = await axios.post(
    oidc.token_endpoint,
    new URLSearchParams({ grant_type: "client_credentials" }),
    { auth: { username: process.env.OAUTH_CLIENT_ID!, password: process.env.OAUTH_CLIENT_SECRET! } },
  )
  return data.access_token
}
3

Send a transaction

Bloc takes a contract address, a method name and named arguments, with no ABI encoding. Amounts are integers in base units (18 decimals for USDST), sent as strings.

Call any contract function by name. Batch the approve with the action so the user confirms once. Each tx in the batch costs 0.01 USDST.

import axios from "axios" // not fetch: Node's fetch User-Agent fails the node's CSRF check

const BLOC = "https://app.testnet.strato.nexus/bloc/v2.2"
type Call = { contractName: string; contractAddress: string; method: string; args: Record<string, unknown> }

export async function submit(token: string, calls: Call[]) {
  const { data } = await axios.post(
    `${BLOC}/transaction/parallel?resolve=true`,
    {
      txs: calls.map((payload) => ({ type: "FUNCTION", payload })),
      txParams: { gasLimit: 32_100_000_000, gasPrice: 1 }, // fixed values; they don't change the fee
    },
    { headers: { Authorization: `Bearer ${token}` } },
  )
  return waitForResults(data.map((r: { hash: string }) => r.hash))
}

async function waitForResults(hashes: string[], timeoutMs = 45_000) {
  const start = Date.now()
  for (;;) {
    const { data } = await axios.post(`${BLOC}/transactions/results`, hashes)
    const failed = data.find((r: any) => r.status === "Failure")
    if (failed) throw new Error(failed.txResult?.message ?? "Transaction failed")
    if (data.every((r: any) => r.status === "Success")) return data
    if (Date.now() - start > timeoutMs) throw new Error("Submitted, not confirmed yet") // Pending is never success
    await new Promise((r) => setTimeout(r, 1500))
  }
}

// STRATO Odds' "split": approve USDST, then call the market. Addresses: 40 hex, no 0x.
await submit(token, [
  { contractName: "Token", contractAddress: USDST, method: "approve", args: { spender: market, value: amount } },
  { contractName: "Market", contractAddress: market, method: "split", args: { amount } },
])
4

Watch it land

Every write follows the same six steps. Results come back from /bloc/v2.2/transactions/results, and the new state shows up in Cirrus a few seconds later.
  1. 1Sign in

    User logs in through Keycloak (OIDC). Your app gets an access token.

    Authorization: Bearer …
  2. 2Build the call

    Name the contract, the function and its arguments as JSON. No ABI encoding.

    { method: "split", args: { amount } }
  3. 3Vault signs

    The node signs with the user's custodial key. Your server never touches it.

    POST /bloc/v2.2/transaction
  4. 4Fee is charged

    0.01 USDST (or a voucher) per tx, before execution. Reverts still pay.

    FeeCollector …100d
  5. 5Block is final

    SolidVM runs it and PBFT validators commit the block. Poll for the result.

    status: "Success"
  6. 6Cirrus indexes

    New state and events land in Postgres tables your UI can query.

    GET /cirrus/search/…
A write from login to indexed state, usually a few seconds end to end.

Write and deploy a contract

SolidVM runs Solidity source directly. A deploy uploads your source; the node typechecks it and runs the constructor. Most Solidity carries over, with a few differences worth learning first.

A two-player USDST escrow, the core pattern behind STRATO Play, written for SolidVM.

// No pragma needed. SolidVM interprets the source directly.
contract Wager {
    address public usdst;      // platform tokens are called by name
    address public referee;
    address public playerA;    // don't name a field "creator": Cirrus reserves it
    address public playerB;
    uint public stake;
    uint public status;        // 0 open, 1 active, 2 settled, 3 refunded
    uint public reportBy;

    event Settled(address winner, uint payout);

    constructor(address _usdst, address _referee, uint _stake) {
        usdst = _usdst;
        referee = _referee;    // pass roles explicitly; msg.sender may be a User contract
        stake = _stake;
    }

    function join() external {
        require(status == 0, "not open");
        require((msg.sender != playerA) && (msg.sender != referee), "can't join");
        _pull(msg.sender);
        if (playerA == address(0)) {
            playerA = msg.sender;
        } else {
            playerB = msg.sender;
            status = 1;
            reportBy = block.timestamp + 3600;   // raw seconds: no "1 hours"
        }
    }

    function settle(address winner) external {
        require(msg.sender == referee, "only the referee");
        require(status == 1, "not active");
        require((winner == playerA) || (winner == playerB), "winner must be a player");
        status = 2;                              // close before paying out
        usdst.call("transfer", winner, stake * 2);
        emit Settled(winner, stake * 2);
    }

    // A silent referee can stall the pot, never keep it.
    function expire() external {
        require((status == 1) && (block.timestamp > reportBy), "not expired");
        status = 3;
        usdst.call("transfer", playerA, stake);
        usdst.call("transfer", playerB, stake);
    }

    function _pull(address from) internal {
        uint before = usdst.call("balanceOf", address(this));
        usdst.call("transferFrom", from, address(this), stake);
        uint held = usdst.call("balanceOf", address(this));
        require(held == (before + stake), "stake didn't arrive");
    }
}

SolidVM vs. Solidity

  • Parenthesize compound conditions
    Mainnet still uses legacy operator precedence: write (a > 1) && (b < 2).
  • No time units
    Use raw seconds: 86400, not 1 days.
  • No msg.value or payable
    Value moves as USDST transfers: approve, then transferFrom.
  • Integers don't wrap or size-check
    uint8 holds 256. Check before you subtract: require(a >= b).
  • Code is immutable
    Ship upgrades as new contracts behind a registry or a Proxy.
  • No array pop()
    And out-of-range storage writes don't fail. Use push.
  • Hash bytes, not strings
    keccak256(bytes(s)) matches Ethereum; keccak256(s) doesn't.
  • block.chainid is the network ID
    It differs from the EIP-155 chain ID that wallets use.
Every difference
1Create a username once

Call UserRegistry.createUser at …0720. The name becomes your Cirrus table prefix.

2Upload with that username

Deploy with ?username=<name>. The constructor then runs as your User contract, so pass owner and admin addresses as arguments.

3Record the addresses

Read the new row from <name>-<Contract> and store it in your backend config. A registry contract makes later upgrades a pointer swap.

Anatomy of a STRATO app

STRATO Odds and STRATO Play were built independently and ended up with the same shape: contracts, a thin backend that acts as the chain gateway, a React front end, and nginx in front. Start from this layout.

Runtime
Browser
Holds an access token (or a connected wallet). Never talks to the node directly.
nginx
/ → ui/api → backend/rpc → nodews → backend
Backend: the chain gateway
no keys · no money
Auth
verify JWT, map user → address
Writes
approve + action batches, per-account queue
Reads
Cirrus queries behind a short cache
Jobs
resolve, settle, expire on a timer
STRATO node
/bloc · /cirrus · /strato-api
Keycloak
realm: mercata
Repository
my-strato-app/
contracts/# SolidVM sources, tests, deploy scripts
MyAppRegistry.sol# points at the live factories
MarketFactory.sol# new Market(...) per market/match
tests/*.test.sol# solid-vm-cli test
deploy/# upload + record addresses
backend/# Express/TS chain gateway
auth/# OIDC code flow, wallet sign-in
chain/tx.ts# build, submit, poll
chain/cirrus.ts# all reads
jobs/# resolver, referee, keepers
ui/# Vite + React SPA
nginx/# one origin: / · /api · /rpc
docker-compose.yml# nginx + backend + ui
The layout STRATO Odds and STRATO Play share. Every read is a Cirrus query and every write is a transaction, so the backend stays stateless.

Case studies

Two production apps, two different answers to the same question: what goes on chain?

STRATO Odds logo
Fully on chain

STRATO Odds

A prediction market on gold and silver prices. YES and NO outcome tokens trade on an on-chain order book, and each market settles against the platform price oracle. A winning token redeems for exactly 1 USDST.

stratoodds.com
  1. 1Create market

    A factory deploys a market with an asset, strike and expiry, plus its own YES and NO tokens.

    MarketFactory.createMarket(…)
  2. 2Split

    Deposit N USDST, receive N YES + N NO. A complete set is always worth exactly 1 USDST.

    Market.split(N)
  3. 3Trade

    Orders are escrowed on an on-chain order book. Selling NO at q is buying YES at 1 − q.

    OrderBook · Offer · Bid
  4. 4Resolve

    After expiry, anyone can call resolve. The market reads the first PriceOracle price after expiry and fixes the outcome.

    PriceOracle …1002
  5. 5Redeem

    Each winning token redeems for 1 USDST. Before then, merge YES + NO back into USDST anytime.

    redeem() · merge(N)
Collateral
The market contract holds every USDST deposited by split. Supply of YES = supply of NO = USDST held.
Orders
Each resting order is its own escrow contract, so tokens on the book are never pooled.
Reads
The UI renders markets, books, fills and positions straight from Cirrus tables.
STRATO Odds: fully on-chain markets that settle against the same oracle the rest of STRATO uses.
What's on chain

Everything with value: markets, outcome tokens, every order as its own escrow contract, fills, resolution and redemption.

What the backend does

Builds approve-plus-action batches, reads Cirrus, and runs timers: a resolver every 15 seconds and a crosser that matches overlapping orders.

Lesson

Composing a platform contract (the PriceOracle) gave the app trusted settlement with no oracle of its own to build or run.

STRATO Play logo
Hybrid

STRATO Play

Head-to-head games (rock paper scissors, Connect Four and a real-time street fight) where two players stake the same amount of USDST and the winner takes the pot. Stakes go on chain, the game is played on a game server, and a referee settles in one transaction.

Players
1Post a challenge

Pick a game and a stake. One click sends approve + createMatch as one batch.

2Accept

The opponent approves the same stake and joins.

4Play

Moves stream over a WebSocket. Nothing is signed per move.

7Verify

Anyone can re-run the record or the fight replay against the chain.

Game server
5Referee the game

Clocks, hidden throws, the 60 fps fight engine, timeouts.

6Report once

Packs the whole game into one tx: every throw, every drop, or a replay hash.

Match.report(winner, …)
STRATO
1Factory + escrow

Factory deploys a match contract. The hub pulls the stake and checks it arrived.

usdst.call("transferFrom", …)
3Match ACTIVE

Show-up and report deadlines start on chain.

showUpBy · reportBy
6Re-check & pay

Contract replays the board or re-scores throws, then releases the pot minus fee. Stats update.

PlayHub.settle(winner)
STRATO Play: the money and the result are on chain; the gameplay isn't. A silent referee can stall a pot, never keep it.
What's on chain

Stakes, match terms and deadlines, one settlement per game carrying the full record, the payout, and every player's win/loss stats.

What's off chain

Seating, clocks, hidden throws, the 60 fps fight engine, Elo ratings and matchmaking. None of it needs a signature per move.

Lesson

Don't put every step on chain. Make the chain the judge: let the contract check the record and bound what the operator can do.

How Connect Four is verified: the referee sends all the drops packed into one number (three bits per column). The contract unpacks them, drops each piece on its own board, checks for four in a row, and only pays the winner if the moves actually produce that result. For the fight, too fast to replay on chain, it stores a hash of the replay that anyone can re-run in the browser.

Patterns you can reuse

Building blocks from the case studies that carry over to most apps.

Registry + factory

Odds · Play

A small registry contract stores the live factory addresses. Factories deploy one contract per market or match, so each gets its own Cirrus row. To upgrade, deploy a new factory and repoint the registry; the backend re-reads it.

Checked escrow

Play

Pull the stake with transferFrom, compare balances before and after, and set the state to closed before every payout. Each escrow can only pay out once.

Complete sets

Odds

1 USDST in, 1 YES + 1 NO out. The pair is always worth exactly 1 USDST, so the market is fully collateralized whatever the price of each side.

Oracle resolution

Odds

Read the platform PriceOracle on chain and require a price stamped after expiry. Anyone can trigger resolve, so no admin is needed to settle.

Verified off-chain play

Play

Run fast logic off chain, then settle once with the whole record. The contract replays or re-scores it, or stores a replay hash anyone can check.

Deadlines that refund

Play

Every trusted role gets a deadline. After it passes, anyone can expire the contract and refund the users, so funds never depend on one operator staying online.

Before you ship: gotchas

Lessons from running STRATO Odds and STRATO Play on mainnet. Each one cost someone an afternoon.

Transactions

  • Fees are per transaction, so approve + action costs 0.02 USDST. Check the user's USDST plus vouchers before asking them to sign.
  • Reverted transactions still pay the fee and use the nonce.
  • Pending is never success. Mempool drops are silent, so read the state back from Cirrus.
  • Queue writes per account. Two batches in flight from one account race for the same nonce.
  • Use axios or curl for server-side POSTs. Node's built-in fetch fails the CSRF check with a 403.
  • Addresses go to Bloc and Cirrus as 40 hex characters with no 0x. viem and wallets need the 0x prefix.

Cirrus

  • Select big numbers as col::text and parse them as BigInt. JSON numbers drop 18-decimal precision.
  • Your tables are named <deployer>-<Contract>. Deploy without a username and none are created.
  • Tables are shared by every deployment under one prefix. Always filter on your parent factory or book.
  • Cirrus reserves creator and id. A field named creator lands in arg_creator.
  • A table returns 404 until its first contract or event exists. Treat that as empty.
  • Timestamps look like "2026-09-12 00:11:54 UTC", not ISO. Convert them before handing them to Safari.
  • Chunk in.(…) filters to about 40 values, or the URL gets a 414.
  • Indexing trails finality by a few seconds. Poll briefly after a write, and cache public reads.

Contracts

  • Parenthesize compound conditions, and avoid mixing the ternary with && or ||.
  • Don't pass bool constructor arguments from JSON. Use a uint flag.
  • Arrays and mappings get their own tables, not columns in the contract's row, and mapping tables skip zero values. Store records you query often as strings or packed uints.
  • Argument names in a transaction must match the Solidity parameter names exactly.
  • Calls to contracts outside your upload use addr.call("fn", args) and return a single value.

Reference

Networks

Mainnet (upquark)Testnet (helium)
Hosthttps://app.strato.nexushttps://app.testnet.strato.nexus
JSON-RPChttps://noderpc.strato.nexus/rpchttps://app.testnet.strato.nexus/rpc
Chain ID123354377739506195049586845898
Explorerhttps://stratoscan.strato.nexushttps://stratoscan.testnet.strato.nexus

Key contracts (same on both networks)

  • USDST · Stablecoin, 18 decimals
    937efa7e3a77e20bbdbd7c0d32b6514f368c1010
  • GOLDST · 1 token = 1 troy oz gold
    cdc93d30182125e05eec985b631c7c61b3f63ff0
  • SILVST · 1 token = 1 troy oz silver
    2c59ef92d08efde71fe1a1cb5b45f4f6d48fcc94
  • PriceOracle · USD prices × 1e18
    0000000000000000000000000000000000001002
  • Voucher · 1 voucher pays 1 tx fee
    000000000000000000000000000000000000100e
  • UserRegistry · Usernames → User contracts
    0000000000000000000000000000000000000720
  • PoolFactory · Lists every swap pool
    000000000000000000000000000000000000100a
  • CDPEngine · Mint USDST against collateral
    0000000000000000000000000000000000001011

Go deeper

Building something on STRATO?

Get API credentials, ask questions in the community chat, or tell us what you're working on.