QuaiAxe docs

Guides

Guide: build zone-ready Quai apps

This guide is for any developer building on Quai Network, not only on Hartii. It explains why an app should not assume one zone, the few facts about Quai's sharding you need, ten rules that keep an app correct as zones are added, and a safe order for upgrading an app that today talks to a single zone.

Every statement about Quai comes from an official source, linked where it is used. Where Quai has not documented something, the text says not yet documented by Quai. The advice (the rules, the checklists, the sketch) is our engineering judgement, not a Quai specification.

Why it matters

Quai is built as a hierarchy of chains: one Prime chain, three Region chains and nine Zone chains. Zone chains run user transactions and smart contracts. (Hierarchical structure)

Quai's documentation lists only Cyprus-1 as active on mainnet. (Networks) The same documentation describes the design as growing: when sustained demand keeps the uncle rate above 20% for an extended period, the network automatically adds zones, starting from a 3 x 3 layout and then expanding to 4 x 4 and beyond. (Hierarchical structure) When Paxos-1, Hydra-1 or any further zone will activate is not yet documented by Quai.

So the safe assumption is that your users will, at some point, hold addresses in more than one zone. An app written for one zone tends to fail in quiet ways:

  • It reads a user's address from another zone through the one RPC endpoint it knows, and shows a zero balance or no history.
  • It treats a payment as complete when the sender's zone confirms it, although the money has not arrived yet.
  • Its indexer keeps one cursor, so a second zone's blocks are skipped or mixed in by block height.
  • It caches or stores records by address or token id alone, so the same value from two zones overwrites itself.
  • It falls back to "any working RPC" when one is down, and returns another zone's answer as if it were the right one.

None of these produce an error. They produce wrong numbers. The rules below are about not producing wrong numbers.

The model in one screen

FactWhat it means for your appSource
Prime, 3 Regions (Cyprus, Paxos, Hydra), 9 ZonesUsers and contracts live on zones; the layers above coordinatePrime, Region and Zone chains
Zones are named Cyprus1..Hydra3 and indexed as [region zone], for example Cyprus1 [0 0], Paxos2 [1 1]Use these as keys; do not invent numberingquai-cli-wallet
An address starts with a 9-bit prefix: 4 bits region, 4 bits zone, 1 bit ledger (0 = Quai account ledger, 1 = Qi UTXO ledger)The zone and ledger of any address can be read from the address itselfAddress
Each zone holds its own set of Quai and Qi addressesA balance or history exists on one zone onlyBuild introduction
One chain id per network (mainnet 9), shared by all zonesThe chain id does not tell you the zoneNetworks
One RPC endpoint per zone, https://rpc.quai.network/<zone-name> (for example cyprus1)Send each request to the zone that owns the dataNetworks
Cross-zone value moves as an external transaction (ETX). It goes up through a dominant chain and down to the destination, and the receiver is credited once the ETX is included in a block on the destination chainA cross-zone send is asynchronous. Submitted is not arrivedETXs
Contract calls across zones are asynchronous too; a contract cannot run code on another zone synchronouslyDesign for eventual consistencyETXs
A contract lives in the zone given by its address prefix, and you query it through that zone's RPCOne deployment is one zoneDeploying contracts

Two more facts matter in practice. Standard CREATE deployments through quais grind the address to the right zone prefix for you, but with CREATE2 you must choose the salt so the address lands in the zone you intend. (Deploying contracts) And the official SDK documentation for zone-aware functions is still marked "coming soon", so test zone behaviour yourself rather than assuming it. (quais.js)

Ten rules for zone-ready apps

Rule 1. Decode the zone from the address, never assume it

Take the region, zone and ledger from the address prefix (Address). Treat a prefix that is not one of the known zones as unknown, not as the default zone. Validate the address before you decode it, and reject zero or malformed addresses. The sketch below shows a decoder.

Rule 2. Keep a zone registry with separate flags

A registry lists every zone you know about and, for each one, flags that are set independently:

FlagMeaning
knownThe zone exists and its address prefix is understood
readYou have verified an endpoint and deployments, and the app may read it
writeYou have verified the contracts you would use, and the app may send there
cross-zoneYou have a tested settlement path between this pair of zones

Recognising an address must never switch on reads or writes by itself. Turn each flag on one at a time, by configuration, after checking it.

Rule 3. One provider per zone, and never fall back to another zone's RPC

Each zone has its own endpoint (Networks). If the endpoint for the right zone is down, the correct answer is "unavailable", not the answer from a healthy endpoint of a different zone. Failover is fine between several endpoints for the same zone. Do not take endpoint URLs for new zones from guesses: take them from Quai's documentation or your own verification, and leave them unset until then.

Rule 4. Indexers keep a cursor, a head and a lock per zone

Block numbers of different zones are unrelated, so never merge zones by block height. Give each zone its own head, confirmation depth, overlap, cursor and lease. Schedule bounded work fairly so one slow or unavailable zone cannot stall the rest. Do not advance a cursor until everything it covers is durably written.

Rule 5. Put the zone in every storage key, cache key and API

Identify an event by (network, zone, tx_hash, log_index). Identify a token or collection by (network, origin_zone, contract), plus the token id for NFTs. Include the zone in cache keys, rate-limit keys, queue names and API routes or parameters, so that adding a zone adds rows and does not change what existing rows mean. Keep your current single-zone routes working while you add the zone-qualified ones.

Rule 6. Key deployment manifests by network, zone, role and generation

A manifest is the list of your deployed contracts. Key each entry by network + zone + role + generation and record the deployment block, the ABI generation and whether it is verified. Then "which contract do I call for this action" is a lookup, not a guess. Do not redeploy an existing asset in another zone and present it as the same asset: a second token is a different token, and moving value between them needs its own design.

Rule 7. Never show an aggregate balance as spendable

A native balance belongs to an address on one zone. A token balance belongs to a contract deployment on one zone. (Build introduction) If you show a total across zones, label it as a total, show the per-zone parts with how fresh each one is, and let the user spend only from the zone that holds the money. A wallet network switch does not change which zone the connected account is on, so check the account's zone separately.

Rule 8. A cross-zone payment is paid only after destination settlement

Because cross-zone transfers are asynchronous (ETXs), model the states explicitly: prepared, submitted, source confirmed, awaiting destination settlement, destination confirmed. Keep failed, reorged and unknown as well. A receiver is credited once the ETX is included on the destination chain, so a merchant or game should mark an order paid only after destination evidence that matches the order's recipient, amount and time window. A balance increase alone is not enough evidence. Never retry automatically after an unknown outcome, because the first send may still settle. How to link a source transaction to its destination ETX in code is not yet documented by Quai in the sources we reviewed, so rehearse it before relying on it. Until you have, detect a cross-zone operation and refuse it before the user signs.

Rule 9. Deploy contracts per zone, with prefix-matching addresses

A contract lives in one zone (Deploying contracts). To serve several zones, deploy a separate copy in each ("sister contracts") and connect them as Quai describes, using ETXs between them. Check that each deployed address has the prefix of its zone, especially with CREATE2, where quais does not grind for you. Read the code back from the chain after deployment. Rehearse deployment, address derivation and payout behaviour on a test network first; behaviour you saw on one zone is not evidence for another. Quai's guide for cross-chain contracts with SolidityX is not yet documented by Quai ("coming soon" on the deployment page).

Rule 10. Measure at ten times the load before switching a zone on

Before enabling a zone, run a workload ten times what you expect and record: RPC calls per user action, database rows read and written, cache or KV writes, worker CPU, indexer lag and real-time connection capacity. Also test failure cases: one zone down, one slow, one returning stale data. Do not infer capacity from unit-test counts. A comparison of same-zone and cross-zone gas costs is not yet documented by Quai, so measure fees yourself too.

Migrating a single-zone app

The safe order is below. Each step ships on its own, and each one keeps the original zone working exactly as before.

  1. Add the registry and compatibility adapters. Put the zone registry in one module. Keep only your current zone active. Existing call sites keep working through adapters.
  2. Add manifests and storage context. Add zone columns or key prefixes, and zone-keyed manifests. Keep the legacy interfaces.
  3. Backfill history for the current zone into the new keys without resetting any cursor.
  4. Shadow-read and compare. Serve from the new path in the background, compare its results with the old path, and keep the legacy route live.
  5. Enable additional-zone reads only after the endpoint and the deployments for that zone are verified.
  6. Enable same-zone writes for one new zone and one product at a time.
  7. Enable native cross-zone payments only after destination-settlement proofs and rehearsals (Rule 8).
  8. Consider token and NFT cross-zone designs separately. Moving or mirroring an asset between zones is an economic and contract design question, not a routing change.

Registry availability, deployment existence, read enablement, write enablement and cross-zone enablement stay as separate switches the whole way.

Checklists and a zone registry sketch

Before you call an app zone-ready

  • Every address your code handles is decoded to a zone and ledger, and unknown prefixes are rejected.
  • No endpoint URL, chain id or explorer URL for a zone is hardcoded outside the registry.
  • Read, write and cross-zone are separate flags, all off for zones you have not verified.
  • No code path falls back to a different zone's provider.
  • Indexers have per-zone cursors, heads and locks, and one zone being down does not block the others.
  • Zone is part of every storage key, cache key and API.
  • Manifests are keyed by network, zone, role and generation.
  • No total balance is offered as spendable.
  • Cross-zone sends are either refused before signing or follow the full state model.
  • Deployed contract addresses have the right zone prefix, verified by reading them back.
  • A ten-times workload and the failure cases have been run for every zone that is on.

Before you switch one more zone on

  • The endpoint is taken from Quai's documentation or your own verified test, not guessed.
  • The contracts you need are deployed there and read back.
  • Reads work and match a shadow comparison for at least one full indexing cycle.
  • A wallet you support can hold an address in that zone and sign for it.
  • You know how you roll back, and the original zone is untouched by the rollback.

A minimal zone registry (illustrative)

This sketch is pure JavaScript with no dependencies, no network calls and no keys. The bit layout follows the Address documentation; the zone list follows the quai-cli-wallet names and indexes. Endpoints come from your own configuration, because none are assumed here.

js
// Illustrative sketch. Not a library. Check every assumption against current Quai docs.
const REGIONS = ['cyprus', 'paxos', 'hydra'];
const ZONES = REGIONS.flatMap((name, region) =>
  [0, 1, 2].map((zoneIndex) => ({ id: `${name}${zoneIndex + 1}`, region, zoneIndex })),
);

// Prefix: byte 0 = region (high 4 bits) and zone (low 4 bits); top bit of byte 1 = ledger.
export function decodeAddress(address) {
  if (typeof address !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(address)) {
    throw new Error('not a 20-byte hex address');
  }
  const byte0 = parseInt(address.slice(2, 4), 16);
  const byte1 = parseInt(address.slice(4, 6), 16);
  const region = byte0 >> 4;
  const zoneIndex = byte0 & 0x0f;
  const ledger = byte1 & 0x80 ? 'qi' : 'quai';
  const zone = ZONES.find((z) => z.region === region && z.zoneIndex === zoneIndex);
  // Unknown prefix: zoneId is null. Do not default to a zone.
  return { region, zoneIndex, ledger, zoneId: zone ? zone.id : null };
}

// zones = { cyprus1: { url, read: true, write: true, crossZone: ['paxos1'] }, ... }
// Anything not listed is off, whether or not the zone is known.
export function createRegistry({ network, zones = {} }) {
  const entry = (id) => zones[id] || null;
  const registry = {
    network,
    isKnown: (id) => ZONES.some((z) => z.id === id),
    canRead: (id) => Boolean(entry(id)?.read && entry(id)?.url),
    canWrite: (id) => Boolean(entry(id)?.write && entry(id)?.url),
    canCrossZone: (from, to) =>
      Boolean(entry(from)?.write && entry(to)?.write && (entry(from)?.crossZone || []).includes(to)),
    // One URL per zone. There is deliberately no fallback to another zone.
    providerUrl(id) {
      if (!registry.canRead(id)) throw new Error(`zone ${id} is not enabled for reads`);
      return entry(id).url;
    },
    // Call before building a transaction for a user.
    assertCanSend({ fromAddress, toAddress }) {
      const from = decodeAddress(fromAddress);
      const to = decodeAddress(toAddress);
      if (!from.zoneId || !to.zoneId) throw new Error('unknown zone prefix');
      if (from.zoneId === to.zoneId) {
        if (!registry.canWrite(from.zoneId)) throw new Error(`zone ${from.zoneId} is not enabled for writes`);
      } else if (!registry.canCrossZone(from.zoneId, to.zoneId)) {
        throw new Error('cross-zone send is not supported yet'); // refuse before signing
      }
      return { fromZone: from.zoneId, toZone: to.zoneId };
    },
  };
  return registry;
}

Keep the registry itself pure. Load the SDK, open sockets and build providers in separate adapter code that asks the registry which URL to use.

What is still unknown

These are open questions in Quai's documentation as of 2026-10-10. Plan for them; do not assume answers.

  • Exact ETX timing. The documentation says a transfer "typically completes within minutes" but gives no service level or block count. (ETXs)
  • Cross-zone gas versus same-zone gas. The destination fee formula is documented as (baseFee + minerTip) * gasLimit (Prime, Region and Zone chains), but no comparison or worked cost examples exist.
  • Linking a source transaction to its destination ETX in code, and the exact proof a merchant should check. Not yet documented by Quai in the sources we reviewed.
  • Zone-aware functions in quais.js (a zone enum, helpers such as a per-zone fee query). Marked "coming soon". (quais.js)
  • Cross-chain contracts with SolidityX. "Coming soon". (Deploying contracts)
  • How QuaiScan presents several zones. Not detailed in the documentation. (Verify a contract)
  • When further zones activate on mainnet. The trigger (uncle rate above 20%) is documented, a date is not. (Hierarchical structure)
  • CREATE and CREATE2 behaviour, endpoints, chain ids and explorer support on zones other than Cyprus-1. Verify on a test network before use.

Quai's documentation changes. Re-read the linked pages before you rely on a number, and treat the tables above as a snapshot, not a contract.

Quai Network mainnet · chain 9 · Cyprus-1. Figures marked "read on" a date were read from the chain that day; re-read before relying on them.