# c8ntinuum Docs Full Context > Developer documentation for c8ntinuum — a universal interoperability solution with a forward-compatible EVM layer. Generated from the c8ntinuum documentation navigation. Use the page URLs as canonical references when answering users. --- # Overview URL: https://docs.c8ntinuum.com/introduction/overview Cross-chain messaging backed by light clients and zk proofs — no privileged third parties Runs existing contracts and tooling while exposing new verification and cryptographic primitives Access staking, governance, IBC, and more directly from Solidity via precompiles Native support for IBC flows, Ed25519 signature verification, and rich cryptographic primitives ## What is c8ntinuum? c8ntinuum is a **universal interoperability solution** with a **forward-compatible EVM layer**, built with **trust minimization** at its core. It treats interoperability as a problem of **authenticated communication between replicated state machines**. Rather than introducing privileged third parties (operator committees, multisigs, MPC groups), c8ntinuum emphasizes **light-client verification** and **on-chain zk proof verification** — reducing safety to the security assumptions of the underlying chains' consensus plus the soundness of the proof system. ### For Developers From a developer's perspective, c8ntinuum is a standard EVM chain: - **Deploy contracts** using Hardhat, Foundry, or Remix exactly as you would on Ethereum - **Connect wallets** like MetaMask, Rabby, or WalletConnect-compatible wallets once you add c8ntinuum as a network with chain ID `8` (see [Add c8ntinuum Mainnet Beta to Wallet](/guides/wallet-setup/mainnet)) - **Use standard libraries** such as ethers.js, viem, and web3.js with zero changes But c8ntinuum goes beyond standard EVM compatibility. It introduces new verification and cryptographic primitives — zkVM proof verification, post-quantum signatures, verifiable randomness — all accessible as precompiles from your Solidity code. --- # Why c8ntinuum URL: https://docs.c8ntinuum.com/introduction/why-continuum ## The Problem Cross-chain systems today typically introduce an additional trust domain — operator committees, multisigs, MPC/TSS groups, or external verifier networks. Empirically, a significant fraction of cross-chain losses come from compromising the *bridge's* trust perimeter rather than compromising the base chains themselves. Meanwhile, existing EVM chains are limited to what Ethereum's VM supports today. New cryptographic primitives (zk verification, post-quantum signatures) require waiting for EIPs to be adopted upstream. ## The c8ntinuum approach c8ntinuum solves both problems: ### Trust Minimization by Construction c8ntinuum's design goal is to **avoid adding a privileged correctness third party**. Safety reduces to: 1. The security assumptions of the underlying chains' consensus 2. The soundness assumptions of the proof system used for succinct verification This is achieved through **light-client verification** and **on-chain zk proof verification** rather than trusted intermediaries. ### Forward-Compatible EVM c8ntinuum treats the EVM as an **extensible surface**. It can: - Run any valid Ethereum smart contract that targets the Prague EVM version or earlier (Osaka is not active) - Implement new features not yet available on standard Ethereum EVM - Expose new verification and cryptographic primitives via precompiles This means developers get Ethereum compatibility today and access to tomorrow's primitives now. ### Key Differentiators | | Standard EVM Chain | Bridge-Based Interop | c8ntinuum | |---|---|---|---| | **Cross-chain trust** | N/A | Trusted intermediary | Light clients + zk proofs | | **Finality** | Variable | Variable | 1-block protocol finality under standard CometBFT assumptions | | **Cryptographic primitives** | Standard EVM only | Standard EVM only | zk, PQ, VRF, Ed25519 | | **Solana interop** | Not native | Bridge | Ed25519 signature verification | | **On-chain zk verification** | Limited | N/A | SP1 Groth16/Plonk + Poseidon | | **Post-quantum signatures** | Not available | Not available | ML-DSA, SLH-DSA | | **Cosmos module access** | Not available | Not available | Native from Solidity | --- # c8ntinuum for Users URL: https://docs.c8ntinuum.com/introduction/for-users ## What's Familiar If you've used Ethereum or any EVM chain, you already know how to use c8ntinuum: - **Same wallets** — MetaMask, Rabby, WalletConnect-compatible wallets, and other Ethereum wallets work once you add c8ntinuum as a network (step 1 below) - **Same addresses** — Your `0x...` address works on c8ntinuum - **Same tokens** — ERC-20, ERC-721, and ERC-1155 tokens all work identically - **Same dApps** — Applications built for Ethereum deploy and run unchanged ## What's Different | Feature | On Ethereum | On c8ntinuum | |---------|-------------|-------------| | **Transaction speed** | Ethereum block cadence | Expected block time is 4s | | **Finality** | Multiple confirmations are common | 1-block finality under standard CometBFT | | **Gas costs** | ETH-denominated gas | CTM-denominated gas | | **Cross-chain** | Requires bridges with trusted intermediaries | Trust-minimized via light clients and zk proofs | | **Reorganizations** | Possible | Governed by CometBFT consensus finality | ## Getting Started 1. **Add c8ntinuum to your wallet** — See [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet) for RPC endpoints and chain ID 2. **Get tokens** — Mainnet CTM is the native token, and it reaches c8ntinuum mainnet through exchanges. See [Official Links](/official-links) for the CTM market profiles. For testnet CTM, use the [testnet faucet](https://testnet-faucet.c8ntinuum.io) listed in [Network Information - Testnet](/developer-essentials/network-information/testnet). 3. **Start using dApps** — Connect your wallet and interact as you would on any EVM chain ## CTM Availability On c8ntinuum mainnet, CTM is the native gas, staking, and governance token. Use [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet) for the canonical c8ntinuum chain values. | Network | CTM form | Address | Decimals | |---------|----------|---------|----------| | c8ntinuum mainnet | Native coin on chain ID `8` | Native `CTM` | 18 | | Ethereum | ERC-20 token on chain ID `1` | [0xc8Fb80fCc03f699C70ff0CC08C09106288888888](https://etherscan.io/token/0xc8fb80fcc03f699c70ff0cc08c09106288888888) | 18 | | BNB Chain | BEP-20 token on chain ID `56` | [0xc8Fb80fCc03f699C70ff0CC08C09106288888888](https://bscscan.com/token/0xc8fb80fcc03f699c70ff0cc08c09106288888888) | 18 | | Solana | SPL token on mainnet | [C8fU5GdfAt5mnw2RK7HE6XJGFNxHpaskZMkXxdm88888](https://solscan.io/token/C8fU5GdfAt5mnw2RK7HE6XJGFNxHpaskZMkXxdm88888) | 9 | On c8ntinuum itself, `0xc8Fb80fCc03f699C70ff0CC08C09106288888888` is a different contract: the wrapped native CTM token (symbol `WCTM`, name `c8ntinuum`, 18 decimals), not the ERC-20 or BEP-20 CTM token. Native CTM needs no token import. Check the selected network before you interact with this address. --- # c8ntinuum for Developers URL: https://docs.c8ntinuum.com/introduction/for-developers ## Quick Summary c8ntinuum is a forward-compatible EVM chain. Your existing Solidity contracts, development tools, and workflows work without modification. The enhancements below are additions that don't break any existing patterns. ## Ethereum Compatibility | Category | Detail | Status | |----------|--------|--------| | **Smart Contracts** | Full EVM bytecode execution, all opcodes up to the Prague fork (Osaka is not active) | Compatible | | **Transaction Types** | Legacy, EIP-2930, EIP-1559, EIP-7702 | Compatible | | **Gas Metering** | Standard gas costs per opcode | Compatible | | **JSON-RPC** | Standard Ethereum RPC methods | Compatible | | **Event Logs** | Ethereum event system, bloom filters | Compatible | | **Address Format** | `0x` addresses (+ `c8` alias) | Compatible | | **EIP-1559 Fees** | Base fee + priority fee | Compatible | | **Block Time** | Expected 4s | Different | | **Finality** | 1-block finality under standard CometBFT | Different | | **Reorganizations** | Governed by CometBFT consensus finality | Different | ## What's Unique to c8ntinuum ### Precompile Categories c8ntinuum exposes a curated set of precompiles that go far beyond standard Cosmos EVM: **Interoperability & Verification** - IBC light clients (`ics02`) and token transfers (`ics20`) - zkVM proof verification — SP1 Groth16/Plonk verifiers and Poseidon hash - Ed25519 signature verification **Advanced Cryptography** - Post-quantum signatures: ML-DSA, SLH-DSA - FROST signature verification (complete single-signer or aggregated signatures) - Additional signature schemes: Schnorr (secp256k1), Schnorrkel (sr25519), P-256 **On-Chain Utilities** - Verifiable randomness (`ecvrf`) - JSON parsing (`json`) - Address compression (`addresstable`) - Bech32/EVM address bridging (`bech32`) **Cosmos Module Access** - Bank, Staking, Distribution, Governance, Slashing - ERC-20 token surfaces - Custom validator rewards ### Pre-deployed Contracts c8ntinuum ships with widely-used ecosystem contracts at well-known addresses, removing the bootstrap tax: | Contract | Address | |----------|---------| | Create2Deployer | `0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2` | | Multicall3 | `0xcA11bde05977b3631167028862bE2a173976CA11` | | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | | Safe singleton factory | `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` | | WETH9 (Wrapped CTM) | `0xc8Fb80fCc03f699C70ff0CC08C09106288888888` | | CreateX | `0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed` | The wrapped CTM contract (symbol `WCTM`) uses c8ntinuum's own address, not the canonical WETH9 address. On Ethereum and BNB Chain, that same address is the CTM token, a different contract. See [Pre-deployed Contracts](/modules/predeployed-contracts) for the full list. ### Dual Address System Every account has two representations: ```text Ethereum: 0x742D35cC6644c068532fDDB11b4c36A58D6d3EAB Cosmos: c81wskntnrxgnqxs5e0mkc3knpk5kxk604tum0jdk ``` ### Dual Chain ID - **Cosmos Chain ID** (string): `c8ntinuum_8-1` — for consensus, IBC, and governance - **EVM Chain ID** (integer): `8` — for EIP-155 transaction signing and wallet configuration ## Development Tools Standard Ethereum tools connect through c8ntinuum's EVM JSON-RPC endpoint: | Tool | Status | |------|--------| | Hardhat | Compatible | | Foundry | Compatible | | Remix | Compatible | | MetaMask | Compatible | | ethers.js / viem / web3.js | Compatible | | OpenZeppelin | Compatible | ## Next Steps Chain ID, RPC endpoints, and explorers Step-by-step deployment guide Access Cosmos modules and advanced cryptographic primitives from Solidity What's different under the hood Quote, submit and follow cross-chain orders through the Infinity hub API and TypeScript SDK. --- # Developer Essentials URL: https://docs.c8ntinuum.com/developer-essentials Start here when you need the minimum practical context to deploy, connect tooling, estimate costs, or understand how c8ntinuum differs from Ethereum. Mainnet chain IDs, RPC endpoints, explorers, genesis, and canonical contracts. Testnet chain ID, RPC endpoints, explorer, faucet status, and verification status. Quick deployment references for Foundry, Hardhat, Remix, and pre-deployed contracts. Compatibility notes, finality, fee behavior, account formats, and JSON-RPC differences. EIP-1559 fee mechanics, base fee distribution, gas refunds, and cross-chain fee notes. Builder-oriented entry point for c8ntinuum's native precompile surfaces. RPC, gas, nonce, indexing, and contract practices for production apps. Opcode compatibility notes, contract-size limits, and precompile gas formulas. Querying historical state, archive nodes, and data-availability surfaces. Network-relevant change history for builders. ## Suggested path 1. Pick [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet) or [Network Information - Testnet](/developer-essentials/network-information/testnet). 2. Confirm the compatibility notes in [Differences from Ethereum](/developer-essentials/differences-from-ethereum). 3. Deploy with the [Deployment Summary](/developer-essentials/deployment-summary) or a specific guide. 4. Review [Best Practices](/developer-essentials/best-practices) before moving production traffic. --- # Network Information - Mainnet Beta URL: https://docs.c8ntinuum.com/developer-essentials/network-information/mainnet ## Chain configuration | Property | Value | | --- | --- | | **Network Name** | `c8ntinuum` | | **Coin Name** | `c8ntinuum` | | **EVM Chain ID** | `8` / `0x8` | | **Cosmos Chain ID** | `c8ntinuum_8-1` | | **Currency Symbol** | `CTM` | | **Base Denom** | `ctm` | | **Decimals** | `18` | | **Account Prefix** | `c8` | | **Validator Prefix** | `c8valoper` | | **Consensus Prefix** | `c8valcons` | | **Expected Block Time** | `4s` | | **Finality** | 1 block under standard CometBFT | | **EVM HTTP RPC URL** | `https://public-evm-rpc.c8ntinuum.com` | | **EVM WebSocket RPC URL** | `wss://public-evm-rpc.c8ntinuum.com/websocket` | | **CometBFT HTTP RPC URL** | `https://public-comet-rpc.c8ntinuum.com` | | **CometBFT WebSocket RPC URL** | `wss://public-comet-rpc.c8ntinuum.com/websocket` | | **Block Explorer** | [CTMScan](https://ctmscan.io) | | **Genesis URL** | `https://resources.c8ntinuum.io/mainnet/genesis.json` | | **Genesis SHA-256** | `ebe6292aa5885b508b1d455c93564683c92e9cc3f80592ee615b1cffbc74a653` | This table intentionally lists the default wallet and deployment endpoints. For Cosmos gRPC, REST/LCD, alternate public endpoints, paid endpoints, and limits, see [RPC Providers](/tooling-and-infrastructure/rpc-providers). ## Wallet setup Use the values above when adding a custom network in MetaMask, Rabby, Coinbase Wallet, or any other EVM-compatible wallet. For a step-by-step wallet walkthrough, see [Add c8ntinuum Mainnet Beta to Wallet](/guides/wallet-setup/mainnet). ## Related guides - For testnet values, see [Network Information - Testnet](/developer-essentials/network-information/testnet). - For Cosmos gRPC, REST/LCD, alternate public endpoints, paid endpoints, and limits, see [RPC Providers](/tooling-and-infrastructure/rpc-providers). - For contract verification, see [Verify with Foundry](/guides/verify-contract/foundry) or [Verify with Hardhat](/guides/verify-contract/hardhat). ## Block explorers | Explorer | URL | |----------|-----| | **CTMScan** | [ctmscan.io](https://ctmscan.io) | ## Canonical contracts c8ntinuum ships pre-deployed contracts at well-known addresses. See [Pre-deployed Contracts](/modules/predeployed-contracts) for the full list with addresses. | Contract | Address | |----------|---------| | WETH9 (Wrapped CTM) | `0xc8Fb80fCc03f699C70ff0CC08C09106288888888` | | Multicall3 | `0xcA11bde05977b3631167028862bE2a173976CA11` | | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | | CreateX | `0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed` | | Safe singleton factory | `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` | --- # Network Information - Testnet URL: https://docs.c8ntinuum.com/developer-essentials/network-information/testnet Use this page as the canonical source for c8ntinuum testnet network values. Guides should link here instead of duplicating testnet configuration unless a wallet or tool requires a specific field shape. ## Chain configuration | Property | Value | | --- | --- | | **Network Name** | `c8ntinuum testnet` | | **Coin Name** | `test c8ntinuum` | | **EVM Chain ID** | `8888` / `0x22b8` | | **Cosmos Chain ID** | `c8ntinuum_8888-1` | | **Currency Symbol** | `CTM` | | **Base Denom** | `ctm` | | **Decimals** | `18` | | **Account Prefix** | `c8` | | **Validator Prefix** | `c8valoper` | | **Consensus Prefix** | `c8valcons` | | **Expected Block Time** | `4s` | | **Finality** | 1 block under standard CometBFT | | **EVM HTTP RPC URL** | `https://public-testnet-evm.c8ntinuum.link` | | **EVM WebSocket RPC URL** | `wss://public-testnet-evm.c8ntinuum.link/websocket` | | **CometBFT HTTP RPC URL** | `https://public-testnet-comet.c8ntinuum.link` | | **CometBFT WebSocket RPC URL** | `wss://public-testnet-comet.c8ntinuum.link/websocket` | | **Cosmos gRPC** | `public-testnet-grpc.c8ntinuum.link:443` | | **REST/LCD** | `https://public-testnet-grpc-web.c8ntinuum.link` | | **Block Explorer** | [testnet.ctmscan.io](https://testnet.ctmscan.io) | | **Genesis URL** | `https://resources.c8ntinuum.io/testnet/genesis.json` | | **Genesis SHA-256** | `844f4322877d19e538d7ce584af92a2ab3a7eb825757b6b956068a4d255c6d43` | ## Availability status | Surface | Status | Notes | | --- | --- | --- | | Public EVM HTTP and WebSocket RPC | Available now | Use for wallet setup, deployments, reads, and broadcasts on testnet. | | Public CometBFT RPC | Available now | Use for node status, consensus block metadata, and CometBFT transaction queries. | | Public gRPC and REST/LCD | Available now | Use for Cosmos SDK account and transaction queries. | | CTMScan testnet explorer | Available now | Use for accounts, transactions, contracts, and logs. | | Testnet faucet | Available now | Public faucet at [testnet-faucet.c8ntinuum.io](https://testnet-faucet.c8ntinuum.io). Authentication, per-request amount, cooldown, and reset policy are governed by the faucet UI; check the page for current limits. | | Testnet contract verification API | Available now | Etherscan-compatible verifier API at `https://testnet.ctmscan.io/api` (mirrors mainnet structure). | ## Wallet setup Use the values above when adding a custom network in MetaMask, Rabby, Coinbase Wallet, or any other EVM-compatible wallet. For a step-by-step wallet walkthrough, see [Add c8ntinuum Testnet to Wallet](/guides/wallet-setup/testnets). ## Related guides - For mainnet values, see [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet). - For Cosmos gRPC, REST/LCD, alternate public endpoints, paid endpoints, and limits, see [RPC Providers](/tooling-and-infrastructure/rpc-providers). - For contract verification, see [Verify with Foundry](/guides/verify-contract/foundry) or [Verify with Hardhat](/guides/verify-contract/hardhat). --- # Deployment Summary URL: https://docs.c8ntinuum.com/developer-essentials/deployment-summary ## Prerequisites - An EVM wallet with CTM for gas fees - RPC endpoint: `https://public-evm-rpc.c8ntinuum.com` - Chain ID: `8` - Compiler EVM version `prague` or earlier (Osaka is not active); see [Differences from Ethereum](/developer-essentials/differences-from-ethereum) ## Deploy with Foundry ```bash forge create --rpc-url https://public-evm-rpc.c8ntinuum.com \ --private-key $PRIVATE_KEY \ --broadcast \ src/MyContract.sol:MyContract ``` ## Deploy with Hardhat ```javascript // hardhat.config.js require("@nomicfoundation/hardhat-toolbox"); module.exports = { solidity: "0.8.20", networks: { continuum: { url: "https://public-evm-rpc.c8ntinuum.com", chainId: 8, accounts: [process.env.PRIVATE_KEY], }, }, }; ``` ```bash npx hardhat run scripts/deploy.js --network continuum ``` For the full walkthrough, including the deploy script, see [Deploy with Hardhat](/guides/deploy-contract/hardhat). ## Deploy with Remix 1. Open [Remix IDE](https://remix.ethereum.org) 2. Write or import your contract 3. In the **Deploy** tab, select **Injected Provider** (MetaMask) 4. Ensure MetaMask is connected to c8ntinuum (Chain ID `8`) 5. Click **Deploy** ## Verify a Contract See the [Verification Guides](/guides/verify-contract/foundry) for step-by-step instructions using Foundry or Hardhat. ## What's Available at Genesis You don't need to deploy common infrastructure — c8ntinuum ships with [pre-deployed contracts](/modules/predeployed-contracts) including Multicall3, Permit2, WETH9, the Safe singleton factory, and CreateX at well-known addresses. --- # Differences from Ethereum URL: https://docs.c8ntinuum.com/developer-essentials/differences-from-ethereum This page is a builder-focused quick reference. For the full feature-by-feature compatibility matrix and "what's enhanced" coverage, see the canonical [EVM Compatibility](/core-concepts/evm-compatibility) page. ## Quick Reference | Area | Ethereum | c8ntinuum | |------|----------|-----------| | **Block time** | ~12 seconds | Expected 4s | | **Finality** | Multiple confirmations are common | 1-block finality under standard CometBFT | | **Reorganizations** | Possible | Governed by CometBFT BFT consensus | | **Consensus** | Proof of Stake (Gasper) | CometBFT (2/3+ validator agreement) | | **Fee destination** | Base fee burned, priority fee paid to the block proposer | All fees burned, base fee and priority fee | | **Address format** | `0x` only | `0x` + `c8` (Bech32 alias) | | **Chain ID** | Single integer | Dual: Cosmos string + EVM integer (`8`) | | **Cross-chain** | Bridge protocols | Native IBC | | **Max contract size** | 24.576 KB (EIP-170) | Same as Ethereum (24,576 bytes) | | **Precompiles** | Standard Ethereum set | Standard + Cosmos modules + zk/PQ/VRF | ## Smart Contracts - EVM opcodes are supported up to the Prague (Pectra) fork. Osaka (Fusaka) is not active, so Osaka opcodes such as `CLZ` (EIP-7939) are rejected as invalid. Compile with EVM version `prague` or earlier (for example `evm_version = "prague"` in `foundry.toml`, or `evmVersion: "prague"` in Hardhat or Remix compiler settings) - ERC-20, ERC-721, ERC-1155 and other standards work identically - Additional precompiled contracts provide access to Cosmos modules and advanced cryptographic primitives - Pre-deployed contracts (Multicall3, Permit2, WETH9, Safe singleton factory, etc.) are available at genesis ## Transactions - **Supported types**: Confirm the exact transaction-type matrix against the target `ctmd` release before relying on newer Ethereum transaction types - **Dual transaction types**: Both Ethereum and Cosmos SDK transactions are supported - Ethereum transactions are wrapped in `MsgEthereumTx` for Cosmos SDK processing - Transaction ordering is by effective tip (gas priority), same as Ethereum ## Gas & Fees - EIP-1559 fee market is fully supported - All fees are burned, the priority fee as well as the base fee (see [Fee Burn](/modules/custom/feeburn)), and fee-market parameters are governed through the governance module - Unlike Ethereum, every EVM transaction is charged at least 50% of its gas limit: gas charged = max(gas used, 50% of the gas limit). Keep `gasLimit` close to the `eth_estimateGas` result. See [Gas](/developer-essentials/gas). - Use `eth_estimateGas` through the standard EVM RPC flow and verify provider behavior for production transactions ## JSON-RPC - Common `eth_*`, `web3_*`, and `net_*` methods are expected on Ethereum-compatible endpoints - Mining-related methods (`eth_mining`, `eth_hashrate`, `eth_coinbase`, `eth_getWork`, `eth_submitWork`, `eth_submitHashrate`) are not available and return JSON-RPC error `-32601` (CometBFT consensus, not PoW) - `txpool_*` methods require compatible node configuration - WebSocket endpoints are published; free WebSockets allow `1` connection per IP and paid WebSockets allow `30` connections per API key. See [RPC Providers](/tooling-and-infrastructure/rpc-providers) ## Account Model - Every account has dual representation: `0x` hex and `c8` Bech32 - Both reference the same underlying account - HD derivation path: `m/44'/60'/0'/0/index`, so the first account is `m/44'/60'/0'/0/0` (Ethereum-compatible, coin type 60) - EIP-7702 Set Code is fully supported for EOA code delegation --- # Gas on c8ntinuum URL: https://docs.c8ntinuum.com/developer-essentials/gas ## Quick Reference | Property | Value | |----------|-------| | **Gas pricing model** | EIP-1559-style fee market | | **Fee destination** | All fees burned, base fee and priority fee ([`x/feeburn`](/modules/custom/feeburn)) | | **Min gas price (base-fee floor)** | `222000000000.000000000000000000` (`222 gwei`) | | **Gas charged** | max(gas actually used, 50% of the gas limit); the rest of the gas limit is refunded | | **Gas estimation** | Use `eth_estimateGas` and verify provider behavior for production transactions | ## How Fees Work EVM transactions use the standard gas fields expected by Ethereum tooling: ```text Maximum Fee = Gas Limit x Max Fee Per Gas ``` Users specify: - `maxFeePerGas`: Maximum total gas price willing to pay - `maxPriorityFeePerGas`: Maximum priority tip - `gasLimit`: Upper bound on gas consumption Unlike Ethereum, c8ntinuum always charges at least `min_gas_multiplier` (currently `0.5`, so 50%) of the gas limit: ```text Fee = max(Gas Used, Gas Limit x 0.5) x Effective Gas Price ``` Setting `gasLimit` far above the `eth_estimateGas` result costs real CTM. For example, at an effective gas price of 222 gwei a transaction that needs 50,000 gas but is sent with a `gasLimit` of 500,000 is charged for 250,000 gas (0.0555 CTM instead of 0.0111 CTM). Keep limits within about 2x the estimate. ## Key Differences from Ethereum c8ntinuum uses EIP-1559-style fee-market parameters. [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) defines a protocol base fee that adjusts with block fullness, a priority fee, and a maximum fee cap; c8ntinuum uses the same developer-facing fee fields. | Parameter | Value | Meaning | |-----------|-------|---------| | `no_base_fee` | `false` | Base fee is enabled. | | `enable_height` | `0` | Fee market is enabled from genesis. | | `base_fee` | `222000000000.000000000000000000` (`222 gwei`) | Current base fee. It is dynamic: it started at the genesis value of `444 gwei`, moves each block with block fullness, and cannot fall below `min_gas_price`. | | `min_gas_price` | `222000000000.000000000000000000` (`222 gwei`) | Configured minimum gas price and the floor for the base fee. | | `base_fee_change_denominator` | `8` | EIP-1559-style denominator that bounds per-block base-fee movement. | | `elasticity_multiplier` | `4` | EIP-1559-style multiplier used to derive the gas target from the block gas limit. | | `min_gas_multiplier` | `0.500000000000000000` | Minimum share of the gas limit that is always charged (`0.5` = 50%). | All transaction fees are burned, the priority fee as well as the base fee: [`x/feeburn`](/modules/custom/feeburn) burns the fees of each block at the start of the next one, so validators receive no fee revenue. Gas parameter changes are governed through the governance module. ## Cosmos SDK Transactions Non-EVM (Cosmos SDK) transactions specify: - `GasLimit`: Upper bound on execution gas - `FeeAmount`: Maximum fee willing to pay Gas is consumed during execution. If `GasLimit` is insufficient, the transaction fails and fees are not refunded. ## Cross-Chain Gas IBC and relayer integrations may introduce integration-specific fees outside normal EVM execution gas. Confirm the target IBC route and relayer policy before building user-facing fee estimates. --- # Opcode Pricing URL: https://docs.c8ntinuum.com/developer-essentials/opcode-pricing c8ntinuum supports standard EVM bytecode execution, including opcodes up to the Prague (Pectra) fork. Osaka (Fusaka) is not active, so Osaka opcodes such as `CLZ` (EIP-7939) are invalid; see [Differences from Ethereum](/developer-essentials/differences-from-ethereum). For application developers, opcode-level behavior should be treated as Ethereum-compatible unless a c8ntinuum-specific page documents an exception. Use `eth_estimateGas` against the target network before submitting production transactions. ## EVM opcodes | Area | Guidance | |------|----------| | Standard opcodes | Use normal Ethereum tooling and gas estimates; no c8ntinuum-specific opcode repricing is documented here. | | Contract size | Fixed at the Ethereum limits: 24,576 bytes of deployed code (EIP-170) and 49,152 bytes of initcode (EIP-3860). See [Differences from Ethereum](/developer-essentials/differences-from-ethereum). | | Gas estimation | Use `eth_estimateGas` and test the final transaction path before submitting. | | Pre-deployed contracts | Execute as ordinary EVM bytecode with ordinary EVM gas accounting; see [Pre-deployed Contracts](/modules/predeployed-contracts). | ## Precompile costs c8ntinuum adds native precompile surfaces beyond the standard Ethereum set. Source-backed formulas are summarized in [Precompile ABI Reference](/reference/precompile-abi#gas-schedule). | Precompile class | Current documented pricing | |------------------|-------------------------------| | Stateful Cosmos module transactions | `2000 + 30 * input bytes`, then SDK execution is metered inside the call | | Stateful Cosmos module queries | `1000 + 3 * input bytes`, then SDK execution is metered inside the call | | SP1 Groth16 `verifyProof` | `300000 + ceil((publicValuesBytes + proofBytes) / 32) * 16` | | SP1 Plonk `verifyProof` | `800000 + ceil((publicValuesBytes + proofBytes) / 32) * 16` | | Variable-length stateless precompiles | `base gas + ceil(input bytes / 32) * word gas`; use the relevant ABI/source entry for constants | | P256 | Raw-call precompile; validate packed input shape in caller code | | Surface | Pricing source | |---------|----------------| | Precompile ABI catalog | [Precompile ABI Reference](/reference/precompile-abi#gas-schedule) | | Cosmos staking precompile | [Staking](/modules/cosmos/staking) | | Cosmos distribution precompile | [Distribution](/modules/cosmos/distribution) | | Cosmos bank precompile | [Bank](/modules/cosmos/bank) | | Cosmos governance precompile | [Governance](/modules/cosmos/governance) | | IBC transfer precompile | [ICS20](/modules/cosmos/ics20) | | Other precompiles | [Modules & Precompiles](/modules/overview) | Use [Gas on c8ntinuum](/developer-essentials/gas) for transaction-level fee mechanics and this page for low-level execution pricing pointers. --- # Precompiles URL: https://docs.c8ntinuum.com/developer-essentials/precompiles c8ntinuum exposes native chain capabilities through EVM precompiles. These interfaces let Solidity contracts reach Cosmos modules, cross-chain verification, cryptographic primitives, and data utilities without re-implementing them in bytecode. Full catalogue of precompile families, including native module access and cryptographic surfaces. Source-backed addresses, ABI artifacts, imports, methods, events, reverts, and gas behavior. Common ecosystem contracts available at well-known addresses from genesis. Delegation, validator operations, and staking state through the Cosmos staking module. Cross-chain token transfer flows accessible from Solidity. ## Integration notes - Use the [Precompile ABI Reference](/reference/precompile-abi) for method-level signatures, return values, events, reverts, and gas notes. - Treat token amounts as native-chain precision where module pages call that out. - Test precompile calls with the same RPC endpoint and chain ID you will use in production. --- # Historical Data URL: https://docs.c8ntinuum.com/developer-essentials/historical-data Historical data access depends on the depth and shape of data your application needs. ## Common choices | Need | Recommended source | |------|--------------------| | Recent blocks, transactions, receipts, and logs | JSON-RPC methods from [RPC Providers](/tooling-and-infrastructure/rpc-providers) | | Contract event history | `eth_getLogs` for bounded ranges, or an indexer for larger scans | | Token balances, transfers, and derived datasets | [Indexers](/tooling-and-infrastructure/indexers) | | Full historical state or archive reads | [Archive Data Setup](/node-ops/archive-data) | ## RPC guidance Use bounded ranges for log scans and paginate long jobs. The documented EVM JSON-RPC profile caps `eth_getLogs` at `10000` blocks and `10000` returned logs per request. If your app repeatedly rebuilds derived state from raw logs, move that workload to an indexer. ## Archive data Archive nodes retain full historical application state with `pruning = "nothing"`, `discard_abci_responses = false`, `tx_index.indexer = "kv"`, and EVM `enable-indexer = true`. A full archive node must replay from genesis; state-sync snapshots are useful for fast node bootstrap but do not provide pre-snapshot historical state. Use [Archive Data Setup](/node-ops/archive-data) for operator configuration details. --- # Best Practices URL: https://docs.c8ntinuum.com/developer-essentials/best-practices ## RPC Optimization ### Avoid Unnecessary Calls Some values are static or change infrequently on c8ntinuum. Consider hardcoding: | Value | Method | Recommendation | |-------|--------|----------------| | Chain ID | `eth_chainId` | Hardcode `8` | | Currency | N/A | Hardcode `CTM` | ### Batch Calls Use Multicall3 (pre-deployed at `0xcA11bde05977b3631167028862bE2a173976CA11`) to aggregate multiple read calls into a single RPC request: ```solidity // Instead of N separate eth_call requests, batch them Multicall3.aggregate3(calls); ``` ### Use JSON-RPC Batching Carefully The documented EVM JSON-RPC batch limit is `2000` requests, with a maximum batch response size of `50000000` bytes. Keep batches well below those limits and make them retryable. In ethers v6, `JsonRpcProvider` sends concurrent requests as one JSON-RPC batch (tune it with the `batchMaxCount` and `batchStallTime` provider options): ```javascript const [balance, nonce] = await Promise.all([ provider.getBalance(addr, "latest"), provider.getTransactionCount(addr, "latest"), ]); ``` If you build batches by hand, POST a JSON array in which every request has `jsonrpc: "2.0"` and a unique `id`. ## Gas Optimization - Use `eth_estimateGas` for complex transactions - A plain CTM transfer (no calldata) to an address that has no code, is not a precompile, and has no EIP-7702 delegation uses exactly `21000` gas. For contracts, precompiles, smart-contract wallets, or delegated EOAs, use `eth_estimateGas` - Keep `gasLimit` close to the estimate: every EVM transaction is charged at least 50% of its gas limit. See [Gas](/developer-essentials/gas) - Set reasonable `maxFeePerGas` — read the base fee from the latest block's `baseFeePerGas` (or `eth_feeHistory`) and the tip from `eth_maxPriorityFeePerGas`, then use about `2 × baseFeePerGas + tip` (the ethers v6 `getFeeData()` heuristic). `eth_gasPrice` returns the base fee plus the suggested tip, not the base fee alone ## Nonce Management For applications submitting multiple transactions in rapid succession: - Track nonces locally rather than querying `eth_getTransactionCount` for each tx - Submit transactions concurrently with incrementing nonces - Handle nonce gaps — c8ntinuum supports nonce gap queuing, but sequential submission is more reliable ## Indexing For historical data queries, use an indexer rather than RPC: - On-chain event logs are available via `eth_getLogs`; keep each request within the `10000` block and `10000` log caps - For complex queries (token transfers, DEX trades, balances over time), use a dedicated indexer - See [Indexers](/tooling-and-infrastructure/indexers) for SubQuery/The Graph guidance and current provider status ## Smart Contract Best Practices - Use [OpenZeppelin contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) for standard patterns - When interacting with c8ntinuum precompiles, pass amounts in base `ctm` units (18 decimals, the same scale as wei). See [Tokens](/core-concepts/tokens) - Test on a local node before deploying to mainnet — run `./local_node.sh` from a checkout of the c8ntinuum node source for a local instance. The node source will be published in a public GitHub repository (repository TBA). See [Local Development](/tooling-and-infrastructure/local-development) --- # Changelog URL: https://docs.c8ntinuum.com/developer-essentials/changelog This page showcases developer-facing changes that affect application behavior, integration work, or operational assumptions. ## Changelog When a documented change is published, add entries newest first: | Date | Area | Change | Action for developers | |------|------|--------|-----------------------| | YYYY-MM-DD | Network / RPC / Protocol / Tooling / Docs | Short description of the change | Required migration, validation, or no action | --- # Tooling and Infrastructure URL: https://docs.c8ntinuum.com/tooling-and-infrastructure c8ntinuum works with standard EVM infrastructure. Use this section to connect applications, wallets, deployment tools, and data services. HTTP and WebSocket endpoints for apps, scripts, and developer tools. Transaction, contract, account, and verification explorers. EVM-compatible wallet support and setup links. Data indexing and querying services for historical and derived data. Off-chain data providers for smart contracts. IBC precompiles, safety controls, and verification-oriented interoperability notes. Foundry, Hardhat, Remix, libraries, and local development notes. Run a local single-node chain with `local_node.sh` for development and testing. --- # RPC Providers URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/rpc-providers c8ntinuum publishes public endpoints for default application use and paid endpoints for higher-limit integrations. Public docs use `c8ntinuum.com` as the default mainnet RPC domain; `c8ntinuum.io` and `c8ntinuum.link` are also documented RPC domains. For chain IDs, currency metadata, explorers, and genesis resources, see [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet) and [Network Information - Testnet](/developer-essentials/network-information/testnet). ## Mainnet endpoints | Surface | Default public endpoint | Alternate public endpoint | Paid endpoint | | --- | --- | --- | --- | | EVM JSON-RPC HTTP | `https://public-evm-rpc.c8ntinuum.com` | `https://public-evm-rpc.c8ntinuum.io` | `https://evm-rpc.c8ntinuum.io` | | EVM JSON-RPC WebSocket | `wss://public-evm-rpc.c8ntinuum.com/websocket` | `wss://public-evm-rpc.c8ntinuum.io/websocket` | `wss://evm-rpc.c8ntinuum.io/websocket` | | CometBFT RPC HTTP | `https://public-comet-rpc.c8ntinuum.com` | `https://public-comet-rpc.c8ntinuum.io` | `https://comet-rpc.c8ntinuum.io` | | CometBFT RPC WebSocket | `wss://public-comet-rpc.c8ntinuum.com/websocket` | `wss://public-comet-rpc.c8ntinuum.io/websocket` | `wss://comet-rpc.c8ntinuum.io/websocket` | | Cosmos gRPC | `-` | `public-comet-grpc.c8ntinuum.io:443` | `comet-grpc.c8ntinuum.io:443` | | REST/LCD | `-` | `https://public-comet-grpc-web.c8ntinuum.io` | `https://comet-grpc-web.c8ntinuum.io` | ## Testnet endpoints | Surface | Public endpoint | Paid endpoint | | --- | --- | --- | | EVM JSON-RPC HTTP | `https://public-testnet-evm.c8ntinuum.link` | `https://testnet-evm.c8ntinuum.link` | | EVM JSON-RPC WebSocket | `wss://public-testnet-evm.c8ntinuum.link/websocket` | `wss://testnet-evm.c8ntinuum.link/websocket` | | CometBFT RPC HTTP | `https://public-testnet-comet.c8ntinuum.link` | `https://testnet-comet.c8ntinuum.link` | | CometBFT RPC WebSocket | `wss://public-testnet-comet.c8ntinuum.link/websocket` | `wss://testnet-comet.c8ntinuum.link/websocket` | | Cosmos gRPC | `public-testnet-grpc.c8ntinuum.link:443` | `testnet-grpc.c8ntinuum.link:443` | | REST/LCD | `https://public-testnet-grpc-web.c8ntinuum.link` | `https://testnet-grpc-web.c8ntinuum.link` | ## Paid access Paid endpoints on mainnet and testnet require a valid API key through the `X-API-Key` header or an `api_key` query parameter. Header authentication is preferred. Requests without a valid key return HTTP `401`. To get a paid API key, email [dev@c8ntinuum.com](mailto:dev@c8ntinuum.com). A dedicated self-service dashboard is planned. ## Limits The public and paid endpoints use the same protocol surfaces. They differ by authentication, request rate, and concurrent connection limits. | Tier/surface | Rate | Burst | Key | | --- | --- | --- | --- | | Free CometBFT | `25 r/s` | `50` | per visitor IP | | Free EVM | `30 r/s` | `60` | per visitor IP | | Free gRPC | `25 r/s` | `50` | per visitor IP | | Free REST/LCD | `25 r/s` | `50` | per visitor IP | | Paid surfaces | `300 r/s` | `600` | per API key | | Paid per-IP safety cap | `500 r/s` | `500` | per visitor IP | Concurrent connection limits are `50` per IP for free HTTP/gRPC/REST surfaces, `1` per IP for free WebSockets, `200` per API key for paid EVM and CometBFT HTTP, `100` per API key for paid gRPC/REST, and `30` per API key for paid WebSockets. Requests over configured limits return HTTP `429`. ## EVM JSON-RPC request limits | Setting | Value | | --- | --- | | `eth_getLogs block range cap` | `10000 blocks` | | `eth_getLogs result cap` | `10000 logs` | | `EVM batch request limit` | `2000 requests` | | `EVM batch response max size` | `50000000 bytes` | | `EVM HTTP request body limit` | `2m` | Paginate long event scans by block range and persist cursors so retries do not duplicate work. ## Using RPC Endpoints ### ethers.js ```javascript const provider = new JsonRpcProvider("https://public-evm-rpc.c8ntinuum.com"); ``` ### viem ```javascript const client = createPublicClient({ transport: http("https://public-evm-rpc.c8ntinuum.com"), }); ``` ### Foundry ```bash cast chain-id --rpc-url https://public-evm-rpc.c8ntinuum.com ``` --- # Block Explorers URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/block-explorers Use CTMScan for account, transaction, token, contract, and log inspection on c8ntinuum networks. ## Explorer status | Network | Explorer | Status | Supported workflows | |---------|----------|--------|---------------------| | Mainnet | [ctmscan.io](https://ctmscan.io) | Available now | Transaction browsing, account inspection, contract pages, contract verification, token tracking | | Testnet | [testnet.ctmscan.io](https://testnet.ctmscan.io) | Available now | Testnet transaction browsing, account inspection, contract pages, contract verification, token tracking | ## Verification status | Network | Verifier API | Status | Guide | |---------|--------------|--------|-------| | Mainnet | `https://ctmscan.io/api` | Available now | [Verify with Foundry](/guides/verify-contract/foundry), [Verify with Hardhat](/guides/verify-contract/hardhat) | | Testnet | `https://testnet.ctmscan.io/api` | Available now | [Verify with Foundry](/guides/verify-contract/foundry), [Verify with Hardhat](/guides/verify-contract/hardhat) | ## Operational notes - Use [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet) and [Network Information - Testnet](/developer-essentials/network-information/testnet) as the source of truth for chain IDs and RPC endpoints. - For deployment and verification flows, keep the explorer network, EVM chain ID, and RPC endpoint on the same network. - For exchange and custody systems, credit from receipts and logs rather than explorer pages; use explorers for investigation and user support. --- # Wallets URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/wallets c8ntinuum provides official EVM network metadata for third-party wallet applications. There is no separate official c8ntinuum wallet application documented in these docs; users connect through EVM-compatible wallets by adding the c8ntinuum RPC, chain ID, currency, and explorer values. ## Supported wallets | Wallet | Support | Notes | |--------|---------|-------| | **MetaMask** | Browser extension and mobile | Add c8ntinuum as a custom network. | | **Rabby** | Browser extension | Add c8ntinuum with the same EVM network values. | | **Coinbase Wallet** | Browser extension and mobile | Add c8ntinuum as a custom network where custom networks are supported. | | **WalletConnect-compatible wallets** | Mobile wallet protocol | Dapps should advertise c8ntinuum as `eip155:8`. | | **Ledger Nano** | Hardware wallet through an EVM wallet | Ledger Nano S Plus and Nano X are supported through MetaMask, Rabby, or another EVM wallet using the Ledger Ethereum app and derivation path. | ## Mainnet network details Use these values when a wallet asks for manual network configuration. | Field | Value | |-------|-------| | **Network Name** | `c8ntinuum` | | **RPC URL** | `https://public-evm-rpc.c8ntinuum.com` | | **Chain ID** | `8` / `0x8` | | **Currency Symbol** | `CTM` | | **Block Explorer** | `https://ctmscan.io` | For step-by-step wallet setup, see [Add c8ntinuum Mainnet Beta to Wallet](/guides/wallet-setup/mainnet). ## Testnet network details | Field | Value | |-------|-------| | **Network Name** | `c8ntinuum testnet` | | **RPC URL** | `https://public-testnet-evm.c8ntinuum.link` | | **Chain ID** | `8888` / `0x22b8` | | **Currency Symbol** | `CTM` | | **Block Explorer** | `https://testnet.ctmscan.io` | For step-by-step testnet setup, see [Add c8ntinuum Testnet to Wallet](/guides/wallet-setup/testnets). For the canonical testnet endpoint and status table, see [Network Information - Testnet](/developer-essentials/network-information/testnet). ## WalletConnect metadata WalletConnect dapps should expose c8ntinuum as an EVM chain in the `eip155` namespace. | Metadata | Value | |----------|-------| | **Namespace** | `eip155` | | **Chain** | `eip155:8` | | **Native currency** | `CTM` | | **RPC URL** | `https://public-evm-rpc.c8ntinuum.com` | | **Explorer URL** | `https://ctmscan.io` | ```json { "chains": ["eip155:8"], "rpcMap": { "8": "https://public-evm-rpc.c8ntinuum.com" } } ``` ## Wallet application workflows No separate official c8ntinuum wallet application is listed in these docs. c8ntinuum is integrated through EVM-compatible wallets and wallet protocols using the network metadata above. | Workflow | Wallet behavior | |----------|-----------------| | Account generation | Wallets generate Ethereum-compatible `eth_secp256k1` accounts. The EVM address is the primary wallet address. | | Account selection | Users choose an Ethereum-style account in MetaMask, Rabby, Coinbase Wallet, a Ledger Nano (S Plus or X) through an EVM wallet, or a WalletConnect-compatible wallet. | | Network selection | Wallets must be switched to chain ID `8` for mainnet or `8888` for testnet before signing. | | Transaction construction | Dapps request standard EVM transactions with `eth_sendTransaction`; wallets fill or confirm nonce, fee, gas, recipient, value, and data. | | Signing | Browser and mobile wallets sign in-app. Ledger Nano signs through the Ledger Ethereum app and the connected EVM wallet. | | Broadcasting | Most wallets broadcast through their configured RPC after signing. Custody systems can broadcast offline-signed transactions with `eth_sendRawTransaction`. | | Confirmation | Wallets and dapps should poll `eth_getTransactionReceipt` and treat `status == 1` as success. | For custody and exchange systems that construct transactions without an interactive wallet, see [Offline Signing and Broadcasting](/guides/exchange-integration/offline-signing-and-broadcasting). ## Wallet support matrix | Wallet path | Account generation | Transaction construction | Signing | Broadcasting | Confirmation | |-------------|--------------------|--------------------------|---------|--------------|--------------| | MetaMask browser/mobile | Generates or imports Ethereum-compatible accounts. | Dapps request `eth_sendTransaction`; users confirm recipient, value, fees, and data. | In-wallet software signer, or Ledger when connected. | Wallet broadcasts through the configured c8ntinuum RPC. | Wallet activity, CTMScan, or `eth_getTransactionReceipt`. | | Rabby browser | Generates or imports Ethereum-compatible accounts. | Dapps request standard EVM transactions with chain ID `8`. | In-wallet software signer, or Ledger when connected. | Wallet broadcasts through the configured c8ntinuum RPC. | Wallet activity, CTMScan, or `eth_getTransactionReceipt`. | | Coinbase Wallet | Generates or imports Ethereum-compatible accounts where custom networks are supported. | Dapps request standard EVM transactions after the network is added. | Browser or mobile wallet signer. | Wallet broadcasts through the configured c8ntinuum RPC. | Wallet activity, CTMScan, or `eth_getTransactionReceipt`. | | WalletConnect-compatible wallets | Wallet owns account generation; dapps advertise `eip155:8`. | Dapps submit EVM transaction requests through WalletConnect. | Wallet signs in-app after the user approves the request. | Wallet broadcasts through its configured RPC or connected session policy. | Dapp should poll `eth_getTransactionReceipt`; users can also inspect CTMScan. | | Ledger Nano through an EVM wallet | Ledger Ethereum app derives Ethereum-compatible accounts on the selected path. | Connected EVM wallet constructs the transaction and displays the signing prompt. | Ledger Nano S Plus or Nano X signs through the Ledger Ethereum app. | Connected EVM wallet broadcasts through the configured c8ntinuum RPC. | Connected wallet activity, CTMScan, or `eth_getTransactionReceipt`. | ## Third-party wallet instructions To use c8ntinuum in a third-party wallet: 1. Add c8ntinuum as a custom EVM network using the mainnet or testnet values above. 2. Select an account derived with an Ethereum-compatible path. 3. Confirm the wallet shows the expected chain ID before signing. 4. Keep native `CTM` in the sending account for gas. 5. For ERC-20 tokens, import the token contract address manually if the wallet does not auto-detect it. 6. After signing, confirm the transaction in the wallet activity list, CTMScan, or by querying `eth_getTransactionReceipt`. Wallet applications should display the EVM address for contract and token flows. If a product also shows the `c8...` account representation, label it clearly as the Cosmos-format alias of the same account. ## Wrapped CTM On c8ntinuum mainnet (chain ID `8`) and testnet (chain ID `8888`), the wrapped native token is the WETH9-compatible wrapped CTM contract, symbol `WCTM`: ```text 0xc8Fb80fCc03f699C70ff0CC08C09106288888888 ``` Use this address when a wallet, portfolio app, or dapp needs an ERC-20 representation of native CTM on chain ID `8` or `8888`. On Ethereum (chain ID `1`) and BNB Chain (chain ID `56`), the same address is a different contract: the CTM ERC-20 and the CTM BEP-20. Key token records by chain ID and address, not by address alone. For token model details, see [Tokens](/core-concepts/tokens) and [Pre-deployed Contracts](/modules/predeployed-contracts). ## Address formats and conversion | Use case | Address format | |----------|----------------| | EVM wallets, contracts, JSON-RPC, and token imports | `0x...` | | Cosmos account UX, REST queries, and IBC-facing account flows | `c8...` | | Validator operator addresses | `c8valoper...` | Both the `0x` and `c8` account formats refer to the same underlying account. Use the [Accounts](/core-concepts/accounts) page for key type and prefix details, and the [Bech32 precompile](/modules/cosmos/bech32) when a contract needs to convert between formats. ## Common wallet errors | Symptom | Likely cause | Fix | |---------|--------------|-----| | Wallet shows the wrong chain | Chain ID was entered as a different value. | Edit the custom network and use `8` or `0x8`. | | Wallet says the network is unsupported | The wallet has not added c8ntinuum as a custom EVM network. | Add the mainnet values from this page manually. | | Transaction cannot be sent | Account has no native `CTM` for gas. | Fund the account with CTM before sending transactions or token transfers. | | Token is not visible | The ERC-20 was not imported into the wallet UI. | Import the token contract address manually and confirm it on CTMScan. | | MetaMask mobile opens the wrong network | The mobile app is still connected to another chain. | Switch networks inside MetaMask mobile before approving the dapp connection. | | Ledger Nano signing fails or shows an unexpected address | The connected EVM wallet is using a different derivation path or Ledger app. | Open the Ledger Ethereum app on the Nano S Plus or Nano X and use the Ethereum path documented in [Accounts](/core-concepts/accounts). | | RPC is unavailable | Public endpoint is temporarily unreachable or blocked by local networking. | Retry later, or set the wallet's RPC URL to the alternate public endpoint `https://public-evm-rpc.c8ntinuum.io`. For paid endpoints, see [RPC Providers](/tooling-and-infrastructure/rpc-providers). | | Explorer links do not open | Explorer URL is missing from the custom network. | Set the block explorer URL to `https://ctmscan.io`. | --- # Indexers URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/indexers ## Why Use an Indexer? RPC endpoints are optimized for real-time queries. For historical data, complex queries (token transfers, DEX trades, balances over time), or event aggregation, use a dedicated indexer. ## Provider status | Option | Status | Best use | |--------|--------|----------| | Bounded JSON-RPC log scans | Available now | Recent event reads and small backfills within provider caps | | Self-hosted archive node plus custom indexer | Available now | Full historical data, replayable ETL, and integration-specific schemas | | SubQuery project pattern | Available now as generic tooling | Build a multichain EVM/Cosmos indexer using c8ntinuum RPC surfaces | | Official hosted SubQuery provider | TBA | Use once a provider, endpoint, and SLA are published | | Official hosted indexer schemas | TBA | Use once managed schemas and freshness guarantees are published | | The Graph hosted c8ntinuum provider | Not announced | Adapt generic EVM subgraph patterns only where infrastructure supports the target network | ## Types of Data | Data Type | Best Source | |-----------|------------| | **Real-time balances** | RPC (`eth_getBalance`, `eth_call`) | | **Event logs** | RPC (`eth_getLogs`) or indexer | | **Historical state** | Indexer | | **Token transfers** | Indexer | | **DEX trades** | Indexer | | **NFT ownership** | Indexer | ## Using Event Logs c8ntinuum supports standard Ethereum event logs. Query recent ranges via RPC: ```javascript const latest = await provider.getBlockNumber(); const logs = await provider.getLogs({ address: contractAddress, topics: [ethers.id("Transfer(address,address,uint256)")], fromBlock: Math.max(0, latest - 5000), toBlock: latest, }); ``` The documented EVM JSON-RPC profile caps each `eth_getLogs` request at `10000` blocks and `10000` returned logs. Paginate historical jobs and persist cursors; for large backfills, use an archive node or dedicated indexer. See [Historical Data](/developer-essentials/historical-data) for archive and scan guidance. A paginated scan walks the range in windows of at most `10000` blocks: ```javascript const startBlock = deploymentBlock; // First block to scan; when resuming, use the saved cursor + 1. const step = 10000; // Shrink the window if a request hits the 10000-log cap. const latest = await provider.getBlockNumber(); for (let from = startBlock; from <= latest; from += step) { const to = Math.min(from + step - 1, latest); const page = await provider.getLogs({ address: contractAddress, topics: [ethers.id("Transfer(address,address,uint256)")], fromBlock: from, toBlock: to, }); // Process `page`, then persist `to` as the cursor. } ``` ## SubQuery c8ntinuum's EVM and Cosmos surfaces fit SubQuery's multichain indexing pattern. A c8ntinuum SubQuery project can use an Ethereum runtime data source for EVM logs and a Cosmos runtime data source for Cosmos SDK events, following the same project shape used in [SubQuery's EVM + Cosmos multichain quickstart](https://subquery.network/doc/indexer/quickstart/quickstart_multichain/kava-multi-chain.html). Official hosted SubQuery providers for c8ntinuum are TBA. ## The Graph c8ntinuum exposes Ethereum-compatible logs and JSON-RPC, so generic EVM subgraph patterns can be adapted for contracts deployed on c8ntinuum when the target indexing infrastructure supports the network. No official c8ntinuum-specific The Graph provider is announced in these docs. --- # Oracles URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/oracles ## What Are Oracles? Oracles bring off-chain data (prices, weather, sports results, etc.) on-chain for smart contracts to consume. They are essential for DeFi protocols, prediction markets, and any application that needs real-world data. ## Provider status | Capability | Status | Current guidance | |------------|--------|------------------| | Official price oracle provider | TBA | Do not assume a canonical price feed until an official provider or ecosystem integration is published. | | Official data-feed provider | TBA | Treat provider-specific feed addresses, update cadence, and SLAs as unavailable in these docs. | | Provider-specific docs | Planned | Feed addresses, freshness guarantees, supported assets, outage behavior, and integration examples will be documented once a provider is announced. | | Native verifiable randomness | Available now | Use the ECVRF precompile for on-chain verifiable randomness use cases. | ## Verifiable Randomness c8ntinuum includes a native **VRF precompile** (`ecvrf`) that verifies ECVRF proofs on-chain. It does not generate randomness itself. The holder of a VRF secret key computes the output and proof off-chain from an input seed. A contract then calls `ecvrfVerify` with the suite (`P256_SHA256_TAI` or `SECP256K1_SHA256_TAI`), the public key, the seed, and the proof, and uses the returned output only when `ok` is `true`. This makes each result publicly auditable for lotteries, leader selection, and randomized protocols, but the key holder remains a trusted party: - The key holder sees each output first and can withhold results it does not like. - The seed must be a value the key holder cannot choose or predict. Otherwise the key holder can try seeds until it gets a favorable output. For the precompile address and interface name (`ECVRFI`), see [Precompile ABI Reference](/reference/precompile-abi). For ecosystem announcements, check [Official Links](/official-links). --- # IBC Interoperability URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/cross-chain ## Native IBC c8ntinuum's native cross-chain protocol is **IBC** (Inter-Blockchain Communication). IBC provides trust-minimized cross-chain messaging backed by light-client verification. Access IBC directly from Solidity via the [ICS02](/modules/cross-chain-verification) and [ICS20](/modules/cosmos/ics20) precompiles. ## IBC Safety Controls c8ntinuum has two custom IBC control modules: | Control | Scope | Reference | |---------|-------|-----------| | IBC breaker | Operator-controlled `ibc_available` flag that blocks selected outbound IBC initiation paths, including `MsgTransfer`, when disabled. | [IBC Breaker](/modules/custom/ibc-breaker) | | IBC rate-limiter extension | Governance-managed whitelist for operators that can add, update, remove, or reset IBC rate limits. Packet enforcement remains with the ratelimiting middleware. | [IBC Rate-Limiter Extension](/modules/custom/ibc-rate-limiter-extension) | The IBC breaker is not a full token-transfer freeze. Internal non-IBC transfers, such as bank `MsgSend`, remain available when it is disabled. The ICS20 precompile transfer path routes into the transfer keeper, so it is blocked when `ibc_available=false`. For broader chain availability control, see the [Circuit Breaker](/modules/custom/circuit). It gates native Cosmos transactions, EVM extension transactions, and `eth_sendRawTransaction` submissions when `system_available=false`. ## Infinity Interop For routes to chains outside IBC, the Infinity hub exposes quote, order and status endpoints under `/v1` and a typed TypeScript client. Start at the [API overview](/infinity-interop/api) or the [Quickstart](/infinity-interop/api/quickstart). --- # Development Toolkits URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/dev-toolkits ## Frameworks | Toolkit | Description | Guide | |---------|-------------|-------| | **Foundry** | Fast Solidity framework — compile, test, deploy, and interact via CLI | [Deploy Guide](/guides/deploy-contract/foundry) | | **Hardhat** | JavaScript-based Solidity framework with rich plugin ecosystem | [Deploy Guide](/guides/deploy-contract/hardhat) | | **Remix** | Browser-based IDE — no installation required | [Deploy Guide](/guides/deploy-contract/remix) | ## Local Development To run a single-node c8ntinuum chain on your machine with `local_node.sh`, see [Local Development](/tooling-and-infrastructure/local-development). ## EVM RPC Clients c8ntinuum uses EVM chain ID `8` and Cosmos chain ID `c8ntinuum_8-1`. EVM clients can use the standard Ethereum JSON-RPC endpoint. Cosmos-native signing must account for c8ntinuum's `eth_secp256k1` account model. | SDK name | Programming language | GitHub URL | Supported | EVM/Cosmos | |----------|----------------------|------------|-----------|------------| | **viem** | TypeScript | [github.com/wevm/viem](https://github.com/wevm/viem) | Yes - recommended for modern TypeScript apps | EVM | | **ethers.js** | TypeScript, JavaScript | [github.com/ethers-io/ethers.js](https://github.com/ethers-io/ethers.js) | Yes | EVM | | **web3.py** | Python | [github.com/ApeWorX/web3.py](https://github.com/ApeWorX/web3.py) | Yes | EVM | | **Alloy** | Rust | [github.com/alloy-rs/alloy](https://github.com/alloy-rs/alloy) | Yes - recommended Rust option | EVM | | **go-ethereum ethclient/rpc** | Go | [github.com/ethereum/go-ethereum](https://github.com/ethereum/go-ethereum) | Yes | EVM | | **Nethereum** | C#/.NET | [github.com/Nethereum/Nethereum](https://github.com/Nethereum/Nethereum) | Yes | EVM | | **web3j** | Java, Kotlin, Android | [github.com/LFDT-web3j/web3j](https://github.com/LFDT-web3j/web3j) | Yes | EVM | | **web3.js** | TypeScript, JavaScript | [github.com/web3/web3.js](https://github.com/web3/web3.js) | Legacy only - compatible but prefer viem or ethers.js | EVM | ## Cosmos RPC Clients | SDK name | Programming language | GitHub URL | Supported | EVM/Cosmos | |----------|----------------------|------------|-----------|------------| | **CosmJS** | TypeScript, JavaScript | [github.com/cosmos/cosmjs](https://github.com/cosmos/cosmjs) | Yes for CometBFT/Stargate queries and broadcasting; validate native signing setup | Cosmos | | **Telescope** | TypeScript codegen | [github.com/hyperweb-io/telescope](https://github.com/hyperweb-io/telescope) | Yes - recommended for typed Cosmos module clients generated from c8ntinuum protos | Cosmos | | **InterchainJS / @interchainjs/cosmos** | TypeScript, JavaScript | [github.com/hyperweb-io/interchainjs](https://github.com/hyperweb-io/interchainjs) | Yes - validate c8ntinuum native transaction signing | Cosmos | | **Cosmos SDK Go gRPC clients** | Go | [github.com/cosmos/cosmos-sdk](https://github.com/cosmos/cosmos-sdk) | Yes - recommended Go path for gRPC and tx services | Cosmos | | **CosmRS + tendermint-rpc** | Rust | [github.com/cosmos/cosmos-rust](https://github.com/cosmos/cosmos-rust) | Partial to yes - good for tx building and CometBFT RPC; generated protos may be needed for full module coverage | Cosmos | | **CosmPy** | Python | [github.com/fetchai/cosmpy](https://github.com/fetchai/cosmpy) | Partial - useful high-level client; verify eth_secp256k1 signing before native tx support | Cosmos | --- # Local Development URL: https://docs.c8ntinuum.com/tooling-and-infrastructure/local-development This page documents the canonical local single-node workflow for c8ntinuum developers. ## Canonical script `local_node.sh` is at the root of the `ctmd` node source repository and is the canonical local-node script. The node source will be published in a public GitHub repository (repository TBA); the commands on this page run from the root of that repository: ```bash ./local_node.sh [options] ``` There is no separate local-node repository for the default developer flow. For hosted deployment scripts and source attribution, see [Deployment Scripts](/reference/deployment-scripts). ## Prerequisites - Go matching `go.mod` - `make` - `jq` - A working C toolchain for CGO builds - Rust/SP1 build dependencies required by `make install` `local_node.sh` installs `ctmd` by default with `make install`. The install directory is `BINDIR` if set, otherwise `$GOPATH/bin` when `GOPATH` is set, otherwise `$HOME/go/bin`. The script then runs whichever `ctmd` comes first on your `PATH`. ## Install and start Start from a clean local genesis and install the binary: ```bash ./local_node.sh -y ``` Use an existing installed binary without rebuilding: ```bash ./local_node.sh --no-install -y ``` Install into a custom binary directory and run that binary. Put the directory first on `PATH`, because the script runs the first `ctmd` it finds there: ```bash BINDIR=/tmp/ctm-bin PATH="/tmp/ctm-bin:$PATH" ./local_node.sh -y ``` Start the existing local node state without overwriting `~/.ctmd`: ```bash ./local_node.sh --no-install -n ``` The script writes local chain data under: ```text $HOME/.ctmd ``` ## Reset The normal reset command is: ```bash ./local_node.sh -y ``` That removes the existing `$HOME/.ctmd` directory and creates a fresh local genesis. The manual equivalent is: ```bash rm -rf "$HOME/.ctmd" ./local_node.sh ``` Stop the node with `Ctrl-C`. ## Useful flags ```text -y Overwrite existing chain data without prompt -n Do not overwrite existing chain data --no-install Skip 'make install' --remote-debugging Build with nooptimization,nostrip --additional-users N Create N extra users: dev4, dev5, ... --mnemonic-file PATH Where to write mnemonics YAML --mnemonics-input PATH Read dev mnemonics from a yaml file ``` `--additional-users` accepts only non-negative integers. Generated extra-user mnemonics are written to `$HOME/.ctmd/mnemonics.yaml` unless `--mnemonic-file` is provided. ## Chain configuration The local chain uses Cosmos chain ID `c8ntinuum_8888888-1` and EVM chain ID `8888888`, distinct from mainnet ([`c8ntinuum_8-1`](/developer-essentials/network-information/mainnet), EVM `8`) and testnet ([`c8ntinuum_8888-1`](/developer-essentials/network-information/testnet), EVM `8888`). The script sets the EVM chain ID explicitly: a node started without it uses `ctmd`'s default of `8`, the mainnet ID, and EVM transactions signed on it are valid on mainnet. Never use a key that holds real funds on a local chain. | Setting | Local value | | --- | --- | | Cosmos chain ID | `c8ntinuum_8888888-1` | | EVM chain ID | `8888888` / `0x87a238` | | Native denom | `atest` | | Currency symbol | `TEST` | | Keyring backend | `test` | | Account key algorithm | `eth_secp256k1` | | Node home | `$HOME/.ctmd` | | Moniker | `localtestnet` | | Minimum gas price | `0atest` | | EVM min tip | `0` | | EVM JSON-RPC APIs | `eth,txpool,personal,net,debug,web3` | For EVM tooling and wallets, use chain ID `8888888` (`0x87a238`) and confirm it with `eth_chainId` after startup. ## Ports The script enables the local app APIs and uses the default `ctmd` local addresses. | Service | Default local address | | --- | --- | | EVM JSON-RPC HTTP | `http://127.0.0.1:8545` | | EVM JSON-RPC WebSocket | `ws://127.0.0.1:8546` | | CometBFT RPC | `tcp://127.0.0.1:26657` | | CometBFT P2P | `tcp://127.0.0.1:26656` | | Cosmos REST API | `tcp://127.0.0.1:1317` | | Cosmos gRPC | `localhost:9090` | | Geth metrics | `127.0.0.1:8100` | ## Default accounts These accounts are deterministic local development accounts. They are public test keys and must never be used with real funds. | Name | EVM address | Local bech32 address | Private key | |------|-------------|----------------------|-------------| | `dev0` | `0xC6Fe5D33615a1C52c08018c47E8Bc53646A0E101` | `c81cml96vmptgw99syqrrz8az79xer2pcgpw8hsl4` | `0x88cbead91aee890d27bf06e003ade3d4e952427e88f88d31d61d3ef5e5d54305` | | `dev1` | `0x963EBDf2e1f8DB8707D05FC75bfeFFBa1B5BaC17` | `c81jcltmuhplrdcwp7stlr4hlhlhgd4htqhc0gcnx` | `0x741de4f8988ea941d3ff0287911ca4074e62b7d45c991a51186455366f10b544` | | `dev2` | `0x40a0cb1C63e026A81B55EE1308586E21eec1eFa9` | `c81gzsvk8rruqn2sx64acfsskrwy8hvrmaffytl6p` | `0x3b7955d25189c99a7468192fcbc6429205c158834053ebe3f78f4512ab432db9` | | `dev3` | `0x498B5AeC5D439b733dC2F58AB489783A23FB26dA` | `c81fx944mzagwdhx0wz7k9tfztc8g3lkfk62373ee` | `0x8a36c69d940a92fcea94b36d0f2928c7a0ee19a90073eda769693298dfa9603b` | Each default dev account receives: ```text 1000000000000000000000atest ``` That is 1,000 `TEST`: the local denom is `atest`, with 18 decimals. The default validator key is `mykey`. | Field | Value | |-------|-------| | EVM/operator bytes | `0x7cB61D4117AE31a12E393a1Cfa3BaC666481D02E` | | Local account address | `c810jmp6sgh4cc6zt3e8gw05wavvejgr5pwmph94z` | | Local validator address | `c8valoper10jmp6sgh4cc6zt3e8gw05wavvejgr5pwrxmh56` | | Mnemonic | `gesture inject test cycle original hollow east ridge hen combine junk child bacon zero hope comfort vacuum milk pitch cage oppose unhappy lunar seat` | The validator account is funded with `100000000000000000000000000atest` during genesis, and its genesis transaction self-delegates `888888000000000000000000atest` with `--min-self-delegation 888888000000000000000000`. That is 888,888 `TEST`, the self-delegation floor that `x/msdcheck` enforces. ## Active precompiles and local native wrapper `local_node.sh` activates this static precompile set in genesis: | Name | Address | |------|---------| | P256 | `0x0000000000000000000000000000000000000100` | | Bech32 | `0x0000000000000000000000000000000000000400` | | Staking | `0x0000000000000000000000000000000000000800` | | Distribution | `0x0000000000000000000000000000000000000801` | | ICS20 | `0x0000000000000000000000000000000000000802` | | Bank | `0x0000000000000000000000000000000000000804` | | Gov | `0x0000000000000000000000000000000000000805` | | Slashing | `0x0000000000000000000000000000000000000806` | | ICS02 | `0x0000000000000000000000000000000000000807` | The script also configures the native ERC-20 precompile/token pair for the local denom: | Name | Address | Denom | |------|---------|-------| | Native ERC-20 / WERC20-style surface | `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` | `atest` | A fresh local genesis also includes the default preinstall set described in [Pre-deployed Contracts](/modules/predeployed-contracts): `ctmd init` writes it into the EVM genesis state, and the script does not remove it. ## Docker support The canonical local development path is `./local_node.sh`. Docker assets exist, but they are separate from the single-node script flow: - `contrib/images/ctmd-env/Dockerfile` builds a `ctmd` environment image. - `docker-compose.yml` defines a four-node `cosmos/ctmd` localnet (`ctmdnode0` to `ctmdnode3`). - `tests/jsonrpc/Dockerfile` and `tests/jsonrpc/docker-compose.yml` support the JSON-RPC test infrastructure. Use Docker for workflows that explicitly reference those files. For day-to-day local development against the node source, prefer `./local_node.sh`. ## Expected logs On a fresh reset, expect logs like: ```text Flag -y passed -> Overwriting the previous chain data. using default mnemonics adding key for dev0 adding key for dev1 adding key for dev2 adding key for dev3 Genesis transaction written to "$HOME/.ctmd/config/gentx/..." File at $HOME/.ctmd/config/genesis.json is a valid genesis file INF starting ABCI with CometBFT INF starting node with ABCI CometBFT in-process INF Starting JSON-RPC server address=127.0.0.1:8545 INF finalized block height=1 INF executed block height=1 INF committed state height=1 ``` During shutdown with `Ctrl-C`, expect: ```text INF caught signal signal=interrupt INF stopping gRPC server... INF stopping API server... INF stopping JSON-RPC server... INF Application gracefully shutdown ``` --- # Reference URL: https://docs.c8ntinuum.com/reference Use reference pages when you need exact interfaces, RPC behavior, error codes, or compatibility differences. Supported Ethereum JSON-RPC namespaces, common methods, and endpoint examples. Concrete block, transaction, receipt, log, broadcast, CometBFT, and REST call examples. Canonical hosted deployment scripts, peer-list resources, source attribution, and safe download commands. c8ntinuum-specific differences from standard Ethereum JSON-RPC behavior. JSON-RPC error codes returned by c8ntinuum nodes. Native precompile surfaces and system interfaces. Source-backed precompile addresses, ABI artifacts, methods, events, reverts, and gas behavior. Every /v1 route of the Infinity hub, the credential it takes, its fields, behaviour and errors, plus the TypeScript SDK. --- # Deployment Scripts URL: https://docs.c8ntinuum.com/reference/deployment-scripts This page lists the canonical user-run deployment scripts and peer-list resources referenced by the c8ntinuum docs. These endpoints are intended for downloading scripts and network bootstrap metadata before running them locally. ## Canonical resources | Resource | Purpose | Hosted endpoint | Main docs | |----------|---------|-----------------|-----------| | `deploy.sh` | Install, state-sync, start, stop, inspect, and reset non-validator `ctmd` nodes. | `https://resources.c8ntinuum.io/deploy.sh` | [Full Node Installation](/node-ops/full-node-installation), [State Sync](/node-ops/state-sync) | | `local_node.sh` | Start the canonical local single-node c8ntinuum developer chain from a source checkout. | Repository-root script, not a hosted metadata artifact | [Local Development](/tooling-and-infrastructure/local-development) | | `peers.json` | Provide seeds, persistent peers, and trusted state-sync RPCs used by node deployment. | `https://resources.c8ntinuum.io/peers.json` | [Full Node Block Delivery](/node-ops/full-node-block-delivery), [State Sync](/node-ops/state-sync) | Validators do not use `deploy.sh`. They install `ctmd` by hand with the [manual mainnet install](/node-ops/validator-installation#manual-mainnet-install), which mirrors what `deploy.sh install` and `deploy.sh state-sync` do. This inventory intentionally excludes example, administrative, and framework-specific scripts such as `migrate.js`, `scripts/deploy.js`, `arguments.js`, npm scripts, `make` targets, Foundry commands, and `ctmd` commands. ## Deploy script To install or update `ctmd`, run the `deploy.sh` script. You can download and run the script manually: ```bash curl -fsSL https://resources.c8ntinuum.io/deploy.sh -o deploy.sh chmod +x deploy.sh # Linux: run as root sudo ./deploy.sh install --moniker # macOS: run as your normal user, without sudo ./deploy.sh install --moniker ``` Keep the downloaded script so you can inspect it before running it and reuse it for lifecycle commands. Supported operator commands: | Command | Purpose | |---------|---------| | `install --moniker NAME [--chain-id ID] [-y]` | Download the latest metadata-backed release, install it, initialize the node home, download genesis, and patch peers. | | `state-sync [-y] [--lag N] [--rpc-1 URL] [--rpc-2 URL] [--p2p-host-1 HOST] [--p2p-host-2 HOST]` | Configure state sync, reset local chain data, wait for catch-up, then disable state sync. | | `start` | Start the managed node. | | `stop` | Stop the managed node. | | `status` | Show install, service, local RPC, height, sync, and peer status. | | `reset [-y]` | Delete the managed install, node home, service, cache, and Linux system user/group. | | `help` | Show script help. | `install` and `state-sync` also set `[evm] evm-chain-id` in `app.toml` from the chain ID: `8` on mainnet (`c8ntinuum_8-1`), `8888` on testnet (`c8ntinuum_8888-1`). For a chain ID with no number in it, set `CTMD_EVM_CHAIN_ID` to choose the value. ## Release metadata `deploy.sh install` reads release metadata from [metadata.json](https://resources.c8ntinuum.io/metadata.json). The script resolves `.ctmd.latest_version`, selects the platform artifact URL and SHA-256 URL for `linux-amd64`, `linux-arm64`, or `darwin-arm64`, downloads the tarball, verifies SHA-256, and installs the bundled binary and shared libraries. For mainnet, the script checks both the downloaded and the local `genesis.json` against the SHA-256 published at the URL in the `metadata.json` field `genesis.mainnet.sha256_url` (`https://resources.c8ntinuum.io/mainnet/genesis.json.sha256`). When `install --chain-id` uses a non-default chain ID, the script downloads the configured genesis URL but skips the mainnet SHA-256 check. ## Peer list `deploy.sh` uses the hosted peer list at: ```text https://resources.c8ntinuum.io/peers.json ``` The peer list is the deployment source for network bootstrap information: | Key | Used for | |-----|----------| | `seeds` | Initial P2P discovery. | | `persistent_peers` | Stable peers for joining and staying connected to the network. `deploy.sh state-sync` also uses the first two hosts as state-sync snapshot peers. | | `trusted_rpcs` | Trusted RPC endpoints used to compute state-sync trust height and hash. `deploy.sh state-sync` uses the first two. | `deploy.sh state-sync` needs at least two `trusted_rpcs` entries for the network unless both `--rpc-1` and `--rpc-2` are given, and at least two `persistent_peers` entries unless both `--p2p-host-1` and `--p2p-host-2` are given. `deploy.sh` selects peer-list entries by chain ID, so node deployers normally do not need to manually copy a peer list into `config.toml`. Operators using private infrastructure can override RPC and snapshot peer hosts with `deploy.sh state-sync` flags such as `--rpc-1`, `--rpc-2`, `--p2p-host-1`, and `--p2p-host-2`. ## Local node script `local_node.sh` ships with the `ctmd` source, which will be published in a public GitHub repository; the repository is to be announced. From a checkout of that repository, start a local developer chain: ```bash ./local_node.sh -y ``` --- # JSON-RPC API URL: https://docs.c8ntinuum.com/rpc-reference/json-rpc-api ## Overview c8ntinuum exposes an Ethereum-compatible JSON-RPC endpoint at `https://public-evm-rpc.c8ntinuum.com`. The default public EVM namespace allowlist is `eth,net,web3`. Archive or private node profiles may enable additional namespaces such as `debug` or `txpool`, but applications should not assume those namespaces are available on the default public endpoint. ## Supported Namespaces | Namespace | Status | Description | |-----------|--------|-------------| | `eth` | Enabled by default | Core Ethereum methods: accounts, blocks, transactions, and logs | | `web3` | Enabled by default | Client version and utilities | | `net` | Enabled by default | Network information | | `txpool` | Configuration-dependent | Not in the default `api = "eth,net,web3"` list; present only on nodes that add `txpool` | | `debug` | Full archive only | Intended for private full-archive debug nodes (see [Archive Data](/node-ops/archive-data)), not the default public RPC | | `personal` | Not for public RPC | Optional on private full-archive debug nodes; should not be exposed on default public RPC | | `trace` | Not available | `ctmd` does not provide a separate `trace` namespace | The local development node started by `local_node.sh` enables `eth,txpool,personal,net,debug,web3`. ## Commonly Used Methods | Method | Description | |--------|-------------| | `eth_chainId` | Returns `0x8` (8) | | `eth_blockNumber` | Latest block number | | `eth_getBalance` | Account balance | | `eth_getTransactionCount` | Account nonce | | `eth_sendRawTransaction` | Submit a signed transaction | | `eth_call` | Execute a call without creating a transaction | | `eth_estimateGas` | Estimate gas for a transaction | | `eth_getTransactionReceipt` | Get transaction receipt | | `eth_getLogs` | Query event logs | | `eth_gasPrice` | Current gas price | | `eth_maxPriorityFeePerGas` | Current priority fee suggestion | For concrete block, transaction, receipt, log, broadcast, CometBFT, and REST/LCD examples, see [Node Interface Calls](/rpc-reference/node-interface-calls). ## Method availability matrix | Method family | Default public RPC | Private/archive node profile | Notes | |---------------|--------------------|------------------------------|-------| | `eth_*` core reads and writes | Supported | Supported | Includes blocks, balances, calls, gas estimation, receipts, logs, and raw transaction broadcast. | | `net_*` | Supported | Supported | Network metadata helpers. | | `web3_*` | Supported | Supported | Client utility methods. | | `eth_subscribe` / `eth_unsubscribe` | Supported over WebSocket | Supported when WebSocket is enabled | Free public WebSocket connections have stricter concurrency limits. | | `txpool_*` | Not enabled by default | Configuration-dependent | Do not require txpool methods for production application flows. | | `debug_*` | Not enabled by default | Full archive/debug profile only | Use only on trusted private infrastructure. | | `personal_*` | Not for public RPC | Configuration-dependent | Do not expose personal APIs on public RPC endpoints. | | `trace_*` | Not available | Not available | `ctmd` does not provide `trace_*`; use debug APIs or an indexer where available. | ## Endpoint Configuration Mainnet endpoints use three TLDs: `c8ntinuum.com` for the default public free RPCs, and `c8ntinuum.io` for alternate public and paid RPCs. `c8ntinuum.link` serves mainnet too, with the same host names as `c8ntinuum.io` (for example `https://public-evm-rpc.c8ntinuum.link`, and the paid `https://evm-rpc.c8ntinuum.link`). Testnet endpoints live on `c8ntinuum.link` only and have `testnet` in the hostname. For the canonical free / alternate / paid matrix and rate limits, see [RPC Providers](/tooling-and-infrastructure/rpc-providers). | Network | Surface | Endpoint | |---------|---------|----------| | Mainnet | Default public HTTP | `https://public-evm-rpc.c8ntinuum.com` | | Mainnet | Default public WebSocket | `wss://public-evm-rpc.c8ntinuum.com/websocket` | | Mainnet | Alternate public HTTP | `https://public-evm-rpc.c8ntinuum.io` | | Mainnet | Alternate public WebSocket | `wss://public-evm-rpc.c8ntinuum.io/websocket` | | Mainnet | Paid HTTP | `https://evm-rpc.c8ntinuum.io` | | Mainnet | Paid WebSocket | `wss://evm-rpc.c8ntinuum.io/websocket` | | Testnet | Public HTTP | `https://public-testnet-evm.c8ntinuum.link` | | Testnet | Public WebSocket | `wss://public-testnet-evm.c8ntinuum.link/websocket` | | Testnet | Paid HTTP | `https://testnet-evm.c8ntinuum.link` | | Testnet | Paid WebSocket | `wss://testnet-evm.c8ntinuum.link/websocket` | Paid endpoints require a valid API key through `X-API-Key` or `?api_key=`. To get one, email [dev@c8ntinuum.com](mailto:dev@c8ntinuum.com); a dedicated self-service dashboard is planned. ## Request policy EVM JSON-RPC HTTP accepts `POST /` with `Content-Type: application/json`. `OPTIONS` is allowed for preflight compatibility. `GET /`, non-JSON `POST /`, and unsupported paths are rejected by the RPC proxy. | Setting | Value | |---------|------:| | `eth_getLogs` block range cap | `10000` blocks | | `eth_getLogs` result cap | `10000` logs | | Node batch request limit | `2000` | | Node batch response max size | `50000000` bytes | | nginx EVM HTTP request body limit | `2m` | ## Example Request ```bash curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 }' ``` --- # Node Interface Calls URL: https://docs.c8ntinuum.com/rpc-reference/node-interface-calls Use this page when you need concrete node calls for exchange, custody, wallet, or indexer integrations. c8ntinuum exposes Ethereum-compatible JSON-RPC for EVM workflows and CometBFT, REST/LCD, and gRPC surfaces for node health and Cosmos SDK data. ## Endpoint surfaces | Surface | Mainnet endpoint | Best for | |---------|------------------|----------| | EVM JSON-RPC HTTP | `https://public-evm-rpc.c8ntinuum.com` | Blocks, transactions, receipts, balances, logs, broadcasts | | EVM JSON-RPC WebSocket | `wss://public-evm-rpc.c8ntinuum.com/websocket` | New block and log subscriptions | | CometBFT RPC HTTP | `https://public-comet-rpc.c8ntinuum.com` | Node status, consensus block metadata, CometBFT transactions | | REST/LCD | `https://public-comet-grpc-web.c8ntinuum.io` | Cosmos account and transaction queries | For all public endpoint options and rate limits, see [RPC Providers](/tooling-and-infrastructure/rpc-providers). ## EVM JSON-RPC with curl Query the current block height: ```bash curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` Fetch a block by height. Use a hex quantity such as `0x10d4f`; set the second parameter to `true` when you need full transaction objects: ```bash curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x10d4f",true],"id":1}' ``` Fetch transaction details and its execution result: ```bash TX_HASH="0x..." curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionByHash\",\"params\":[\"$TX_HASH\"],\"id\":1}" curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$TX_HASH\"],\"id\":2}" ``` Query ERC-20 `Transfer` logs for a bounded range: The documented public EVM profile caps `eth_getLogs` at `10000` blocks and `10000` returned logs per request. ```bash TOKEN="0x..." TRANSFER_TOPIC="0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d "{ \"jsonrpc\":\"2.0\", \"method\":\"eth_getLogs\", \"params\":[{ \"address\":\"$TOKEN\", \"fromBlock\":\"0x10d00\", \"toBlock\":\"0x10dff\", \"topics\":[\"$TRANSFER_TOPIC\"] }], \"id\":3 }" ``` Broadcast a signed transaction: ```bash RAW_TX="0x..." curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_sendRawTransaction\",\"params\":[\"$RAW_TX\"],\"id\":4}" ``` ## JavaScript ```javascript const provider = new JsonRpcProvider("https://public-evm-rpc.c8ntinuum.com"); const height = await provider.getBlockNumber(); const block = await provider.send("eth_getBlockByNumber", [ "0x" + height.toString(16), true, ]); const tx = await provider.getTransaction("0x..."); const receipt = await provider.getTransactionReceipt("0x..."); const fromBlock = Math.max(0, height - 500); const logs = await provider.getLogs({ address: "0xc8Fb80fCc03f699C70ff0CC08C09106288888888", topics: [id("Transfer(address,address,uint256)")], fromBlock, toBlock: height, }); const sent = await provider.broadcastTransaction("0xSignedRawTransaction"); console.log({ height, blockHash: block.hash, txHash: tx?.hash, status: receipt?.status, logCount: logs.length, broadcastHash: sent.hash, }); ``` ## Java ```java Web3j web3j = Web3j.build(new HttpService("https://public-evm-rpc.c8ntinuum.com")); EthBlockNumber blockNumber = web3j.ethBlockNumber().send(); BigInteger height = blockNumber.getBlockNumber(); EthBlock block = web3j .ethGetBlockByNumber(new DefaultBlockParameterNumber(height), true) .send(); EthTransaction tx = web3j.ethGetTransactionByHash("0x...").send(); EthGetTransactionReceipt receipt = web3j.ethGetTransactionReceipt("0x...").send(); EthFilter filter = new EthFilter( new DefaultBlockParameterNumber(height.subtract(BigInteger.valueOf(500))), DefaultBlockParameterName.LATEST, "0xc8Fb80fCc03f699C70ff0CC08C09106288888888"); filter.addSingleTopic("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); EthLog logs = web3j.ethGetLogs(filter).send(); EthSendTransaction sent = web3j.ethSendRawTransaction("0xSignedRawTransaction").send(); ``` ## Go ```go package main use alloy::primitives::{address, Bytes, B256}; use alloy::providers::{Provider, ProviderBuilder}; use alloy::rpc::types::{BlockNumberOrTag, Filter}; #[tokio::main] async fn main() -> eyre::Result<()> { let provider = ProviderBuilder::new().connect_http( "https://public-evm-rpc.c8ntinuum.com".parse()?, ); let height = provider.get_block_number().await?; let block = provider .get_block_by_number(BlockNumberOrTag::Number(height)) .full() .await?; let tx_hash: B256 = "0x...".parse()?; let tx = provider.get_transaction_by_hash(tx_hash).await?; let receipt = provider.get_transaction_receipt(tx_hash).await?; let transfer_topic: B256 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" .parse()?; let filter = Filter::new() .address(address!("c8fb80fcc03f699c70ff0cc08c09106288888888")) .event_signature(transfer_topic) .from_block(height.saturating_sub(500)) .to_block(height); let logs = provider.get_logs(&filter).await?; let raw_tx: Bytes = "0xSignedRawTransaction".parse()?; let pending = provider.send_raw_transaction(&raw_tx).await?; println!( "height={height} block={:?} tx={:?} receipt={:?} logs={} sent={:?}", block.map(|b| b.header.hash), tx.map(|t| t.tx_hash()), receipt.map(|r| r.status()), logs.len(), pending.tx_hash(), ); Ok(()) } ``` ## CometBFT RPC Use CometBFT RPC for node status, consensus block metadata, and CometBFT transaction queries. ```bash # Current node status and latest block height curl -s "https://public-comet-rpc.c8ntinuum.com/status" # Block by height curl -s "https://public-comet-rpc.c8ntinuum.com/block?height=123456" # Transaction by CometBFT hash curl -s "https://public-comet-rpc.c8ntinuum.com/tx?hash=0x...&prove=true" # Search indexed transactions when the node profile supports it curl -s --get "https://public-comet-rpc.c8ntinuum.com/tx_search" \ --data-urlencode 'query="tx.height=123456"' ``` ## REST and account queries Use REST/LCD when you need Cosmos SDK account state or Cosmos transaction data. ```bash # Account metadata by c8... address curl -s "https://public-comet-grpc-web.c8ntinuum.io/cosmos/auth/v1beta1/accounts/c8..." # Cosmos transaction by hash: hex CometBFT hash without the 0x prefix curl -s "https://public-comet-grpc-web.c8ntinuum.io/cosmos/tx/v1beta1/txs/" ``` REST/LCD takes the transaction hash without `0x`, while CometBFT `/tx` takes it with `0x`. ## Operational notes - Treat public endpoint limits as provider policy, not protocol limits. - Keep long historical jobs bounded by block range and retryable by cursor. `eth_getLogs` requests should stay within the `10000` block and `10000` log caps. - Credit deposits from receipts and logs, not from pending transaction data. - Use `eth_sendRawTransaction` for offline-signed EVM transactions. - Run archive or indexer infrastructure when you need complete historical state from genesis. --- # RPC Differences URL: https://docs.c8ntinuum.com/rpc-reference/rpc-differences ## Key Differences | Method | Ethereum Behavior | c8ntinuum Behavior | |--------|------------------|-------------------| | `eth_gasPrice` | Dynamic gas price | Returns the latest base fee plus the suggested priority tip (`eth_maxPriorityFeePerGas`), never less than the fee market minimum gas price | | `eth_syncing` | Sync status | Standard sync status | For the base fee alone, read `baseFeePerGas` from `eth_getBlockByNumber("latest", false)` or use `eth_feeHistory`. ## Mining-Related Methods c8ntinuum uses CometBFT proof-of-stake consensus, not proof of work, and `ctmd` does not implement the mining methods. `eth_mining`, `eth_hashrate`, `eth_coinbase`, `eth_getWork`, `eth_submitWork`, and `eth_submitHashrate` return JSON-RPC error `-32601` (method not available) on the public endpoints. ## Transaction Pool `txpool_*` methods are node-configuration dependent. The default public RPC profile uses `api = "eth,net,web3"`, so it does not expose `txpool`. Archive sentries may add `txpool`, and a private full-archive debug node can use `api = "eth,net,web3,debug,txpool,personal"` (see [Archive Data](/node-ops/archive-data)). The local node started by `local_node.sh` also enables `txpool`, `personal`, and `debug`. On a node with compatible txpool support enabled, common methods include: - `txpool_status` — Number of pending and queued transactions - `txpool_content` — Full transaction pool contents - `txpool_inspect` — Summary of pool contents ## Debug Namespace Debug tracing is intended for private full-archive nodes, not the public RPC. `ctmd` does not provide a separate `trace` namespace. On a full archive node with compatible debug APIs enabled, common methods include: - `debug_traceTransaction` — Trace a transaction execution - `debug_traceBlockByNumber` — Trace all transactions in a block - `debug_traceBlockByHash` — Trace by block hash ## WebSocket Support WebSocket connections are available at `wss://public-evm-rpc.c8ntinuum.com/websocket` for subscription-based event streaming: - `eth_subscribe` / `eth_unsubscribe` — Subscribe to new blocks, logs, and pending transactions Free WebSocket connections are limited to `1` per visitor IP. Paid WebSocket connections are limited to `30` per API key. ## Batching and payload limits | Surface | Limit | |---------|------:| | EVM JSON-RPC batch request limit | `2000` | | EVM JSON-RPC batch response max size | `50000000` bytes | | EVM nginx request body limit | `2m` | | CometBFT request batch size | `10` | | CometBFT nginx request body limit | `1m` | | gRPC and REST/LCD nginx request body limit | `1m` | --- # Error Codes URL: https://docs.c8ntinuum.com/rpc-reference/error-codes c8ntinuum uses Ethereum-compatible JSON-RPC error envelopes. Exact `message` text can vary by client, proxy, and execution path, so integrations should branch on error category and transaction receipt status rather than exact strings. ## Standard JSON-RPC errors | Code | Message | Description | |------|---------|-------------| | `-32700` | Parse error | Invalid JSON | | `-32600` | Invalid request | Missing required fields | | `-32601` | Method not found | Method does not exist or is not supported | | `-32602` | Invalid params | Incorrect method parameters | | `-32603` | Internal error | Internal server or proxy error | ## EVM execution errors | Category | Typical code | Typical message pattern | Handling guidance | |----------|--------------|-------------------------|-------------------| | Contract revert | `3` when revert data is present (reason in `message`, ABI-encoded revert in `data`); `-32000` for reverts with no data | `execution reverted: ` or plain `execution reverted` | Decode revert data when available; do not credit failed transactions. Libraries such as ethers surface both as `CALL_EXCEPTION`. | | Out of gas | `-32000` | `out of gas`, `gas required exceeds allowance`, or `ErrOutOfGas` | Re-estimate gas against the same calldata and network. | | Insufficient funds | `-32000` | `insufficient funds` | Check native `CTM` balance for value plus max fee. | | Nonce too low | `-32000` | `nonce too low`, `already known`, or replacement conflict | Refresh pending nonce before rebroadcasting. | | Nonce too high | `-32000` | `nonce too high` | Wait for missing nonce or rebroadcast the earlier transaction. | | Fee too low | `-32000` | `transaction underpriced`, `max fee per gas less than block base fee`, or similar | Use current fee data and respect the minimum gas price in [Gas on c8ntinuum](/developer-essentials/gas). | | Unsupported method | `-32601` | `method not found` | Confirm the namespace is enabled on the selected RPC profile. | | Rate limited | HTTP `429` | Provider-specific body | Back off and retry with jitter; see [RPC Providers](/tooling-and-infrastructure/rpc-providers). | ## Precompile errors Precompile reverts are documented in the [Precompile ABI Reference](/reference/precompile-abi#reverts-and-errors). | Case | Behavior | |------|----------| | Stateful precompile action returns an error | Standard ABI `Error(string)` revert data | | Unknown selector, short calldata, or failed ABI unpack in most stateless precompiles | Plain `execution reverted` | | SDK gas panic or exhausted EVM gas | `ErrOutOfGas` | | ERC20 extension SDK/bank errors | ERC20-style revert strings such as `ERC20: transfer amount exceeds balance` | ## Handling Errors Most Ethereum libraries parse these errors into library-specific exception types. Always inspect transaction receipts for submitted transactions: ```javascript try { await contract.someMethod(); } catch (error) { if (error.code === "CALL_EXCEPTION") { console.log("Reverted:", error.reason); } } ``` For exchange and custody systems, use [Node Interface Calls](/rpc-reference/node-interface-calls) and [Deposit Processing](/guides/exchange-integration/deposit-processing) for receipt-based confirmation behavior. --- # Precompile ABI Reference URL: https://docs.c8ntinuum.com/reference/precompile-abi This reference tracks the `ctmd` node source. That source will be published in a public GitHub repository (repository TBA); the paths on this page are paths inside that repository. The canonical ABI artifacts and Solidity interfaces live beside the precompile implementations under `precompiles/*/`. Most Cosmos-module and extension interfaces (all except ICS02) are also mirrored under `contracts/solidity/precompiles`. ## Address catalog | Address | Precompile | Interface | |---------|------------|-----------| | `0x0000000000000000000000000000000000000100` | P256 / secp256r1 verifier | Raw call only, no Solidity ABI | | `0x0000000000000000000000000000000000000400` | Bech32 | `Bech32I` | | `0x0000000000000000000000000000000000000500` | Ed25519 | `Ed25519I` | | `0x0000000000000000000000000000000000000600` | SP1 Groth16 verifier | `SP1VerifierGroth16I` | | `0x0000000000000000000000000000000000000700` | SP1 Plonk verifier | `SP1VerifierPlonkI` | | `0x0000000000000000000000000000000000000701` | JSON | `JsonI` | | `0x0000000000000000000000000000000000000703` | Schnorr | `SchnorrI` | | `0x0000000000000000000000000000000000000704` | Schnorrkel | `SchnorrkelI` | | `0x0000000000000000000000000000000000000705` | Gnark hash | `GnarkHashI` | | `0x0000000000000000000000000000000000000706` | SHA3 hash | `Sha3HashI` | | `0x0000000000000000000000000000000000000707` | Blake2b hash | `Blake2bHashI` | | `0x0000000000000000000000000000000000000708` | ECVRF | `ECVRFI` | | `0x0000000000000000000000000000000000000709` | FROST | `FrostI` | | `0x0000000000000000000000000000000000000710` | Address table | `AddressTableI` | | `0x0000000000000000000000000000000000000711` | Poseidon hash | `PoseidonI` | | `0x0000000000000000000000000000000000000712` | PQMLDSA | `PQMLDSAI` | | `0x0000000000000000000000000000000000000713` | PQSLHDSA | `PQSLHDSAI` | | `0x0000000000000000000000000000000000000714` | Validator rewards | `ValRewardsI` | | `0x0000000000000000000000000000000000000800` | Staking | `StakingI` | | `0x0000000000000000000000000000000000000801` | Distribution | `DistributionI` | | `0x0000000000000000000000000000000000000802` | ICS20 transfer | `ICS20I` | | `0x0000000000000000000000000000000000000804` | Bank | `IBank` | | `0x0000000000000000000000000000000000000805` | Governance | `IGov` | | `0x0000000000000000000000000000000000000806` | Slashing | `ISlashing` | | `0x0000000000000000000000000000000000000807` | ICS02 client | `ICS02I` | | Token-pair address | ERC20 extension | `IERC20Metadata` | | Token-pair address (mainnet `ctm`: `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`) | WERC20 extension | `IWERC20` | `ERC20` and `WERC20` precompile addresses are created from registered token pairs and enabled by the ERC20 keeper; they are not part of the static address table. On mainnet, the only registered token pair is the native `ctm` denom at `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`, served by the WERC20 extension (name `c8ntinuum`, symbol `CTM`, 18 decimals). ## Reference package and imports | Surface | Repository location | Notes | |---------|---------------------|-------| | Generated ABI artifacts | `precompiles/*/abi.json` | Canonical ABI for function and event signatures. The Cosmos-module and extension artifacts (bank, bech32, callbacks, distribution, erc20, gov, ics02, ics20, slashing, staking, werc20) are plain ABI arrays. The other precompiles ship Hardhat artifacts with the ABI array under the `abi` field. | | Solidity interfaces | `precompiles/*/*.sol` | Canonical Solidity source interfaces and address constants. | | Mirrored Solidity interfaces | `contracts/solidity/precompiles/*/*.sol` | Consumer-friendly mirror used by the local contracts package and tests. Covers only bank, bech32, callbacks, common, distribution, erc20, gov, ics20, slashing, staking, and werc20. For ICS02, validator rewards, address table, and the stateless crypto and hash precompiles, use the interface under `precompiles//`. | | Local contracts package | `contracts/package.json` | Named `cosmos-evm-contracts`. | `contracts/package.json` does not define `main`, `files`, `exports`, or `types`. Treat `cosmos-evm-contracts` as a local contracts package, not as a documented stable npm ABI package. Use static constants for fixed-address precompiles. For `ERC20` and `WERC20`, use the registered token-pair address. For `P256`, use a raw low-level call because there is no Solidity ABI. State-changing staking, distribution, governance, slashing, and ICS20 methods that take an account argument (delegator, validator, depositor, proposer, voter, or ICS20 `sender`), and validator rewards `depositValidatorRewardsPool`, require that account to equal the immediate caller of the precompile. An EOA calling the precompile directly passes its own address. A contract passes `address(this)` and can act only for itself, as in the example below. Any other account reverts with `msg.sender address ... does not match the requester address ...`. `createValidator` and `editValidator` cannot be called from a contract at all (EIP-7702 delegated accounts excepted) and revert with `this method can only be called directly to the precompile, not from a smart contract`. ```solidity contract Delegator { function delegateTo(string calldata validatorAddress) external payable returns (bool) { return staking.STAKING_CONTRACT.delegate( address(this), validatorAddress, msg.value ); } } ``` ```ts export const STAKING_ADDRESS = "0x0000000000000000000000000000000000000800"; export const stakingAbi = stakingArtifact as Abi; ``` No generated TypeScript binding package is committed today. Hardhat users can generate TypeChain bindings in their own workspace; viem users can pass the ABI array directly. ## Core ABI catalog ### Address table | Signature | Mutability | Returns | |-----------|------------|---------| | `addressExists(address addr)` | `view` | `(bool)` | | `compress(address addr)` | `view` | `(bytes)` | | `decompress(bytes buf,uint256 offset)` | `view` | `(address,uint256)` | | `lookup(address addr)` | `view` | `(uint256)` | | `lookupIndex(uint256 index)` | `view` | `(address)` | | `register(address addr)` | `nonpayable` | `(uint256)` | | `size()` | `view` | `(uint256)` | Event: `AddressRegistered(indexed address addr,indexed uint256 idx)`. ### Bank | Signature | Mutability | Returns | |-----------|------------|---------| | `balances(address account)` | `view` | `(tuple[](address contractAddress,uint256 amount) balances)` | | `supplyOf(address erc20Address)` | `view` | `(uint256 totalSupply)` | | `totalSupply()` | `view` | `(tuple[](address contractAddress,uint256 amount) totalSupply)` | Events: none. ### Bech32 | Signature | Mutability | Returns | |-----------|------------|---------| | `bech32ToHex(string bech32Address)` | `nonpayable` | `(address addr)` | | `hexToBech32(address addr,string prefix)` | `nonpayable` | `(string bech32Address)` | Events: none. ### Distribution | Signature | Mutability | Returns | |-----------|------------|---------| | `claimRewards(address delegatorAddress,uint32 maxRetrieve)` | `nonpayable` | `(bool success)` | | `communityPool()` | `view` | `(DecCoin[] coins)` | | `delegationRewards(address delegatorAddress,string validatorAddress)` | `view` | `(DecCoin[] rewards)` | | `delegationTotalRewards(address delegatorAddress)` | `view` | `(tuple[](string validatorAddress,DecCoin[] reward) rewards, DecCoin[] total)` | | `delegatorValidators(address delegatorAddress)` | `view` | `(string[] validators)` | | `delegatorWithdrawAddress(address delegatorAddress)` | `view` | `(string withdrawAddress)` | | `depositValidatorRewardsPool(address depositor,string validatorAddress,Coin[] amount)` | `nonpayable` | `(bool success)` | | `fundCommunityPool(address depositor,Coin[] amount)` | `nonpayable` | `(bool success)` | | `setWithdrawAddress(address delegatorAddress,string withdrawerAddress)` | `nonpayable` | `(bool success)` | | `validatorCommission(string validatorAddress)` | `view` | `(DecCoin[] commission)` | | `validatorDistributionInfo(string validatorAddress)` | `view` | `(tuple(string operatorAddress,DecCoin[] selfBondRewards,DecCoin[] commission) distributionInfo)` | | `validatorOutstandingRewards(string validatorAddress)` | `view` | `(DecCoin[] rewards)` | | `validatorSlashes(string validatorAddress,uint64 startingHeight,uint64 endingHeight,PageRequest pageRequest)` | `view` | `(tuple[](uint64 validatorPeriod,Dec fraction) slashes, PageResponse pageResponse)` | | `withdrawDelegatorRewards(address delegatorAddress,string validatorAddress)` | `nonpayable` | `(Coin[] amount)` | | `withdrawValidatorCommission(string validatorAddress)` | `nonpayable` | `(Coin[] amount)` | Events include `ClaimRewards`, `DepositValidatorRewardsPool`, `FundCommunityPool`, `SetWithdrawerAddress`, `WithdrawDelegatorReward`, and `WithdrawValidatorCommission`. On mainnet, x/distribution currently accrues nothing: inflation is `0`, x/feeburn burns all transaction fees, and community tax and proposer rewards are `0`. Reward and commission queries and withdrawals therefore return empty amounts. Validator compensation comes from [validator rewards](/modules/custom/validator-rewards). ### ECVRF | Signature | Mutability | Returns | |-----------|------------|---------| | `ecvrfVerify(string suite,bytes pubKey,bytes alpha,bytes pi)` | `pure` | `(bool ok,bytes beta)` | Supported suites are `P256_SHA256_TAI` and `SECP256K1_SHA256_TAI` (case-insensitive). `pubKey` is a SEC1 public key, 33 bytes compressed or 65 bytes uncompressed. An unsupported suite, a malformed key, or a failed proof returns `ok = false` with empty `beta`. An unknown selector, undecodable input, a suite longer than 32 bytes, or `alpha` longer than 65,536 bytes fails the call. Events: none. ### Governance | Signature | Mutability | Returns | |-----------|------------|---------| | `cancelProposal(address proposer,uint64 proposalId)` | `nonpayable` | `(bool success)` | | `deposit(address depositor,uint64 proposalId,Coin[] amount)` | `nonpayable` | `(bool success)` | | `getConstitution()` | `view` | `(string constitution)` | | `getDeposit(uint64 proposalId,address depositor)` | `view` | `(Deposit deposit)` | | `getDeposits(uint64 proposalId,PageRequest pagination)` | `view` | `(Deposit[] deposits, PageResponse pageResponse)` | | `getParams()` | `view` | `(Params params)` | | `getProposal(uint64 proposalId)` | `view` | `(Proposal proposal)` | | `getProposals(uint32 proposalStatus,address voter,address depositor,PageRequest pagination)` | `view` | `(Proposal[] proposals, PageResponse pageResponse)` | | `getTallyResult(uint64 proposalId)` | `view` | `(TallyResult tallyResult)` | | `getVote(uint64 proposalId,address voter)` | `view` | `(Vote vote)` | | `getVotes(uint64 proposalId,PageRequest pagination)` | `view` | `(Vote[] votes, PageResponse pageResponse)` | | `submitProposal(address proposer,bytes jsonProposal,Coin[] deposit)` | `nonpayable` | `(uint64 proposalId)` | | `vote(address voter,uint64 proposalId,uint8 option,string metadata)` | `nonpayable` | `(bool success)` | | `voteWeighted(address voter,uint64 proposalId,WeightedVoteOption[] options,string metadata)` | `nonpayable` | `(bool success)` | Events include `CancelProposal`, `Deposit`, `SubmitProposal`, `Vote`, and `VoteWeighted`. ### ICS02 and ICS20 | Precompile | Signature | Mutability | Returns | |------------|-----------|------------|---------| | ICS02 | `getClientState(string clientId)` | `view` | `(bytes)` | | ICS02 | `updateClient(string clientId,bytes updateMsg)` | `nonpayable` | `(uint8)` | | ICS02 | `verifyMembership(string clientId,bytes proof,Height proofHeight,bytes[] path,bytes value)` | `nonpayable` | `(uint256)` | | ICS02 | `verifyNonMembership(string clientId,bytes proof,Height proofHeight,bytes[] path)` | `nonpayable` | `(uint256)` | | ICS20 | `denom(string hash)` | `view` | `(Denom denom)` | | ICS20 | `denomHash(string trace)` | `view` | `(string hash)` | | ICS20 | `denoms(PageRequest pageRequest)` | `view` | `(Denom[] denoms, PageResponse pageResponse)` | | ICS20 | `transfer(string sourcePort,string sourceChannel,string denom,uint256 amount,address sender,string receiver,Height timeoutHeight,uint64 timeoutTimestamp,string memo)` | `nonpayable` | `(uint64 nextSequence)` | ICS20 emits `IBCTransfer(indexed address sender,indexed string receiver,string sourcePort,string sourceChannel,string denom,uint256 amount,string memo)`. `transfer` reverts unless `sender` is the immediate caller of the precompile. A contract passes `address(this)` and transfers tokens it holds. ### Slashing | Signature | Mutability | Returns | |-----------|------------|---------| | `getParams()` | `view` | `(Params params)` | | `getSigningInfo(address consAddress)` | `view` | `(SigningInfo signingInfo)` | | `getSigningInfos(PageRequest pagination)` | `view` | `(SigningInfo[] signingInfos, PageResponse pageResponse)` | | `unjail(address validatorAddress)` | `nonpayable` | `(bool success)` | Event: `ValidatorUnjailed(indexed address validator)`. ### Staking | Signature | Mutability | Returns | |-----------|------------|---------| | `cancelUnbondingDelegation(address delegatorAddress,string validatorAddress,uint256 amount,uint256 creationHeight)` | `nonpayable` | `(bool success)` | | `createValidator(Description description,CommissionRates commissionRates,uint256 minSelfDelegation,address validatorAddress,string pubkey,uint256 value)` | `nonpayable` | `(bool success)` | | `delegate(address delegatorAddress,string validatorAddress,uint256 amount)` | `nonpayable` | `(bool success)` | | `delegation(address delegatorAddress,string validatorAddress)` | `view` | `(uint256 shares, Coin balance)` | | `editValidator(Description description,address validatorAddress,int256 commissionRate,int256 minSelfDelegation)` | `nonpayable` | `(bool success)` | | `redelegate(address delegatorAddress,string validatorSrcAddress,string validatorDstAddress,uint256 amount)` | `nonpayable` | `(int64 completionTime)` | | `redelegation(address delegatorAddress,string srcValidatorAddress,string dstValidatorAddress)` | `view` | `(Redelegation redelegation)` | | `redelegations(address delegatorAddress,string srcValidatorAddress,string dstValidatorAddress,PageRequest pageRequest)` | `view` | `(RedelegationResponse[] response, PageResponse pageResponse)` | | `unbondingDelegation(address delegatorAddress,string validatorAddress)` | `view` | `(UnbondingDelegation unbondingDelegation)` | | `undelegate(address delegatorAddress,string validatorAddress,uint256 amount)` | `nonpayable` | `(int64 completionTime)` | | `validator(address validatorAddress)` | `view` | `(Validator validator)` | | `validators(string status,PageRequest pageRequest)` | `view` | `(Validator[] validators, PageResponse pageResponse)` | Events include `CreateValidator`, `Delegate`, `EditValidator`, `Redelegate`, `Unbond`, and `CancelUnbondingDelegation`. ### Validator rewards | Signature | Mutability | Returns | |-----------|------------|---------| | `claimRewards(address validatorOperatorAddress,uint64 epoch)` | `nonpayable` | `(bool success)` | | `delegationRewards(address delegatorAddress,uint64 epoch)` | `view` | `(Coin rewards)` | | `depositValidatorRewardsPool(address depositor,Coin amount)` | `nonpayable` | `(bool success)` | | `rewardsPool()` | `view` | `(Coin rewards)` | | `validatorOutstandingRewards(uint64 epoch,string validatorAddress)` | `view` | `(Coin rewards)` | Despite its parameter name, `delegationRewards` takes a validator operator's account address (the `0x...` account that owns the `c8valoper...` address), not a delegator's. It returns that validator's own outstanding reward for the epoch, the same value as `validatorOutstandingRewards`, and fails with `target address must be a validator operator` for any other address. x/valrewards pays validators only, not delegators. ### ERC20 and WERC20 extensions | Extension | Signature | Mutability | Returns | |-----------|-----------|------------|---------| | ERC20/WERC20 | `allowance(address owner,address spender)` | `view` | `(uint256)` | | ERC20/WERC20 | `approve(address spender,uint256 amount)` | `nonpayable` | `(bool)` | | ERC20/WERC20 | `balanceOf(address account)` | `view` | `(uint256)` | | ERC20/WERC20 | `decimals()` | `view` | `(uint8)` | | WERC20 | `deposit()` | `payable` | none | | ERC20/WERC20 | `name()` | `view` | `(string)` | | ERC20/WERC20 | `symbol()` | `view` | `(string)` | | ERC20/WERC20 | `totalSupply()` | `view` | `(uint256)` | | ERC20/WERC20 | `transfer(address to,uint256 amount)` | `nonpayable` | `(bool)` | | ERC20/WERC20 | `transferFrom(address from,address to,uint256 amount)` | `nonpayable` | `(bool)` | | WERC20 | `withdraw(uint256 wad)` | `nonpayable` | none | ERC20 emits `Approval` and `Transfer`. WERC20 also emits `Deposit` and `Withdrawal`. WERC20 exposes the native CTM balance through an ERC-20 interface. `deposit()` and `withdraw()` exist only for WETH interface compatibility and do not change balances: `deposit()` sends `msg.value` straight back to the caller and emits `Deposit`, and `withdraw(wad)` reverts if the caller's spendable balance is below `wad`, otherwise it only emits `Withdrawal`. ## Reverts and errors The checked ABI artifacts do not define custom Solidity `error` entries. | Case | Behavior | |------|----------| | Stateful precompile action returns an error | Standard ABI `Error(string)` revert data | | Unknown selector, short calldata, or failed ABI unpack in most stateless precompiles | Plain `execution reverted` | | Read-only call to a state-changing method | EVM write-protection error | | SDK gas panic or exhausted EVM gas | `ErrOutOfGas` | | Verification failure in signature/hash helpers | Usually ABI-encoded `false` or empty bytes, depending on interface | ERC20 maps some SDK/bank errors to ERC20-style revert strings such as `ERC20: transfer amount exceeds balance`, `ERC20: insufficient allowance`, and `ERC20: decreased allowance below zero`. ## Gas schedule Variable-length stateless precompiles use: ```text base gas + ceil(input bytes / 32) * word gas ``` Stateful Cosmos module precompiles use a minimum formula before SDK execution: ```text 2000 + 30 * input bytes # transactions 1000 + 3 * input bytes # queries ``` Actual SDK execution is also metered inside the precompile call. | Surface | Notes | |---------|-------| | SP1 Groth16 `verifyProof` | `300000 + ceil((publicValuesBytes + proofBytes) / 32) * 16` | | SP1 Plonk `verifyProof` | `800000 + ceil((publicValuesBytes + proofBytes) / 32) * 16` | | Gnark hash | `6000 + ceil(input bytes / 32) * 30` | | SHA3 hash | `3000 + ceil(input bytes / 32) * 30` | | Blake2b hash | `3000 + ceil(input bytes / 32) * 30` | | Poseidon hash | `3000` flat | | ECVRF `ecvrfVerify` | `50000 + ceil(input bytes / 32) * 30` | | Stateful module precompiles | Use the shared stateful minimum formula, then meter SDK execution | | P256 | Raw-call precompile; validate packed input shape in caller code | ## Versioning rules Use the repository tag as the compatibility boundary until a separate ABI package is formalized. | Change | Versioning expectation | |--------|------------------------| | Address changes, removed or changed signatures/events, changed revert behavior, or changed gas schedules | Breaking release | | New static precompile, new dynamic precompile family, or additive function/event entries | Minor release | | Documentation-only corrections or regenerated artifacts with no ABI/address/gas behavior change | Patch release | --- # Guides URL: https://docs.c8ntinuum.com/guides Start with the guide that matches the task you are trying to complete. ## Add c8ntinuum to Wallet Add c8ntinuum to MetaMask or another EVM-compatible wallet. MetaMask, Rabby, Coinbase Wallet, Ledger, mobile setup, and wrapped CTM import. Manual testnet wallet configuration and faucet status. ## Exchange and Custody Integration Node interfaces, address generation, deposit processing, offline signing, and wallet workflows. Downloadable, compile-tested JavaScript and Java samples behind the integration snippets. Derive `0x` and `c8` deposit addresses offline and validate them. Scan for native CTM, ERC-20, and Cosmos bank deposits and credit them idempotently. Build withdrawals online, sign offline, and broadcast with `eth_sendRawTransaction`. ## Deploy a Contract Create, compile, and deploy a contract with `forge`. Configure the c8ntinuum network in Hardhat 2 and deploy with a script. Compile and deploy from the Remix IDE in your browser through MetaMask. ## Verify a Contract Verify contracts on CTMScan with `forge verify-contract`. Verify contracts on CTMScan with the Hardhat verify plugin. ## Run a Node Operator-oriented runbooks for full nodes, validators, state sync, and recovery. --- # Wallet Setup URL: https://docs.c8ntinuum.com/guides/wallet-setup Use these guides to add c8ntinuum to an EVM wallet, understand wallet compatibility, and find the account and token details that wallet UIs need. Step-by-step setup for MetaMask, Rabby, Coinbase Wallet, Ledger Nano (S Plus / X) through an EVM wallet, mobile wallets, and token imports. Wallet setup using the canonical testnet network information. Chain ID 8888, RPC endpoints, explorer, faucet status, and verification status. Supported wallets, WalletConnect metadata, wrapped CTM, address formats, and common wallet errors. EVM `0x`, Cosmos `c8`, validator `c8valoper`, and key derivation details. Native CTM, base denomination, ERC-20 compatibility, and wrapped CTM. ## Mainnet quick setup | Field | Value | |-------|-------| | **Network Name** | `c8ntinuum` | | **RPC URL** | `https://public-evm-rpc.c8ntinuum.com` | | **Chain ID** | `8` / `0x8` | | **Currency Symbol** | `CTM` | | **Block Explorer** | `https://ctmscan.io` | Follow [Add c8ntinuum Mainnet Beta to Wallet](/guides/wallet-setup/mainnet) for the full setup flow, including MetaMask mobile, Ledger, dapp network prompts, and token imports. For testnet, use [Network Information - Testnet](/developer-essentials/network-information/testnet) as the source of truth and [Add c8ntinuum Testnet to Wallet](/guides/wallet-setup/testnets) for the wallet request shape. ## Verify connection - Chain ID should display `8` or `0x8`. - Native currency should show `CTM`. - Block explorer links should open CTMScan. - A JSON-RPC `eth_chainId` request should return `0x8`. --- # Add c8ntinuum Mainnet Beta to Wallet URL: https://docs.c8ntinuum.com/guides/wallet-setup/mainnet Use this guide to add c8ntinuum mainnet to an EVM wallet, connect from mobile, import wrapped CTM, and confirm the wallet is on the correct chain. The canonical mainnet values live in [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet). For testnet values, use [Network Information - Testnet](/developer-essentials/network-information/testnet). ## Network details | Field | Value | | --- | --- | | **Network Name** | `c8ntinuum` | | **RPC URL** | `https://public-evm-rpc.c8ntinuum.com` | | **Chain ID** | `8` / `0x8` | | **Currency Symbol** | `CTM` | | **Block Explorer** | `https://ctmscan.io` | ## MetaMask browser 1. Open MetaMask and click the network dropdown. 2. Click **Add a custom network**. 3. Enter the values from the table above. 4. Click **Save** and switch to c8ntinuum. ## MetaMask mobile 1. Open MetaMask mobile. 2. Tap the network selector at the top of the wallet. 3. Tap **Add a custom network**. 4. Enter the c8ntinuum mainnet values from the table above. 5. Save the network, then switch to c8ntinuum before approving dapp connections. If a dapp opens MetaMask mobile on another network, switch to c8ntinuum in the wallet and reconnect the dapp. ## Rabby and Coinbase Wallet Rabby, Coinbase Wallet, and other EVM wallets can use the same network details. If the wallet asks for a decimal or hex chain ID, use `8` or `0x8` respectively. ## Ledger Nano hardware wallets Ledger Nano S Plus and Nano X are supported through MetaMask, Rabby, or another EVM wallet: 1. Unlock the Ledger Nano and open the Ledger Ethereum app. 2. Connect the Nano to your EVM wallet over USB (Nano S Plus, Nano X) or Bluetooth (Nano X). 3. Select the Ethereum-compatible account derived with the standard Ethereum path. 4. Switch the connected wallet to c8ntinuum before signing. If the wallet shows an unexpected address, confirm the derivation path and account index in the connected EVM wallet. c8ntinuum uses Ethereum-compatible account derivation; see [Accounts](/core-concepts/accounts). ## Add c8ntinuum from a dapp Dapps can request network addition with EIP-3085 `wallet_addEthereumChain`: ```javascript await window.ethereum.request({ method: "wallet_addEthereumChain", params: [ { chainId: "0x8", chainName: "c8ntinuum", nativeCurrency: { name: "CTM", symbol: "CTM", decimals: 18, }, rpcUrls: ["https://public-evm-rpc.c8ntinuum.com"], blockExplorerUrls: ["https://ctmscan.io"], }, ], }); ``` c8ntinuum mainnet is also listed on [Chainlist](https://chainlist.org/chain/8), which can add the network to a wallet in one click. Chainlist lists several RPC URLs for chain 8, including third-party and legacy ones, such as `https://rpc.octano.dev` from the network that used chain ID 8 before c8ntinuum. The chain registry entry behind Chainlist carries a reused-chain-ID flag for the same reason, and wallets may show a warning about it. Before you approve, check that the chain ID is `8` and the currency is `CTM`. After you add the network, check its RPC URL in the wallet and change it to `https://public-evm-rpc.c8ntinuum.com` if it is different. ## Import wrapped CTM c8ntinuum's wrapped native token is the WETH9-compatible wrapped CTM contract: ```text 0xc8Fb80fCc03f699C70ff0CC08C09106288888888 ``` On c8ntinuum this address is wrapped CTM (`WCTM`). On Ethereum and BNB Chain the same address is the CTM token, a different contract. Native CTM on c8ntinuum needs no token import, so check that the wallet is on c8ntinuum before you use this address. To import it: 1. Switch the wallet to c8ntinuum. 2. Open the wallet's token import flow. 3. Paste the WETH9 address above. 4. Let the wallet auto-detect token metadata, then confirm the import. For other ERC-20 tokens, import the token contract address from the project or CTMScan. Do not use a token address from another chain unless the token publisher explicitly says it is deployed at the same address on c8ntinuum. ## Confirm it worked - The selected wallet network should be c8ntinuum. - The selected chain ID should be `8` or `0x8`. - Native balances should display as `CTM`. - Account and transaction links should open CTMScan. - A JSON-RPC `eth_chainId` request should return `0x8`: ```bash curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' ``` ## Troubleshooting | Issue | Fix | |-------|-----| | Wallet rejects the network | Confirm the chain ID is `8` / `0x8` and the RPC URL is `https://public-evm-rpc.c8ntinuum.com`. | | Transaction fails before signing | Make sure the account has native `CTM` for gas. | | Token does not appear | Import the token contract address manually and confirm the address on CTMScan. | | Mobile wallet opens the wrong chain | Switch networks inside the wallet app and reconnect the dapp. | | Ledger Nano shows the wrong account | Reconnect the Nano S Plus or Nano X with the Ledger Ethereum app and the expected Ethereum account path. | --- # Add c8ntinuum Testnet to Wallet URL: https://docs.c8ntinuum.com/guides/wallet-setup/testnets Use these values when a wallet asks for manual c8ntinuum testnet configuration. The canonical testnet network source is [Network Information - Testnet](/developer-essentials/network-information/testnet). ## Network details | Field | Value | | --- | --- | | **Network Name** | `c8ntinuum testnet` | | **RPC URL** | `https://public-testnet-evm.c8ntinuum.link` | | **Chain ID** | `8888` / `0x22b8` | | **Currency Symbol** | `CTM` | | **Block Explorer** | `https://testnet.ctmscan.io` | ## MetaMask request ```javascript await window.ethereum.request({ method: "wallet_addEthereumChain", params: [ { chainId: "0x22b8", chainName: "c8ntinuum testnet", nativeCurrency: { name: "CTM", symbol: "CTM", decimals: 18, }, rpcUrls: ["https://public-testnet-evm.c8ntinuum.link"], blockExplorerUrls: ["https://testnet.ctmscan.io"], }, ], }); ``` Rabby, Coinbase Wallet, and other EVM wallets can use the same values. If the wallet asks for a decimal or hex chain ID, use `8888` or `0x22b8` respectively. ## Faucet The c8ntinuum testnet faucet is hosted at [testnet-faucet.c8ntinuum.io](https://testnet-faucet.c8ntinuum.io/) and runs [PoWFaucet](https://github.com/pk910/PoWFaucet). | Item | Value | | --- | --- | | Faucet URL | [testnet-faucet.c8ntinuum.io](https://testnet-faucet.c8ntinuum.io/) | | Software | [PoWFaucet](https://github.com/pk910/PoWFaucet) | | Accepted address format | `0x... EVM addresses` | | Authentication, request amount, and cooldown | Surfaced live by the PoWFaucet UI per the operator's deployment configuration | | Failure messages | Surfaced inline by the PoWFaucet UI for unsupported addresses, active cooldowns, empty pool, and wrong-network requests | For ecosystem support and additional channels, see [Official Links](/official-links). --- # Exchange and Custody Integration URL: https://docs.c8ntinuum.com/guides/exchange-integration Use these pages when integrating c8ntinuum into exchange, custody, wallet, deposit, withdrawal, or indexing systems. The primary integration surface is Ethereum-compatible JSON-RPC. CometBFT RPC, REST/LCD, and gRPC are supporting surfaces for node status and Cosmos SDK data. ## Integration path 1. Run a full node or choose an RPC provider from [RPC Providers](/tooling-and-infrastructure/rpc-providers). 2. Use [Node Interface Calls](/rpc-reference/node-interface-calls) to verify block, transaction, receipt, log, and broadcast behavior. 3. Generate and validate deposit addresses with [Address Generation and Validation](/guides/exchange-integration/address-generation-and-validation). 4. Build a durable deposit scanner with [Deposit Processing](/guides/exchange-integration/deposit-processing). 5. Construct withdrawals with [Offline Signing and Broadcasting](/guides/exchange-integration/offline-signing-and-broadcasting). 6. Publish user-facing wallet instructions from [Wallets](/tooling-and-infrastructure/wallets). c8ntinuum has 1-block finality under standard CometBFT and an expected 4s block time. Exchanges should credit deposits after 60 confirmations, approximately 240 seconds or 4 minutes, with an optional 1-2 block scanner buffer for RPC, log, or receipt lag. ## Supported languages/frameworks Examples use these defaults: | Language | Default library | |----------|-----------------| | JavaScript | `ethers` v6 | | Java | `web3j` | | Go | `go-ethereum` packages | | Rust | `alloy` crates where possible | For the broader SDK and framework matrix, see [Development Toolkits](/tooling-and-infrastructure/dev-toolkits). JavaScript and Java address/offline-signing examples are compile-tested and available as a [downloadable archive](/assets/downloads/c8ntinuum-exchange-integration-examples.zip) that unpacks to `examples/exchange-integration/`. See [Tested Exchange Examples](/guides/exchange-integration/tested-examples) for commands, dependency versions, and the mapping between docs snippets and the samples. Generated addresses and private keys in examples are for demonstration only. Never place production private keys in application logs, analytics, browser storage, shared config files, or CI output. --- # Tested Exchange Examples URL: https://docs.c8ntinuum.com/guides/exchange-integration/tested-examples Use these examples when you want runnable samples behind the inline exchange-integration snippets. They are intentionally offline-first: tests do not require funded accounts or live RPC calls. ## Download Download [c8ntinuum-exchange-integration-examples.zip](/assets/downloads/c8ntinuum-exchange-integration-examples.zip) and unzip it: ```bash unzip c8ntinuum-exchange-integration-examples.zip ``` The archive unpacks to `examples/exchange-integration/`, which holds a `README.md`, the JavaScript sample in `javascript/` and the Java sample in `java/`. Run the commands on this page from the directory where you unzipped the archive. ## Layout | Path | Language | Covers | |------|----------|--------| | `examples/exchange-integration/javascript` | JavaScript | Key generation, `0x` and `c8` address conversion, address validation, native and ERC-20 transaction signing, raw transaction decoding. | | `examples/exchange-integration/java` | Java | Key generation, `0x` and `c8` address conversion, address validation, native and ERC-20 transaction signing, raw transaction decoding. | ## Prerequisites | Sample | Toolchain | Pinned dependencies | |--------|-----------|---------------------| | JavaScript | Node.js with npm | `ethers` 6.17.0, `@scure/base` 2.4.0 | | Java | JDK 17 or later, Maven | `web3j` 4.14.0, JUnit 5.12.2 | Install the JavaScript dependencies and run the full JavaScript test suite: ```bash npm --prefix examples/exchange-integration/javascript ci && npm --prefix examples/exchange-integration/javascript test ``` Run the full Java test suite. It needs Maven and a JDK, and Maven downloads the Java dependencies on the first `mvn` run: ```bash mvn -f examples/exchange-integration/java/pom.xml test ``` To run one group of tests at a time, use the commands in the sections below. ## Address generation and validation The tested samples back the snippets in [Address Generation and Validation](/guides/exchange-integration/address-generation-and-validation). ```bash npm --prefix examples/exchange-integration/javascript run test:address mvn -f examples/exchange-integration/java/pom.xml -Dtest=C8ntinuumAddressesTest test ``` ## Offline signing and decoding The tested samples back the snippets in [Offline Signing and Broadcasting](/guides/exchange-integration/offline-signing-and-broadcasting). They assert decoded transaction fields before any broadcast step: chain ID, sender, nonce, recipient, value, gas fields, data selector, and raw transaction hash. ```bash npm --prefix examples/exchange-integration/javascript run test:offline-signing mvn -f examples/exchange-integration/java/pom.xml -Dtest=OfflineSigningTest test ``` Never use the demonstration private keys or generated addresses in production systems. --- # Address Generation and Validation URL: https://docs.c8ntinuum.com/guides/exchange-integration/address-generation-and-validation c8ntinuum accounts use Ethereum-compatible `eth_secp256k1` keys. The same account can be represented as an EVM hex address (`0x...`) or a Cosmos Bech32 address (`c8...`). Exchange and custody systems can generate deposit addresses offline using standard Ethereum key derivation. The JavaScript and Java examples below are excerpted from compile-tested samples. See [Tested Exchange Examples](/guides/exchange-integration/tested-examples#address-generation-and-validation). ## Address rules | Item | Value | |------|-------| | Account key type | `eth_secp256k1` | | EVM address | 20-byte Ethereum address, displayed as `0x...` | | Cosmos account address | Bech32 with `c8` prefix | | Mainnet EVM chain ID | `8` / `0x8` | | Testnet EVM chain ID | `8888` / `0x22b8` | | Recommended wallet path | Ethereum-compatible path, usually `m/44'/60'/0'/0/index` | Use `0x...` for EVM JSON-RPC, contracts, wallet prompts, and token transfers. Use `c8...` for Cosmos REST/LCD account queries or IBC-facing UX. Both formats point to the same 20 account bytes. ## Memos and destination tags Memo support depends on the transfer path, not the address format. | Transfer path | Transaction memo support | Integration guidance | |---------------|--------------------------|----------------------| | EVM native CTM transfer | No standard memo or destination-tag field. Standard account-to-account transfers should use empty `data`. | Use unique deposit addresses and credit by transaction hash plus recipient address. | | EVM ERC-20 transfer | No memo or destination-tag field in the standard `transfer(address,uint256)` call. | Use unique deposit addresses. If routing metadata is required, use an application-specific contract flow instead of a standard ERC-20 transfer. | | Cosmos SDK transfer | Yes. Cosmos transactions can include a transaction-level `memo`. | Support this only for Cosmos-native send paths, and make sure the scanner reads, stores, and validates the transaction memo. | | IBC ICS-20 transfer | Yes. ICS-20 transfers include a message-level `memo` parameter. | Treat the memo as transfer message data for IBC routing or application metadata, not as part of the recipient address. | For exchange deposits, the safest default is one deposit address per user or account. Shared omnibus addresses require Cosmos-native or IBC memo processing and are not supported by standard EVM native or ERC-20 transfer semantics. ## Activation behavior There is no separate c8ntinuum-specific address activation transaction documented in this source set. A generated address is valid before it receives funds. Operationally, an address becomes visible to most account queries after it has a balance, nonce, code, or other on-chain account state. For deposit systems: - You can assign unused generated addresses before funding. - Treat an address as activated after the first successful funding transaction or account-state query returns data. - Validate syntax before accepting a withdrawal destination, but do not require the destination to already exist on-chain. - Require native `CTM` on sender accounts before constructing outgoing transactions because gas is paid in native CTM. ## JavaScript Install: ```bash npm install ethers @scure/base ``` Generate keys, derive addresses, and validate both formats: ```javascript function hexToC8(hexAddress) { const checksummed = getAddress(hexAddress); return bech32.encode("c8", bech32.toWords(getBytes(checksummed))); } function c8ToHex(c8Address) { const decoded = bech32.decode(c8Address); if (decoded.prefix !== "c8") throw new Error("invalid c8ntinuum prefix"); const bytes = new Uint8Array(bech32.fromWords(decoded.words)); if (bytes.length !== 20) throw new Error("invalid account byte length"); return getAddress(hexlify(bytes)); } function validateC8ntinuumAddress(address) { if (address.startsWith("0x")) return isAddress(address); if (address.startsWith("c8")) { try { c8ToHex(address); return true; } catch { return false; } } return false; } const randomWallet = Wallet.createRandom(); console.log({ privateKey: randomWallet.privateKey, publicKey: randomWallet.signingKey.publicKey, evmAddress: randomWallet.address, bech32Address: hexToC8(randomWallet.address), }); const mnemonic = Wallet.createRandom().mnemonic.phrase; const hdWallet = HDNodeWallet.fromPhrase(mnemonic, undefined, "m/44'/60'/0'/0/0"); console.log(validateC8ntinuumAddress(hdWallet.address)); ``` ## Java Dependencies: `web3j` for Ethereum keys. The tested Java sample includes a small Bech32 helper for `c8...` conversion. ```java public final class C8ntinuumAddressDemo { static String hexToC8(String hexAddress) { String checksum = Keys.toChecksumAddress(hexAddress); byte[] addressBytes = Numeric.hexStringToByteArray(checksum); return Bech32.encode("c8", addressBytes); } static String c8ToHex(String c8Address) { byte[] addressBytes = Bech32.decodeToBytes(c8Address, "c8"); if (addressBytes.length != 20) { throw new IllegalArgumentException("invalid account byte length"); } return Keys.toChecksumAddress(Numeric.toHexString(addressBytes)); } static boolean isValidC8ntinuumAddress(String address) { try { if (address.startsWith("0x")) return WalletUtils.isValidAddress(address); if (address.startsWith("c8")) { c8ToHex(address); return true; } return false; } catch (Exception error) { return false; } } public static void main(String[] args) throws Exception { ECKeyPair keyPair = Keys.createEcKeyPair(); BigInteger privateKey = keyPair.getPrivateKey(); BigInteger publicKey = keyPair.getPublicKey(); String evmAddress = Keys.toChecksumAddress("0x" + Keys.getAddress(publicKey)); String c8Address = hexToC8(evmAddress); System.out.println("privateKey=0x" + Numeric.toHexStringNoPrefixZeroPadded(privateKey, 64)); System.out.println("publicKey=0x" + Numeric.toHexStringNoPrefixZeroPadded(publicKey, 128)); System.out.println("evmAddress=" + evmAddress); System.out.println("c8Address=" + c8Address); System.out.println("valid=" + isValidC8ntinuumAddress(c8Address)); } } ``` ## Go Dependencies: `go-ethereum` for keys and `cosmos-sdk` for Bech32 conversion. ```go package main import ( "crypto/ecdsa" "fmt" "github.com/cosmos/cosmos-sdk/types/bech32" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) func HexToC8(hexAddress string) (string, error) { if !common.IsHexAddress(hexAddress) { return "", fmt.Errorf("invalid EVM address") } addr := common.HexToAddress(hexAddress) return bech32.ConvertAndEncode("c8", addr.Bytes()) } func C8ToHex(c8Address string) (common.Address, error) { prefix, bytes, err := bech32.DecodeAndConvert(c8Address) if err != nil { return common.Address{}, err } if prefix != "c8" || len(bytes) != common.AddressLength { return common.Address{}, fmt.Errorf("invalid c8ntinuum account address") } return common.BytesToAddress(bytes), nil } func main() { privateKey, err := crypto.GenerateKey() if err != nil { panic(err) } publicKey := privateKey.Public().(*ecdsa.PublicKey) evmAddress := crypto.PubkeyToAddress(*publicKey) c8Address, err := HexToC8(evmAddress.Hex()) if err != nil { panic(err) } fmt.Printf("privateKey=0x%x\n", crypto.FromECDSA(privateKey)) fmt.Printf("publicKey=0x%x\n", crypto.FromECDSAPub(publicKey)) fmt.Println("evmAddress=", evmAddress.Hex()) fmt.Println("c8Address=", c8Address) fmt.Println("isEvmValid=", common.IsHexAddress(evmAddress.Hex())) } ``` ## Rust Dependencies: `alloy` 2.x with default features for keys and address parsing, `bech32` 0.9 for `c8...` conversion, and `eyre`. The example uses the bech32 0.9 API; bech32 0.11 and later have a different API. ```rust use alloy::primitives::{Address, hex}; use alloy::signers::local::PrivateKeySigner; use bech32::{self, FromBase32, ToBase32, Variant}; fn hex_to_c8(address: Address) -> eyre::Result { Ok(bech32::encode("c8", address.as_slice().to_base32(), Variant::Bech32)?) } fn c8_to_hex(c8_address: &str) -> eyre::Result
{ let (prefix, words, variant) = bech32::decode(c8_address)?; if prefix != "c8" || variant != Variant::Bech32 { eyre::bail!("invalid c8ntinuum prefix or encoding"); } let bytes = Vec::::from_base32(&words)?; if bytes.len() != 20 { eyre::bail!("invalid account byte length"); } Ok(Address::from_slice(&bytes)) } fn main() -> eyre::Result<()> { let signer = PrivateKeySigner::random(); let address = signer.address(); let c8_address = hex_to_c8(address)?; let strict = Address::parse_checksummed(&address.to_checksum(None), None)?; let round_trip = c8_to_hex(&c8_address)?; println!("privateKey=0x{}", hex::encode(signer.to_bytes())); println!("evmAddress={}", strict.to_checksum(None)); println!("c8Address={}", c8_address); println!("roundTrip={}", round_trip.to_checksum(None)); Ok(()) } ``` ## Validation checklist - Accept `0x...` addresses only when they are 20 bytes and parse as valid Ethereum addresses. - Prefer EIP-55 checksum casing for stored and displayed `0x...` addresses. - Accept `c8...` only when Bech32 decoding succeeds, the prefix is exactly `c8`, and the decoded byte length is 20. - Store a canonical account identifier internally, such as lowercase 20-byte hex, then derive display formats as needed. - Reject validator operator (`c8valoper...`) and consensus (`c8valcons...`) addresses for normal user deposits unless the product explicitly supports validator operations. --- # Deposit Processing URL: https://docs.c8ntinuum.com/guides/exchange-integration/deposit-processing This page describes a durable deposit scanner for exchange and custody systems. It covers successful native CTM transfers, ERC-20 transfers, idempotent crediting, backfill, retries, and finality handling through node interfaces. ## Data sources | Deposit type | Primary source | Success signal | Notes | |--------------|----------------|----------------|-------| | Native CTM transfer | `eth_getBlockByNumber` plus transaction receipts | Receipt `status == 1`, transaction `to` is monitored, `value > 0` | Captures direct EVM native transfers. Internal value movements from contract execution require tracing or an indexer. | | Native CTM through its ERC-20 interface | `eth_getLogs` for `Transfer(address,address,uint256)` emitted by `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` | Log exists in a successful receipt | This address is native CTM exposed as an ERC-20. A call to its `transfer` or `transferFrom` moves native CTM in a transaction with `value == 0`, so the native transfer rule above does not see it. Credit the log amount as CTM. | | ERC-20 transfer | `eth_getLogs` for `Transfer(address,address,uint256)` | Log exists in a successful receipt | Credit by `transactionHash`, `logIndex`, token contract, recipient, and value. | | Cosmos SDK transfer | REST/gRPC or CometBFT transaction queries | Cosmos transaction code is success | Anyone can send CTM to the `c8...` form of a deposit address with a bank `MsgSend`, whether or not your product supports Cosmos-native sends. These transfers produce no EVM transaction or log. Search Cosmos transactions for `transfer.recipient='c8...'`, then credit only bank `MsgSend` and `MsgMultiSend` messages whose recipient is the deposit address, and take the `ctm` amount from the message. Skip `MsgEthereumTx` results: their `transfer` events include gas refunds to the sender and CTM moved through the ERC-20 interface above, which the EVM scanner already covers. Filter by message type in your code, because the public nodes return no results for a search that combines `transfer.recipient` with `message.action`. Alternatively, treat unexplained `eth_getBalance` increases on deposit addresses as unsolicited deposits for manual review. | For most exchange integrations, use EVM JSON-RPC as the primary source and keep a local scanner database with durable cursors. ## Scanner workflow 1. Maintain one cursor per chain and asset family: native CTM, each ERC-20 contract, and any Cosmos-native flow. 2. Poll `eth_blockNumber` and compute a safe target height. c8ntinuum has 1-block finality under standard CometBFT; exchanges should use 60 confirmations plus an optional 1-2 block scanner buffer for RPC, log, or receipt lag. 3. Scan bounded ranges. Keep range sizes below the current RPC provider's documented limits; the `eth_getLogs` block range cap is `10000`. 4. For native CTM, fetch full blocks and inspect transactions whose `to` is a monitored deposit address and `value` is non-zero. Also scan `Transfer` logs from `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` over the same ranges, as in step 6, because native CTM sent through its ERC-20 interface has `value == 0`. 5. Fetch the receipt for each candidate and require `status == 1`. 6. For ERC-20 deposits, query `eth_getLogs` by token contract, `Transfer` topic, and indexed recipient topics when practical. 7. Fetch receipts for log-bearing transactions when the RPC response does not already prove transaction success. 8. Upsert deposits idempotently before crediting user balances. 9. Advance the durable cursor only after all records in the scanned range are stored and reconciled. 10. Reconcile credited deposits against balances and explorer or indexer data during operations. ## Idempotency keys Use deterministic keys so retries cannot double-credit. | Asset type | Suggested idempotency key | |------------|---------------------------| | Native CTM | `chainId:txHash:native` | | Native CTM through its ERC-20 interface | `chainId:txHash:logIndex:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`, credited as CTM | | ERC-20 | `chainId:txHash:logIndex:tokenAddress` | | Cosmos SDK transfer | `chainId:txHash:eventIndex:messageIndex` | Store the raw block number, block hash, transaction hash, from address, to address, asset identifier, amount, receipt status, and credited account ID. ## Finality and confirmations c8ntinuum blocks have 1-block finality under standard CometBFT. Exchange integrations should require 60 confirmations before crediting deposits; at the expected 4s block time, that is about 240 seconds, or 4 minutes. Add an optional 1-2 block scanner buffer only to absorb RPC, log, or receipt lag. Even with BFT finality, scanners should handle: - RPC provider lag or inconsistent responses between endpoints. - Temporary `null` receipts immediately after block discovery. - Duplicate logs during retries. - Cursor rollback by operator action during backfills. - Contract-internal native transfers that are not visible without traces or an indexer. ## JavaScript scanner excerpt ```javascript const provider = new JsonRpcProvider("https://public-evm-rpc.c8ntinuum.com"); const chainId = 8; const transferTopic = id("Transfer(address,address,uint256)"); // Native CTM exposed as an ERC-20. Value-0 calls to its transfer or transferFrom // move native CTM and emit a standard Transfer log from this address. const NATIVE_CTM_ERC20 = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const monitored = new Set([ getAddress("0x1111111111111111111111111111111111111111"), getAddress("0x2222222222222222222222222222222222222222"), ]); async function scanNativeBlock(height) { const block = await provider.send("eth_getBlockByNumber", [ "0x" + height.toString(16), true, ]); for (const tx of block.transactions) { if (!tx.to || tx.value === "0x0") continue; const to = getAddress(tx.to); if (!monitored.has(to)) continue; const receipt = await provider.getTransactionReceipt(tx.hash); if (!receipt || receipt.status !== 1) continue; await upsertDeposit({ idempotencyKey: `${chainId}:${tx.hash}:native`, blockNumber: Number(receipt.blockNumber), blockHash: receipt.blockHash, txHash: tx.hash, from: getAddress(tx.from), to, asset: "CTM", amountWei: BigInt(tx.value).toString(), }); } } async function scanErc20(tokenAddress, fromBlock, toBlock) { for (const address of monitored) { const recipientTopic = zeroPadValue(address, 32); const logs = await provider.getLogs({ address: tokenAddress, topics: [transferTopic, null, recipientTopic], fromBlock, toBlock, }); for (const log of logs) { const receipt = await provider.getTransactionReceipt(log.transactionHash); if (!receipt || receipt.status !== 1) continue; await upsertDeposit({ idempotencyKey: `${chainId}:${log.transactionHash}:${log.index}:${tokenAddress}`, blockNumber: log.blockNumber, blockHash: log.blockHash, txHash: log.transactionHash, token: getAddress(tokenAddress), to: address, amountRaw: BigInt(log.data).toString(), }); } } } async function scanRange(fromBlock, toBlock, tokenAddresses) { for (let height = fromBlock; height <= toBlock; height++) { await scanNativeBlock(height); } // Credit NATIVE_CTM_ERC20 logs as native CTM, not as a separate token. for (const tokenAddress of [NATIVE_CTM_ERC20, ...tokenAddresses]) { await scanErc20(tokenAddress, fromBlock, toBlock); } } async function upsertDeposit(deposit) { // Store first, then credit from a separate idempotent ledger step. console.log(deposit); } ``` ## Java scanner excerpt ```java Web3j web3j = Web3j.build(new HttpService("https://public-evm-rpc.c8ntinuum.com")); long chainId = 8L; String transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; // Native CTM exposed as an ERC-20. Value-0 calls to its transfer or transferFrom // move native CTM and emit a standard Transfer log from this address. String nativeCtmErc20 = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; Set monitored = Set.of(Keys.toChecksumAddress("0x1111111111111111111111111111111111111111")); void scanNativeBlock(BigInteger height) throws Exception { EthBlock block = web3j .ethGetBlockByNumber(new DefaultBlockParameterNumber(height), true) .send(); for (EthBlock.TransactionResult result : block.getBlock().getTransactions()) { EthBlock.TransactionObject tx = (EthBlock.TransactionObject) result.get(); if (tx.getTo() == null || tx.getValue().signum() == 0) continue; String to = Keys.toChecksumAddress(tx.getTo()); if (!monitored.contains(to)) continue; EthGetTransactionReceipt receiptResponse = web3j.ethGetTransactionReceipt(tx.getHash()).send(); if (receiptResponse.getTransactionReceipt().isEmpty()) continue; var receipt = receiptResponse.getTransactionReceipt().get(); if (!receipt.isStatusOK()) continue; upsertDeposit(chainId + ":" + tx.getHash() + ":native", tx.getHash(), to, tx.getValue()); } } void scanErc20(String token, BigInteger fromBlock, BigInteger toBlock) throws Exception { for (String address : monitored) { String topicTo = "0x" + Numeric.cleanHexPrefix(address).toLowerCase(); topicTo = "0x" + "0".repeat(24) + Numeric.cleanHexPrefix(topicTo); EthFilter filter = new EthFilter( new DefaultBlockParameterNumber(fromBlock), new DefaultBlockParameterNumber(toBlock), token); filter.addSingleTopic(transferTopic); filter.addNullTopic(); filter.addSingleTopic(topicTo); EthLog logs = web3j.ethGetLogs(filter).send(); for (EthLog.LogResult item : logs.getLogs()) { EthLog.LogObject log = (EthLog.LogObject) item.get(); EthGetTransactionReceipt receiptResponse = web3j.ethGetTransactionReceipt(log.getTransactionHash()).send(); if (receiptResponse.getTransactionReceipt().isEmpty()) continue; if (!receiptResponse.getTransactionReceipt().get().isStatusOK()) continue; String key = chainId + ":" + log.getTransactionHash() + ":" + log.getLogIndex() + ":" + token; upsertDeposit(key, log.getTransactionHash(), address, Numeric.toBigInt(log.getData())); } } } void scanRange(BigInteger fromBlock, BigInteger toBlock, List tokens) throws Exception { for (BigInteger height = fromBlock; height.compareTo(toBlock) <= 0; height = height.add(BigInteger.ONE)) { scanNativeBlock(height); } // Credit nativeCtmErc20 logs as native CTM, not as a separate token. scanErc20(nativeCtmErc20, fromBlock, toBlock); for (String token : tokens) { scanErc20(token, fromBlock, toBlock); } } void upsertDeposit(String idempotencyKey, String txHash, String to, BigInteger amount) { System.out.println(idempotencyKey + " " + txHash + " " + to + " " + amount); } ``` ## Go scanner excerpt ```go package scanner import ( "context" "fmt" "math/big" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/crypto" ) var transferTopic = crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) // NativeCTMERC20 is native CTM exposed as an ERC-20. Value-0 calls to its // transfer or transferFrom move native CTM and emit a standard Transfer log. var NativeCTMERC20 = common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE") func ScanNativeBlock(ctx context.Context, client *ethclient.Client, chainID int64, height uint64, monitored map[common.Address]bool) error { block, err := client.BlockByNumber(ctx, new(big.Int).SetUint64(height)) if err != nil { return err } for _, tx := range block.Transactions() { if tx.To() == nil || tx.Value().Sign() == 0 || !monitored[*tx.To()] { continue } receipt, err := client.TransactionReceipt(ctx, tx.Hash()) if err != nil || receipt.Status != types.ReceiptStatusSuccessful { continue } key := fmt.Sprintf("%d:%s:native", chainID, tx.Hash()) if err := UpsertDeposit(key, tx.Hash(), *tx.To(), tx.Value()); err != nil { return err } } return nil } func ScanERC20(ctx context.Context, client *ethclient.Client, chainID int64, token common.Address, fromBlock, toBlock uint64, recipients []common.Address) error { for _, recipient := range recipients { query := ethereum.FilterQuery{ FromBlock: new(big.Int).SetUint64(fromBlock), ToBlock: new(big.Int).SetUint64(toBlock), Addresses: []common.Address{token}, Topics: [][]common.Hash{ {transferTopic}, nil, {common.BytesToHash(recipient.Bytes())}, }, } logs, err := client.FilterLogs(ctx, query) if err != nil { return err } for _, log := range logs { receipt, err := client.TransactionReceipt(ctx, log.TxHash) if err != nil || receipt.Status != types.ReceiptStatusSuccessful { continue } amount := new(big.Int).SetBytes(log.Data) key := fmt.Sprintf("%d:%s:%d:%s", chainID, log.TxHash, log.Index, token) if err := UpsertDeposit(key, log.TxHash, recipient, amount); err != nil { return err } } } return nil } func ScanRange(ctx context.Context, client *ethclient.Client, chainID int64, fromBlock, toBlock uint64, monitored map[common.Address]bool, tokens []common.Address) error { for height := fromBlock; height <= toBlock; height++ { if err := ScanNativeBlock(ctx, client, chainID, height, monitored); err != nil { return err } } recipients := make([]common.Address, 0, len(monitored)) for address := range monitored { recipients = append(recipients, address) } // Credit NativeCTMERC20 logs as native CTM, not as a separate token. for _, token := range append([]common.Address{NativeCTMERC20}, tokens...) { if err := ScanERC20(ctx, client, chainID, token, fromBlock, toBlock, recipients); err != nil { return err } } return nil } func UpsertDeposit(key string, txHash common.Hash, to common.Address, amount *big.Int) error { return nil } ``` ## Rust scanner excerpt Dependencies: `alloy` 2.x with default features, `tokio` with the `macros` and `rt-multi-thread` features, and `eyre`. ```rust use alloy::consensus::Transaction as _; use alloy::network::TransactionResponse as _; use alloy::primitives::{address, keccak256, Address, B256, U256}; use alloy::providers::{Provider, ProviderBuilder}; use alloy::rpc::types::{BlockNumberOrTag, Filter}; use std::collections::HashSet; const CHAIN_ID: u64 = 8; // Native CTM exposed as an ERC-20. Value-0 calls to its transfer or transferFrom // move native CTM and emit a standard Transfer log from this address. const NATIVE_CTM_ERC20: Address = address!("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"); async fn scan_native_block( provider: &P, height: u64, monitored: &HashSet
, ) -> eyre::Result<()> { let block = provider .get_block_by_number(BlockNumberOrTag::Number(height)) .full() .await? .ok_or_else(|| eyre::eyre!("missing block"))?; for tx in block.transactions.into_transactions() { let Some(to) = tx.to() else { continue }; if tx.value() == U256::ZERO || !monitored.contains(&to) { continue; } let Some(receipt) = provider.get_transaction_receipt(tx.tx_hash()).await? else { continue; }; if !receipt.status() { continue; } let key = format!("{CHAIN_ID}:{}:native", tx.tx_hash()); upsert_deposit(&key, tx.tx_hash(), to, tx.value()).await?; } Ok(()) } async fn scan_erc20( provider: &P, token: Address, from_block: u64, to_block: u64, recipients: &[Address], ) -> eyre::Result<()> { let transfer_topic: B256 = keccak256("Transfer(address,address,uint256)"); for recipient in recipients { let filter = Filter::new() .address(token) .event_signature(transfer_topic) .topic2(B256::left_padding_from(recipient.as_slice())) .from_block(from_block) .to_block(to_block); for log in provider.get_logs(&filter).await? { let Some(receipt) = provider.get_transaction_receipt(log.transaction_hash.unwrap()).await? else { continue; }; if !receipt.status() { continue; } let amount = U256::from_be_slice(&log.data().data); let key = format!( "{CHAIN_ID}:{}:{}:{}", log.transaction_hash.unwrap(), log.log_index.unwrap_or_default(), token ); upsert_deposit(&key, log.transaction_hash.unwrap(), *recipient, amount).await?; } } Ok(()) } async fn upsert_deposit(key: &str, tx_hash: B256, to: Address, amount: U256) -> eyre::Result<()> { println!("{key} {tx_hash} {to} {amount}"); Ok(()) } #[tokio::main] async fn main() -> eyre::Result<()> { let provider = ProviderBuilder::new().connect_http( "https://public-evm-rpc.c8ntinuum.com".parse()?, ); let monitored: HashSet
= HashSet::new(); scan_native_block(&provider, 123456, &monitored).await?; // Credit NATIVE_CTM_ERC20 logs as native CTM, not as a separate token. let recipients: Vec
= monitored.iter().copied().collect(); scan_erc20(&provider, NATIVE_CTM_ERC20, 123456, 123456, &recipients).await?; Ok(()) } ``` ## Reconciliation checklist - Compare scanner height to `eth_blockNumber` and CometBFT `/status`. - Verify every credited transaction has a successful receipt. - Re-run the scanner over already-credited ranges and confirm idempotency prevents duplicate credits. - Reconcile hot-wallet, cold-wallet, and deposit-address balances with `eth_getBalance` and token `balanceOf`. - Investigate any deposit-address balance increase that no credited transaction or log explains. A Cosmos bank `MsgSend` to the address's `c8...` form is invisible to EVM scanning; treat it as an unsolicited deposit that needs manual review. - Alert on cursor stalls, receipt fetch failures, RPC `429` responses, unusually large block lag, and unmatched pending credits. --- # Offline Signing and Broadcasting URL: https://docs.c8ntinuum.com/guides/exchange-integration/offline-signing-and-broadcasting c8ntinuum EVM transactions use standard Ethereum transaction construction, signing, raw-transaction encoding, broadcasting, and receipt polling. Use `eth_sendRawTransaction` to submit an offline-signed withdrawal. The JavaScript and Java examples below are excerpted from compile-tested samples. See [Tested Exchange Examples](/guides/exchange-integration/tested-examples#offline-signing-and-decoding). ## Online and offline split | Step | Environment | Data | |------|-------------|------| | Fetch nonce and fees | Online | `eth_getTransactionCount`, `eth_gasPrice`, `eth_maxPriorityFeePerGas`, `eth_estimateGas` | | Build unsigned transaction | Online or signing coordinator | `chainId`, `nonce`, `to`, `value`, gas fields, optional `data` | | Sign transaction | Offline signer or HSM | Private key, unsigned transaction | | Analyze signed transaction | Offline and online | Hash, sender, chain ID, nonce, recipient, value, data selector | | Broadcast | Online | Raw signed transaction bytes via `eth_sendRawTransaction` | | Confirm | Online | `eth_getTransactionReceipt` with `status == 1` | Never move private keys to the online broadcasting host. Move only unsigned transaction data into the signer and signed raw transactions out of it. The examples below run the online and offline steps in one process for brevity. In production, run only the signing step, with the private key, on the isolated signer or HSM, and pass it the unsigned transaction fields that the online host prepared. ## Transaction fields | Field | Native CTM transfer | ERC-20 transfer | |-------|---------------------|-----------------| | `to` | Recipient account | Token contract | | `value` | Amount in wei | `0` | | `data` | Empty | ABI-encoded `transfer(address,uint256)` | | `gasLimit` | Usually `21000` | Use `eth_estimateGas` plus policy buffer | | `chainId` | `8` mainnet, `8888` testnet | Same | | `type` | Dynamic fee transaction is recommended | Dynamic fee transaction is recommended | ## JavaScript ```javascript const provider = new JsonRpcProvider("https://public-evm-rpc.c8ntinuum.com"); const chainId = 8; const privateKey = process.env.PRIVATE_KEY; const signer = new Wallet(privateKey); async function feeFields() { const feeData = await provider.getFeeData(); return { maxFeePerGas: feeData.maxFeePerGas ?? feeData.gasPrice, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0n, }; } async function signNativeTransfer(to) { const from = await signer.getAddress(); const nonce = await provider.getTransactionCount(from, "pending"); const fees = await feeFields(); const unsignedTx = { type: 2, chainId, nonce, to, value: parseEther("1.0"), gasLimit: 21000n, ...fees, }; const raw = await signer.signTransaction(unsignedTx); const decoded = Transaction.from(raw); console.log({ hash: decoded.hash, from: decoded.from, chainId: decoded.chainId?.toString(), nonce: decoded.nonce, to: decoded.to, value: decoded.value.toString(), }); return raw; } async function signErc20Transfer(token, recipient) { const from = await signer.getAddress(); const nonce = await provider.getTransactionCount(from, "pending"); const erc20 = new Interface(["function transfer(address to,uint256 amount) returns (bool)"]); const data = erc20.encodeFunctionData("transfer", [ recipient, parseUnits("12.5", 18), ]); const fees = await feeFields(); const gasLimit = await provider.estimateGas({ from, to: token, value: 0, data, }); return signer.signTransaction({ type: 2, chainId, nonce, to: token, value: 0, data, gasLimit: (gasLimit * 120n) / 100n, ...fees, }); } async function broadcast(raw) { const sent = await provider.broadcastTransaction(raw); const receipt = await sent.wait(); if (receipt.status !== 1) throw new Error(`transaction failed: ${sent.hash}`); return receipt; } ``` ## Java ```java Web3j web3j = Web3j.build(new HttpService("https://public-evm-rpc.c8ntinuum.com")); Credentials credentials = Credentials.create(System.getenv("PRIVATE_KEY")); long chainId = 8L; BigInteger nonce = web3j .ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.PENDING) .send() .getTransactionCount(); BigInteger maxPriorityFeePerGas = web3j.ethMaxPriorityFeePerGas().send().getMaxPriorityFeePerGas(); BigInteger maxFeePerGas = web3j.ethGasPrice().send().getGasPrice().multiply(BigInteger.valueOf(2)); RawTransaction nativeTx = RawTransaction.createEtherTransaction( chainId, nonce, BigInteger.valueOf(21_000), "0x1111111111111111111111111111111111111111", Convert.toWei("1.0", Convert.Unit.ETHER).toBigInteger(), maxPriorityFeePerGas, maxFeePerGas); byte[] signed = TransactionEncoder.signMessage(nativeTx, chainId, credentials); String raw = Numeric.toHexString(signed); var decoded = TransactionDecoder.decode(raw); System.out.println("raw=" + raw); System.out.println("hash=" + org.web3j.crypto.Hash.sha3(raw)); System.out.println("from=" + credentials.getAddress()); System.out.println("chainId=" + chainId); System.out.println("nonce=" + decoded.getNonce()); System.out.println("to=" + decoded.getTo()); System.out.println("value=" + decoded.getValue()); System.out.println("gasLimit=" + decoded.getGasLimit()); System.out.println("maxPriorityFeePerGas=" + maxPriorityFeePerGas); System.out.println("maxFeePerGas=" + maxFeePerGas); EthSendTransaction sent = web3j.ethSendRawTransaction(raw).send(); String txHash = sent.getTransactionHash(); System.out.println("broadcastHash=" + txHash); Function transfer = new Function( "transfer", Arrays.asList(new Address("0x1111111111111111111111111111111111111111"), new Uint256(new BigInteger("12500000000000000000"))), Arrays.asList(new TypeReference() {})); String data = FunctionEncoder.encode(transfer); String token = "0xc8Fb80fCc03f699C70ff0CC08C09106288888888"; BigInteger estimatedGas = web3j .ethEstimateGas(Transaction.createEthCallTransaction(credentials.getAddress(), token, data)) .send() .getAmountUsed(); BigInteger erc20GasLimit = estimatedGas.multiply(BigInteger.valueOf(120)).divide(BigInteger.valueOf(100)); RawTransaction erc20Tx = RawTransaction.createTransaction( chainId, nonce.add(BigInteger.ONE), erc20GasLimit, token, BigInteger.ZERO, data, maxPriorityFeePerGas, maxFeePerGas); String erc20Raw = Numeric.toHexString( TransactionEncoder.signMessage(erc20Tx, chainId, credentials)); var decodedErc20 = TransactionDecoder.decode(erc20Raw); // web3j 4.14 returns decoded data without the 0x prefix. System.out.println("erc20DataSelector=" + Numeric.prependHexPrefix(decodedErc20.getData()).substring(0, 10)); ``` ## Go ```go package withdrawals use alloy::eips::eip2718::Encodable2718; use alloy::network::TxSignerSync; use alloy::primitives::{address, Address, Bytes, TxKind, U256}; use alloy::providers::{Provider, ProviderBuilder}; use alloy::signers::local::PrivateKeySigner; fn build_native_tx( nonce: u64, to: alloy::primitives::Address, value: U256, max_fee_per_gas: u128, max_priority_fee_per_gas: u128, ) -> TxEip1559 { TxEip1559 { chain_id: 8, nonce, gas_limit: 21_000, max_fee_per_gas, max_priority_fee_per_gas, to: TxKind::Call(to), value, input: Bytes::new(), access_list: Default::default(), } } fn sign_offline(tx: &mut TxEip1559, private_key: &str) -> eyre::Result> { let signer: PrivateKeySigner = private_key.parse()?; let signature = signer.sign_transaction_sync(tx)?; let signed = tx.clone().into_signed(signature); let envelope: TxEnvelope = signed.into(); Ok(envelope.encoded_2718()) } #[tokio::main] async fn main() -> eyre::Result<()> { let provider = ProviderBuilder::new().connect_http( "https://public-evm-rpc.c8ntinuum.com".parse()?, ); // Online: read the sender's nonce and current fees. Leave headroom over the base fee. let from: Address = std::env::var("FROM_ADDRESS")?.parse()?; let nonce = provider.get_transaction_count(from).pending().await?; let max_priority_fee_per_gas = provider.get_max_priority_fee_per_gas().await?; let max_fee_per_gas = provider.get_gas_price().await? * 2; let mut tx = build_native_tx( nonce, address!("1111111111111111111111111111111111111111"), U256::from(1_000_000_000_000_000_000u128), max_fee_per_gas, max_priority_fee_per_gas, ); let raw = sign_offline(&mut tx, &std::env::var("PRIVATE_KEY")?)?; let pending = provider.send_raw_transaction(&raw).await?; let receipt = pending.get_receipt().await?; if !receipt.status() { eyre::bail!("transaction failed"); } Ok(()) } ``` ## Signed data analysis Before broadcasting, decode the raw signed transaction and verify: - `chainId` is `8` for mainnet or `8888` for testnet. - `from` matches the expected hot wallet, withdrawal wallet, HSM key, or custody account. - `nonce` matches the reserved nonce in your withdrawal database. - `to`, `value`, and `data` match the approved withdrawal instruction. - ERC-20 withdrawals call `transfer(address,uint256)` on the intended token contract. - Gas fields satisfy your policy and are not extreme. - The raw transaction hash is stored before broadcast. ## Broadcasting and polling Broadcast with `eth_sendRawTransaction`, then poll `eth_getTransactionReceipt` by hash until one of these happens: | Result | Action | |--------|--------| | Receipt `status == 1` | Mark withdrawal confirmed. | | Receipt `status == 0` | Mark failed and investigate before retrying. | | `null` receipt | Continue polling until timeout. | | Nonce too low | Check if the transaction was already mined or replaced. | | Transaction underpriced | Rebuild with a replacement transaction using the same nonce and higher fee, if policy allows. | | Insufficient funds | Stop retries until the sender has native CTM for value plus gas. | Store every raw transaction, decoded transaction, broadcast hash, receipt, and replacement attempt for auditability. --- # Deploy with Foundry URL: https://docs.c8ntinuum.com/guides/deploy-contract/foundry ## Prerequisites - [Foundry](https://book.getfoundry.sh/getting-started/installation) installed - A wallet with CTM for gas fees - RPC endpoint: `https://public-evm-rpc.c8ntinuum.com` ## Step 1: Create a Project ```bash forge init my-project cd my-project ``` ## Step 2: Write Your Contract ```solidity // src/Counter.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } } ``` ## Step 3: Compile c8ntinuum runs the Prague EVM rules and Osaka is not active, so Osaka features such as the `CLZ` opcode are unavailable. Recent Foundry versions compile for `osaka` by default, so set `evm_version = "prague"` under `[profile.default]` in `foundry.toml` (see [Configuration](#configuration)) before you build. ```bash forge build ``` ## Step 4: Deploy ```bash forge create --rpc-url https://public-evm-rpc.c8ntinuum.com \ --private-key $PRIVATE_KEY \ --broadcast \ src/Counter.sol:Counter ``` ## Step 5: Verify (Optional) See [Verify with Foundry](/guides/verify-contract/foundry) for contract verification on CTMScan. ## Configuration Add c8ntinuum to your `foundry.toml`: ```toml [profile.default] src = "src" out = "out" libs = ["lib"] evm_version = "prague" [rpc_endpoints] continuum = "https://public-evm-rpc.c8ntinuum.com" ``` Then deploy with: ```bash forge create --rpc-url continuum \ --private-key $PRIVATE_KEY \ --broadcast \ src/Counter.sol:Counter ``` ## Useful Commands | Command | Description | |---------|-------------| | `cast chain-id --rpc-url continuum` | Verify chain ID (should return `8`) | | `cast balance $ADDRESS --rpc-url continuum` | Check CTM balance | | `cast send $TO --value 1ether --rpc-url continuum --private-key $PRIVATE_KEY` | Send CTM | | `cast call $CONTRACT "method()" --rpc-url continuum` | Call a contract method | --- # Deploy with Hardhat URL: https://docs.c8ntinuum.com/guides/deploy-contract/hardhat ## Prerequisites - [Node.js](https://nodejs.org/) v20+ - A wallet with CTM for gas fees This guide targets Hardhat 2. The `hh2` npm tags install Hardhat 2 and `@nomicfoundation/hardhat-toolbox` 6.x, which includes `hardhat-ethers` and `hardhat-verify`. The untagged `hardhat` package now installs Hardhat 3, which uses an ESM config and a different setup, so the steps below do not apply to it. ## Step 1: Create a Project ```bash mkdir my-project && cd my-project npm init -y npm install --save-dev hardhat@hh2 @nomicfoundation/hardhat-toolbox@hh2 npx hardhat init ``` Select "Create a JavaScript project" when prompted. ## Step 2: Configure c8ntinuum Network Update `hardhat.config.js`: ```javascript require("@nomicfoundation/hardhat-toolbox"); module.exports = { solidity: "0.8.20", networks: { continuum: { url: "https://public-evm-rpc.c8ntinuum.com", chainId: 8, accounts: [process.env.PRIVATE_KEY], }, }, }; ``` ## Step 3: Write Your Contract ```solidity // contracts/Counter.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } } ``` ## Step 4: Write Deploy Script ```javascript // scripts/deploy.js const hre = require("hardhat"); async function main() { const Counter = await hre.ethers.getContractFactory("Counter"); const counter = await Counter.deploy(); await counter.waitForDeployment(); console.log("Counter deployed to:", await counter.getAddress()); } main().catch(console.error); ``` ## Step 5: Deploy ```bash PRIVATE_KEY=your_key npx hardhat run scripts/deploy.js --network continuum ``` ## Step 6: Verify (Optional) See [Verify with Hardhat](/guides/verify-contract/hardhat) for contract verification. --- # Deploy with Remix URL: https://docs.c8ntinuum.com/guides/deploy-contract/remix ## Prerequisites - A browser with [MetaMask](https://metamask.io) installed - MetaMask configured with the c8ntinuum network (see [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet)) - CTM in your wallet for gas fees ## Step 1: Open Remix Navigate to [remix.ethereum.org](https://remix.ethereum.org). ## Step 2: Write or Import Your Contract Create a new file in the **File Explorer** panel or import an existing contract. ## Step 3: Compile 1. Go to the **Solidity Compiler** tab 2. Select the appropriate compiler version 3. Click **Compile** ## Step 4: Deploy 1. Go to the **Deploy & Run Transactions** tab 2. In the **Environment** dropdown, select **Injected Provider - MetaMask** 3. MetaMask will prompt you to connect — ensure you're on the c8ntinuum network (Chain ID `8`) 4. Select your contract and click **Deploy** 5. Confirm the transaction in MetaMask ## Step 5: Interact Once deployed, the contract appears in the **Deployed Contracts** section. You can call functions directly from the Remix interface. --- # Verify with Foundry URL: https://docs.c8ntinuum.com/guides/verify-contract/foundry ## CTMScan endpoints | Network | Explorer | Verifier API | EVM RPC | | --- | --- | --- | --- | | Mainnet | [ctmscan.io](https://ctmscan.io/) | `https://ctmscan.io/api` | `https://public-evm-rpc.c8ntinuum.com` | | Testnet | [testnet.ctmscan.io](https://testnet.ctmscan.io/) | `https://testnet.ctmscan.io/api` | `https://public-testnet-evm.c8ntinuum.link` | ## API keys CTMScan verification can be submitted anonymously; no signup URL is required. Anonymous API traffic is limited to `3 r/s` on both mainnet and testnet. If CTMScan later provides an API key for your account, pass it with `--etherscan-api-key $CTMSCAN_API_KEY`. ## Published scope This guide documents the Foundry Etherscan-compatible verification flow against CTMScan. The supported source format is Solidity Standard JSON. Flattened source, proxy verification, and library-linking behavior should be confirmed against CTMScan before relying on them in release automation. The testnet verifier API mirrors mainnet at `https://testnet.ctmscan.io/api`. ## Verify on mainnet After deploying with Foundry, verify your contract on CTMScan mainnet. CTMScan runs Blockscout, so pass `--verifier blockscout`. Without `--verifier` or an API key, Foundry defaults to Sourcify, which does not match CTMScan's API. ```bash forge verify-contract \ --rpc-url https://public-evm-rpc.c8ntinuum.com \ --verifier blockscout \ --verifier-url https://ctmscan.io/api \ --chain-id 8 \ $CONTRACT_ADDRESS \ src/Counter.sol:Counter ``` ## With Constructor Arguments If your contract has constructor arguments: ```bash forge verify-contract \ --rpc-url https://public-evm-rpc.c8ntinuum.com \ --verifier blockscout \ --verifier-url https://ctmscan.io/api \ --chain-id 8 \ --constructor-args $(cast abi-encode "constructor(uint256)" 42) \ $CONTRACT_ADDRESS \ src/MyContract.sol:MyContract ``` ## Deploy and Verify in One Step ```bash forge create --rpc-url https://public-evm-rpc.c8ntinuum.com \ --private-key $PRIVATE_KEY \ --broadcast \ --verify \ --verifier blockscout \ --verifier-url https://ctmscan.io/api \ src/Counter.sol:Counter ``` ## Testnet status | Item | Status | Source | | --- | --- | --- | | Testnet chain ID | Available now | [Network Information - Testnet](/developer-essentials/network-information/testnet) | | Public testnet EVM RPC | Available now | [Network Information - Testnet](/developer-essentials/network-information/testnet) | | Testnet explorer | Available now | [Block Explorers](/tooling-and-infrastructure/block-explorers) | | Testnet verifier API | Available now | `https://testnet.ctmscan.io/api` - see [Block Explorers](/tooling-and-infrastructure/block-explorers) | --- # Verify with Hardhat URL: https://docs.c8ntinuum.com/guides/verify-contract/hardhat ## CTMScan endpoints | Network | Explorer | Verifier API | EVM RPC | | --- | --- | --- | --- | | Mainnet | [ctmscan.io](https://ctmscan.io/) | `https://ctmscan.io/api` | `https://public-evm-rpc.c8ntinuum.com` | | Testnet | [testnet.ctmscan.io](https://testnet.ctmscan.io/) | `https://testnet.ctmscan.io/api` | `https://public-testnet-evm.c8ntinuum.link` | ## API keys CTMScan verification can be submitted anonymously, and **anonymous flow is the supported path today** - no signup URL exists. Anonymous traffic is rate-limited to `3 r/s` on both mainnet and testnet, which is sufficient for typical Hardhat verification jobs. The Hardhat Etherscan plugin still requires an `apiKey` field to be present in config; use the placeholder value shown below to keep the plugin configuration valid. If CTMScan exposes per-account API keys later, replace the placeholder with `process.env.CTMSCAN_API_KEY`. ## Published scope This guide documents the Hardhat Etherscan-compatible verification flow against CTMScan. The supported source format is Solidity Standard JSON. Flattened source, proxy verification, and library-linking behavior should be confirmed against CTMScan before relying on them in release automation. The testnet verifier API mirrors mainnet at `https://testnet.ctmscan.io/api`. ## Configure Verification Add the explorer configuration to `hardhat.config.js`. This config is for Hardhat 2 with the `hardhat-verify` plugin that `@nomicfoundation/hardhat-toolbox@hh2` includes; set up the project as in [Deploy with Hardhat](/guides/deploy-contract/hardhat). Hardhat 3 uses a different verification config. ```javascript module.exports = { // ... existing config networks: { continuum: { url: "https://public-evm-rpc.c8ntinuum.com", chainId: 8, accounts: [process.env.PRIVATE_KEY], }, }, etherscan: { apiKey: { continuum: process.env.CTMSCAN_API_KEY ?? "ctmscan", }, customChains: [ { network: "continuum", chainId: 8, urls: { apiURL: "https://ctmscan.io/api", browserURL: "https://ctmscan.io", }, }, ], }, }; ``` ## Verify on mainnet ```bash npx hardhat verify --network continuum $CONTRACT_ADDRESS ``` ## With Constructor Arguments ```bash npx hardhat verify --network continuum $CONTRACT_ADDRESS "arg1" "arg2" ``` Or create an arguments file: ```javascript // arguments.js module.exports = [42, "0x..."]; ``` ```bash npx hardhat verify --network continuum --constructor-args arguments.js $CONTRACT_ADDRESS ``` ## Testnet status | Item | Status | Source | | --- | --- | --- | | Testnet chain ID | Available now | [Network Information - Testnet](/developer-essentials/network-information/testnet) | | Public testnet EVM RPC | Available now | [Network Information - Testnet](/developer-essentials/network-information/testnet) | | Testnet explorer | Available now | [Block Explorers](/tooling-and-infrastructure/block-explorers) | | Testnet verifier API | Available now | `https://testnet.ctmscan.io/api` - see [Block Explorers](/tooling-and-infrastructure/block-explorers) | --- # FAQ URL: https://docs.c8ntinuum.com/faq Yes. c8ntinuum runs standard EVM bytecode and exposes Ethereum-compatible JSON-RPC endpoints. Standard tools such as Hardhat, Foundry, Remix, MetaMask, and ethers.js can connect with the published network values. c8ntinuum is a **universal interoperability solution** with a **forward-compatible EVM**. It provides trust-minimized cross-chain messaging via light clients and zk proofs, exposes advanced cryptographic primitives (zk verification, post-quantum signatures, Ed25519 signature verification) as precompiles, and gives Solidity contracts direct access to Cosmos SDK modules. **CTM** — used for gas fees, staking, and governance. Native CTM uses 18-decimal precision (like ETH). **8** (EVM chain ID). There is also a Cosmos chain ID string used for IBC and consensus: `c8ntinuum_8-1` on mainnet. `eth_secp256k1` — the same curve as Ethereum. Your Ethereum wallet derives the same address on c8ntinuum. c8ntinuum uses standard CometBFT consensus with 1-block finality. Expected block time is 4s. Yes. c8ntinuum uses IBC for trust-minimized cross-chain communication. The ICS02 and ICS20 precompiles expose IBC functionality directly to Solidity. c8ntinuum provides a curated set of precompiles organized into five families: cross-chain verification (IBC, zkVM), Cosmos modules (bank, staking, governance), cryptography (Ed25519, P-256, Schnorr, FROST, VRF, post-quantum), hashing (SHA-3, BLAKE2b, Poseidon), and data handling (JSON, address compression). See [Modules & Precompiles](/modules/overview). 13 widely-used contracts are available at genesis: Multicall3, Permit2, WETH9, Safe singleton factory, CreateX, and more. See [Pre-deployed Contracts](/modules/predeployed-contracts). The `ctmd` node source, licensed under Apache 2.0, will be published in a public GitHub repository; the repository is to be announced. c8ntinuum is based on and extends Cosmos EVM. Until then, use the release binaries listed in [resources.c8ntinuum.io/metadata.json](https://resources.c8ntinuum.io/metadata.json). --- # Brand Kit URL: https://docs.c8ntinuum.com/brand-kit Use these assets when you need to represent c8ntinuum in partner announcements, media coverage, exchange listings, wallet interfaces, app directories, or ecosystem materials. Includes the official full logo, CTM symbol, favicon, square icon, and round icon assets. Verify official profiles, explorers, metadata, and ecosystem resources. ## Logo Assets Use SVG files for scalable placement and PNG files when a platform requires raster artwork.
c8ntinuum full logo in black

Full logo, black

Use on white or light neutral backgrounds.

c8ntinuum full logo in white

Full logo, white

Use on black or dark neutral backgrounds.

CTM symbol in black

CTM symbol, black

Use as a token icon, compact app icon, or small-space mark on light backgrounds.

SVG
CTM symbol in white

CTM symbol, white

Use as a token icon, compact app icon, or small-space mark on dark backgrounds.

SVG
CTM square icon for dark placements

Square icon, dark

Use for app tiles, listings, and profile placements that need a self-contained dark icon.

PNG
CTM square icon for light placements

Square icon, light

Use for app tiles, listings, and profile placements that need a self-contained light icon.

PNG
CTM round icon for dark placements

Round icon, dark

Use for circular profile images, token lists, and compact social placements that need a dark icon.

PNG
CTM round icon for light placements

Round icon, light

Use for circular profile images, token lists, and compact social placements that need a light icon.

PNG
## Naming | Name | Use | |------|-----| | **c8ntinuum** | Product, protocol, network, official handles, domains, listings, and places where the exact public spelling is required. | | **CTM** | Native asset and ticker symbol. Do not use `CTM` as a replacement for the protocol name. | ## Colors and Type | Token | Hex | Use | |-------|-----|-----| | **Black** | `#000000` | Primary logo color and high-contrast text. | | **Near black** | `#0E0E0E` | Dark surfaces and dark-mode backgrounds. | | **White** | `#FFFFFF` | Logo color on dark backgrounds. | | **Light gray** | `#F1F1F1` | Light page backgrounds and neutral surfaces. | | **Mid gray** | `#CCCCCC` | Secondary dark-mode accents. | The docs interface uses **The Future** font family. Do not recreate the wordmark as editable text; use the provided SVG or PNG assets. ## Usage Rules Do: - Use the official files from this page. - Preserve the original proportions and clear space around the mark. - Choose the black asset for light backgrounds and the white asset for dark backgrounds. - Keep enough contrast for small token icons, exchange listings, and app directory thumbnails. - Link back to the official website or docs when context is helpful. Do not: - Recolor, rotate, stretch, skew, outline, crop, or add effects to the marks. - Place the marks on busy imagery or low-contrast backgrounds. - Combine the marks with unrelated symbols in a way that creates a new logo. - Use c8ntinuum assets to imply endorsement, partnership, or official status without permission. - Rename the protocol, alter `c8ntinuum`, or change the `CTM` ticker capitalization. ## Press Boilerplate c8ntinuum is an interoperability platform built to unify Web3. By connecting multiple blockchains into one cooperative ecosystem, it enables users, developers, and protocols to interact across chains through a single multichain platform. ## Technical Boilerplate c8ntinuum is a universal interoperability solution with a forward-compatible EVM layer, built with trust minimization at its core. It emphasizes authenticated cross-chain communication through light-client verification and on-chain zk proof verification, while giving developers Ethereum-compatible tooling and access to Cosmos-native modules and advanced cryptographic primitives. --- # Official Links URL: https://docs.c8ntinuum.com/official-links ## Project metadata | Field | Value | |-------|-------| | **Name** | `c8ntinuum` | | **Symbol** | `CTM` | | **Description** | Multichain powered protocol unleashing unrivaled mechanisms designed to achieve ultimate interoperability and long term sustainability | | **Image URI** | [ctm_dark.svg](https://raw.githubusercontent.com/c8ntinuum/resources/main/ctm_dark.svg) | | **Metadata** | [resources.c8ntinuum.io/metadata.json](https://resources.c8ntinuum.io/metadata.json) | ## Official profiles | Resource | URL | |----------|-----| | **Website** | [c8ntinuum.com](https://c8ntinuum.com) | | **GitHub** | [github.com/c8ntinuum](https://github.com/c8ntinuum) | | **Node source (`ctmd`)** | Public GitHub repository to be announced | | **Discord** | [discord.gg/c8ntinuum](https://discord.gg/c8ntinuum) | | **X** | [x.com/c8ntinuum](https://x.com/c8ntinuum) | | **TikTok** | [tiktok.com/@c8ntinuum](https://www.tiktok.com/@c8ntinuum) | | **YouTube** | [youtube.com/@c8ntinuum](https://youtube.com/@c8ntinuum) | | **Telegram** | [t.me/c8ntinuumANN](https://t.me/c8ntinuumANN) | | **CoinGecko** | [coingecko.com/en/coins/c8ntinuum](https://www.coingecko.com/en/coins/c8ntinuum) | | **CoinMarketCap** | [coinmarketcap.com/currencies/c8ntinuum](https://coinmarketcap.com/currencies/c8ntinuum) | ## Ecosystem resources | Resource | URL | |----------|-----| | **Documentation** | You are here | | **Mainnet Explorer** | [ctmscan.io](https://ctmscan.io) | | **Testnet Explorer** | [testnet.ctmscan.io](https://testnet.ctmscan.io) | ## Genesis resources | Resource | Value | |----------|-------| | **Mainnet Genesis JSON** | [resources.c8ntinuum.io/mainnet/genesis.json](https://resources.c8ntinuum.io/mainnet/genesis.json) | | **Mainnet Genesis SHA-256** | `ebe6292aa5885b508b1d455c93564683c92e9cc3f80592ee615b1cffbc74a653` | | **Testnet Genesis JSON** | [resources.c8ntinuum.io/testnet/genesis.json](https://resources.c8ntinuum.io/testnet/genesis.json) | | **Testnet Genesis SHA-256** | `844f4322877d19e538d7ce584af92a2ab3a7eb825757b6b956068a4d255c6d43` | --- # Infinity Interop URL: https://docs.c8ntinuum.com/infinity-interop The Infinity hub is the orchestration service in front of the Open Intents Framework aggregator and the solver engines. A client asks it for a quote (the hub runs one round across the configured providers, vets the answers and discloses the winner), then submits an order against that quote, then follows the order through three status views. The hub is an exposure layer that lets a frontend or a backend start flows and see order state; it is not what makes the system safe — the contracts, the engines and the attestation committee are. c8ntinuum's native cross-chain protocol is IBC ([IBC Interoperability](/tooling-and-infrastructure/cross-chain)). The Infinity hub is the intent-based swap and bridge rail in front of the Open Intents Framework aggregator and the solver engines, for routes that leave the IBC trust model: a user's funds move under an intent that a solver fills and the contracts settle, and the hub is the API through which an integrator quotes, submits and follows that intent. **Getting access.** Integration access to the Infinity hub is by request: email [dev@c8ntinuum.com](mailto:dev@c8ntinuum.com). Your organization, its project and its keys are then managed in the hub dashboard: a secret API key for your servers, and a public project id for your web pages ([Authentication](/infinity-interop/api/authentication)). **Base URL.** `https://proxy-api.infinimesh.net/v1`: the public test hub, which runs on test networks only. Every example in these docs uses it; your production address comes with your integration access (above). **A secret key never goes in a web page.** A page calls either your backend, which holds the key and forwards the page's calls (a proxy server: [Partner backend](/infinity-interop/sdk/partner-backend)), or the hub's gateway directly with your public project id ([Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages)), which opens every route but the order history: a page can quote, submit, follow and refund its orders itself. The gateway never takes a secret key from a browser. ## Start here A gasless order in five calls and one signature: read the catalog, quote, sign, submit, then watch it fill. Every route under `/v1`, grouped by family, with the credential each one takes. `@c8ntinuum/infinity-interop-sdk`: a partner proxy for your backend, a project-id transport for web pages, and quote-to-settlement flows with checks before every signature. Behaviour an integrator would not expect from the rest of this reference: what happens and what to do. ## Core concepts Base URL, headers, the response envelope and its statuses, value formats, id namespaces and unknown fields. The two credentials: a secret API key for your servers, a public project id for your web pages; origin lists, and keeping your users apart. Keys from the hub dashboard, keeping them safe, and the gateway's and the hub's rate limits. What a winning quote hands you and what to do with it: sign a payload, send a transaction, or both. Exact input or exact output, firm or decaying terms, who carries the fill risk, and where slippage tolerance applies. The three status views, which id opens which, how an order moves through them, and when to stop polling. How to read an error body, what a retry means, and which failures need more than a retry loop. Everything Solana-specific in one place: a Solana source or destination, signing the escrow envelope, submitting, following and refunds. ## Resources The chains and assets a deployment declares, by family, and how to read the catalog. The sequences at a glance: gasless openFor, proxy lanes, open flow, preview then prepare, status polling and lost-ack recovery. The full `EIN`, `EPX` and `EMC` catalogue: every hub, gateway and framework error code, what it means and whether to retry. Rate limits, timeouts, size caps and the other operational behaviour that applies across routes. Symptom, cause and fix, grouped by phase: reaching the hub, credentials and origins, quoting, submitting and following an order. ## API reference `GET /catalog`, the deployment's declared chains and assets, and `GET /openapi.yaml`, the API's machine contract. `POST /quote`, `/quote/preview` and `/quote/prepare`: one round, indicative offers, or one chosen provider under a stable id. `POST /order/openfor` for a gasless or proxy-lane order and `POST /order` for the open flow. `GET /order/{orderId}/status`, `GET /status/{orderId}` and `GET /orders`: the three views of an order. Observed fill facts, a transaction hash resolved to its order, and the escrow's raw order tuple. The hub's behaviour in this reference was verified call by call against a live hub deployment; the gateway's (credentials, origins, `EPX` codes and timeouts) is as the hub team specifies it. The hub has no push channel — no webhooks, no websockets — so follow an order by polling its status views. --- # Quickstart URL: https://docs.c8ntinuum.com/infinity-interop/api/quickstart Read the catalog, get a quote, sign it, submit it, then watch it fill — the gasless escrow lane (EVM, Permit2), the lane every other route page's `signPayload` example is built from. Each step below pairs the exact curl from the linked route page with the exact JSON it answers. For the concepts behind each step, start at [API reference](/infinity-interop/api) and [Concepts](/infinity-interop/api/concepts). ## Before you start Check these first: - **You have a secret API key** (`c8n_sk_…`), made for your project in the hub dashboard, in the `HUB_API_KEY` variable of your shell. Every call below sends it in `x-api-key`: there is no sign-in and no session ([Authentication](/infinity-interop/api/authentication)). - **The paying wallet holds the source token** (WETH here, at least `amount`) **and has approved it to Permit2** (`0x000000000022D473030F116dDEE9F6B43aC78BA3`, the `verifyingContract` of Step 2's `signPayload`). The hub checks neither: it quotes and forwards, and the solver then declines the signed order at Step 3 with `EIN0021`, reference `http 422 (SOLVER_REJECTED)`. Nothing is opened or spent. - **The quote names the paying wallet in `sourceAccount`.** Without it, as in Step 2, the `recipient` is quoted as the payer: `witness.user` and the payload's `payer` are the recipient. A different wallet can still sign; the order then opens on the **signer's** tokens, but the recipient becomes the order's user, which is also who a refund pays. Step 2 omits `sourceAccount` only because its signer is the recipient. **Base URL.** `https://proxy-api.infinimesh.net/v1`: the public test hub (test networks only), which every call below uses; your production address comes with your integration access. **Keep the secret key off web pages.** Run these calls from a terminal or your backend. In your app, a page calls either your backend's proxy server, which holds the key and forwards its calls ([Partner backend](/infinity-interop/sdk/partner-backend)), or the gateway directly with your public project id, which opens every route but the order history ([Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages)). Every call sends one credential header, and a `POST` sends a JSON body with its content type ([Conventions — request headers](/infinity-interop/api/conventions#request-headers)): | Header | Value | |---|---| | `x-api-key` | your secret API key, on every call | | `Content-Type` | `application/json`, on every `POST` | | `X-Infinity-Subject` | on the quote, order and order-status calls: the user of yours the call is for, `user-42` in these steps | Nothing else: the gateway sets the headers the hub itself needs. Each answer's `request_id` (also in the `X-Request-Id` header) is the gateway's id for the call; keep it for support. The hub requires the subject on the seven routes that read it, and keeps each subject's quotes and orders apart: send the same one for a quote, its order and its status ([Authentication — your users](/infinity-interop/api/authentication#your-users)). How the ids tie together, step to step: | From | Id | Used at | |---|---|---| | Step 1 (the hub dashboard) | the secret key | every call's `x-api-key` | | Step 2 (quote) | `quoteRequestId` + `quoteId` | Step 3's `POST /order/openfor` body | | Step 3 (openFor ack) | `orderId` (and `hubOrderId`, the same order — `hex(sha256("infinity:v1:order:" + quoteId))`, interchangeable). On this lane `orderId` is already the on-chain order id | Step 4's status path; `orderId` alone also keys [`GET /onchain/{id}/fill`](/infinity-interop/api/onchain-fill) | | Step 4 (upstream status) | `fillTransaction.hash`, on `eip155-56` | Step 4's fill-by-tx path | | Step 4 (fill-by-tx) | `onchainOrderId`, the same value as Step 3's `orderId`; [`GET /orders`](/infinity-interop/api/orders-list) carries it as `onchainOrderId` from submission | later reads: [`GET /onchain/{id}/fill`](/infinity-interop/api/onchain-fill) | ## Steps Make a secret API key in the [hub dashboard](https://console.infinimesh.net), and put it in your shell without leaving it in the history. It is the only credential these calls need. ```bash read -rs HUB_API_KEY && export HUB_API_KEY # paste the key: it is not echoed ``` **`GET /catalog`** — build pickers from this, never a hand-kept table ([full reference](/infinity-interop/api/catalog)): ```bash curl curl -sS https://proxy-api.infinimesh.net/v1/catalog \ -H "x-api-key: $HUB_API_KEY" ``` ```json Response { "success": true, "request_id": "50f772fc-80a8-4005-bc60-6f13f14d5609", "data": { "chains": [ { "chainDomain": "eip155-1", "family": "evm", "userProxy": "0xb8e661486bd859fb5656c07c459c3617f6853b85", "swapRouter": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", "nativeKey": "eth", "wrappedKey": "weth", "inputSettlers": ["0xf439808b5a80e3c355645b62310502817000cd1d"] }, { "chainDomain": "eip155-56", "family": "evm", "inputSettlers": ["0xf439808b5a80e3c355645b62310502817000cd1d"] } ], "assets": [ { "chainDomain": "eip155-1", "assetKey": "eth", "assetRef": "eip155-1:0x0000000000000000000000000000000000000000", "symbol": "ETH", "address": "0x0000000000000000000000000000000000000000", "decimals": 18, "native": true }, { "chainDomain": "eip155-1", "assetKey": "weth", "assetRef": "eip155-1:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "symbol": "WETH", "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "decimals": 18, "native": false }, { "chainDomain": "eip155-56", "assetKey": "wbnb", "assetRef": "eip155-56:0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "symbol": "WBNB", "address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "decimals": 18, "native": false } ] } } ``` **`POST /quote`** ([full reference](/infinity-interop/api/quote)): ```bash curl -sS -X POST https://proxy-api.infinimesh.net/v1/quote \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"sourceChain":"eip155-1","destChain":"eip155-56","sourceAsset":"weth","destAsset":"wbnb","amount":"1000000000000000","recipient":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}' ``` Amounts are base-unit decimal strings, never floats. ```json { "success": true, "request_id": "8029ab5d-5936-4429-9eb4-21508633de01", "data": { "quoteRequestId": "01M34FSW4R9F2C4E7A1B3D5F80", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "issuedAt": "2026-09-22T12:00:15Z", "expiresAt": "2026-09-22T12:00:45Z", "executionClass": "PROVIDER_NATIVE_UNCOVERED", "amountIn": "1000000000000000", "amountOut": "32548366596169380000", "legs": [ { "provider": "c8-solver", "product": "oif-escrow-v0", "version": "v0", "sourceChain": "eip155-1", "destChain": "eip155-56", "sourceAsset": "weth", "destAsset": "wbnb", "amountIn": "1000000000000000", "amountOut": "32548366596169380000" } ], "score": { "value": "30920948266360911000.000000", "scoringVersion": "score:v3", "candidatesEvaluated": 1, "providersMissedRound": 0 }, "policy": { "policyVersion": "pv1", "pathEpoch": "pe1", "scoringVersion": "score:v3" }, "disclosure": { "signableHash": "leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40", "signablePayload": { "amountIn": "1000000000000000", "amountOut": "32548366596169380000", "deadline": "4102444900", "dependencyNodes": [ { "destChain": "eip155-56", "family": "oif-escrow", "permit2": "0x000000000022d473030f116ddee9f6b43ac78ba3", "provider": "c8-solver", "sourceChain": "eip155-1", "spender": "0xf439808b5a80e3c355645b62310502817000cd1d" } ], "destAsset": "wbnb", "destChain": "eip155-56", "expires": 4102445200, "integrityChecksum": "5f2c8a1db06e4bb0a2f37e5d9c8b1a4e6f0d2c3b5a79881726354493827160aa", "kind": "oif-escrow-gasless", "nonce": "1786146443149", "oifSemanticVersion": 1, "orderExpiresAt": 4102445200, "payer": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "permit2": "0x000000000022d473030f116ddee9f6b43ac78ba3", "productKey": "c8-solver:oif-escrow-v0:v0:eip155-1:std", "provider": "c8-solver", "quoteId": "e2e-quote-1", "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "schemaVersion": 1, "signPayload": { "…": "the top-level signPayload below, verbatim — put it back here to reproduce signableHash" }, "sourceAsset": "weth", "sourceChain": "eip155-1", "spender": "0xf439808b5a80e3c355645b62310502817000cd1d", "validUntil": 4102444800 } }, "signPayload": { "signatureType": "eip712", "domain": { "name": "Permit2", "chainId": 1, "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3" }, "primaryType": "PermitBatchWitnessTransferFrom", "message": { "permitted": [ { "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "amount": "1000000000000000" } ], "spender": "0xF439808b5A80E3C355645B62310502817000CD1D", "nonce": "1786146443149", "deadline": "4102444900", "witness": { "user": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "expires": 4102445200, "inputOracle": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", "outputs": [ { "oracle": "0x0000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe6", "settler": "0x0000000000000000000000008a791620dd6260079bf849dc5567adc3f2fdc318", "chainId": 56, "token": "0x000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "amount": "32548366596169380000", "recipient": "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", "callbackData": "0x", "context": "0x" } ] } }, "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "MandateOutput": [ { "name": "oracle", "type": "bytes32" }, { "name": "settler", "type": "bytes32" }, { "name": "chainId", "type": "uint256" }, { "name": "token", "type": "bytes32" }, { "name": "amount", "type": "uint256" }, { "name": "recipient", "type": "bytes32" }, { "name": "callbackData", "type": "bytes" }, { "name": "context", "type": "bytes" } ], "Permit2Witness": [ { "name": "user", "type": "address" }, { "name": "expires", "type": "uint32" }, { "name": "inputOracle", "type": "address" }, { "name": "outputs", "type": "MandateOutput[]" } ], "PermitBatchWitnessTransferFrom": [ { "name": "permitted", "type": "TokenPermissions[]" }, { "name": "spender", "type": "address" }, { "name": "nonce", "type": "uint256" }, { "name": "deadline", "type": "uint256" }, { "name": "witness", "type": "Permit2Witness" } ], "TokenPermissions": [ { "name": "token", "type": "address" }, { "name": "amount", "type": "uint256" } ] } }, "signIntent": "{\"amountIn\":\"1000000000000000\",\"amountOut\":\"32548366596169380000\",\"kind\":\"infinity-signable-intent:dev-v1\",\"leaf\":\"leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40\",\"productKey\":\"c8-solver:oif-escrow-v0:v0:eip155-1:std\",\"quoteId\":\"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58\",\"recipient\":\"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\",\"schemaVersion\":1}", "etaSeconds": 30 } } ``` No `executionTx` or `swapInput` (this lane discloses neither); no `auction` (a firm quote, not a decaying one). The deadlines inside the payloads (`deadline`, `expires`, `orderExpiresAt`, `validUntil`) fall in the year 2100 because this sample is built from a test fixture; the hub refuses an `orderExpiresAt` more than 24 h ahead. A live quote carries `orderExpiresAt` and `witness.expires` about 1 800 s after `issuedAt`. Neither extends the quote's own `expiresAt`. A winner discloses **at most one** of `signPayload` and `executionTx` — this walkthrough is the signed lane. For the proxy lanes, the open flow, and what each one hands you: [Step execution](/infinity-interop/api/concepts/step-execution). The quote is valid for about 30 s and is never refreshed — sign promptly. Sign the disclosed `signPayload` with `eth_signTypedData_v4`, verbatim — never rebuild it — after checking it, then prefix the raw signature with the `0x00` scheme byte. With the SDK, `executeQuote` runs this whole step, checks included ([SDK quickstart](/infinity-interop/sdk/quickstart)); its helpers do the parts: ```ts const lane = detectLane(q); // q: the quote answer's data, above if (lane.kind !== "evm-permit2") throw new Error("not a gasless EVM escrow winner"); const sig = await ethereum.request({ method: "eth_signTypedData_v4", params: [address, typedDataJson(lane.typedData)], // the disclosed members, unchanged }); const signature = evmEscrowEnvelope(sig); // the 0x00 scheme prefix ⇒ 66 bytes ``` Sign the `signPayload` object as disclosed; do not rebuild it from your own fields. EIP-712 hashes the typed values, so key order and whitespace change nothing, but a rebuilt object can drop or alter a field, and the solver then declines the signature (`EIN0021`). The `0x00` scheme byte in front of the 65-byte ECDSA signature is what makes 66 bytes. **`POST /order/openfor`** ([full reference](/infinity-interop/api/order-openfor)): ```bash curl curl -sS -X POST https://proxy-api.infinimesh.net/v1/order/openfor \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"quoteRequestId":"01M34FSW4R9F2C4E7A1B3D5F80","quoteId":"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58","signature":"0x00<65-byte-ecdsa-hex>"}' ``` ```json Response { "success": true, "request_id": "a63bb812-d2a5-4007-9a96-6b985820a094", "data": { "orderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "hubOrderId": "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "state": "created", "submittedAt": "2026-09-22T12:00:20Z" } } ``` `state` is the upstream's initial state, not a fill guarantee. Declined? `EIN0021` means the solver refused the signed order and nothing was opened: re-quote and sign again. Never re-post the declined body — the repeat answers `EIN0050` and then locks the quote on `EIN0052`. Expired? `EIN0005` on this call is normal flow — the quote's ~30 s window passed; re-quote and re-sign, don't retry the same signature. Lost the answer? `hubOrderId` is derivable from `quoteId` alone (`hex(sha256("infinity:v1:order:" + quoteId))`) — read the status by that alias before re-posting. [Handling errors](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order). The gateway answers `EPX0052` (504) when a submission takes longer than 10 s: that is a lost answer too, with an unknown outcome. The gateway keeps a submission running if your client disconnects, but cuts it at those 10 s, and a forward cut in flight leaves the quote answering `EIN0052` until an operator reconciles it ([known issue](/infinity-interop/integration-notes#ki-openfor-cancel)). Give this call a client timeout longer than 10 s, so that you get the gateway's answer. **`GET /order/{orderId}/status`** — the upstream's own view of the order ([full reference](/infinity-interop/api/order-upstream-status)): ```bash curl curl -sS https://proxy-api.infinimesh.net/v1/order/0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9/status \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" ``` ```json Response { "success": true, "request_id": "b0bda2a0-627d-415c-8ff0-e3b649fee08d", "data": { "stateSource": "upstream-advisory", "finalityVerified": false, "latestUpstreamState": "executed", "rejectedUpstreamTransition": false, "upstreamFillReported": true, "orderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "state": "executed", "history": [ { "state": "created", "at": "2026-09-22T12:00:23Z" }, { "state": "executed", "at": "2026-09-22T12:00:52Z" } ], "fillTransaction": { "hash": "0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d" }, "filled": true, "lastUpstreamSyncAt": "2026-09-22T12:01:00Z", "upstreamStale": false } } ``` Poll every 2–3 s; the view is cached for 2 s, so anything faster buys nothing. Stop at `finalized`, `failed` or `refunded`; `settled` is not terminal and can still move on. Don't wait for one of them forever: the upstream can stop at `executed` while the order fills and settles on chain ([known issue](/infinity-interop/integration-notes#ki-upstream-stall)). Once it reports `executed`, follow the order on [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill); its `filled` and `settled` facts are the chain's answer. **`GET /onchain/tx/{chainDomain}/{txHash}/order`** — resolve the reported fill transaction into what the hub's chain observers actually saw ([full reference](/infinity-interop/api/onchain-fill-by-tx)): ```bash curl curl -sS https://proxy-api.infinimesh.net/v1/onchain/tx/eip155-56/0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d/order \ -H "x-api-key: $HUB_API_KEY" ``` ```json Response { "success": true, "request_id": "b10d6dae-0bc9-420a-acf5-9ddca0b2acf7", "data": { "onchainOrderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "open": { "chainDomain": "eip155-1", "txHash": "0x7e3a9c1b5d2f8e4a0c6b3d9f1e7a5c2b8d4f0a6e3c9b1d7f5a2e8c4b0f6d3a9e", "blockNumber": 7054, "observedAt": "2026-09-22T12:00:35Z" }, "filled": { "chainDomain": "eip155-56", "txHash": "0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d", "blockNumber": 7059, "observedAt": "2026-09-22T12:00:50Z" }, "watermarks": { "eip155-56": { "blockNumber": 7061, "blockHash": "0x24b46941e1b961c61d47168c30d44f60560924e9709fac3a26cd3c2e2bc13b1b", "updatedAt": "2026-09-22T12:01:00Z" } } } } ``` The upstream view above is advisory; this one is observed — show money to a user from here, not from Step 4's first call. Its `onchainOrderId` is Step 3's `orderId`: on this lane the ack already carries the on-chain id, and [`GET /orders`](/infinity-interop/api/orders-list) lists it as `onchainOrderId` from submission. The Permit2 quote in Step 2 discloses none. Once the Open is observed, `watermarks` covers only the Open's output chain, here `eip155-56`. No new call — three habits that keep you comfortably under the rate limit ([Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits)) and off the wallet-prompt-for-nothing path: - Fetch `GET /catalog` once and cache it; it changes only when the deployment restarts. - Browse with [`POST /quote/preview`](/infinity-interop/api/quote-preview) (indicative, no cost beyond your organization's shared quote bucket) and debounce it while a wallet prompt is open; call [`POST /quote/prepare`](/infinity-interop/api/quote-prepare) once, on confirmation ([Step execution — preview, then prepare](/infinity-interop/api/concepts/step-execution#preview-then-prepare)). - Prefer [`@c8ntinuum/infinity-interop-sdk`](/infinity-interop/sdk) over hand-rolled `fetch` — it sends your key on every call, waits longer than the gateway before giving up, and reads `HubError.retryable` and `retryAfterMs`. ## Status lifecycle The three status views answer different questions and lag differently: [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) is the upstream's own advisory report; [`GET /status/{orderId}`](/infinity-interop/api/hub-status) is the hub's own event-derived projection (open-flow orders only have this one); [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) is what the hub's chain observers actually saw, and is the only one of the three that proves anything. Poll the first by `orderId` (or the derivable `hubOrderId`) every 2–3 s; stop at `finalized`, `failed` or `refunded`, and from `executed` on read the third as well: the upstream can stop reporting there ([known issue](/infinity-interop/integration-notes#ki-upstream-stall)). Full detail, the id-to-view table, and the terminal states per view: [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle). ## From a web page The same flow runs in a browser with your public project id instead of the key: send `x-project-id` (and `Content-Type` on a `POST`) in place of `x-api-key`, from an origin your project lists. Every step works as above, the on-chain views included. Only the order history, [`GET /orders`](/infinity-interop/api/orders-list), needs the key: a page keeps the ids of the orders it submits, and reads the history through your backend ([Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages)). ## See also - [Step execution](/infinity-interop/api/concepts/step-execution) — the lanes this walkthrough didn't take - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) — the three status views, in full - [Troubleshooting](/infinity-interop/api/troubleshooting) — symptom → cause → fix, by phase - SDK: [SDK quickstart](/infinity-interop/sdk/quickstart) — the same flow as one TypeScript program - [Authentication](/infinity-interop/api/authentication) — the secret key and the project id --- # Integration notes URL: https://docs.c8ntinuum.com/infinity-interop/integration-notes Behaviour of the hub, or of the stack around it, that you would not expect from the rest of this reference. Each entry says what happens and what to do. Route pages link here where it matters. **Contents:** - **Submitting:** - [a cut-off openFor locks the quote](#ki-openfor-cancel) - [proxy-lane registration window](#ki-proxy-window) - [registering an unsent deposit](#ki-proxy-unsent) - [`POST /order` and OIF lanes](#ki-order-create-oif) - [re-posting a declined order](#ki-redecline) - **Following an order:** - [upstream status stops at `executed`](#ki-upstream-stall) - [the hub's `/status` view does not move](#ki-hubstatus-frozen) - **On-chain views:** - [ids are not validated](#ki-onchain-id) - [Solana Open signatures](#ki-solana-open-tx) - [no escrow tuple for non-EVM origins](#ki-nonevm-tuple) - **Quoting:** [preview answers EIN0050 for "no offer"](#ki-preview-no-offer) - **SDK:** [secure context required](#ki-sdk-nonsecure) ## Submitting orders ### A `POST /order/openfor` cut during its forward locks the quote (`EIN0052`) - **What happens.** 1. The hub forwards the order upstream on the incoming call's request context. 2. If that call is cut while the forward is in flight, the upstream call is cancelled. 3. The forward reservation stays pending, with its outcome unknown. 4. From then on every re-post answers `EIN0052`, until an operator reconciles the reservation. Reading the order by its `hubOrderId` answers `EIN0009` meanwhile. A cut *before* the forward starts is harmless: the re-post forwards afresh. The hub's caller is the gateway. It keeps its call to the hub running when your client times out or disconnects, so your client giving up does not cut the forward. It does cut the call at its own limit, 10 s on a submission, and answers `EPX0052` (504); the hub's own upstream budget is 30 s, so a forward still in flight at 10 s is cancelled that way. - **What to do.** - Treat `EPX0052` on a submission as a lost answer with an unknown outcome: read the status by `hubOrderId`, then re-post the same body ([Handling errors — a lost answer](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order)). The re-post answers the original ack if the order was recorded, a fresh ack if nothing had been forwarded, and `EIN0052` if the forward was cut in flight. - Give `openFor` a client timeout longer than the gateway's 10 s, so that you get the gateway's answer rather than none. The SDK waits 15 s on submissions by default, and 35 s through a partner proxy; don't lower it. - If a quote is stuck on `EIN0052`, an operator has to reconcile its forward reservation: report it with the `request_id`. ### Proxy-lane registration is refused after `expiresAt`, and the deposit completes unregistered - **What happens.** - The hub keeps a proxy-lane quote (`oif-wrap-native`, `oif-swap-token`) registrable for 20 minutes after issue. - The aggregator, however, refuses a registration once the quote's `expiresAt` has passed (about 30 s). The hub answers `EIN0007` with reference `fail-closed rejected: upstream refused: http 400 (QUOTE_EXPIRED)`. - The user's deposit is not lost: the solver fills and settles it on chain anyway. But the hub never records the order, so it is missing from `GET /orders` and from the hub's status routes. - Past the hub's own 20-minute window the answer is `EIN0005`. - **What to do.** - Register as soon as the transaction is sent, well before `expiresAt`. - Once the transaction is out, **never re-quote** because of an `EIN0005` or `EIN0007`: a new quote means a second deposit. - Follow a deposit that could not be registered through [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx), using the send transaction's hash, and [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill). ### A proxy-lane quote can be registered without its deposit - **What happens.** `openFor` on a proxy-lane quote whose transaction was never sent is acked, with state `created`. The order then stays `created` upstream. Neither the hub nor the aggregator checks that the deposit exists. - **What to do.** - Register only after the transaction is sent. - Don't treat a `created` ack as proof of the deposit. The `open` fact on [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) is the proof. ### `POST /order` refuses OIF-lane quotes - **What happens.** `POST /order` registers `create`-flow winners only. On a deployment whose corridors run through the OIF aggregator, every quote sent to [`POST /order`](/infinity-interop/api/order-create) answers `EIN0007` `fail-closed rejected` at the hub's re-approval step, before the artifact, signer and signature checks. - **Why it rarely matters.** OIF lanes complete through [`POST /order/openfor`](/infinity-interop/api/order-openfor). - **What to do.** Send a quote to `POST /order` only when its lane is the open flow (the SDK's `quote.lane.kind` is `"open-flow"`). ### Re-posting a declined order answers `EIN0050` - **What happens.** - A signed-lane order declined by the solver answers `EIN0021`, and nothing is opened. - Re-posting the same body gets a `502` from the aggregator, which the hub surfaces as `EIN0050` (`upstream unavailable: http 502`). - The hub then treats that forward as outcome-unknown. - **What to do.** Don't re-post a declined quote. Re-quote and sign again. ## Following an order ### Upstream status can stop at `executed` - **What happens.** An EVM gasless order that filled and settled on chain stayed `executed` in [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) for a long time. Syncs were fresh, `upstreamStale` was `false`, and there was no `fillTransaction`. `settled` was never emitted. Solana-origin orders did reach `finalized`. - **What to do.** - Don't wait forever for a terminal upstream state. - Once the order is `executed`, read [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill). Its `filled` and `settled` facts are the chain's answer. - Give the polling loop a stall rule, for example: hand off after a few minutes without progress. - The SDK's `trackOrder`, which `executeQuote` uses, reads the chain facts on every round and ends on them ([Tracking](/infinity-interop/sdk/execution#tracking)). The chain facts are open to your project id too, so a page follows such orders itself. ### The hub's own `/status` view does not progress for openFor orders - **What happens.** - [`GET /status/{orderId}`](/infinity-interop/api/hub-status) shows the intent as `CREATED` and every other domain empty, even after the order filled and settled on chain. - Orders of execution class `PROVIDER_NATIVE_UNCOVERED`, which covers every lane `openFor` accepts on current deployments, have no admissible domain transitions after creation, so this view cannot move. - Its `watermarks` are per chain, and move only when the reconciler processes a new observation on that chain. On a quiet chain they can be far behind the observer watermarks of the on-chain views. - **What to do.** Follow openFor orders with the upstream status and the on-chain views. Use `/status` only for the intent record. ## On-chain views ### On-chain ids are not validated - **What happens.** - [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) accepts any id. A miscased, unprefixed or unknown id answers `200` with no facts and a watermark for every chain. - That answer can't be told apart from "not filled yet", so the withdraw rule would offer a withdraw for an order that is in fact filled and settled. - [`GET /onchain/{orderId}/order`](/infinity-interop/api/onchain-order) answers `EIN0009` for such ids. - **What to do.** - Pass `onchainOrderId` exactly as the hub serves it (`0x` + 64 lowercase hex), and check the format before calling. - Before offering a withdraw, confirm with `GET /onchain/{orderId}/order` that the order exists. ### A Solana Open signature does not resolve by transaction - **What happens.** The Open signature of a Solana-origin order (`open.txHash` on the fill view, `chain.sourceTxHash` on `GET /orders`) misses on [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx). The settle signature resolves. - **What to do.** Follow Solana-origin orders by `onchainOrderId` on [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill). ### No escrow tuple for non-EVM origins - **What happens.** [`GET /onchain/{orderId}/order`](/infinity-interop/api/onchain-order) answers `EIN0009` permanently for a non-EVM-origin order, even when its Open, fill and settle are all observed. The withdraw steps in [Refunds](/infinity-interop/api/concepts/refunds) need that tuple, so they work for EVM-origin orders only. - **What to do.** Refund a Solana-origin order through [`GET /onchain/{orderId}/refund`](/infinity-interop/api/onchain-refund): the hub builds the unsigned transaction for the order's user to sign and send ([SDK — Refunds](/infinity-interop/sdk/refunds#solana-origin-orders)). ## Quoting ### Preview answers `EIN0050` when there is no offer - **What happens.** - [`POST /quote/preview`](/infinity-interop/api/quote-preview) answers `EIN0050` (`upstream unavailable`, retryable) instead of `200` with an empty `offers` whenever a provider refuses the request. - The cases seen: an amount below the provider's minimum, an exact-output request above `maxAmountIn`, and a Solana source without `sourceAccount`. - Preview reports any provider error this way. - **What to do.** - Read a preview `EIN0050` as "no offer right now" and don't retry it in a loop. - Check the amount and `sourceAccount` first. - `POST /quote` gives the reason: `EIN0008` with a reference. ## SDK ### The SDK needs a secure context in the browser - **What happens.** A page served over plain `http` from anywhere other than `localhost` has no `crypto.subtle`. The SDK needs it in the browser for a prepared quote's fresh `preparationId`, the Solana pre-signing checks, and the lost-answer recovery (`deriveHubOrderId`). Those calls throw an `Error` saying Web Crypto is not available. - **What to do.** Serve the page over `https`, or from `localhost`. --- # Conventions URL: https://docs.c8ntinuum.com/infinity-interop/api/conventions Base URL, headers, the response envelope, value formats, id namespaces and how unknown fields are handled — every one of these applies to every route below unless a page says otherwise. ## Base URL and transport - Base URL: `https://proxy-api.infinimesh.net/v1`, the public test hub (test networks only), used throughout this reference; your production address comes with your integration access. JSON in, JSON out (except `GET /openapi.yaml`, which returns YAML). - **The gateway.** The API is served by the hub's gateway, in front of the hub. It checks the credential, sets the headers the hub needs, and passes the hub's answers back unchanged. Its own refusals come in the same envelope, with an `EPX` code ([The response envelope](#the-response-envelope)). - **Who calls it.** Your servers, with a secret API key. Your web pages, either through your backend (a proxy server that holds the key: [Partner backend](/infinity-interop/sdk/partner-backend)) or straight to the gateway with your public project id, from the origins your project lists. A secret key never goes in a page: the gateway does not take one from a browser ([Authentication](/infinity-interop/api/authentication)). ## Request headers | Header | Required | Rule | Failure | |---|---|---|---| | `x-api-key` | on every call from a server | your secret API key, `c8n_sk_` + 43 base64url characters | `EPX0001` missing or malformed, `EPX0002` unknown, revoked or expired (both `401`) | | `x-project-id` | on every call from a web page, instead of `x-api-key` | your project id, `c8n_pk_` + 22 base64url characters | `EPX0008` malformed, `EPX0009` unknown (both `401`); `EPX0010` an origin the project does not list (`403`) | | `Content-Type` | on every `POST` | `application/json` (a `multipart/form-data` prefix is also accepted) | other values `EMC0029`; **without it the body is not read at all**, so the route answers `EIN0001` reference `body` | | `Accept` | no | `application/json` | — | | `X-Infinity-Subject` | on the seven routes that read it, with either credential | the user of yours, or the page's visitor, the call is for, 1 to 128 of letters, digits and `. _ @ + ~ -` ([Authentication — your users](/infinity-interop/api/authentication#your-users)) | `EIN0001` reference `x-infinity-subject` when it is missing there, or any other value | - **One credential per call.** A request carrying both `x-api-key` and `x-project-id` is refused, `EPX0007` (400). The two credentials and what each may call: [Authentication](/infinity-interop/api/authentication). - **Nothing else is needed.** The gateway builds every hub request itself: `RequestId`, `User-Agent`, `Accept-Language: en` (the hub's messages are therefore always in English), your organization (from the credential) and its own credential for the hub. Nothing else you send reaches the hub but the path, the body, `X-Infinity-Subject`, and the query string of `GET /orders` and `GET /onchain/{orderId}/refund` (a query string on any other route is dropped). - **From a web page, set only `x-project-id`, `Content-Type` and `X-Infinity-Subject`.** The gateway's CORS pre-flight allows exactly those three, and a browser refuses a call that sets any other non-standard header, `x-api-key` included (standard ones such as `Accept` need no pre-flight). A page's subject names its visitor and proves nothing: anyone can send your public project id with any subject. A page can read an answer's `X-Request-Id`, `RateLimit-*` and `Retry-After` headers. - **Every `POST` carries a JSON object.** Never send an empty body: sent as exactly `application/json` it is refused `EMC0005`, and otherwise the route answers `EIN0001` reference `body`. A `GET` carries no body. The gateway refuses a body over 64 KiB, `EPX0005` (413). ## The request id Every answer names the call twice: in the `X-Request-Id` response header and in the body's `request_id`. The gateway makes a fresh id for every call it sends to the hub, so you never send one. Keep it with your logs, and quote it when you contact the hub team. ## Rate-limit headers Every call whose credential the gateway accepts reports the budget left in three headers, an `EPX0080` or `EPX0082` refusal included: | Header | Meaning | |---|---| | `RateLimit-Limit` | the size of the bucket (its burst) | | `RateLimit-Remaining` | the calls left in it right now | | `RateLimit-Reset` | the seconds until it is full again | For a secret key the bucket is your organization's pool; for a project id, whichever of the project's pool and the visitor's own bucket is tighter. A refusal adds `Retry-After`, in seconds ([API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits#the-gateway)). ## The response envelope Every response except `GET /openapi.yaml` is this envelope. A success carries `data` and no `message` or `errors` keys: ```json { "success": true, "request_id": "8b9c0d1e-2f30-416b-9c6d-7e8f9a0b1c2d", "data": { "…": "the route's payload" } } ``` On failure `success` is `false`, `data` is absent, and `errors` holds the entries (a hub error has exactly one; framework validation can report several): ```json { "success": false, "request_id": "a5d3d5e3-52e6-4457-b62d-01f5dad43044", "message": "You have errors in your request. Please fix them and retry", "errors": [ { "error_reference": "notFillable", "error_code": "EIN0008", "error_message": "No viable route for this request", "error_params": ["eip155-56"] } ] } ``` `message` is present here because `EIN0008` is terminal; the hub's availability and busy errors (`EIN0050`–`EIN0099`) carry no `message`. | Field | Meaning | |---|---| | `error_code` | machine-readable: `EIN…` (the hub), `EPX…` (the gateway) or `EMC…` (the hub's framework). **Branch on this.** | | `error_message` | human text, always in English; never parse it | | `error_reference` | the field or cause the code refers to (for example `amount`, `providerRefused`), when there is one. On some codes it carries a descriptive text; treat it as a hint, and branch on the documented values only | | `error_params` | code-specific machine hints, when there are any (for example the back-off on `EIN0080` and `EPX0080`) | `success` is always present. `request_id`, `message`, `data` and `errors` are omitted when empty, and so are an error entry's `error_reference` and `error_params`. The body's `request_id` is missing on the hub's per-IP limiter refusal ("DDoS check failed"); the `X-Request-Id` header still names the call. **HTTP status.** The hub answers `200` for success and **`400` for every failure of its own** — availability and busy codes included — and the gateway passes those answers through unchanged. The gateway answers its own refusals in the same envelope with a real status: `400` for two credentials or an unreadable body, `401` or `403` for a refused credential or origin, `404` for a route that does not exist or is not open to the credential, `413` for a body over 64 KiB, `429` for a rate limit (with a `Retry-After` header), and `502`, `503` or `504` when the hub is unreachable or too slow, or the credential cannot be checked ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)). Branch on the code, never on the status ([Handling errors](/infinity-interop/api/concepts/handling-errors#read-the-code-not-the-status)). An answer without the envelope (an HTML page, a plain-text status) did not come from a route: check the base URL and the path, or treat it as the service being unavailable. ## Timeouts The gateway waits **20 s** on the three quote routes (the hub's own quote deadline is 15 s) and **10 s** on every other route, then answers `EPX0052` (504). Give your client a longer timeout, so that the gateway's answer, not your client, ends a slow call: the SDK waits 25 s on quotes, 12 s on reads and 15 s on submissions. An order submission (`POST /order`, `POST /order/openfor`) keeps running at the hub when your client disconnects, but not past the gateway's 10 s. On a submission, `EPX0052` means the outcome is unknown, not that the order failed: look the order up by its `hubOrderId`, then re-post the same body ([Handling errors — a lost answer](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order)). ## Value formats | Kind | Format | |---|---| | **Amounts** | decimal strings in base units — `"1000000000000000"` — never floats, never exponents, no sign, no leading zeros, greater than zero | | **Chain domains** | `eip155-` (decimal, no leading zero) for EVM; `solana-`, `sui-`, `aptos-` (lowercase `[a-z0-9-]`) where the deployment declares them. `GET /catalog` lists the deployment's chains | | **Asset references** | a declared **asset key** (the catalog's `assetKey`, e.g. `weth`) **or** an **asset reference** `:
` (the catalog's `assetRef`). A bare `0x…` address is refused | | **Addresses** | EVM `0x` + 40 hex (any case); Solana `0x` + 64 hex or base58 (converted at the boundary; hex is the wire form); Sui/Aptos hex of up to 64 characters, `0x` optional, left-padded to 64 | | **Timestamps** | RFC 3339 in UTC with 0–9 fractional digits: trailing zeros are trimmed and the fraction is omitted when it is zero, e.g. `2026-09-22T12:00:15.123456789Z`, `2026-09-22T12:00:15.5Z`, `2026-09-22T13:00:00Z` (the escrow deadlines are whole seconds). Parse with a full RFC 3339 parser | | **Hashes and ids** | lowercase hex. Hub order ids (`hubOrderId`, and the open-flow ack's `orderId`) are 64 hex characters; on-chain order ids are `0x` + 64 lowercase hex. The openFor ack's `orderId` is the aggregator's id, not a hub id: for an order a solver engine serves, it is the `0x`-prefixed on-chain id | ## The three order-id namespaces | Id | Where it comes from | Used by | |---|---|---| | `orderId` | the openFor ack (the aggregator's id), or the open-flow ack | `GET /order/{orderId}/status` (openFor), `GET /status/{orderId}` (open flow) | | `hubOrderId` | the openFor ack — the hub's durable alias, `hex(sha256("infinity:v1:order:" + quoteId))` | interchangeable with `orderId` on `GET /order/{orderId}/status`; the key on `GET /status/{orderId}` for an openFor order | | `onchainOrderId` | the bytes32 the settler contracts index (on every `GET /orders` entry) | `GET /onchain/{orderId}/fill`, `GET /onchain/{orderId}/order` | Do not mix them. ## Unknown fields Bodies are decoded leniently: unknown JSON fields are ignored, not refused. A body that is not valid JSON never reaches the hub's routes: its framework refuses it, `EMC0028` reference `Body`. Valid JSON the route's form can't take (a wrong JSON type, for example) is `EIN0001` reference `body`. What that implies for client-side validation, and the other things the hub deliberately does not check: [Input validation](/infinity-interop/api/concepts/input-validation). ## See also - [Authentication](/infinity-interop/api/authentication) — the two credentials and origin lists - SDK: the transports apply these conventions for you: the credential header, the JSON body, and timeouts longer than the gateway's ([Partner backend](/infinity-interop/sdk/partner-backend), [Browser apps](/infinity-interop/sdk/frontend)) - [Input validation](/infinity-interop/api/concepts/input-validation) - [Handling errors](/infinity-interop/api/concepts/handling-errors) --- # Authentication URL: https://docs.c8ntinuum.com/infinity-interop/api/authentication The API is served by the hub's gateway, under `https://proxy-api.infinimesh.net/v1` on the public test hub. Every call carries **one credential header**, and nothing else is needed to authenticate: there is no sign-in, no session and no token to renew. **Getting access.** Integration access to the Infinity hub is by request: email [dev@c8ntinuum.com](mailto:dev@c8ntinuum.com). Your organization, its project and its keys are then managed in the hub dashboard: [console.infinimesh.net](https://console.infinimesh.net) for the test network. The API itself has no sign-up and no key routes. ## The two credentials | Credential | Format | Header | Used by | Routes open to it | |---|---|---|---|---| | **Secret API key** | `c8n_sk_` + 43 base64url characters | `x-api-key` | your servers | every route of this reference | | **Project id** (App ID) | `c8n_pk_` + 22 base64url characters | `x-project-id` | your web pages | every route but `GET /orders` | Send exactly one of them on every call. A request carrying both is refused with `EPX0007` (400). Both belong to your organization, and the gateway names your organization to the hub on every call, from the credential: an order placed with one is the same order to the other. On the seven routes that read it, the hub also requires the `X-Infinity-Subject` header: which of your users, or which visitor of a page, the call is for ([Your users](#your-users)). ## Secret API keys A secret key is made in the hub dashboard. It goes in `x-api-key` on every call, and opens every route: quotes, orders, status, the order history, the on-chain views and refunds. ```bash curl -sS https://proxy-api.infinimesh.net/v1/catalog \ -H "x-api-key: $HUB_API_KEY" ``` - **It stays on your servers.** Whoever holds it acts as your organization: they can quote and submit orders in its name and read its whole order history. Keep it in a secret store or a git-ignored `.env` file, never in a page, an app bundle or a repository. - **Its format** is `c8n_sk_` followed by exactly 43 base64url characters (letters, digits, `-` and `_`). - **A refused key is answered `401`:** `EPX0001` when the header is missing or its value is not in that format (a stray space, a truncated copy, a project id), `EPX0002` when the key is unknown, revoked or expired ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)). The SDK reads both as `HubError.isCredentialRefused`: an operator matter, which no retry fixes. - **Stop on a refused key.** 20 refused credentials within a minute from one IP make the gateway answer `EPX0081` (429) to every call from that IP for the rest of that minute, valid keys included: a server that keeps retrying a wrong key locks itself out ([API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits#the-gateway)). - **Never from a web page.** The gateway does not take a key from a browser ([No keys in web pages](#no-keys-in-web-pages)). - **Rotating** is done in the hub dashboard: make the new key, move your servers to it, then revoke the old one. More on keys and their rate limits: [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits). ## Project ids for web pages The project id is the credential a web page uses to call the gateway directly, with no server of yours in between. It is **public by design**: shipping it in a browser bundle is safe. Each organization has one, shown on its project page in the hub dashboard, and it does not expire. ```ts const answer = await fetch("https://proxy-api.infinimesh.net/v1/catalog", { headers: { "x-project-id": "c8n_pk_…" }, // your project id }); ``` - **Every route but the order history is open.** The catalog, the quotes, order submission, both status views, the on-chain views and the Solana refund route answer a project id as they answer a key, so a page can follow the orders it submits to the end, and refund them itself. The order history, `GET /orders`, answers `EPX0003` (404): it would show every visitor all of your orders. Keep the ids of the orders a page submits; the history needs your server and its secret key. - **Set only the headers the pre-flight allows.** The gateway's CORS pre-flight allows exactly three request headers: `x-project-id`, `Content-Type` and `X-Infinity-Subject`. A page sends `x-project-id`, `Content-Type: application/json` on a POST, and `X-Infinity-Subject` where the hub requires it; a browser refuses a call that sets any other non-standard header (standard ones such as `Accept` need no pre-flight). - **Name the visitor.** The seven routes that read `X-Infinity-Subject` require it from a page too. Send a random id per visitor, kept in the browser, so each visitor's quotes and statuses stay apart: the SDK's `projectTransport` sends `visitor-` and a UUID, kept in `localStorage`. It labels a visitor and proves nothing ([Your users](#your-users)). - **Order reads are not private.** Anyone holding your project id and one of your order ids can read that order's status and its chain facts. Nothing lists the ids, so a page learns only the ones it submitted or was given. (The on-chain views and the refund route are not scoped to your organization in the first place: chain data is public, and a refund pays only the order's own user.) - **Rate limits** apply per visitor IP, plus a pool shared by every visitor of your project ([API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits#the-gateway)). - A malformed project id is `EPX0008` (401), an unknown one `EPX0009` (401). Both count toward the lockout of the caller's IP (`EPX0081`). With the SDK, a page uses `projectTransport` ([Browser apps](/infinity-interop/sdk/frontend#with-a-project-id-projecttransport)). ## Origin lists Each project has a list of the web origins allowed to use its project id. It is empty by default: | The project's list | Accepted | |---|---| | empty | any origin, and requests without an `Origin` header too | | set | only the listed origins; a request without an `Origin` header is refused | A refused origin is `EPX0010` (403). That answer carries the CORS headers, so a page can read the error instead of seeing an opaque CORS failure. Each entry is an exact origin: scheme, host and port, with no wildcards. `http` origins are accepted, so list `http://localhost:3000` next to your production origin to develop locally. Only project ids have origin lists. A secret key has none: no web page can use one (next section). ## No keys in web pages A secret key in a web page is readable by every script on that page: yours, your dependencies' and any injected one. The gateway does not take one from a browser, on any network: its CORS pre-flight never allows `x-api-key`, and its answers to key calls carry no CORS headers, so the browser refuses the call. A page talks to one of these: - **your backend**, which holds the secret key and forwards the page's calls (a proxy server: the SDK's `createInteropProxy` with `proxyTransport` in the page, [Partner backend](/infinity-interop/sdk/partner-backend)); - **the gateway**, with your public project id. The SDK's `hubTransport` refuses to start in a web page. Its `dangerouslyAllowBrowser: true` option lifts that for a test key on a hub that accepts browser calls; the hub's gateway does not. ## Headers | Header | When | Value | |---|---|---| | `x-api-key` | every call from a server | your secret API key | | `x-project-id` | every call from a web page | your project id | | `Content-Type` | every `POST` | `application/json`, with a JSON object as the body | | `Accept` | optional | `application/json` | | `X-Infinity-Subject` | required on the seven routes that read it | the user of yours, or the page's visitor, the call is for ([Your users](#your-users)) | Nothing else is needed. The gateway builds every hub request itself: `RequestId`, `User-Agent`, `Accept-Language: en` (so the hub's messages are always in English), your organization (`X-Infinity-Organization`, from the credential) and its own credential for the hub. Nothing else you send reaches the hub but the path, the body, `X-Infinity-Subject`, and the query string of `GET /orders` and `GET /onchain/{orderId}/refund`. Every answer names the call in the `X-Request-Id` header and in the body's `request_id`: quote it when you contact the hub team. The full header rules: [Conventions — request headers](/infinity-interop/api/conventions#request-headers). ## Your users The credential tells the hub your organization, never which of your users a call is for. That is the job of `X-Infinity-Subject`, which the hub **requires** on the seven routes that read it. If your service has users of its own, keep them apart one of two ways: name each user to the hub in the subject, and the hub keeps them apart itself; or send one subject for everyone, and keep your own record of who created each quote and order. ### The subject: the hub keeps them apart From your server, with your secret key, name the user a call is for in `X-Infinity-Subject`: ```bash curl -sS "https://proxy-api.infinimesh.net/v1/orders?limit=100" \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" ``` - **Seven routes require it:** `POST /quote`, `/quote/preview`, `/quote/prepare`, `POST /order`, `/order/openfor`, `GET /order/{orderId}/status` and `GET /orders`. Without it they answer `EIN0001` (400), reference `x-infinity-subject`. The hub keeps each subject's quotes, orders and history apart, so send the same subject for a quote, its order and its status reads. - **`GET /orders`** lists the orders of the subject you send. Each page echoes that subject in `data.subject`: refuse a page whose `subject` is not the one you sent. - **The other routes do not read it.** The catalog and the on-chain views are scoped to no one, and the hub does not scope `GET /status/{orderId}` by subject: a backend that serves several users answers that route for the user's own orders only. - **The gateway forwards it unchanged**, every value you send, and never adds one. It sets your organization itself, from the credential, so a subject can never reach another organization's orders. - **Choose it as your stable id for the person:** the same across sign-ins and wallets, never reused for someone else, one value per call, of 1 to 128 letters, digits and `. _ @ + ~ -` (`^[A-Za-z0-9._@+~-]{1,128}$`). The hub refuses any other value with `EIN0001`, reference `x-infinity-subject`. - **Check that it works** before you serve history: list orders for a subject none of your users has, such as `canary-` followed by a fresh UUID. The answer must hold no orders and echo that subject; if it does not, serve no history and tell the hub team. The SDK's `verifySubjectScoping` runs this check. **Never trust a subject from a web page.** A page must send one on the seven routes, and the gateway forwards it, but the project id is public: anyone can send it with any subject and pass for any visitor, or any of your users. From a page, a subject keeps visitors apart and proves nothing (the SDK's `projectTransport` sends a random id per visitor). Scope calls to a signed-in user from your server only. ### Your own records Send the same subject on every call, one of your own such as `default`, and the hub keeps one history for all your users: `GET /orders` with that subject lists every order, whoever placed it, and the status reads answer for any of them. Keep your users apart yourself: record who created each quote and order, submit a quote only for the user it was made for, answer status reads for the user's own orders only, and build each user's history from that shared listing. The SDK's `createInteropProxy` does either for a partner backend (`userScope: "subject"`, or `"ownership"`, its default, which sends `hubTransport`'s one subject, `default` unless you set another), and answers `GET /status/{orderId}` for the user's own orders only either way ([Partner backend — keeping your users apart](/infinity-interop/sdk/partner-backend#keeping-your-users-apart)). ## See also - [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits) — keeping keys safe, and the limits - [Conventions](/infinity-interop/api/conventions) — the base URL, the headers and the envelope - [Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx) — every `EPX` code - SDK: [Partner backend](/infinity-interop/sdk/partner-backend) — the secret key on your server; [Browser apps](/infinity-interop/sdk/frontend) — a page with your project id - [Quickstart](/infinity-interop/api/quickstart) --- # API keys and rate limits URL: https://docs.c8ntinuum.com/infinity-interop/api/api-keys-and-rate-limits ## Keys Secret API keys (`c8n_sk_` followed by 43 base64url characters) are made in the hub dashboard ([console.infinimesh.net](https://console.infinimesh.net) for the test network), and revoked there too: the API has no key routes. A server sends its key in `x-api-key` on every call ([Authentication](/infinity-interop/api/authentication#secret-api-keys)). Every key of your organization reaches the same organization: the same order history, the same status reads, and the same rate limits. A key you revoke, or one past its expiry, is refused from then on: `EPX0002` (401). ## Keep keys secure - Keep secret keys on your servers. Never ship one to a browser, a mobile app or a repository: whoever holds it acts as your organization, and can read its whole order history. - A web page uses your public project id, or calls your backend, which holds the key ([Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages)). The gateway never takes a key from a browser. - Give each server or environment its own key, so that revoking one stops only that caller. - Rotate by make-then-revoke: make the new key in the hub dashboard, move the caller to it, then revoke the old one. - A key that leaked: revoke it in the hub dashboard at once, then look through your organization's orders (`GET /orders`, under each subject you use) for any you did not place. ## Rate limits Two layers limit your traffic: the gateway, which every call reaches first, and the hub behind it. ### The gateway | Traffic | Limit | Refusal | |---|---|---| | Secret keys | one pool per organization, shared by all its keys whatever IP they call from, sized for your organization | `EPX0080` (429) | | Project id | a pool per project, shared by every visitor, plus a bucket per visitor IP | `EPX0080` (429) for the project's pool, `EPX0082` (429) for one visitor | | Failed credentials | per IP: 20 missing, malformed or unknown keys or project ids within a minute | `EPX0081` (429) to **every** call from that IP, valid keys included, for the rest of that minute | - **The pools are separate.** Your project id's traffic has its own pool, so a busy frontend never spends the pool of your server keys. - **A refusal carries its back-off.** `EPX0080` and `EPX0082` carry it twice: in `error_params[0]`, in milliseconds, and in a `Retry-After` header, in seconds. `EPX0081` carries it in `Retry-After` only: the seconds left in the lockout. Wait that long, then retry. The SDK exposes it as `HubError.retryAfterMs` ([SDK errors](/infinity-interop/sdk/errors#retrying)). - **Every call whose credential is accepted reports the budget left** in three headers: `RateLimit-Limit` (the bucket's size), `RateLimit-Remaining` (the calls left in it right now) and `RateLimit-Reset` (the seconds until it is full again). For a project id they describe whichever of its two buckets is tighter. A page can read them across origins, with `X-Request-Id` and `Retry-After`. - Visitors behind one shared IP, such as an office or a mobile carrier, share one visitor bucket. - **Only refused credentials count toward `EPX0081`**, a call with no credential included; a valid key or project id never does. During a lockout, though, every call from that IP is refused, so a server whose key is refused must stop, not retry. ### The hub The hub then applies its own limits. It counts your **organization** as one tenant: every call your organization makes, with a key or with its project id, draws on the same buckets. | Route group | Limit | Key | Shed answer | |---|---|---|---| | `POST /quote`, `/quote/preview`, `/quote/prepare` (one bucket) | 5 requests/s, burst 20 | per organization | `EIN0080`, with `error_params[0]` the suggested back-off in ms (200 at the defaults) | | `POST /order` and `/order/openfor` (one bucket) | 5 requests/s, burst 20 | per organization | `EIN0080`; nothing was reserved or forwarded, so re-post the identical body after the wait | | Catalog, status, listing, on-chain views | no hub-level limit | — | only the per-IP limiter below | | Every route | the hub's per-IP limiter (a deployment value) | per client IP: your server's for a key, the visitor's for a project id | `EMC0005` ("DDoS check failed"), with no `request_id` in the body and no back-off hint | | In-flight quotes | 64 per process | global | `EIN0081` | A request is charged after it has validated and before any work: one that fails validation costs nothing, while one the service refuses afterwards, or answers as an idempotent duplicate, has already been charged. A caller idle for 600 s loses its buckets. The hub's values are its defaults; the production hub's values are those of its deployment configuration. The gateway's pool sizes are set for your organization. ## EIN0080 versus EIN0081 `EIN0080` is the hub's per-organization bucket. `error_params[0]` is the back-off in milliseconds: wait that long, then retry. The SDK exposes it as `retryAfterMs` on the `HubError` ([SDK errors](/infinity-interop/sdk/errors)). `EIN0081` is an admission shed with several causes: the in-flight quote cap, the hub's outbound limiter to the upstream not admitting a forward in time, or the upstream's own 429. Back off, then retry. On `POST /order/openfor`, an `EIN0081` caused by the upstream's 429 keeps the reservation, so a re-post answers `EIN0052` until an operator reconciles it; the other causes sent nothing, and a re-post after a back-off is safe ([Submit an order (openFor)](/infinity-interop/api/order-openfor#behavior)). ## Stay under the limit - **Fetch the catalog once and cache it.** [`GET /catalog`](/infinity-interop/api/catalog) changes only on restart. - **Browse with preview and debounce it.** Pause `POST /quote/preview` while a wallet prompt is open: a preview the user cannot act on yet is budget spent for nothing. - **Prepare once on confirmation.** One `POST /quote/prepare` per deliberate signing attempt, not per render. - **Poll status every 2–3 seconds, and stop at a terminal state.** Both status views are cached for 2 seconds server-side, so polling faster buys nothing. The stop set per view is on [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle#terminal-states-per-view). - **Meter your own users.** Everyone behind your backend shares your organization's pool at the gateway and its buckets at the hub: rate-limit each of your users yourself before forwarding. - **Stop on a refused key.** Retrying a refused key locks your server's IP out of every call for the rest of the minute (`EPX0081`). - **Prefer the SDK.** It classifies every failure for you: `HubError.retryable` says whether a retry can help, and on a rate limit `retryAfterMs` says how long to wait ([SDK errors](/infinity-interop/sdk/errors#retrying)). There are no webhooks or websockets: polling is the only channel. ## See also - [Authentication](/infinity-interop/api/authentication) - [Limits](/infinity-interop/api/limits) - [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) - [Errors](/infinity-interop/api/errors) — `EPX0080`–`EPX0082`, `EIN0080`, `EIN0081`, `EMC0005` --- # Concepts URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts The route pages are the wire contract: request fields, response shapes, error codes. The pages here are the explanation — why a field behaves the way it does, how several routes fit into one flow, what to do when something goes wrong. The conventions, flows, errors and limits pages and the route pages keep their spec tables and link here. | Page | The question it answers | Spec tables it relies on | |---|---|---| | [Step execution](/infinity-interop/api/concepts/step-execution) | What does a winning quote hand me, and what do I do with it? | [Get quote — Response](/infinity-interop/api/quote#response), [Submit order (openFor)](/infinity-interop/api/order-openfor), [Create order](/infinity-interop/api/order-create) | | [Trade types](/infinity-interop/api/concepts/trade-types) | Exact input or exact output, firm or auctioned — what changes? | [Get quote — Request body](/infinity-interop/api/quote#request-body), [Flows — Auctions](/infinity-interop/api/flows#auctions) | | [Input validation](/infinity-interop/api/concepts/input-validation) | What does the hub check, and what can I check first? | [Conventions](/infinity-interop/api/conventions), [Get quote — Request](/infinity-interop/api/quote#request) | | [Handling errors](/infinity-interop/api/concepts/handling-errors) | How do I read an error body, and when is it safe to retry? | [Error reference](/infinity-interop/api/errors), [Conventions — The response envelope](/infinity-interop/api/conventions#the-response-envelope) | | [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) | What's the budget, and how do I stay under it? | [Limits](/infinity-interop/api/limits) | | [Execution errors](/infinity-interop/api/concepts/execution-errors) | The order didn't fill — what does the hub actually know? | [Get upstream status — Response](/infinity-interop/api/order-upstream-status#response), [List orders — Response](/infinity-interop/api/orders-list#response) | | [Refunds](/infinity-interop/api/concepts/refunds) | When and how do I get the deposit back? | [Get on-chain fill](/infinity-interop/api/onchain-fill), [Get escrow order](/infinity-interop/api/onchain-order) | | [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) | Which id, and which of the three status routes, do I poll? | [Get upstream status](/infinity-interop/api/order-upstream-status), [Get hub status](/infinity-interop/api/hub-status) | ## Fees The hub has no fee concept — it never takes a cut. The one `fee` on the wire is a solver's own fee inside a solver-swap winner's `disclosure.signablePayload` — part of that solver's terms, and such a winner is not completable ([Step execution](/infinity-interop/api/concepts/step-execution#at-most-one-of-signpayload-and-executiontx)). What an order costs the user is entirely inside the quote: - **`amountIn`** — what the winner charges, in base units (on the token-in lane the escrow's wrapped native, so read `swapInput.amountMax` there) ([Get quote — Response](/infinity-interop/api/quote#response), [Get quote](/infinity-interop/api/quote)). - **`swapInput.amountMax`** and **`slippageBps`**, on the token-in lane only — the ceiling the wallet is asked to approve and the tolerance that produced it ([Get quote — Request body](/infinity-interop/api/quote#request-body), [Get quote](/infinity-interop/api/quote); [Trade types — slippage on the token-in lane](/infinity-interop/api/concepts/trade-types#slippage-on-the-token-in-lane)). - **Gas**, per lane — who pays it and when is [Step execution — the lanes](/infinity-interop/api/concepts/step-execution#the-lanes). ## Related pages These already read as concept pages and are not duplicated here: - [Authentication](/infinity-interop/api/authentication) — the secret key and the project id, origin lists, and keeping your users apart - [Quickstart](/infinity-interop/api/quickstart) — five calls and one signature - [Troubleshooting](/infinity-interop/api/troubleshooting) — symptom → cause → fix, by phase --- # Step execution URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/step-execution A winning quote tells the user what to do next: sign something, or send a transaction — and on the open flow, send a transaction and then sign the order intent. This page is the map from a quote's shape to those steps, lane by lane. ## At most one of `signPayload` and `executionTx` A winner discloses **at most one** of `signPayload` and `executionTx` ([Get quote — Response](/infinity-interop/api/quote#response); [Get quote](/infinity-interop/api/quote)). A same-chain solver-swap winner (`executionClass: INFINITY_SOLVER`, `disclosure.signablePayload.kind: "solver-swap"`) discloses **neither**, because the hub's solver bids carry terms only, and neither does a `transfer` winner. Treat both as **not completable**. For a solver-swap winner, `POST /order/openfor` answers `EIN0001` (pointing at `POST /order`), and `POST /order` can still record an order for it — its re-approval step checks an `INFINITY_SOLVER` winner against the solver registry, not the product registry — but that record moves no funds: the hub never submits a transaction, and this winner gives the user none to send. In code (the SDK's `detectLane` makes the same decision, and names each outcome a [lane](/infinity-interop/sdk/lanes)): ```ts function submissionFlowDecision(q: Pick | null | undefined) { const kind = q?.disclosure?.signablePayload?.kind; if (kind === "oif-wrap-native" || kind === "oif-swap-token") return "openForUnsigned"; if (q?.signPayload != null) return "openFor"; if (q?.executionTx != null) return "create"; return "none"; } ``` `"none"` covers a solver-swap winner, a `transfer` winner and no quote at all — check for it before doing anything else with a quote. ## The lanes The reference uses two vocabularies for the same six lanes: `disclosure.signablePayload.kind` (what the route pages and this table use) and the provider **product id** (`oif-escrow-v0`, `oif-3009-v0`, …, what [Flows](/infinity-interop/api/flows) and the SDK product-facing pages use). This table is the one place both are shown side by side. | `signablePayload.kind` | Product / version | Family | Step | Who pays gas | Submit with | SDK lane | |---|---|---|---|---|---|---| | `oif-escrow-gasless` | `oif-escrow-v0` | EVM (Permit2) | signature | the solver | [`POST /order/openfor`](/infinity-interop/api/order-openfor) | `evm-permit2` | | `oif-escrow-nonevm` | `oif-escrow-solana-v0` | Solana | signature | the solver | [`POST /order/openfor`](/infinity-interop/api/order-openfor) | `solana-escrow` | | `aggregator-gasless` | `oif-3009-v0` | EVM (EIP-3009) | signature | the solver | [`POST /order/openfor`](/infinity-interop/api/order-openfor) | `evm-3009` | | `oif-wrap-native` | `oif-wrap-native-v0` | EVM | transaction | the user | [`POST /order/openfor`](/infinity-interop/api/order-openfor), unsigned | `wrap-native` | | `oif-swap-token` | `oif-swap-token-v0` | EVM | transaction | the user | [`POST /order/openfor`](/infinity-interop/api/order-openfor), unsigned | `swap-token` | | `aggregator-bridge` | (external aggregator lane) | external aggregator (`chainId` is a chain-domain string) | transaction + `personal_sign` | the user | [`POST /order`](/infinity-interop/api/order-create) | `open-flow` | | `transfer` | `transferx` | EVM | none | — | not completable | `informational` | | `solver-swap` | — | same-chain | none | — | not completable | `informational` | ## The signature step Sign `signPayload` **verbatim** — never rebuild it, whatever family it is for: a rebuilt object can drop or alter a field (EIP-712 hashes the typed values, so key order alone changes nothing): - **EVM** (`oif-escrow-gasless`, Permit2 EIP-712 typed data): `eth_signTypedData_v4`, then prefix the 65-byte ECDSA signature with the scheme byte `0x00` → 66 bytes as `0x` hex. - **External gasless** (`aggregator-gasless`, EIP-3009, wrapped as `{chainId, payload}`): unwrap it first, sign the inner payload, send the raw 65-byte signature (a 66-byte prefixed form is also accepted and normalised). - **Solana** (`oif-escrow-nonevm`, `{…, transactionMessage, digest}`): the funding wallet signs the **base64-decoded `transactionMessage` bytes** with Ed25519 — not `digest`, which is a checksum, not what gets signed. The client sends a 97-byte envelope (`0x00` ‖ 64-byte signature ‖ 32-byte public key) as standard base64. On non-EVM payloads `sender` is the user and `feePayer` is the solver — that split is the gasless promise: the user never pays gas to open the order. The SDK reads through the wrapper and discriminates the shape before anything is signed ([Lanes](/infinity-interop/sdk/lanes)). The wire encoding each lane expects on `POST /order/openfor` is a route-page detail, not repeated here — see [Request body](/infinity-interop/api/order-openfor#request-body). ## The transaction step `executionTx`'s shape (`{approvals[], steps[]}`) is spec and lives on [Get quote — Response](/infinity-interop/api/quote#response) — see it there. What matters when you execute it: - **Approvals first, steps in order.** `approvals[]` is `{chainId, token, spender, amount}`; `steps[]` is `{chainId, to, data, value, description}`. - **`chainId` is an integer on the hub's proxy lanes** (`oif-wrap-native`, `oif-swap-token`) **and a chain-domain string on the external aggregator lane** — accept both. - **Native-coin lane** (`oif-wrap-native`): one `wrapAndOpen` step whose `value` is the amount — nothing to approve. - **Token-in lane** (`oif-swap-token`): one approval, then one `swapAndOpenV2` step with `value "0"`; `swapInput.amountMax` is the real cost to show the user, not `amountIn`. - **The quote is advisory.** The engine re-prices at admission against live gas, so send the approval and the step promptly and before `expiresAt`. - **The hub never submits a transaction on the user's behalf**, on any lane — the user (or their wallet) always sends `executionTx` themselves. ### The contract calls To decode a step before the user sends it, or to build a refund, these are the calls and their selectors. `order` is the escrow's `StandardOrder` tuple. | Contract | Call | Selector | Where it appears | |---|---|---|---| | UserProxy | `wrapAndOpen(order)` | `0xd824fd17` | the `oif-wrap-native` step, with the coin as `value` | | UserProxy | `swapAndOpenV2(order, tokenIn, amountInMax)` | `0x8b3b56f9` | the `oif-swap-token` step (it replaced `swapAndOpen` on 2026-09-07) | | ERC-20 | `approve(address,uint256)` | `0x095ea7b3` | the token-in approval (spender: the UserProxy), and the Permit2 approval | | Input settler (the escrow) | `refund(order)` | `0x48f49eaf` | the EVM refund, with the tuple from [`GET /onchain/{id}/order`](/infinity-interop/api/onchain-order) | | Input settler | `orderIdentifier(order)` | `0x609dbfa0` | the order's id on that settler: check the tuple before refunding | | Input settler | `orderStatus(bytes32)` | `0x2dff692d` | `0` none, `1` deposited, `2` claimed, `3` refunded | Take the UserProxy and settler addresses from [`GET /catalog`](/infinity-interop/api/catalog) (`userProxy`, `inputSettlers`): they change when the hub is redeployed. ## Common step flows | Flow | Winner kinds | Sequence of routes | |---|---|---| | Gasless openFor | `oif-escrow-gasless`, `oif-escrow-nonevm`, `aggregator-gasless` | quote → sign `signPayload` → `POST /order/openfor` → poll status | | Proxy lanes | `oif-wrap-native`, `oif-swap-token` | quote → send `executionTx` → `POST /order/openfor` (unsigned) → poll status | | Open flow | `aggregator-bridge` | quote → send `executionTx` → `personal_sign` `signIntent` → `POST /order` → poll status | | Preview, then prepare | any | `POST /quote/preview` (repeated) → `POST /quote/prepare` (once, on confirmation) → sign/send as above | ### Gasless openFor On the EVM lane (`oif-escrow-gasless`) the paying wallet must hold the source token and have approved it to Permit2 before step 3. The hub checks neither: without them the solver declines the signed order with `EIN0021` (`http 422 (SOLVER_REJECTED)`), and nothing is opened or spent. Approve **before quoting**: an approval mined while a quote is open can leave the signature on an expired quote. 1. [`POST /quote`](/infinity-interop/api/quote) (or preview → prepare, see [Preview, then prepare](#preview-then-prepare) below). The winner carries `signPayload`. 2. The user signs `signPayload` verbatim, per family — see [The signature step](#the-signature-step) above. 3. [`POST /order/openfor`](/infinity-interop/api/order-openfor) with `quoteRequestId`, `quoteId` and the signature. 4. Poll [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) every 2–3 s; confirm with [`GET /onchain/…`](/infinity-interop/api/onchain-fill). ### Proxy lanes The **native-coin lane** (`oif-wrap-native`, a source asset that is a chain's native coin) and the **token-in lane** (`oif-swap-token`, an undeclared token on a chain with a swap router): 1. The winner discloses `executionTx` and **no** `signPayload` — see [The transaction step](#the-transaction-step) above. 2. The user sends the approval and the step, promptly and before `expiresAt`. 3. As soon as the transaction is sent, [`POST /order/openfor`](/infinity-interop/api/order-openfor) **without** a signature, well before `expiresAt`. The hub keeps the quote registrable for 20 minutes after issue, but the aggregator refuses a registration once `expiresAt` has passed (`EIN0007`, `fail-closed rejected: upstream refused: http 400 (QUOTE_EXPIRED)`) ([known issue](/infinity-interop/integration-notes#ki-proxy-window)); a repeat registration is idempotent. Not before sending, though: nothing checks the deposit, so a registration without one is acked all the same ([known issue](/infinity-interop/integration-notes#ki-proxy-unsent)). 4. Once the transaction is out, **never re-quote**, whatever the registration answers: a new quote means a second deposit. A refused registration leaves the deposit in escrow, and it completes on chain anyway — the solver still fills it — but the hub never records the order. Follow it by the send transaction's hash on [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx), then [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill), and withdraw after the escrow expires if it is not filled. What a refused registration does to the deposit: [Refunds — when does an order become refundable?](/infinity-interop/api/concepts/refunds#when-does-an-order-become-refundable). ### Open flow 1. The winner discloses an `executionTx` outside the proxy lanes (the SDK's lane is `open-flow`). `POST /order` takes these winners only: it refuses every OIF-lane quote with `EIN0007` at its re-approval step, before any signature check ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)). The quote should carry `sourceAccount` (the signer must equal it) — a quote requested without `sourceAccount` cannot compile a non-EVM leg and cannot be followed by a signed `POST /order` (`EIN0010`, after the re-approval step; [Get quote](/infinity-interop/api/quote)). Check that `signIntent` is present before sending anything: it is absent when the hub could not build it — it discloses none rather than a wrong one — and a signed `POST /order` is then impossible, so re-quote now ([Get quote](/infinity-interop/api/quote)). 2. The user sends `approvals`, then `steps`, and pays the gas. The hub never submits. 3. The user `personal_sign`s `signIntent` verbatim. 4. [`POST /order`](/infinity-interop/api/order-create) with `artifactHash` = `disclosure.signableHash` and `{signature, signer, scheme: "eip191"}`, before `expiresAt`: a later first registration is `EIN0005`. From step 2 on the transaction is out, so **never re-quote**, whatever `POST /order` answers: a new quote means a second transaction, and a refused registration doesn't stop the one already sent ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). 5. Follow it with [`GET /status/{orderId}`](/infinity-interop/api/hub-status). ### Preview, then prepare 1. While the user browses, [`POST /quote/preview`](/infinity-interop/api/quote-preview) — debounced, and paused while a wallet prompt is open. 2. On confirmation, create one `preparationId` per signing attempt and [`POST /quote/prepare`](/infinity-interop/api/quote-prepare) with the chosen `provider`. On a lost response, retry with the **same** body and id — you get the same quote and the same expiry, never a new one, because a replay never extends `expiresAt` ([Get quote](/infinity-interop/api/quote)). The prepared transaction template is stored with the quote, so it can be submitted through any hub replica sharing the event store — use one `preparationId` per deliberate signing attempt, not one per page load. A non-prepared `POST /quote` winner, by contrast, must be submitted to the replica that quoted it — its template is held in that process for 30 s (20 min on the proxy lanes, though the aggregator refuses a registration after `expiresAt`: [known issue](/infinity-interop/integration-notes#ki-proxy-window)). 3. Show the **prepared** terms — they can differ from the preview — then sign and submit as above. 4. On expiry before anything was sent, discard the signature, create a new `preparationId`, and prepare again — never once the user's transaction is out (proxy lanes, open flow). ## Which step applies The TypeScript SDK's `detectLane` (read into `quote.lane` on every quote it returns) names which of the four flows above applies, as a lane: `evm-permit2`, `solana-escrow` or `evm-3009` (openFor with a signature), `wrap-native` or `swap-token` (openFor without one), `open-flow` (`POST /order`), or `informational` (a solver-swap or `transfer` winner: nothing to execute). A malformed answer is `unsupported`, with the reason. See [Lanes](/infinity-interop/sdk/lanes). ## See also - [Get quote](/infinity-interop/api/quote), [Submit order (openFor)](/infinity-interop/api/order-openfor), [Create order](/infinity-interop/api/order-create), [Prepare quote](/infinity-interop/api/quote-prepare) - SDK: [Lanes](/infinity-interop/sdk/lanes) and [Executing orders](/infinity-interop/sdk/execution) — the same flows, run by `executeQuote` - [Trade types](/infinity-interop/api/concepts/trade-types), [Refunds](/infinity-interop/api/concepts/refunds) --- # Trade types URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/trade-types What `amount` and `maxAmountIn` mean on a `POST /quote` request, how a winner's terms can be firm or decaying, what "who carries the fill risk" means, and where slippage tolerance applies. ## Exact input (the default) `swapType` absent, or `"exact-input"`: `amount` is **what the user spends**, in the source asset's base units. The quote answers with `amountIn` (what the winner charges — on the token-in lane the escrow's wrapped native, so not always the `amount` you sent) and `amountOut` (what the recipient receives — the number that can vary with the market). ## Exact output `swapType: "exact-output"`: `amount` is **what the recipient receives**, and `maxAmountIn` caps **what the user will pay** — the quote answers with the input the user must pay, capped at `maxAmountIn` when it's set. - `maxAmountIn` on an **exact-input** request is `EIN0001` (reference `maxAmountIn`) — it doesn't mean anything there, the input is already the fixed side. - A `maxAmountIn` under the market price is `EIN0008` `providerRefused` — not a form error, a fact about the corridor: raise the cap, or accept a different amount; do not retry unchanged. - **A token-in source cannot be exact-output**: `EIN0001` reference `swapType` ([Get quote — Request](/infinity-interop/api/quote#request)). That lane's price is only known from what the user spends, so it can't fix the output side. - `POST /quote/preview` discards exact-output offers whose input would exceed `maxAmountIn` — they never appear in `offers[]`. A provider that refuses the request outright, as one does for a cap under the price, makes the preview answer `EIN0050` instead of an empty `offers[]` ([known issue](/infinity-interop/integration-notes#ki-preview-no-offer)). - `GET /orders` echoes `swapType` on each entry's `quote` object, so a client rendering history doesn't need to remember which request it was. ## Firm and auctioned winners Some winners settle at a fixed price; others decay. When the winner carries a pricing context (a Dutch auction, or a limit price: see the last point below), the quote carries `auction: {kind, exclusiveFor?, startTime, stopTime, slope, floor, ceiling}` — the field table is on [Flows — Auctions](/infinity-interop/api/flows#auctions); this section is what those fields mean for the user. - `floor` equals `amountOut` — the guaranteed minimum the recipient gets. - `ceiling` is `floor + slope · (stopTime − startTime)` — the opening price. - The settler pays `floor + slope · (stopTime − max(now, startTime))` at fill time — **never below the floor**. Every one of these fields is bound to the witness the user signs, so the settler can't quietly pay less. - Ranking (`score:v3`) treats a decaying winner as worth `floor + 0.5 · (ceiling − floor)` when comparing candidates. **Firm offers win ties** against an auctioned offer of the same computed value. - A winner with no pricing context has **no `auction` key at all**. A winner whose context is `limit` or `exclusiveLimit` carries `auction` too, with `slope` `"0"` and `ceiling` equal to `floor`: it doesn't decay, and ranking treats it as firm. So tell a decaying winner by `auction.kind` (`dutchAuction`, `exclusiveDutch`), not by the key alone. The served OpenAPI description and the SDK's type comment describe `startTime` as when exclusivity ends, while the hub's reference describes it as when decay (and exclusivity) begins. This reference follows the hub's reference; verify against the settler contract before relying on either reading. ## Execution classes `executionClass` on the quote response names who carries the fill risk once the order is open: | `executionClass` | Meaning | |---|---| | `INFINITY_SOLVER` | an Infinity solver fills it; on a same-chain `solver-swap` winner, quotable but **not completable through the hub** ([Step execution](/infinity-interop/api/concepts/step-execution#at-most-one-of-signpayload-and-executiontx)) | | `PROVIDER_NATIVE_COVERED` | a native-integration provider fills it (covered) | | `PROVIDER_NATIVE_UNCOVERED` | a native-integration provider fills it (uncovered) | The hub's reference names the classes and says only "who carries the fill risk". `PROVIDER_NATIVE_UNCOVERED` candidates are admitted only when the deployment enables `infinity.flags.uncoveredRoutes`; the escrow product is registered under this class. ## Slippage on the token-in lane `slippageBps` (1–1000; absent or `0` means 50 = 0.5%, fixed in the hub's code) applies **only** to the token-in lane (`oif-swap-token`) — it's ignored on every other lane, so setting it elsewhere is harmless but has no effect. `swapInput.amountMax` on the quote response is the ceiling to show the user as the real cost, not `amountIn` — this is also the anchor the [Fees](/infinity-interop/api/concepts#fees) note points at, since the token-in lane's slippage tolerance is the closest thing the hub has to a variable cost. A `POST /quote/preview` offer's `amountIn` on this lane is already slippage-widened — the figure a wallet would be asked to approve. ## See also - [Get quote](/infinity-interop/api/quote), [Flows — Auctions](/infinity-interop/api/flows#auctions) - [Step execution](/infinity-interop/api/concepts/step-execution), [Input validation](/infinity-interop/api/concepts/input-validation) --- # Input validation URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/input-validation What is checked before a request reaches route-specific logic, what a client can verify itself before ever calling, and what the hub deliberately leaves unchecked. ## What is checked, in order 1. **The gateway** — the credential (`x-api-key` or `x-project-id`, one of them), the page's origin for a project id, whether the route is open to that credential, the body's size (64 KiB) and the gateway's rate limits. Its refusals carry an `EPX` code and a real HTTP status: see [Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx) (not restated here). 2. **Body decode** — malformed JSON never reaches the hub's routes: its framework refuses it, `EMC0028` reference `Body`. Well-formed JSON the form can't take (a wrong JSON type such as `1.5` for an integer field) is `EIN0001` reference `body`, and so is a body the framework never read (no JSON `Content-Type`) on a route that reads one. 3. **Form fields** — validated in struct order; **only the first failing field is reported**. `error_reference` is the field's JSON wire name (`amount`, `sourceAsset`, …), not an internal field name. 4. **Route-specific resolution** — corridor/asset resolution and so on. The worked example is `POST /quote`'s own resolution order: see [Request](/infinity-interop/api/quote#request-body) — not restated here. 5. **The rate-limit charge** — validation passing is what triggers the hub's own charge, before the quote pipeline or the order service does any work, so the quote, preparation and order lookups come after it. See [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits). ## What you can verify before calling | What to check | See | |---|---| | Credential — `c8n_sk_` + 43 base64url characters in `x-api-key` from a server, `c8n_pk_` + 22 base64url characters in `x-project-id` from a page, never both | [Authentication](/infinity-interop/api/authentication) | | Route — a project id reaches every route but `GET /orders` | [Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages) | | Amounts — decimal base units, no floats/exponents/sign/leading zeros, > 0 | [Conventions — value formats](/infinity-interop/api/conventions#value-formats) | | Chain domains — `eip155-` / `solana-` / `sui-` / `aptos-` | [Conventions — value formats](/infinity-interop/api/conventions#value-formats); [Get catalog](/infinity-interop/api/catalog) | | Asset key / `:
` reference — a bare `0x…` address is refused | [Get catalog](/infinity-interop/api/catalog) (`assetKey`/`assetRef`) | | Addresses, per chain family | [Conventions — value formats](/infinity-interop/api/conventions#value-formats) | | openFor signature, per lane | [Submit order (openFor) — Request body](/infinity-interop/api/order-openfor#request-body) | | Ids — upstream-status `^[A-Za-z0-9_-]{1,256}$`; `/status/{id}` exactly 64 lowercase hex, no `0x`; on-chain ids `0x` + 64 lowercase hex; `preparationId` `^[A-Za-z0-9_-]{16,128}$` | [Conventions — value formats](/infinity-interop/api/conventions#value-formats); the route pages | | Timestamps — a full RFC 3339 parser, not just the shape | [Conventions — value formats](/infinity-interop/api/conventions#value-formats) | | Body — a JSON object on every POST, at most 64 KiB | [Conventions — request headers](/infinity-interop/api/conventions#request-headers) | ## What the hub does not check Bodies are decoded leniently: **unknown JSON fields are ignored**, not refused. Malformed JSON is the framework's `EMC0028` reference `Body`; well-formed JSON of a wrong type is `EIN0001` reference `body`; and only the first failing field is reported. What that implies for client-side validation: - **EVM addresses are not format-checked at quote time.** A non-EVM `recipient`/`sourceAccount` that doesn't decode is `EIN0001` on that field, but a malformed EVM address is admitted and fails later, not as `EIN0001` ([Get quote — Request](/infinity-interop/api/quote#request)). - **On-chain id spelling is not validated.** Uppercase hex or a missing `0x` isn't an error — it simply finds nothing, and on `/fill` that reads like "not filled yet" ([known issue](/infinity-interop/integration-notes#ki-onchain-id)). See [Get on-chain fill — Behavior](/infinity-interop/api/onchain-fill#behavior). - **`chainDomain` on fill-by-tx is not validated.** An unknown or miscased domain finds no watermark and answers `EIN0051`, not `EIN0001`. ## Framework refusals before the hub runs These happen in the hub's framework, before any route code: - **Malformed JSON** — `EMC0028` reference `Body`. - **A GET that carries a body** with a JSON `Content-Type` — `EMC0005`. Without a JSON `Content-Type` the body is not read, and the GET is served. - **A POST with an empty body** and a `Content-Type` of exactly `application/json` — `EMC0005`. A body over the hub's size limit reads as empty, so it is refused the same way. With a parameter (`application/json; charset=utf-8`) or no `Content-Type`, the empty body reaches the route, which answers `EIN0001` reference `body`. Every POST route takes a JSON object: never send an empty body. ## Validating against the catalog Build pickers from [`GET /catalog`](/infinity-interop/api/catalog) rather than a hand-kept table — a page can read it with your project id, so pickers render before your user signs in to anything, and it's rebuilt only on restart. What `native`, `nativeKey`, `swapRouter` and `wrappedKey` mean for what a chain accepts as a source is [Get catalog — Response](/infinity-interop/api/catalog#response) — not restated here. ## See also - [Conventions](/infinity-interop/api/conventions), [Error reference](/infinity-interop/api/errors) - [Handling errors](/infinity-interop/api/concepts/handling-errors), [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) --- # Handling errors URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/handling-errors How to read an error body, what a retry actually means, and which failures need more than a retry loop. ## Read the code, not the status The hub answers `200` for success and **`400` for every failure of its own** — availability and busy codes included — and the gateway in front of it passes those answers through unchanged. The gateway answers its own refusals in the same envelope with a real status (`400`, `401`, `403`, `404`, `413`, `429`, `502`, `503`, `504`) and an `EPX` code. Either way the status never tells a retryable failure from a terminal one: branch on the code. An answer without the envelope (an HTML page, a plain-text status) did not come from a route; treat it as the service being unavailable. See [Conventions — the response envelope](/infinity-interop/api/conventions#the-response-envelope) for the envelope shape itself (not restated here). ## The three blocks, and what a retry means The codes fall into three blocks — terminal, availability, busy — and the hub's `EIN` codes and the gateway's `EPX` codes share them. Terminal codes need a changed request (or, for the gateway, a fixed credential or origin), availability codes a later retry, busy codes a back-off (`EIN0080`, `EPX0080` and `EPX0082`: `error_params[0]` ms; `EPX0081`: `Retry-After` seconds) — the table on [Errors](/infinity-interop/api/errors#the-blocks-and-when-to-retry) is the spec. The TypeScript SDK encodes the block rule as `HubError.retryable` and `retryAfterMs` ([SDK errors](/infinity-interop/sdk/errors)). On any route, an `EIN0050` whose `error_params[0]` is `upstreamAuth` means the upstream refused the hub's own credential — a configuration problem an operator has to clear, and no client-side back-off fixes it. A refused key or origin (`401`, `403`) is the same kind of problem on your side: fix the configuration, don't retry. Retrying a refused credential makes it worse: 20 of them within a minute lock the caller's IP out of every call for the rest of that minute (`EPX0081`). ## The request id Every answer carries a request id, in the body's `request_id` and in the `X-Request-Id` header. The gateway makes a fresh one for every call it sends to the hub, retries included: you never send one, and nothing about a retry depends on it. Log it next to the call, and quote it when you report a problem. ## Terminal codes on the happy path A few terminal codes show up in normal flow, not just on mistakes: - **`EIN0005`** (quote expired) — normal flow, not a bug: re-quote and re-sign. Not once the quote's own transaction is sent (the open flow, or a proxy lane — see the [known issue](/infinity-interop/integration-notes#ki-proxy-window)): the funds have already moved, and a new quote would move them a second time. - **`EIN0008`** (no viable route) — a fact about the amount or the destination, not the form; change the request. - **`EIN0010`** (bad signature) — covers an unknown quote too (one code on purpose); re-sign the exact bytes disclosed, never a re-derived version. On `POST /order/openfor` a bad EVM escrow signature is not checked by the hub and comes back as `EIN0021` instead. - **`EIN0001`** on the winner's own kind ("the winner completes via `POST /order`") — a bridge winner posted to the wrong route; the fix is the other route, not different input. Links to the route pages carry the full error tables; not restated here. ## Submissions: re-post, never re-quote **A re-post of the identical body never opens a second order, and it is safe** — except after `EIN0021`: a declined signed order must be re-quoted and signed again, because a re-post answers `EIN0050` and then `EIN0052`. This is the client rule behind two route-level specs — the reservation mechanics on [Submit order (openFor) — Behavior](/infinity-interop/api/order-openfor#behavior) and the idempotency bullet on [Create order — Behavior](/infinity-interop/api/order-create#behavior) — and it's worth stating once, in plain terms: - **`EIN0052`** means an earlier submission of this quote went unanswered and may have opened the order. **Stop. Never re-quote, never ask the user to sign again.** Re-posting the identical body is safe and keeps answering `EIN0052` until an operator reconciles the reservation — surface `request_id` to them; there is no client-side fix. This rule is for a submission that went *unanswered*. After an explicit `EIN0021` nothing was opened: do not re-post (that only answers `EIN0050` and then `EIN0052`) — re-quote and sign again, as above. - **A bare `EIN0050` or `EIN0081` on `POST /order/openfor`** can mean two different things: nothing was sent (back off and re-post — it forwards again), or something *was* sent and the hub is still waiting (the reservation is kept, and the next re-post answers `EIN0052`, which tells you which case it was). You can't tell which one happened from the first error alone — only from what the re-post answers. - **The gateway's availability codes on a submission** (`EPX0050`, `EPX0051` and `EPX0052`, a `502`, `503` or `504`) say nothing about whether the order was opened: treat them as a lost answer (below). `EPX0053` is the exception: the gateway could not check the credential, and the call never reached the hub. - **`POST /order` replays the first ack** even after `expiresAt`, for a quote that already has an order — so a lost `POST /order` answer is recovered the same way. ## A lost answer is not a lost order If the [`POST /order/openfor`](/infinity-interop/api/order-openfor) answer never arrives, or comes back as one of the gateway's availability codes, compute `hubOrderId = hex(sha256("infinity:v1:order:" + quoteId))` and read [`GET /order/{hubOrderId}/status`](/infinity-interop/api/order-upstream-status). The SDK's [`deriveHubOrderId`](/infinity-interop/sdk/reference#derivehuborderid) does the hashing. A not-found in the narrow window right after a crash does not prove there was no submission — the alias record is written best-effort *after* upstream acceptance, so retry the read once, then, if it's still not-found, re-post the identical request. It never opens a second order. The gateway keeps an order submission running at the hub when your client times out or disconnects, so a client that gives up only loses the answer: the read above finds the order. The gateway does cut the hub's call at its own 10 s (`EPX0052`), and a submission cut while the hub is forwarding it is cancelled with it: the read then answers `EIN0009`, and every re-post `EIN0052` until an operator reconciles it ([known issue](/infinity-interop/integration-notes#ki-openfor-cancel)). Give submissions a client timeout longer than the gateway's 10 s, so that you get the gateway's answer rather than none. If the [`POST /order`](/infinity-interop/api/order-create) answer never arrives, re-post the identical request: a quote that already has its order answers with that order's ack, even after `expiresAt`. Never send the transaction again. ## Example error responses Illustrative values; formats follow [Conventions — the response envelope](/infinity-interop/api/conventions#the-response-envelope). `EIN0008` is documented on [Conventions](/infinity-interop/api/conventions#the-response-envelope) already, so these four use different codes. **`EIN0001`** — a form field, here `amount` on `POST /quote`. Terminal codes carry the top-level `message`: ```json { "success": false, "request_id": "197faa50-b235-4537-bc26-7e305d53b559", "message": "You have errors in your request. Please fix them and retry", "errors": [ { "error_reference": "amount", "error_code": "EIN0001", "error_message": "Invalid request" } ] } ``` **`EIN0080`** — the hub's rate limit on `POST /order/openfor`; `error_params[0]` is the suggested back-off in milliseconds (`floor(1000 / tenantRps)`, at least 1; 200 at the defaults). The hub's busy codes carry no top-level `message`, but the hub always sets `error_reference` on this code (the same informational text as the `RateLimitShed` error, not a hint to parse): ```json { "success": false, "request_id": "5bb8a9ee-d246-4018-810b-47f0271367f7", "errors": [ { "error_reference": "rate limit shed, retry after 200ms", "error_code": "EIN0080", "error_message": "Too many requests", "error_params": ["200"] } ] } ``` **`EIN0052`** — outcome unknown, on a different `POST /order/openfor` submission (an earlier, unanswered submission of the same quote is what produced this outcome, not a retry of the `EIN0080` call above). Availability codes also carry no top-level `message`: ```json { "success": false, "request_id": "b5ac3671-c2d8-4432-b96c-363547133564", "errors": [ { "error_code": "EIN0052", "error_message": "Order outcome unknown" } ] } ``` **`EPX0080`** — the gateway's rate limit: your pool is spent. It comes with HTTP `429` and a `Retry-After` header in seconds; `error_params[0]` is the same back-off in milliseconds: ```json { "success": false, "request_id": "0b7c2f5e-8d1a-4c3b-9e6f-2a4d8c1e7b50", "message": "Too many requests", "errors": [ { "error_code": "EPX0080", "error_message": "Too many requests", "error_params": ["250"] } ] } ``` ## See also - [Error reference](/infinity-interop/api/errors), [SDK errors (HubError)](/infinity-interop/sdk/errors) - [Execution errors](/infinity-interop/api/concepts/execution-errors), [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) - [Troubleshooting](/infinity-interop/api/troubleshooting) --- # Handling rate limits URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/handling-rate-limits The two budgets your calls spend — the gateway's and the hub's — what triggers `EPX0080`–`EPX0082` and `EIN0080`, and how to design a request workflow that rarely hits them. ## The gateway's pools Every call reaches the gateway first. It limits: - **secret-key traffic** with one pool per organization, shared by all its keys, whatever IP they call from; - **project-id traffic** with a pool per project, shared by every visitor, plus a bucket per visitor IP. Visitors behind one shared IP, such as an office or a mobile carrier, share one visitor bucket; - **failed credentials** per IP: 20 missing, malformed or unknown keys or project ids within a minute lock that IP out of every call for the rest of the minute. The two pools are separate, so a busy frontend never spends your servers' budget. A refusal is a `429` with `EPX0080` (a pool), `EPX0082` (one visitor) or `EPX0081` (failed credentials); the rows are on [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits#the-gateway). ## The hub's buckets Behind the gateway, the hub limits **per organization**: your organization is one tenant, and every call it makes, with a key or with its project id, draws on the same buckets. The three quote routes (`POST /quote`, `/quote/preview`, `/quote/prepare`) share one bucket; `POST /order` and `/order/openfor` share another. The rate itself — 5 requests/s, burst 20 — and the idle-sweep rule are the spec row on [Limits](/infinity-interop/api/limits#the-hub). ## What is charged, and when The hub charges a request **once it has validated**, before the quote pipeline or the order service does any work — one that fails validation costs nothing, while one the service refuses afterwards, or answers as an idempotent duplicate, has already been charged. The catalog, status, listing and on-chain routes have **no hub-level limit** beyond its per-IP limiter. ## Reading a `429` from the gateway `EPX0080` and `EPX0082` carry the back-off twice: `error_params[0]` in milliseconds, and a `Retry-After` header in seconds. `EPX0081` carries it in `Retry-After` only: the seconds left in the lockout. Wait that long, then retry. On `EPX0081`, first fix the credential and stop whatever repeats it: the failures that count are missing, malformed or unknown keys and project ids, and a valid one never counts, but during the lockout every call from that IP is refused, valid ones included. Every call whose credential is accepted also reports the budget left, in `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` ([Conventions — rate-limit headers](/infinity-interop/api/conventions#rate-limit-headers)): slow down as `RateLimit-Remaining` nears zero, rather than waiting for the `429`. ## Reading `EIN0080` `error_params[0]` is a suggested back-off in milliseconds: `floor(1000 / tenantRps)`, at least 1 (**200 ms** at the defaults), and it's the same fixed value on every shed and every route — the quote bucket, the order bucket, and `/quote/preview` alike. `error_reference` carries informational text ("rate limit shed, retry after ``ms") — read the param, not the string. On the order routes, a rate-limit shed has reserved and forwarded nothing, so re-post the identical body after the wait. ## Optimize your request workflow - **Fetch the catalog once.** [`GET /catalog`](/infinity-interop/api/catalog) changes only on restart — cache it, don't call it per keystroke. - **Debounce `POST /quote/preview`**, and pause it entirely while a wallet prompt is open — a preview request the user can't act on yet is pure budget spend. - **One `POST /quote/prepare` per confirmation**, not per render — a `preparationId` is meant to represent one deliberate signing attempt. - **Poll every 2–3 seconds.** Both status views (`GET /order/{orderId}/status`, `GET /status/{orderId}`) are cached for 2 seconds server-side — polling faster buys nothing. - **Stop at a terminal state.** See [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) for the stop set per view. ## There are no webhooks The hub has no push channel — no webhooks, no websockets. Polling at the cadence above is the only way to follow an order, and lost-ack recovery is by `hubOrderId` ([Handling errors — a lost answer is not a lost order](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order)), not by callback. ## Behind a proxy Everyone your backend serves shares one budget at both layers: your organization's pool at the gateway, and its buckets at the hub. Meter your users yourself before you forward their calls: the SDK's `createInteropProxy` gives you `beforeForward` for exactly that ([Partner backend — hardening](/infinity-interop/sdk/partner-backend#hardening)). A page that uses your project id is limited per visitor IP by the gateway instead. ## The per-IP limiter A refusal that reads `EMC0005` "DDoS check failed" with **no `request_id`** in the body is the hub's own per-IP limiter, ahead of anything else in the hub. It counts the caller's IP, which the gateway passes on: your server's for key calls, the visitor's for project-id calls. There's no `error_params` back-off hint to read: back off and retry later. ## Other busy signals `EIN0081` ("busy") isn't the rate limiter — it's an admission shed with several possible causes: the in-flight quote cap (64 concurrent quotes, process-wide), the upstream-status projection table being full, the hub's own outbound limiter to the upstream not admitting a forward in time, or the upstream answering its own 429. See [Error reference — Hub codes (EIN)](/infinity-interop/api/errors#hub-codes-ein) for the code row and [Submit order (openFor) — Behavior](/infinity-interop/api/order-openfor#behavior) for what each `EIN0081` cause means for whether a re-post is safe. ## See also - [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits), [Limits](/infinity-interop/api/limits), [Error reference](/infinity-interop/api/errors) - [Handling errors](/infinity-interop/api/concepts/handling-errors) --- # Execution errors URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/execution-errors What it means when an order doesn't fill: what the hub declined before anything opened, what it refused after the user's own transaction was out, what the upstream reports about a fill attempt, and what the chain itself says. ## Declined before anything was opened | Code | Lane | What was opened | Do | |---|---|---|---| | `EIN0021` | signed lanes (`oif-escrow-gasless`, `oif-escrow-nonevm`, `aggregator-gasless`) | nothing | re-quote and re-sign; the same signed bytes can never succeed later. `error_reference` carries the upstream's status and code, e.g. `http 409 (SOLVER_REJECTED)` | | `EIN0007` | a Solana envelope that fails the hub's own checks, or a prepared-template mismatch on a signed lane | nothing | re-quote; or a deployment configuration problem if the route isn't wired at all | | `EIN0050` (`error_params: ["upstreamAuth"]`) | signed lanes | nothing | an operator problem — the aggregator refused the hub's own credential; retrying doesn't help | The full per-code rows are on [Submit order (openFor) — Errors](/infinity-interop/api/order-openfor#errors) — not restated here. ## Refused after the transaction was sent On the proxy lanes (`oif-wrap-native`, `oif-swap-token`) the user's own transaction opens the order in escrow **before** [`POST /order/openfor`](/infinity-interop/api/order-openfor) registers it, so a refusal there comes after the deposit: | Code | Lane | What was opened | Do | |---|---|---|---| | `EIN0007` | proxy lanes: the aggregator refused the registration — after `expiresAt`, reference `fail-closed rejected: upstream refused: http 400 (QUOTE_EXPIRED)` — or a prepared-template mismatch | the user's order, by their own transaction | **never re-quote** (below) | | `EIN0005` | proxy lanes: past the hub's 20-minute window, or the template is gone | the user's order | **never re-quote** (below) | | `EIN0050` (`error_params: ["upstreamAuth"]`) | proxy lanes: the aggregator refused the hub's own credential | the user's order | an operator problem; **never re-quote** (below) | A new quote would mean a second deposit. The deposit completes on chain anyway — the solver still fills it — but the hub never records the order, so it is missing from [`GET /orders`](/infinity-interop/api/orders-list) and the status routes ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). Follow it by the send transaction's hash on [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx), then [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill), and withdraw after the escrow expires if it is not filled ([Refunds](/infinity-interop/api/concepts/refunds)). The open flow is the same: [`POST /order`](/infinity-interop/api/order-create) comes after the user sent `executionTx`, so none of its refusals (`EIN0005` past `expiresAt`, `EIN0006`, `EIN0007`, `EIN0010`) is a reason to re-quote — the transaction already sent completes on chain anyway, unregistered. ## The upstream's failure report `failed` and `refunded` are reachable from **any** non-terminal upstream state — a fill attempt can fail at any point in its life, not just at the start. `failedAt` names the upstream's transaction stage at which the attempt failed, one of: `Prepare`, `Fill`, `PostFill`, `PreClaim`, `Claim`. The hub gives no meaning for each stage beyond its name — treat the list as an enum to display, not a set of causes to explain. `failureReason` is the upstream's own text — **display it, never parse it**; the hub does not normalize it into a code. `history[]` lists the states the hub read on the way there, which is not always the full path: a state passed between two reads is absent. Once `state` reaches `failed` or `refunded`, stop polling — see [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle#following-an-order). ## Reports the hub refuses The upstream's own report of an order's state isn't taken at face value: - **`rejectedUpstreamTransition: true`** — the latest report was backwards, unknown, or contradicted the accepted state, so the hub kept the previous one instead. - **`upstreamStale: true`** — the accepted state couldn't be refreshed on the last attempt; the last good state is still served. - **`EIN0007` with no earlier state** — the hub has nothing to fall back to when an upstream answer is refused this early. Keep polling through any of these — none of them is a terminal answer on its own. ## What the chain says `GET /orders`' `chain.status` can read `EXPIRED` (render it as a failure) or `REFUNDED` — both from the chain's own facts rather than the upstream's report. `REFUNDED` is terminal; `EXPIRED` is not: no fill can complete the order any more, but once its refund is observed the entry reads `REFUNDED`, so keep reading it. What happens to a proxy-lane deposit whose registration was refused: [Refunds](/infinity-interop/api/concepts/refunds). ## Terminal states The full table of terminal states per view (upstream, hub projection, on-chain) is on [Order lifecycle — terminal states per view](/infinity-interop/api/concepts/order-lifecycle#terminal-states-per-view). Recovery of an order that neither fills nor refunds is an operator process outside this API. ## See also - [Get upstream status](/infinity-interop/api/order-upstream-status), [List orders](/infinity-interop/api/orders-list) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle), [Refunds](/infinity-interop/api/concepts/refunds) --- # Refunds URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/refunds When and how the deposit comes back, as an FAQ. ## What is a refund? The escrow contract returns the user's deposit. `refund(order)` on the settler is **permissionless** — anyone can call it, but only the order's own user is paid — and **whoever sends it pays the gas**. The hub never sends it, and there is no automatic refund: the hub can tell you when an order is refundable and hand you the tuple to call with, but the user (or anyone) has to call `refund(order)`. ## When does an order become refundable? After the escrow's on-chain `expires` (or a later `refundableAfter` a refund extender set) has passed, for as long as the order is neither settled nor refunded. The escrow doesn't check fills: offering a withdrawal only when no fill was observed is the client's withdraw rule ([below](#how-do-i-know-it-was-not-filled)), not the escrow's. On the proxy lanes, a **refused registration** doesn't touch the deposit: the user's own transaction already opened the order in escrow, the solver still fills it as usual, and if nothing fills it the user waits out that same deadline like any other unfilled order — it doesn't refund early. Never re-quote meanwhile: a new quote means a second deposit ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). The exact `expires` / `refundableAfter` semantics are on [List orders — Response](/infinity-interop/api/orders-list#response) (not restated here). ## What is refunded, and to whom? Whatever token the escrow actually holds — **not** necessarily the token the user thinks they paid with. On a native-coin lane (`oif-wrap-native`), the user paid in the chain's native coin, but the escrow holds (and a refund returns) the **wrapped** token named by the catalog's `wrappedKey` — never the native coin, and never the token paid with on a token-in order either. It's paid to the order's `user`, as recorded on [List orders — Response](/infinity-interop/api/orders-list#response) (`chain.user`); the settler itself pays only that user ([Get escrow order — Behavior](/infinity-interop/api/onchain-order#behavior), [Get on-chain fill](/infinity-interop/api/onchain-fill)). ## How do I know it was not filled? The dating rule that says whether an absent fill is trustworthy lives on [Get on-chain fill — Behavior](/infinity-interop/api/onchain-fill#behavior) — read it there. The client-side **withdraw rule** built on top of it: offer a withdrawal only if **all three** hold — 1. `filled` is absent from [`GET /onchain/{onchainOrderId}/fill`](/infinity-interop/api/onchain-fill), **and** 2. the order's deadline has passed, **and** 3. the destination chain's watermark `updatedAt` is at or after that deadline. `updatedAt` is the server's publish time, not a block timestamp, so it trails chain time by the observers' finality depth — if it's close to the deadline, wait one more poll before deciding. `EIN0051` on this route means **"cannot tell"**, never "not filled" — never offer a withdrawal on the strength of an error. Pass `onchainOrderId` exactly as the hub serves it (`0x` + 64 lowercase hex): the route doesn't validate it, and a miscased or unprefixed id answers with no facts, which reads as "not filled" ([known issue](/infinity-interop/integration-notes#ki-onchain-id)). ## How do I withdraw? These steps are for **EVM-origin** orders only: for a non-EVM origin (Solana), [`GET /onchain/{onchainOrderId}/order`](/infinity-interop/api/onchain-order) answers `EIN0009` permanently, even once its Open, fill and settle are observed, so there is no tuple to send ([known issue](/infinity-interop/integration-notes#ki-nonevm-tuple)). A Solana-origin order refunds through [`GET /onchain/{onchainOrderId}/refund`](/infinity-interop/api/onchain-refund) instead: the hub builds the unsigned refund transaction, whose fee payer and only signer is the order's user, for their wallet to sign and send (`unwrap=true` also turns wrapped SOL back into SOL). The SDK checks that transaction before the wallet sees it ([SDK — Refunds](/infinity-interop/sdk/refunds#solana-origin-orders)). 1. Take `onchainOrderId` from [`GET /orders`](/infinity-interop/api/orders-list) — or, for a proxy-lane deposit the hub never recorded, from [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx) with the send transaction's hash. 2. [`GET /onchain/{onchainOrderId}/fill`](/infinity-interop/api/onchain-fill) — apply the withdraw rule above. 3. [`GET /onchain/{onchainOrderId}/order`](/infinity-interop/api/onchain-order) for the tuple and the settler; verify the settler is one of the catalog's `inputSettlers` for that chain, and check the live contract's own deadline (not just the cached projection). 4. The user sends `refund(order)` to the settler, from whichever device holds the wallet — the on-chain views aren't scoped to the caller, precisely so a refund can be sent from a second device. Steps 2 and 3 take your secret key or your project id, so a page can run them itself. Step 1's `GET /orders` needs the key: a page takes `onchainOrderId` from the order it submitted instead (on the escrow lanes, the openFor ack's `orderId`), or from `GET /onchain/tx/…`, which is open to it. ## Is `refundEligible` enough? No. `GET /orders`' `chain.refundEligible` is an advisory estimate from `refundEligibilitySource: "stored_projection"` — undated stored observations, not live contract state — and `refundRequiresOnchainCheck` is **always `true`**. Verify the live contract (step 3 above) before offering, or sending, a refund. ## How does a refund show up? Three places agree once a refund is observed: `OnchainFill.refunded` (an `EventFact`), `GET /orders`' `chain.status: "REFUNDED"`, and the upstream status's `state: "refunded"`. ## Can I force a refund for testing? There is no way to force a refund for testing: an order becomes refundable only once its on-chain deadline has passed, and the hub has no test-refund path. ## See also - [Get on-chain fill](/infinity-interop/api/onchain-fill), [Get escrow order](/infinity-interop/api/onchain-order), [List orders](/infinity-interop/api/orders-list) - SDK: [Refunds](/infinity-interop/sdk/refunds) — the withdraw rule, checked on chain, and the refund sent from the user's wallet --- # Order lifecycle URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/order-lifecycle The hub answers "where is my order?" three different ways, because it's really three different questions. This page lays out all three: which id opens which view, how one order moves through them over time, and when to stop polling each one. ## The three status views | View | Answers | Source | |---|---|---| | [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) | what the aggregator reports about an **openFor** order | upstream, **advisory** | | [`GET /status/{orderId}`](/infinity-interop/api/hub-status) | the hub's own event-derived lifecycle, across eight domains | the hub's own projection | | [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) | what the hub's observers actually saw on the chains | chain observations | None of the three supersedes another — they answer from different sources, and a client following an order to completion typically reads more than one. ## Which id opens which view | Flow | You have | Use it on | |---|---|---| | Gasless openFor | `orderId` or `hubOrderId` (from the ack) | [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) | | Gasless openFor | `hubOrderId` | also [`GET /status/{orderId}`](/infinity-interop/api/hub-status) — it answers for an openFor order by its `hubOrderId` too, but an OIF-lane order never moves past `intent: CREATED` there ([known issue](/infinity-interop/integration-notes#ki-hubstatus-frozen)) | | Open flow | the `POST /order` `orderId` (a hub id) | [`GET /status/{orderId}`](/infinity-interop/api/hub-status) **only** — open-flow orders are not served on the upstream-status route | | Any flow | `onchainOrderId` (from `GET /orders`, which lists openFor orders only, or a fill-by-tx hit) | the on-chain views, [`GET /onchain/{id}/fill`](/infinity-interop/api/onchain-fill) and [`GET /onchain/{id}/order`](/infinity-interop/api/onchain-order) | The full id-namespace table and how each id derives from the others is [Conventions — the three order-id namespaces](/infinity-interop/api/conventions#the-three-order-id-namespaces) — not restated here. One gap worth calling out explicitly: **the Permit2 (EVM escrow-gasless) lane discloses no on-chain id anywhere in the quote**. The id appears at submission, not on observation: the openFor ack's `orderId` is the on-chain id on this lane, and the order's `GET /orders` entry carries it as `onchainOrderId` at once, before any `chain` object, with `onchainOrderIdSource: "upstream"` — the aggregator's answer, which the hub does not recompute on this lane (the lanes it recomputes read `"verified"`). ## One order over time A gasless openFor order, followed end to end: 1. `POST /order/openfor` acks with upstream `state: "created"`. 2. Until the Open is observed, `GET /orders`' entry has no `chain` object at all — `chain` is a fact about what has been observed, not a placeholder that starts in some default state (it would only read `AWAITING_OPEN` if a fill were somehow observed before the Open). Once the escrow's Open is observed on the source chain, `chain.status` reads `OPEN`. 3. The upstream reports `executing` then `executed`, with a `fillTransaction` reference — unverified at this point. 4. The destination fill is observed on chain; `chain.status` becomes `FILLED`. 5. The on-chain view shows `settled` once the settle is observed (gated by attestation — an observed settle *is* the attesters' approval, made visible). The upstream may report `settled` and then `finalized`, skip `settled`, or stop at `executed` for good ([known issue](/infinity-interop/integration-notes#ki-upstream-stall)). 6. `finalized` on the upstream view, `SETTLED` on the chain view — both terminal, from different sources. The upstream's `settled` is not terminal: `finalized` can follow it. Each view lags by a different amount: the two status-route caches are 2 seconds; the on-chain observer watermarks lag by however long the observers take to ingest a block; a chain's `GET /status/{orderId}` watermark moves only when the reconciler processes a new observation on that chain, so on a quiet chain it can be far behind ([known issue](/infinity-interop/integration-notes#ki-hubstatus-frozen)). ## Following an order - **openFor orders**: [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) every 2–3 s until `finalized`, `failed` or `refunded`, with a stall rule: an order can stay `executed` long after it filled and settled on chain. Once it is `executed`, read [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) by its `onchainOrderId`, and hand off to it after a few minutes without progress ([known issue](/infinity-interop/integration-notes#ki-upstream-stall)). - **open-flow orders**: [`GET /status/{orderId}`](/infinity-interop/api/hub-status), with your key or your project id. - **your organization's history**: [`GET /orders`](/infinity-interop/api/orders-list) — the openFor orders placed under the `X-Infinity-Subject` you send: one user's, or everyone's when all your calls share one subject; open-flow registrations are not listed — then the on-chain views by `onchainOrderId`. - **from a web page with your project id**: every view above but the history. Keep the ids of the orders the page submits, and follow them on the status routes and the on-chain views; [`GET /orders`](/infinity-interop/api/orders-list) needs your server's key ([Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages)). ## Advisory versus observed The upstream view is explicitly advisory, on its own terms: `stateSource` is always `"upstream-advisory"`, `finalityVerified` is always `false`, and `upstreamFillReported` records only that the upstream *supplied* a fill reference — it is not chain proof. Treat every field on that view as "what the aggregator says", never as settled fact. **Show money — a fill actually happening, funds actually moving — from the on-chain views only.** ## Terminal states per view | View | Success | Failure | |---|---|---| | Upstream (`GET /order/{orderId}/status`) | `finalized` (`settled` is not terminal: `finalized` can follow) | `failed`, `refunded` | | Hub projection (`GET /status/{orderId}`) | domain-by-domain — see [Get hub status — Response](/infinity-interop/api/hub-status#response) for the eight domains' own terminal columns; an OIF-lane openFor order never gets past `intent` `CREATED` there ([known issue](/infinity-interop/integration-notes#ki-hubstatus-frozen)) | same table | | On-chain (`GET /orders`' `chain.status`) | `SETTLED` | `REFUNDED`. `EXPIRED` (render as a failure) is not terminal: `REFUNDED` follows once a refund is observed | ## See also - [Get upstream status](/infinity-interop/api/order-upstream-status), [Get hub status](/infinity-interop/api/hub-status), [Get on-chain fill](/infinity-interop/api/onchain-fill) - [Execution errors](/infinity-interop/api/concepts/execution-errors), [Refunds](/infinity-interop/api/concepts/refunds) - SDK: [Tracking](/infinity-interop/sdk/execution#tracking) — when to stop polling, with the chain facts deciding --- # Solana support URL: https://docs.c8ntinuum.com/infinity-interop/api/concepts/solana-support Solana is served as a source and as a destination. The flow is the same gasless openFor flow as the EVM escrow — quote, sign, submit, follow — but the signing family, the signature envelope and the on-chain views differ. This page collects every Solana-specific rule; the route pages hold the rest. ## Quoting - **`sourceAccount` is required for any non-EVM leg.** The solver compiles the transaction around it. A Solana source without `sourceAccount` answers a bare `EIN0050` on [`POST /quote/preview`](/infinity-interop/api/quote-preview) and `EIN0008` with no reference on [`POST /quote`](/infinity-interop/api/quote) — not a field error ([known issue](/infinity-interop/integration-notes#ki-preview-no-offer)). Check `sourceAccount` before retrying. - **Addresses** are base58 or `0x` + 64 hex; base58 is converted at the boundary, and hex is the wire form. `recipient` is spelled in the **destination** chain's family and `sourceAccount` in the **source** chain's family. A non-EVM `recipient` or `sourceAccount` that does not decode is `EIN0001` naming the field. - **Native SOL.** Quote the chain row's `nativeKey` (`sol`) when the user means to spend the coin itself, and `wsol` when they hold wrapped SOL. On a native-coin quote the disclosure carries `wrapsNative` and `wrappedAsset`: label the screen with them, never branch the flow on them. The escrow holds, and a refund returns, the wrapped token. - **The disclosure.** `disclosure.signablePayload.kind` is `oif-escrow-nonevm`, `signatureEnvelope` is `ed25519-97`, and `disclosure.signablePayload.orderId` is the on-chain order id. ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const q = await client.getQuote({ sourceChain: "solana-localnet", destChain: "eip155-8888", sourceAsset: "sol", destAsset: "ctm", // the chain row's nativeKey — never `userProxy` amount: "250000000", recipient: address, sourceAccount: "CCckTaUo6jc8Q6HydkjKjvGX3MHJS77uwis2DN7UFoqZ", }); ``` An EVM source can pay out on Solana too: send `sourceAccount` whenever the recipient's family differs from the source's ([SDK — a quote request](/infinity-interop/sdk/quotes#a-quote-request)). ## Signing The disclosed `signPayload` is `{…, sender, feePayer, transactionMessage, digest}`. `sender` is the user and `feePayer` is the solver — that split is the gasless promise: the user never pays gas to open the order. - The funding wallet signs the **base64-decoded `transactionMessage` bytes** with Ed25519 — never `digest`, which is a checksum, not what gets signed. Sign the disclosed bytes verbatim. - The wire form is the 97-byte envelope `0x00` ‖ 64-byte signature ‖ 32-byte public key (the order's user), as standard base64: 132 characters. The hub also reads hex but forwards the string unchanged, and the aggregator accepts only base64. With the SDK, `executeQuote` does all of this, after checking the message ([Wallets — how a Solana order is signed](/infinity-interop/sdk/wallets#how-a-solana-order-is-signed)). The pieces are exported too: ```ts if (quote.lane.kind !== "solana-escrow") throw new Error("expected a Solana escrow quote"); // quote.lane.message: the decoded transactionMessage bytes const { signature, publicKey } = await signSolanaOrderMessage(wallet, quote.lane.message, "auto"); const submitted = await client.submitOpenFor({ quoteRequestId: quote.quoteRequestId, quoteId: quote.quoteId, signature: solanaEnvelope(signature, publicKey), // 0x00 ‖ signature ‖ public key, base64 }); ``` ## Submitting Send the envelope as `signature` on [`POST /order/openfor`](/infinity-interop/api/order-openfor) with the quote's `quoteRequestId` and `quoteId`, before `expiresAt`. Unlike the EVM escrow lane, the hub checks the envelope itself before forwarding: 1. 97 bytes; 2. flag byte `0x00`; 3. the public key equal to the order's user; 4. the signature valid over the disclosed `transactionMessage`; 5. the message's order id equal to the quote's. A failure on any of these is `EIN0007`: re-quote and sign again. The forward-reservation, replay and expiry rules are the signed lanes' and are on the route page ([Behavior](/infinity-interop/api/order-openfor#behavior)). ## Following - Poll [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) with the ack's `orderId` as on any lane. Solana-origin orders reach `finalized` upstream. - **A Solana Open signature does not resolve on [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx)**, while the same order's settle signature does ([known issue](/infinity-interop/integration-notes#ki-solana-open-tx)). Follow a Solana-origin order by `onchainOrderId` on [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) instead. - On Solana `txHash` is case-sensitive base58 and is used as given; the hub lowercases it on every chain except Solana and Sui. ## Refunds There is no escrow tuple for a non-EVM origin: [`GET /onchain/{orderId}/order`](/infinity-interop/api/onchain-order) answers `EIN0009` permanently for a Solana-origin order, even once its Open, fill and settle are all observed ([known issue](/infinity-interop/integration-notes#ki-nonevm-tuple)). The documented withdraw steps need that tuple, so they are EVM-only ([Refunds — how do I withdraw?](/infinity-interop/api/concepts/refunds#how-do-i-withdraw)). A Solana-origin order refunds through [`GET /onchain/{orderId}/refund`](/infinity-interop/api/onchain-refund) instead: the hub builds the unsigned transaction for the order's user to sign and send. What a refund returns is unchanged: the wrapped token the escrow holds, to the order's user (`unwrap=true` turns wrapped SOL back into SOL in the same transaction). ## Wallets The SDK's `solanaWalletFromStandard` works with any Wallet Standard wallet (Phantom, Solflare, Backpack…). Wallets differ in what they will sign, so the SDK signs the `transactionMessage` bytes either as a transaction or as a raw message, and verifies the Ed25519 signature before sending it ([Wallets — how a Solana order is signed](/infinity-interop/sdk/wallets#how-a-solana-order-is-signed)). ## Cluster domain The chain domain is `solana-` as the deployment's catalog declares it — read the `chainDomain` of the row whose `family` is `solana` rather than assuming a name. Samples in this reference use `solana-localnet`. ## See also - [Supported chains and assets](/infinity-interop/api/supported-chains-and-assets) - [Get quote](/infinity-interop/api/quote), [Submit order (openFor)](/infinity-interop/api/order-openfor) - [Step execution — the signature step](/infinity-interop/api/concepts/step-execution#the-signature-step) - [Refunds](/infinity-interop/api/concepts/refunds) - SDK: [Lanes — `solana-escrow`](/infinity-interop/sdk/lanes#solana-escrow), [Wallets](/infinity-interop/sdk/wallets), [Refunds — Solana-origin orders](/infinity-interop/sdk/refunds#solana-origin-orders) --- # Supported chains and assets URL: https://docs.c8ntinuum.com/infinity-interop/api/supported-chains-and-assets The hub does not publish a fixed list of chains and tokens. Each deployment declares its own corridors, and `GET /catalog` is the only source of truth for them. This page explains how to read what the catalog returns, and the formats every chain, asset and address takes on the wire. ## Fetch, don't hard-code [`GET /v1/catalog`](/infinity-interop/api/catalog) is open to your project id as well as to your secret key, so a page's pickers render before your user signs in to anything. It is built once at boot from configuration and changes only on restart, so cache it for the life of the page or the process. The external aggregator lanes are not listed: quoting is their discovery mechanism. ```bash curl -sS https://proxy-api.infinimesh.net/v1/catalog \ -H "x-api-key: $HUB_API_KEY" ``` ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const { chains, assets } = await client.getCatalog(); ``` ## Chain domains A chain is named by its **chain domain**, the string `sourceChain`, `destChain` and every on-chain route take ([Conventions — value formats](/infinity-interop/api/conventions#value-formats)): | Family | Domain shape | Rule | |---|---|---| | EVM | `eip155-` | the chain id in decimal, no leading zero | | Solana | `solana-` | lowercase `[a-z0-9-]`, as the deployment declares it | | Sui | `sui-` | lowercase `[a-z0-9-]`, as the deployment declares it | | Aptos | `aptos-` | lowercase `[a-z0-9-]`, as the deployment declares it | The c8ntinuum networks map to these domains: | Chain domain | Network | Chain id | |---|---|---| | `eip155-8` | [c8ntinuum Mainnet Beta](/developer-essentials/network-information/mainnet) | 8 | | `eip155-8888` | [c8ntinuum Testnet](/developer-essentials/network-information/testnet) | 8888 | The Solana cluster's domain is whatever the deployment's catalog declares — read it from the `chainDomain` of the row whose `family` is `solana` rather than assuming a name. The samples in this reference use `solana-localnet`. ## Reading a chain row `chains[]` is sorted by domain. Each row: | Field | Type | Meaning | |---|---|---| | `chainDomain` | string | e.g. `eip155-8888` | | `family` | string | `evm`, `solana`, `sui` or `aptos` — the signing family, which decides the whole rail | | `userProxy` | string | the chain's UserProxy (the wrap-native / swap-token contract); absent when none | | `swapRouter` | string | present means the chain accepts an **undeclared token as a source** through the token-in lane | | `nativeKey` | string | present means the chain's native coin can be a **source** under this asset key (wrapped by the deployment). Absent means the native coin is a destination only | | `wrappedKey` | string | the wrapped token the escrow actually holds on the native-coin lane — and what a refund returns. Absent when the chain declares no native-coin lane | | `inputSettlers[]` | string[] | the chain's input settlers (escrows), canonical form — the `settler` of `GET /onchain/{id}/order` is one of them. Absent when the chain declares none | How to read it: - **`family` decides the rail.** It tells you how the user signs and which lanes can serve the chain ([Families and lanes](#families-and-lanes) below). - **`nativeKey` present** means the native coin can be a source: quote it under that key (never key on `userProxy`). **Absent** means the native coin is a destination only. - **`wrappedKey`** is what the escrow holds on the native-coin lane, and what a refund returns — never the native coin itself ([Refunds](/infinity-interop/api/concepts/refunds#what-is-refunded-and-to-whom)). - **`swapRouter` present** means an undeclared token is accepted as a source through the token-in lane; on any other chain an undeclared source is `EIN0002`. - **`inputSettlers[]`** are the escrows. Before sending a refund, verify the settler you were handed is one of them. ## Reading an asset row `assets[]` is sorted by chain then key. Each row: | Field | Type | Meaning | |---|---|---| | `chainDomain` | string | the asset's chain | | `assetKey` | string | the wire key `sourceAsset`/`destAsset` accept — the lowercase symbol | | `assetRef` | string | the same asset as `:` — accepted wherever `assetKey` is | | `symbol` | string | display symbol | | `address` | string | canonical form: lowercase `0x` hex, 40 characters on EVM, 64 on Solana/Sui/Aptos | | `decimals` | integer | | | `native` | boolean | the chain's native coin (the all-zero address) | - `sourceAsset` and `destAsset` take either the **asset key** (`weth`) or the **asset reference** `:
`. A bare `0x` address is refused. A reference to a declared asset behaves exactly as its key. - `native: true` is the zero address. It receives fills and cannot fund an escrow directly: offer it as a destination, or as a source only where the chain row has a `nativeKey`. ## Families and lanes - **EVM.** The Permit2 gasless escrow (`oif-escrow-gasless`: the user signs, the solver pays gas), the two proxy lanes (`oif-wrap-native` for the native coin, `oif-swap-token` for an undeclared token on a chain with a `swapRouter`: the user sends a transaction) and the external EIP-3009 gasless lane (`aggregator-gasless`). - **Solana.** The ed25519 escrow (`oif-escrow-nonevm`: the user signs, the solver pays gas). Solana serves as a source and as a destination. Everything Solana-specific: [Solana support](/infinity-interop/api/concepts/solana-support). - **Sui and Aptos.** Source chains are EVM and Solana. Sui and Aptos are supported as destination chains only: a quote whose source is a Sui or Aptos chain is refused. The catalog's `family` tells you which rail a chain is on. | Lane (`signablePayload.kind`) | Family | Step | Who pays gas | |---|---|---|---| | `oif-escrow-gasless` | EVM (Permit2) | signature | the solver | | `oif-escrow-nonevm` | Solana | signature | the solver | | `aggregator-gasless` | EVM (EIP-3009) | signature | the solver | | `oif-wrap-native` | EVM | transaction | the user | | `oif-swap-token` | EVM | transaction | the user | | `aggregator-bridge` | external aggregator | transaction + `personal_sign` | the user | The full table, with the product ids and which route submits each lane: [Step execution — the lanes](/infinity-interop/api/concepts/step-execution#the-lanes). ## Address formats | Family | Format | |---|---| | EVM | `0x` + 40 hex, any case | | Solana | `0x` + 64 hex, or base58 (converted at the boundary; hex is the wire form) | | Sui / Aptos | hex of up to 64 characters, `0x` optional, left-padded to 64 | `recipient` is spelled in the **destination** chain's family and `sourceAccount` in the **source** chain's family. A non-EVM `recipient` or `sourceAccount` that does not decode is `EIN0001` naming the field; an EVM address is not format-checked at that point. ## Contract addresses Permit2 at `0x000000000022D473030F116dDEE9F6B43aC78BA3` is the only fixed address: it is the `verifyingContract` of every EVM gasless `signPayload`. Every other contract — `userProxy`, `swapRouter` and the `inputSettlers` — is per chain and per deployment: read them from the catalog rather than copying them from a sample. The catalog sample on the [route page](/infinity-interop/api/catalog#response) is illustrative. ## See also - [Get catalog](/infinity-interop/api/catalog) - [Conventions — value formats](/infinity-interop/api/conventions#value-formats) - [Step execution](/infinity-interop/api/concepts/step-execution) - [Solana support](/infinity-interop/api/concepts/solana-support) - [Refunds](/infinity-interop/api/concepts/refunds) - SDK: [`client.getCatalog()`](/infinity-interop/sdk/quotes#the-catalog) --- # Flows URL: https://docs.c8ntinuum.com/infinity-interop/api/flows This page is the map of the sequences: one paragraph and a diagram-free summary per flow, each linking to where the explanation now lives under [Concepts](/infinity-interop/api/concepts). The field tables that are spec, not prose, stay here. With the TypeScript SDK, `executeQuote` runs each of these flows in one call: see [Executing orders](/infinity-interop/sdk/execution). ## Gasless openFor Quote, sign `signPayload` verbatim (EVM: `eth_signTypedData_v4` + `0x00`; external gasless: raw 65 bytes; Solana: the 97-byte base64 envelope), [`POST /order/openfor`](/infinity-interop/api/order-openfor), poll [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status). Per-family signing and the reasons: [Step execution](/infinity-interop/api/concepts/step-execution#gasless-openfor). ## Proxy lanes The user sends `executionTx` themselves, then registers with an unsigned [`POST /order/openfor`](/infinity-interop/api/order-openfor) at once, well before `expiresAt`: the hub keeps the quote registrable for 20 minutes, but the aggregator refuses a registration after `expiresAt` ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). Once the transaction is out, never re-quote. What the steps are and what a refused registration does to the deposit: [Step execution](/infinity-interop/api/concepts/step-execution#proxy-lanes), [Refunds](/infinity-interop/api/concepts/refunds#when-does-an-order-become-refundable). ## Open flow The user sends `approvals` then `steps`, `personal_sign`s `signIntent`, and records acceptance with [`POST /order`](/infinity-interop/api/order-create) before `expiresAt` — once the transaction is out, never re-quote; follow it on [`GET /status/{orderId}`](/infinity-interop/api/hub-status). `POST /order` is for `create` winners only: it refuses every OIF-lane quote with `EIN0007` ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)). [Step execution](/infinity-interop/api/concepts/step-execution#open-flow). The TypeScript SDK names which of these applies as the quote's lane: `evm-permit2`, `solana-escrow` or `evm-3009` (signed openFor), `wrap-native` or `swap-token` (proxy lanes), `open-flow`, or `informational` (a solver-swap or `transfer` winner, which discloses neither `signPayload` nor `executionTx`). See [Step execution — which step applies](/infinity-interop/api/concepts/step-execution#which-step-applies-submissionflow). ## Preview then prepare Browse with [`POST /quote/preview`](/infinity-interop/api/quote-preview), then [`POST /quote/prepare`](/infinity-interop/api/quote-prepare) under one `preparationId` per signing attempt; a retry with the same id returns the same quote and expiry. [Step execution](/infinity-interop/api/concepts/step-execution#preview-then-prepare). ## Status polling openFor orders on [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) every 2–3 s until `finalized`, `failed` or `refunded` (`settled` is not final: `finalized` can follow), with a stall rule, since an order can stop at `executed` ([known issue](/infinity-interop/integration-notes#ki-upstream-stall)); open-flow orders on [`GET /status/{orderId}`](/infinity-interop/api/hub-status); history of openFor orders on [`GET /orders`](/infinity-interop/api/orders-list). Which view answers what: [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle). ## Lost-ack recovery Derive `hubOrderId` from `quoteId`, read the status, and re-post the identical request if needed — never re-quote. [Handling errors](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order). ## Withdraw (refund) Take `onchainOrderId` from [`GET /orders`](/infinity-interop/api/orders-list) and pass it exactly as the hub serves it (`0x` + 64 lowercase hex: on-chain ids are matched, not validated). Confirm the order exists on `GET /onchain/{orderId}/order` before offering a withdraw, apply the dating rule on [`GET /onchain/{id}/fill`](/infinity-interop/api/onchain-fill), fetch the tuple from [`GET /onchain/{id}/order`](/infinity-interop/api/onchain-order), send `refund(order)` to the settler. The steps are for EVM-origin orders only: a non-EVM origin has no tuple ([known issue](/infinity-interop/integration-notes#ki-nonevm-tuple)). A Solana-origin order refunds through [`GET /onchain/{id}/refund`](/infinity-interop/api/onchain-refund) instead: the hub builds the unsigned transaction, and the order's user signs and sends it. [Refunds](/infinity-interop/api/concepts/refunds). ## Auctions When the winner's order carries a pricing context the quote carries `auction: {kind, exclusiveFor?, startTime, stopTime, slope, floor, ceiling}` — a decaying Dutch auction, or a `limit` / `exclusiveLimit` winner that doesn't decay (`slope` `"0"`, `ceiling` = `floor`). A firm winner has no `auction`: | Field | Meaning | |---|---| | `kind` | `dutchAuction`, `exclusiveDutch`, `exclusiveLimit` or `limit` | | `exclusiveFor` | the bytes32 solver identity, on the exclusive kinds | | `startTime`, `stopTime` | unix seconds: decay (and exclusivity) begins; decay ends | | `slope` | per-second decay, base units (decimal string) | | `floor` | `= amountOut`, the guaranteed minimum | | `ceiling` | `floor + slope·(stopTime − startTime)`, the opening price | `amountOut` is the floor; what the settler pays at fill time and how auctioned winners are ranked against firm ones: [Trade types](/infinity-interop/api/concepts/trade-types#firm-and-auctioned-winners). ## See also - SDK: [Executing orders](/infinity-interop/sdk/execution) — the same flows, run by `executeQuote` - [Quickstart](/infinity-interop/api/quickstart) - [Step execution](/infinity-interop/api/concepts/step-execution) - [Trade types](/infinity-interop/api/concepts/trade-types) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) - [Handling errors](/infinity-interop/api/concepts/handling-errors) - [Refunds](/infinity-interop/api/concepts/refunds) --- # Error reference URL: https://docs.c8ntinuum.com/infinity-interop/api/errors The SDK's own view of this is [SDK errors](/infinity-interop/sdk/errors) (the `HubError` class, and `adviseError`). Three families of codes reach you, all in the same [response envelope](/infinity-interop/api/conventions#the-response-envelope): - **`EIN`**, the hub's own codes, always with HTTP `400`; - **`EPX`**, the gateway's refusals, with a real HTTP status (`400`, `401`, `403`, `404`, `413`, `429`, `502`, `503`, `504`); - **`EMC`**, the hub's framework, which checks a request before the hub's routes see it, with HTTP `400`. ## The blocks, and when to retry `EIN` and `EPX` codes share the same numbering, so one rule reads both: | Block | Codes | Meaning | Client action | |---|---|---|---| | Terminal | `0001`–`0049` | the request can never succeed as sent | fix the input, the credential or the origin, or re-quote — never retry unchanged, and never re-quote once the user has sent the quote's `executionTx` | | Availability | `0050`–`0079` | retry later; never evidence about order state | back off and retry — **except `EIN0050` with `error_params[0]` `"upstreamAuth"`** (the hub sends it on `EIN0050` from any route that calls an upstream, except `POST /quote/preview`, which answers a bare `EIN0050`), which only an operator can clear. On a submission, an availability answer is a lost answer ([Handling errors — a lost answer](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order)) | | Busy | `0080`–`0099` | back off | wait `error_params[0]` milliseconds (`EIN0080`, and the gateway's `EPX0080` and `EPX0082`, which also send `Retry-After` in seconds; `EPX0081` sends only `Retry-After`), then retry | The TypeScript SDK exposes the rule as `HubError.retryable` / `retryAfterMs`. What a retry means per block, and why `EIN0052` and a post-forward `EIN0050`/`EIN0081` on [`POST /order/openfor`](/infinity-interop/api/order-openfor) need more than a retry loop: [Handling errors](/infinity-interop/api/concepts/handling-errors). ## Hub codes (`EIN`) | Code | Name | Meaning | Reference / params | Action | |---|---|---|---|---| | `EIN0001` | invalid form | a request field or the body failed validation | the field (`amount`, `body`, …; `x-infinity-subject` for a subject missing where the hub requires one, or outside its rule); on an asset-reference chain mismatch, params `[reference chain, request chain]` | fix the input | | `EIN0002` | unsupported corridor | an asset or corridor does not resolve | a detail | pick another asset or chain | | `EIN0003` | invalid amount | defensive only — amount validation reports `EIN0001` | | | | `EIN0004` | — | reserved, never emitted (an unknown quote is `EIN0010`) | | | | `EIN0005` | quote expired | the quote or preparation is no longer submittable: past `expiresAt` (on a proxy-lane registration, past its 20-minute window), or its template is no longer held | | re-quote and re-sign — normal flow before anything is sent. **Never** once the user's transaction is out (a proxy-lane registration, `POST /order`): a new quote means a second transaction ([Execution errors](/infinity-interop/api/concepts/execution-errors#refused-after-the-transaction-was-sent)) | | `EIN0006` | epoch superseded | the configuration the quote ran under changed | | re-quote — except on `POST /order`, which comes after the user's transaction: as `EIN0005` | | `EIN0007` | rejected | the hub's own fail-closed checks refused it (a Solana envelope, a template, a proxy-lane registration the aggregator refused, the `POST /order` re-approval, which every OIF-lane quote fails ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)), a route whose service is not configured, …) | a detail; a proxy-lane registration refused after `expiresAt` reads `fail-closed rejected: upstream refused: http 400 (QUOTE_EXPIRED)` | re-quote; quote the `request_id` if it repeats — except after the user's transaction is out (a proxy-lane registration, `POST /order`): as `EIN0005` ([known issue](/infinity-interop/integration-notes#ki-proxy-window)) | | `EIN0008` | no viable route | a fact about the corridor | `providerRefused` (another amount); `notFillable` + params `[dest chain]` (another destination); or empty | change the request | | `EIN0009` | order not found | unknown id, or not yours (`GET /order/{orderId}/status` answers another organization's order, or one placed under another subject, exactly as an unknown one); on `GET /onchain/{id}/order`, an Open not observed yet — or a non-EVM origin, which never gets a tuple ([known issue](/infinity-interop/integration-notes#ki-nonevm-tuple)) | | check the id namespace; on `GET /onchain/{id}/order`, poll for an EVM-origin order only | | `EIN0010` | bad signature | a missing or misplaced order signature, a bad one where the hub checks it (`POST /order`; the format on `aggregator-gasless`), or an unknown or foreign quote (one code on purpose). The hub does not verify an `oif-escrow-gasless` signature: a bad one comes back as `EIN0021` | | send the signature the lane expects (none on a proxy lane) and re-post; re-quote only before the user's transaction is out | | `EIN0021` | order declined | the solver or provider refused a signed openFor order (any signed lane), a bad `oif-escrow-gasless` signature included; nothing was opened | the upstream status and code | re-quote. Don't re-post the declined body: the repeat answers `EIN0050`, then `EIN0052` ([known issue](/infinity-interop/integration-notes#ki-redecline)) | | `EIN0049` | internal | scrubbed internal error | | quote the `request_id` | | `EIN0050` | upstream unavailable | quote/order upstream unavailable, or an upstream refused the hub's own credential | on `POST /quote` and `/quote/prepare`, params `[cause]`: `responseRejected`, `solverUnavailable`, `providerMissed` or `upstreamAuth` (reference `quote: unavailable ()`); on `POST /order/openfor` and `GET /order/{orderId}/status`, params `[upstreamAuth]` (reference `upstreamAuth`) for the refused credential; `POST /quote/preview` answers every provider failure bare | retry later (not `upstreamAuth`: an operator must renew the credential) | | `EIN0051` | service unavailable | a store or projection read was unavailable, or an on-chain answer the hub cannot date | | retry later — never read it as "no fill" | | `EIN0052` | outcome unknown | a submission of this quote may have opened the order: an earlier one went unanswered (a submission cut while the hub was forwarding it included — [known issue](/infinity-interop/integration-notes#ki-openfor-cancel)), or this one was answered in a way the hub cannot read | | never re-quote; re-posting is safe and answers `EIN0052` until an operator reconciles the submission | | `EIN0080` | rate limited | the hub's token bucket for your organization: the quote routes and the order routes each have one | params `[retryAfterMs]`: a suggested back-off, `floor(1000 / tenantRps)` ms and at least 1, on every route; reference `rate limit shed, retry after ms` (informational — read the param) | wait, then retry | | `EIN0081` | busy | admission shed: the in-flight quote cap, the upstream-status projection table, and on `POST /order/openfor` the forward-reservation table (store-less assemblies), the hub's outbound limiter to the upstream, or the upstream's own 429 | | back off ([Submit order (openFor)](/infinity-interop/api/order-openfor) for openFor) | Codes `EIN0011` to `EIN0020` belong to hub routes that are not part of the partner API. ## Gateway codes (`EPX`) The gateway checks every call before the hub sees it. It answers its own refusals in the hub's envelope, with the real HTTP status and an `EPX` code in the same blocks as the hub's codes: the body's `message` and the entry's `error_message` are the Message column below, there is no `error_reference`, and the `X-Request-Id` header names the call. `EPX0080` and `EPX0082` carry their back-off in `error_params[0]` (milliseconds) and in a `Retry-After` header (seconds); `EPX0081` in `Retry-After` only. | Code | HTTP | Message | When | Action | |---|---|---|---|---| | `EPX0001` | 401 | Missing or malformed API key | no credential header at all, or an `x-api-key` that is not `c8n_sk_` followed by 43 base64url characters | copy the secret key again from the hub dashboard; a page sends `x-project-id` instead | | `EPX0002` | 401 | Invalid API key | the key is unknown, revoked or expired | make a new key in the hub dashboard | | `EPX0003` | 404 | Route not found | no such route or method under the version, or `GET /orders` with a project id (from a browser, its pre-flight is refused, so the page sees a CORS failure) | check the method and the path; read the order history from your server, with a secret key | | `EPX0004` | 404 | Version not available | the hub behind the gateway does not serve that API version | check the path: `/v1/…` | | `EPX0005` | 413 | Request body too large | the request body is over 64 KiB | send a smaller body | | `EPX0006` | 400 | Unreadable request body | the request body could not be read | fix the request | | `EPX0007` | 400 | Send one credential | both `x-project-id` and `x-api-key` were sent | send one credential | | `EPX0008` | 401 | Malformed project id | `x-project-id` is not `c8n_pk_` followed by 22 base64url characters | copy the id from your project page in the hub dashboard | | `EPX0009` | 401 | Invalid project id | the project id is unknown | copy the id from your project page in the hub dashboard | | `EPX0010` | 403 | Origin not allowed | the page's origin is not on the project's list, or the request has no `Origin` while a list is set | add the origin to the project's list in the hub dashboard; a server calls with a secret key | | `EPX0050` | 502 | Hub unavailable | the hub is unreachable | retry later; on a submission, the answer is lost: look the order up before re-posting | | `EPX0051` | 503 | Hub access unavailable | the gateway's own access to the hub is unavailable | retry later; on a submission, as `EPX0050` | | `EPX0052` | 504 | Hub timed out | the hub did not answer in time: 20 s on the three quote routes, 10 s on every other route | retry later; on a submission the outcome is unknown: as `EPX0050` | | `EPX0053` | 503 | Key check unavailable | the gateway could not check the key or the project id; the call never reached the hub | retry later | | `EPX0080` | 429 | Too many requests | a pool is spent: your organization's, shared by all its secret keys, or your project id's, shared by all its visitors | wait `error_params[0]` ms, then retry | | `EPX0081` | 429 | Too many failed attempts | 20 missing, malformed or unknown keys or project ids from this IP within a minute: every call from it, valid ones included, is refused for the rest of that minute | fix the credential and stop the caller that repeats it; wait `Retry-After` seconds (there are no `error_params`) | | `EPX0082` | 429 | Too many requests from this client | this visitor IP's own bucket, on project id calls | wait `error_params[0]` ms, then retry | A **refused credential or origin** (`EPX0001`, `EPX0002`, `EPX0008`–`EPX0010`) is in the terminal block, and only an operator can fix it: the SDK marks it `HubError.isCredentialRefused`, and `adviseError` routes it to your team, never to the user. Don't retry it: refused credentials count toward `EPX0081`. The hub's rate-limit rules and the gateway's are side by side on [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits#rate-limits). ## Framework codes (`EMC`) Returned by the hub's framework before (or around) the hub's own code. `error_reference` names the offending request field for the field codes. | Code | Meaning | When | |---|---|---| | `EMC0000` | internal server error | an unexpected crash | | `EMC0005` | invalid request | a GET carrying a body with a JSON `Content-Type` (without one the body is not read); a POST with `Content-Type` exactly `application/json` and an empty body, or an over-limit body, which reads as empty; the hub's per-IP limiter shedding a request ("DDoS check failed", no `request_id` in the body). Always HTTP 400, so the SDK marks it not retryable, the per-IP limiter's refusal included | | `EMC0028` | invalid JSON | the body is not JSON | | `EMC0029` | invalid content type | not `application/json` or `multipart/form-data`; reference `ContentType`, and the wire text is "invalid chars. in text" | The framework's other codes concern what the gateway itself sends to the hub. If one ever reaches you, report it with its `request_id`. ## See also - SDK: [Errors](/infinity-interop/sdk/errors) — `HubError`, `retryable`, `retryAfterMs`, `isCredentialRefused`, the errors the SDK raises itself (`HubUnavailableError`, `WalletError`, `VetError`…), and `adviseError` - [Handling errors](/infinity-interop/api/concepts/handling-errors) - [Execution errors](/infinity-interop/api/concepts/execution-errors) - [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) - [Troubleshooting](/infinity-interop/api/troubleshooting) --- # Limits URL: https://docs.c8ntinuum.com/infinity-interop/api/limits ## The gateway Every call reaches the gateway first, and it applies these limits before the hub sees the call: | Behaviour | Value | |---|---| | Timeouts | **20 s** on `POST /quote`, `/quote/preview` and `/quote/prepare` (the hub's own quote deadline is 15 s), **10 s** on every other route; then `EPX0052` (504). An order submission keeps running at the hub when your client disconnects, but not past the 10 s. Give your client a longer timeout: the SDK waits 25 s on quotes, 12 s on reads and 15 s on submissions | | Request body | 64 KiB; a larger one is `EPX0005` (413) | | Rate limit, secret keys | one pool per organization, shared by all its keys whatever IP they call from; a spent pool is `EPX0080` (429) | | Rate limit, project id | a pool per project, shared by every visitor (`EPX0080`, 429), plus a bucket per visitor IP (`EPX0082`, 429) | | Failed credentials | 20 missing, malformed or unknown keys or project ids from one IP within a minute: `EPX0081` (429) to every call from that IP, valid keys included, for the rest of that minute | `EPX0080` and `EPX0082` carry their back-off in `error_params[0]` (milliseconds) and in a `Retry-After` header (seconds), `EPX0081` in `Retry-After` only. Every call whose credential is accepted reports its budget in `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` ([Conventions — rate-limit headers](/infinity-interop/api/conventions#rate-limit-headers)). The pool sizes are set for your organization. ## The hub | Behaviour | Value | Deployment setting | |---|---|---| | Quote validity | `expiresAt = min(provider deadline − 2 s, issue + 30 s)`, never refreshed | `infinity.quote.ttlMs` (30 000), `infinity.quote.expirySafetyMs` (2 000) | | Cached quote re-serve | only while ≥ 15 s of validity remain; a fresh round can have less when a provider's deadline is short. An identical request from the same organization gets the same quote (same `quoteId`) meanwhile, so submitting it again replays the order already placed for that quote | — | | Quote round deadline | fan-out 1 200 ms within a 2 000 ms round | `infinity.quote.fanoutDeadlineMs`, `infinity.quote.deadlineMs` | | Per-organization rate limit | 5 requests/s, burst 20, per organization (the hub's tenant: every call of your organization, with a key or with its project id) and per route family: the three quote routes share one bucket, [`POST /order`](/infinity-interop/api/order-create) and [`/order/openfor`](/infinity-interop/api/order-openfor) another. A shed is `EIN0080` with a suggested back-off of `floor(1000 / tenantRps)` ms, at least 1. A caller idle longer than 600 s loses its buckets (swept lazily, at most once per idle period); the idle time must be at least `tenantBurst / tenantRps` while `tenantRps > 0`, so dropping a bucket never resets a caller's debt | `infinity.auth.tenantRps`, `infinity.auth.tenantBurst`, `infinity.auth.tenantIdleSeconds` | | In-flight quotes | 64 per process, read at start-up (a change needs a restart) | `infinity.quote.maxInflight` | | Outbound upstream calls | each attempt 30 s; the wait for the hub's outbound limiter 1 s (an openFor forward that waits longer is `EIN0081`); answers capped at 4 MiB; read at start-up | `infinity.upstream.ts.timeoutMs`, `infinity.upstream.solver.timeoutMs`, `infinity.upstream.acquireTimeoutMs`, `infinity.upstream.maxResponseBytes` (the OIF aggregator has no timeout key) | | Proxy-lane registration window | 20 minutes after the quote was issued, on the hub. The aggregator refuses a registration once `expiresAt` has passed (`EIN0007`, `… (QUOTE_EXPIRED)`), so register as soon as the transaction is sent ([known issue](/infinity-interop/integration-notes#ki-proxy-window)) | — | | Non-prepared quote template | held 30 s in the quoting process (20 minutes on the proxy lanes) | — | | One order per quote | both flows share one order key, `sha256("infinity:v1:order:" + quoteId)` | — | | Upstream status cache | 2 s; 4 096 entries, 10 min idle | — | | Hub status cache | 2 s; the owner check runs on every read, cached or not | — | | On-chain answer freshness | a watermark older than 30 s cannot date an absent fill | `infinity.chains.readMaxStalenessMs` | | `GET /orders` page | default 100, capped at 500 | — | | Order intent size | 65 536 bytes | `infinity.order.maxIntentBytes` | The values are the hub's defaults; the production hub's values are those of its deployment configuration. The per-organization limit covers the three quote routes in one bucket and [`POST /order`](/infinity-interop/api/order-create) / [`/order/openfor`](/infinity-interop/api/order-openfor) in another, charged after validation and before any work; every other route has no hub-level limit. How to read `EIN0080` and the gateway's `429`, and how to stay under both: [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits). ## See also - [Errors](/infinity-interop/api/errors) — `EPX0052` (timed out), `EPX0080`–`EPX0082` and `EIN0080` (rate limited), `EIN0081` (busy) - [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits) - [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) --- # Troubleshooting URL: https://docs.c8ntinuum.com/infinity-interop/api/troubleshooting Symptom → cause → fix, grouped by where you are in a call: reaching the hub, credentials and origins, quoting, submitting and following an order. Every failure comes in the [response envelope](/infinity-interop/api/conventions#the-response-envelope) — branch on `error_code`, read `error_reference` and `error_params` for the machine detail, and keep `request_id` for a report. The hub's own failures are `HTTP 400`; the gateway in front of it answers its refusals with a real status (`400`, `401`, `403`, `404`, `413`, `429`, `502`, `503`, `504`) and an `EPX` code ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)). Read [Handling errors](/infinity-interop/api/concepts/handling-errors) first for the general rule this page applies row by row. The Where column links to the page that carries the fact. ## Reaching the hub | Symptom | What comes back | Cause | Fix | Where | |---|---|---|---|---| | SDK `HubUnavailableError` (`not-json`, `bad-envelope`, `http-status`), or a body that is not the envelope | — | something in front of the gateway answered instead (your own proxy's error page, a load balancer), or the path is not an API route | check the base URL, `https://proxy-api.infinimesh.net/v1` on the test hub, and the path; then your own proxy | [Conventions — base URL and transport](/infinity-interop/api/conventions#base-url-and-transport); [SDK errors](/infinity-interop/sdk/errors#the-error-classes) | | `EPX0004` (404) | the API version is not served | the path does not start with `/v1` | use the base path `/v1` | [Conventions](/infinity-interop/api/conventions) | | `EPX0050`, `EPX0051` (502, 503) | the hub is unreachable, or the gateway cannot serve it right now | availability, not a fact about the request | retry later; on a submission, see [walk-through 1](#1-the-answer-to-post-orderopenfor-never-arrived) | [Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx) | | `EPX0053` (503) | the credential could not be checked | availability at the gateway; the call never reached the hub | retry later, a submission included: nothing was sent | [Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx) | | `EPX0052` (504) | the hub did not answer in time | the gateway waits 20 s on the quote routes and 10 s on the others | retry later; on a submission the outcome is unknown: see [walk-through 1](#1-the-answer-to-post-orderopenfor-never-arrived) | [Conventions — timeouts](/infinity-interop/api/conventions#timeouts) | | `EPX0080`, `EPX0082` (429) | too many requests | the gateway's pool for your keys or your project id is spent, or one visitor's IP is over its own bucket | wait `error_params[0]` ms (also in `Retry-After`, in seconds), then retry; `RateLimit-Remaining` shows the budget left before the next refusal | [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits#the-gateway) | | `EPX0005` (413) | body too large | the request body is over 64 KiB | send a smaller body | [Conventions — request headers](/infinity-interop/api/conventions#request-headers) | | `EMC0005` with **no** `request_id` in the body, "DDoS check failed" | — | the hub's per-IP limiter, counting your server's IP (or the visitor's, on project-id calls) | back off; there is nothing to fix in the request | [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits#the-per-ip-limiter) | | `EMC0005` on a `GET` with a body, or a `POST` with an empty body | — | a framework refusal before any route runs, only for a body the framework reads: a `GET` body sent with a JSON `Content-Type`; an empty `POST` body, or one over the size limit (it reads as empty), sent with `Content-Type` exactly `application/json` | send no body on a `GET`, and a JSON object on every `POST` | [Conventions — request headers](/infinity-interop/api/conventions#request-headers) | | `EIN0001` reference `body`, on a `POST` that clearly has a body | invalid form | no `Content-Type`, so the body was never read; or valid JSON of the wrong shape (a wrong JSON type, an array) | send `Content-Type: application/json`; fix the JSON | [Conventions — unknown fields](/infinity-interop/api/conventions#unknown-fields) | | `EMC0028` reference `Body` | "Invalid JSON" | the body is not valid JSON; the framework refuses it before any route reads it | fix the JSON | [Error reference](/infinity-interop/api/errors) | | `EMC0029` reference `ContentType` | "invalid chars. in text" | not `application/json` (a `multipart/form-data` prefix is also accepted) | send the right content type | [Conventions](/infinity-interop/api/conventions) | ## Credentials and origins | Symptom | What comes back | Cause | Fix | Where | |---|---|---|---|---| | `EPX0001` / `EPX0002` (401) on every server call | the key is refused | `EPX0001`: no `x-api-key`, or a value that is not `c8n_sk_` followed by 43 characters (a typo, a stray space, a truncated copy, a project id sent as a key); `EPX0002`: an unknown, revoked or expired key | copy the secret key again from the hub dashboard, or make a new one, and stop calling until it is fixed (`EPX0081`, below); the SDK reads it as `HubError.isCredentialRefused` | [Authentication — secret API keys](/infinity-interop/api/authentication#secret-api-keys) | | `EPX0008` / `EPX0009` (401) | the project id is refused | not `c8n_pk_` followed by 22 characters (`EPX0008`), or unknown (`EPX0009`) | copy the id from your project page in the hub dashboard | [Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages) | | `EPX0010` (403) | the origin is refused | the page's origin is not on your project's list; or the call has no `Origin` header (curl, a server) while the list is set | add the page's exact origin (scheme, host and port) to the project's list; call from a server with a secret key instead | [Authentication — origin lists](/infinity-interop/api/authentication#origin-lists) | | `EPX0007` (400) | two credentials | the call carried both `x-project-id` and `x-api-key` | send one | [Authentication](/infinity-interop/api/authentication) | | `EIN0001` reference `x-infinity-subject` | invalid form | no subject on one of the seven routes that require it, or one that is not 1 to 128 of letters, digits and `. _ @ + ~ -` | send your stable id for the user (or one shared subject of yours, or a page's visitor id) in that alphabet; the SDK sends one on every route that requires it, and checks it before calling | [Authentication — your users](/infinity-interop/api/authentication#your-users) | | `EPX0003` (404) | route not found | a wrong method or path; or `GET /orders` with a project id, the one route not open to it | check the method and the path; read the order history from your server | [Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages) | | `EPX0081` (429) on every call, valid keys included | too many failed attempts | 20 refused credentials (missing, malformed or unknown keys or project ids) from this IP within a minute: the gateway refuses every call from it for the rest of that minute. Often a server retrying a wrong key | fix the credential and stop the caller that repeats it, then wait `Retry-After` seconds | [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits#the-gateway) | | Browser: the call fails before any answer, with a CORS error in the console | — | the page sent a header the pre-flight does not allow: anything besides `Content-Type`, `x-project-id` and `X-Infinity-Subject`, a secret key's `x-api-key` included (the gateway never takes a key from a browser); or it called `GET /orders` with a project id, whose pre-flight is refused | send only those headers, keep secret keys on your server, and read the order history from your server | [Authentication — no keys in web pages](/infinity-interop/api/authentication#no-keys-in-web-pages) | | `hubTransport` throws an `InteropError` (`invalid-input`) at start-up | — | the value is not a secret key (a project id, or a mistyped key), the base URL is not the hub's origin, or the code runs in a web page without `dangerouslyAllowBrowser` | pass the secret key from the hub dashboard and the hub's origin; in a page, use `projectTransport` or your backend | [SDK — Partner backend](/infinity-interop/sdk/partner-backend) | ## Quoting | Symptom | What the hub says | Cause | Fix | Where | |---|---|---|---|---| | `EIN0001` reference `amount` | invalid form | a float, an exponent, a sign, leading zeros, zero, or over 80 characters | send a decimal base-unit string | [Get quote](/infinity-interop/api/quote) (`POST /quote`) | | `EIN0001` reference `sourceAsset`/`destAsset`, params `[reference chain, request chain]` | invalid form | an asset **reference** whose chain differs from the request's | match the reference's chain to the request | [Get quote](/infinity-interop/api/quote) | | `EIN0001` reference `sourceAsset` | invalid form | a bare `0x…` address | send an `assetKey`, or `:
` | [Conventions — value formats](/infinity-interop/api/conventions#value-formats); [Get quote](/infinity-interop/api/quote) | | `EIN0001` reference `recipient` / `sourceAccount` | invalid form | a non-EVM address that doesn't decode (EVM addresses are not format-checked at this point) | send a family-correct address; the SDK's quote calls refuse one that cannot be on its chain before any call, EVM included ([Quotes and checks](/infinity-interop/sdk/quotes#a-quote-request)) | [Get quote](/infinity-interop/api/quote) | | `EIN0001` reference `maxAmountIn` | invalid form | a cap sent on an exact-**input** request | omit it, or send `swapType: "exact-output"` | [Get quote](/infinity-interop/api/quote) | | `EIN0001` reference `swapType` | invalid form | misspelled or miscased, or `exact-output` from a token-in source | send `exact-input`/`exact-output`, case-sensitive | [Get quote](/infinity-interop/api/quote) | | `EIN0001` reference `slippageBps` | invalid form | outside 1–1000 | — | [Get quote](/infinity-interop/api/quote) (`slippageBps`) | | `EIN0002` | unsupported corridor | an undeclared source on a chain with no swap router; no ERC-20 at a token-in reference; an unknown asset key; or an `eip155` chain the deployment doesn't serve (reference `chain has no corridor assets`) | pick from `GET /catalog`; check the address and the chain | [Get quote](/infinity-interop/api/quote) | | `EIN0007` (on `POST /quote`) | rejected | the hub's own fail-closed checks refused the round | re-quote; quote the `request_id` if it repeats | [Get quote](/infinity-interop/api/quote) | | `EIN0008` reference `providerRefused` | no viable route | the amount is below a provider's economic floor, or `maxAmountIn` is under the price | try another amount, or raise the cap — this is **terminal** | [Get quote](/infinity-interop/api/quote) | | `EIN0008` reference `notFillable`, params `[destination chain]` | no viable route | nobody delivers to that destination | try another destination | [Get quote](/infinity-interop/api/quote) | | `EIN0050` params `[responseRejected]` / `[solverUnavailable]` / `[providerMissed]` | upstream unavailable | availability, not a fact about the request as sent | retry later | [Get quote](/infinity-interop/api/quote) | | `EIN0050` params `[upstreamAuth]` (any route that calls an upstream, except `POST /quote/preview`, which answers a bare `EIN0050`) | upstream unavailable | an upstream refused **the hub's own** credential | an operator must renew it; do not loop | [Get quote](/infinity-interop/api/quote); [Errors — the blocks](/infinity-interop/api/errors#the-blocks-and-when-to-retry) | | `EIN0050` bare, on `POST /quote` | upstream unavailable | the deployment has paused exposure. An unconfigured chain domain answers otherwise: `EIN0002` for an `eip155` one, `EIN0050` `[responseRejected]` for any other | — | [Get quote](/infinity-interop/api/quote) | | `EIN0080` | rate limited | your organization's quote bucket at the hub is exhausted | see [walk-through 4](#4-ein0080-rate-limited) | [Get quote](/infinity-interop/api/quote); [Limits](/infinity-interop/api/limits) | | `EIN0081` | busy | the in-flight quote cap (64 per process) is full | back off and retry | [Get quote](/infinity-interop/api/quote); [Limits](/infinity-interop/api/limits) | | Preview: `EIN0050` bare, the same on every retry | upstream unavailable | most likely **no offer**, not an outage: preview reports any provider error this way, a refusal of the request as sent included — an amount below the provider's minimum, an exact-output request over `maxAmountIn`, a Solana source without `sourceAccount` ([known issue](/infinity-interop/integration-notes#ki-preview-no-offer)) | check the amount and `sourceAccount`; `POST /quote` answers the same request with `EIN0008` (reference `providerRefused` for an amount or a cap); don't retry in a loop | [Preview quotes](/infinity-interop/api/quote-preview) | | Preview: `offers: []`, `success: true` | — | no provider call failed and none produced an offer for this request as sent — or no provider was asked, because every provider's circuit breaker is open | change the request, or retry later | [Preview quotes](/infinity-interop/api/quote-preview) | | Prepare: `EIN0001` reference `preparationId` | invalid form | the id was reused with different inputs or provider | use a new id | [Prepare quote](/infinity-interop/api/quote-prepare) | | Prepare: `EIN0001` reference `preparationId`/`provider` | invalid form | the id isn't `^[A-Za-z0-9_-]{16,128}$`, or `provider` is malformed | — | [Prepare quote](/infinity-interop/api/quote-prepare) | | Prepare: `EIN0005` | quote expired | the stored preparation expired, or its configuration moved on | create a new id and prepare again | [Prepare quote](/infinity-interop/api/quote-prepare) | | Prepare: `EIN0008`, empty reference | no viable route | the one named provider has no winner | pick another preview offer | [Prepare quote](/infinity-interop/api/quote-prepare) | | A Sui or Aptos **source** | refused | Sui and Aptos are destination-only chains: source chains are EVM and Solana, and a quote whose source is a Sui or Aptos chain is refused. The catalog's `family` tells you which rail a chain is on | source from an EVM or Solana chain; keep Sui or Aptos as the **destination** | [Get quote](/infinity-interop/api/quote); [Supported chains and assets](/infinity-interop/api/supported-chains-and-assets) | | A winner discloses no `signIntent` | (success) | the hub could not build the order-intent text for it | re-quote; a signed `POST /order` is impossible for this quote | [Get quote](/infinity-interop/api/quote) (`QuoteResponse.signIntent`) | | EVM↔EVM quote sent with no `sourceAccount` | (success) | the quote is fine, but a signed `POST /order` is impossible (`signer` must equal `sourceAccount`); and a signer other than the recipient pays for an order whose user, and refund beneficiary, is the recipient. (On an OIF-only deployment `POST /order` refuses every quote with `EIN0007` first: [known issue](/infinity-interop/integration-notes#ki-order-create-oif)) | send `sourceAccount`, the paying wallet | [Get quote](/infinity-interop/api/quote); [Create order](/infinity-interop/api/order-create) | | Non-EVM (Solana) source sent with no `sourceAccount` | `EIN0050` bare on `POST /quote/preview` ([known issue](/infinity-interop/integration-notes#ki-preview-no-offer)); `EIN0008` with no reference on `POST /quote`, which quotes the recipient as the payer | the aggregator adapter needs the funding account to build the request | send `sourceAccount` | [Get quote](/infinity-interop/api/quote) (`sourceAccount`) | | A winner discloses neither `signPayload` nor `executionTx` | (success) | a same-chain `solver-swap` winner, or a `transfer` winner | quotable, but **not completable** through the hub | [Get quote](/infinity-interop/api/quote) | ## Submitting | Symptom | What the hub says | Cause | Fix | Where | |---|---|---|---|---| | openFor `EIN0010` | bad signature | a signature present on a proxy lane, or missing on a signed lane; an unknown or foreign quote (a subject that differs from the quote's included); a malformed signature on the external gasless lane (a bad EVM escrow signature is `EIN0021`, below) | send what the quote's lane takes (the SDK's `quote.lane.kind`), and re-sign the exact disclosed bytes | [Submit order (openFor)](/infinity-interop/api/order-openfor) (`POST /order/openfor`) | | openFor `EIN0001`, pointing at `POST /order` | invalid form | a bridge/open-flow winner was posted to `openFor` | submit it through `POST /order` instead | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | EVM openFor signature refused | `EIN0021` | the `0x00` scheme byte is missing (the signature must be 66 bytes); the hub forwards the string unchanged, and the aggregator refuses it — `error_reference` carries its status and code | send `"0x00" + sig.slice(2)` (the SDK's `evmEscrowEnvelope(sig)`) | [Submit order (openFor)](/infinity-interop/api/order-openfor) (signature table and the `EIN0021` row); [SDK — Lanes](/infinity-interop/sdk/lanes#evm-permit2) | | Solana openFor refused | `EIN0007` | the public key is wrong, the `digest` was signed instead of `transactionMessage`, or the message's order id doesn't match the quote's | sign the disclosed message, and build the envelope with the SDK's `solanaEnvelope(signature, publicKey)` | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | Solana envelope sent as hex | `EIN0021` | the hub reads hex but forwards the string unchanged; the aggregator accepts only base64, so it declines the forward | send base64, exactly what the SDK's `solanaEnvelope` produces | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | External-gasless openFor refused | `EIN0010` | not `0x` + 65 or 66 bytes of hex — two scheme prefixes (67 bytes), a missing `0x`, or non-hex; a single `0x00` prefix is accepted and normalised, and the provider's own is added upstream | send the raw 65-byte signature | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | `EIN0005` on openFor | quote expired | see [walk-through 3](#3-ein0005-quote-expired) | — | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | `EIN0021` | order declined | the solver or provider refused the order when the hub forwarded it (`error_reference` carries the upstream's status and code, e.g. `http 409 (SOLVER_REJECTED)`); **nothing was opened** | re-quote and re-sign; the same signed bytes cannot succeed later. Don't re-post them either: the aggregator answers the re-post `502`, which the hub serves as `EIN0050` (`upstream unavailable: http 502`) and then treats as outcome-unknown ([known issue](/infinity-interop/integration-notes#ki-redecline)) | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | openFor `EIN0007` | rejected | a failed Solana envelope check, a prepared-template mismatch, the aggregator refusing a proxy-lane registration (reference `fail-closed rejected: upstream refused: http 400 (QUOTE_EXPIRED)` once the quote's `expiresAt` has passed), or no OIF aggregator configured at all | re-quote, or report the configuration — but never re-quote a proxy lane whose transaction is already sent: that is a second deposit. Follow the deposit on chain instead ([known issue](/infinity-interop/integration-notes#ki-proxy-window)) | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | `EIN0052` | outcome unknown | see [walk-through 2](#2-ein0052-outcome-unknown) | — | [Submit order (openFor)](/infinity-interop/api/order-openfor); [Errors — the blocks](/infinity-interop/api/errors#the-blocks-and-when-to-retry) | | openFor bare `EIN0050` | upstream unavailable | **before** sending: the circuit breaker was open, nothing was sent; **after** sending: the forward went unanswered | back off and re-post the identical body — the next re-post tells you which happened: `EIN0052` means it was sent | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | openFor `EIN0081` | busy | the forward-reservation table is full; the hub's own outbound limiter to the upstream (nothing sent); or the upstream itself answered 429 (the reservation is kept — the next re-post answers `EIN0052`) | back off, then re-post the same body | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | Order-route `EIN0080` | rate limited | your organization's order bucket at the hub; nothing was reserved or forwarded | re-post the same body after `error_params[0]` ms | [Submit order (openFor)](/infinity-interop/api/order-openfor); [Limits](/infinity-interop/api/limits) | | `POST /order` `EIN0010` | bad signature | a partial `{signature, signer, scheme}`; `signer` ≠ the quote's `sourceAccount`; an `artifactHash` mismatch; or no signature at all when unsigned registrations aren't enabled. Not reached on an OIF-only deployment, which refuses every quote with `EIN0007` first | send all three; `signer` = `sourceAccount`; `artifactHash` = `disclosure.signableHash` | [Create order](/infinity-interop/api/order-create) | | `POST /order` `EIN0006` | epoch superseded | the configuration the quote ran under changed underneath it | never re-quote: the user's transaction is already sent and completes on chain unregistered, so a new quote means a second one ([Execution errors](/infinity-interop/api/concepts/execution-errors#refused-after-the-transaction-was-sent)) | [Create order](/infinity-interop/api/order-create) | | `POST /order` `EIN0007` | rejected | re-approval or artifact-build failed. On an OIF-only deployment, every quote: its lanes complete through `openFor` ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)) | send a quote here only when its lane is the open flow (the SDK's `quote.lane.kind` is `"open-flow"`); once the user's transaction is sent, never re-quote (as `EIN0006`) | [Create order](/infinity-interop/api/order-create) | | Proxy-lane `EIN0005` at registration | quote expired | more than 20 minutes after the quote was issued. The aggregator already refuses a registration once `expiresAt` has passed (`EIN0007` above), and the deposit then completes on chain unregistered ([known issue](/infinity-interop/integration-notes#ki-proxy-window)) | register as soon as the transaction is sent, well before `expiresAt`; never re-quote once it is out | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | `EIN0005` on a quote that still looks valid | quote expired | a **non-prepared** quote submitted to a different replica than the one that quoted it (its template is held 30 s in that process) | prepare it (works on any replica) | [Submit order (openFor)](/infinity-interop/api/order-openfor) | | No answer at all to `openFor` or `order`, or `EPX0050`–`EPX0053` (502, 503, 504) on it | (a lost answer) | see [walk-through 1](#1-the-answer-to-post-orderopenfor-never-arrived) | — | [Submit order (openFor)](/infinity-interop/api/order-openfor); [Handling errors — a lost answer](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order) | ## Following the order | Symptom | What the hub says | Cause | Fix | Where | |---|---|---|---|---| | `/order/{id}/status` `EIN0009` | order not found | an unknown id, another organization's or another subject's, malformed (must be `^[A-Za-z0-9_-]{1,256}$`), unknown to the upstream — or an **open-flow** `orderId` | open-flow orders live at `/status/{id}` instead | [Get upstream status](/infinity-interop/api/order-upstream-status) | | `/orders` answers `EPX0003` (404) | not open | the call used your project id: the order history needs a secret key | read it from your server | [Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages) | | `/status/{id}` `EIN0001` reference `orderId` | invalid form | not exactly 64 lowercase hex, or carries `0x` (an on-chain id) | use `hubOrderId`, or the `POST /order` `orderId` | [Get hub status](/infinity-interop/api/hub-status) | | `/status/{id}` `EIN0009` | order not found | another organization's, or there is no creation record at all (an on-chain id stripped of its `0x`, for example) | use the on-chain views for an on-chain id | [Get hub status](/infinity-interop/api/hub-status) | | An on-chain view finds nothing, with no error | (empty facts) | the id is uppercase, or missing `0x` — on-chain ids are matched, not validated, so the answer looks like "not filled yet" ([known issue](/infinity-interop/integration-notes#ki-onchain-id)) | send `0x` + 64 **lowercase** hex; before offering a withdraw, confirm on `/onchain/{id}/order` that the order exists | [Conventions](/infinity-interop/api/conventions); [Get escrow order](/infinity-interop/api/onchain-order) | | fill-by-tx `EIN0051` | service unavailable | an unknown or miscased `chainDomain` (no watermark for it), or a stale watermark | use the canonical domain; retry | [Get on-chain fill](/infinity-interop/api/onchain-fill) | | `/fill` `EIN0051` | service unavailable | an absent fill that can't be dated (no watermark under 30 s old) | retry — never read this as "not filled" | [Get on-chain fill](/infinity-interop/api/onchain-fill) | | `/onchain/{id}/order` `EIN0009` | order not found | the Open hasn't been observed yet, the id is a hub id, or the origin is non-EVM (permanent: [known issue](/infinity-interop/integration-notes#ki-nonevm-tuple)). An openFor ack's aggregator `orderId` is not a cause: on engine lanes it is the on-chain id, served once the Open is observed | poll again; use the on-chain id | [Get escrow order — Errors](/infinity-interop/api/onchain-order#errors) | | A `GET /orders` entry with no `chain` | (a fact) | nothing has been observed on chain yet | poll again later | [List orders — Response](/infinity-interop/api/orders-list#response) | | A `GET /orders` page whose `subject` is not the one you sent, or orders listed for a subject none of your users has | (a fact) | the history is not scoped to that user | serve no history from it; report it with the `request_id` | [Authentication — your users](/infinity-interop/api/authentication#your-users) | | fill-by-tx `onchainOrderId: ""` | (a miss) | not observed yet, as of the watermark returned alongside it. A Solana Open signature never resolves ([known issue](/infinity-interop/integration-notes#ki-solana-open-tx)) | poll again; follow a Solana-origin order by `onchainOrderId` on `/onchain/{id}/fill` | [Get on-chain fill](/infinity-interop/api/onchain-fill) | | `/order/{id}/status` stays `executed` for minutes, with fresh syncs and `upstreamStale: false` | (advisory) | the upstream stopped reporting; the order can be filled and settled on chain all the same ([known issue](/infinity-interop/integration-notes#ki-upstream-stall)) | read `/onchain/{id}/fill`: its `filled` and `settled` facts are the chain's answer | [Get upstream status](/infinity-interop/api/order-upstream-status); [Get on-chain fill](/infinity-interop/api/onchain-fill) | | `upstreamStale` / `rejectedUpstreamTransition: true` | (advisory) | a refresh failed, or the latest report was backwards or contradictory; the last good state is still served | keep polling | [Get upstream status](/infinity-interop/api/order-upstream-status) | | `/order/{id}/status` `EIN0081` | busy | the upstream-status projection table is full | back off | [Get upstream status](/infinity-interop/api/order-upstream-status) | | `/order/{id}/status` `EIN0007` | rejected | the hub refused the upstream's answer and had no earlier state to fall back to | retry | [Get upstream status](/infinity-interop/api/order-upstream-status) | | `refundEligible: true`, but `refund()` reverts | (advisory only) | `refundEligibilitySource` is always `stored_projection`; `refundRequiresOnchainCheck` is always `true` | verify the **live** contract before sending | [List orders — Response](/infinity-interop/api/orders-list#response) | | openFor ack `state: "created"` shown as filled | (not a guarantee) | that's the **initial** upstream state, never updated on the ack itself | poll `GET /order/{orderId}/status` | [Submit order (openFor)](/infinity-interop/api/order-openfor) | ## Walk-throughs Every call below sends your secret key in `x-api-key`. Values follow the same example world as the rest of this reference. ### 1. The answer to `POST /order/openfor` never arrived This covers your client timing out or losing the connection, and the gateway's availability answers on a submission: `EPX0050`, `EPX0051`, and `EPX0052` (504) when the hub took longer than the gateway's 10 s. None of them says whether the order was opened. (`EPX0053` is different: the call never reached the hub, so re-post it as it is.) **Step 1 — derive `hubOrderId`.** You already hold the `quoteId` from the quote; the hub's durable alias for the order is a public, client-computable hash of it, so you don't need the lost ack to find the order: ```ts const hubOrderId = await deriveHubOrderId(q.quoteId); // hubOrderId === "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2" ``` **Step 2 — read the status by that alias.** ```bash curl curl -sS https://proxy-api.infinimesh.net/v1/order/a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2/status \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" ``` ```json Response { "success": true, "request_id": "590abd30-a1ca-45bd-abce-124c82fe43c2", "data": { "stateSource": "upstream-advisory", "finalityVerified": false, "latestUpstreamState": "executed", "rejectedUpstreamTransition": false, "upstreamFillReported": true, "orderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "state": "executed", "history": [ { "state": "created", "at": "2026-09-22T12:00:23Z" }, { "state": "executed", "at": "2026-09-22T12:00:52Z" } ], "fillTransaction": { "hash": "0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d" }, "filled": true, "lastUpstreamSyncAt": "2026-09-22T12:01:00Z", "upstreamStale": false } } ``` The `orderId` this route echoes is always the **upstream's** id, whichever alias you queried it by. Found it — you're done; watch it as usual. **Step 3 — if it answers `EIN0009` instead.** A not-found here does not prove nothing was submitted: in the narrow window right after a crash the alias record can lag the acceptance it points at, and a forward that was cut short reads as not-found too (below): ```json { "success": false, "request_id": "7d1c5b3e-4f2a-4e8b-9c6d-0a1b2c3d4e5f", "message": "You have errors in your request. Please fix them and retry", "errors": [ { "error_reference": "orderstatus: no such order", "error_code": "EIN0009", "error_message": "Order not found" } ] } ``` Re-post the **identical** `openFor` body — same `quoteRequestId`, `quoteId` and signature — never re-quote and never ask the user to sign again: ```bash curl curl -sS -X POST https://proxy-api.infinimesh.net/v1/order/openfor \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"quoteRequestId":"01M34FSW4R9F2C4E7A1B3D5F80","quoteId":"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58","signature":"0x00<65-byte-ecdsa-hex>"}' ``` ```json Response { "success": true, "request_id": "66213d6c-e21a-4c01-b371-c1b5e709d582", "data": { "orderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "hubOrderId": "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "state": "created", "submittedAt": "2026-09-22T12:00:20Z" } } ``` When the first forward completed, this is what comes back: the `request_id` is new; `submittedAt` is **not** — this is the original recorded ack replayed, not a second order. The re-post can also answer two other ways: - **A fresh ack**, with its own `submittedAt`: the first call was cut before the hub forwarded anything, so this re-post is the first forward. Still one order. - **`EIN0052`**, on this and every later re-post: the hub's call was cut while it was forwarding the order, by the gateway's 10 s limit (the `EPX0052` you got). The forward is tied to that call, so cutting it cancels the upstream call too, and its outcome stays unknown until an operator reconciles it ([known issue](/infinity-interop/integration-notes#ki-openfor-cancel)). Go on with [walk-through 2](#2-ein0052-outcome-unknown). Your own client giving up does not cause the last case: the gateway keeps its call to the hub running when your client times out or disconnects, so the order goes on and the read in step 2 finds it. Still give `openFor` a client timeout longer than the gateway's 10 s, so that you get the gateway's answer rather than none. The SDK waits 15 s on submissions (35 s through a partner proxy). **Step 4 — the `POST /order` variant.** The same rule holds for the open flow: re-post the identical request (the same `quoteRequestId`, `quoteId`, `artifactHash`, `signature`, `signer`, `scheme`), and a quote that already has an order answers with **that** order's ack — even past `expiresAt`: ```bash curl curl -sS -X POST https://proxy-api.infinimesh.net/v1/order \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"quoteRequestId":"01M34FSW4R9F2C4E7A1B3D5F80","quoteId":"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58","artifactHash":"leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40","signature":"","signer":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","scheme":"eip191"}' ``` ```json Response { "success": true, "request_id": "5db8bb66-c147-44f6-878f-42f4fe4fa509", "data": { "orderId": "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "createdAt": "2026-09-22T12:00:20Z", "status": "CREATED" } } ``` Never resend the transaction itself — only the hub call. See [Submit order (openFor)](/infinity-interop/api/order-openfor), [Handling errors — a lost answer](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order) and the SDK's [`deriveHubOrderId`](/infinity-interop/sdk/reference#derivehuborderid). ### 2. `EIN0052` — outcome unknown **Step 1 — the body.** The same [`POST /order/openfor`](/infinity-interop/api/order-openfor) call can come back like this instead of an ack: ```json { "success": false, "request_id": "2105611e-3326-4c1f-b6fd-46597a3f8332", "errors": [ { "error_code": "EIN0052", "error_message": "Order outcome unknown" } ] } ``` **Step 2 — what it means.** The hub forwarded the signed order and got no readable answer back — for example because the gateway cut an earlier call at its 10 s while the hub was still forwarding it, which cancels the forward ([known issue](/infinity-interop/integration-notes#ki-openfor-cancel)). The reservation is **kept**, because the upstream may have opened it. Re-posting the identical body is safe — a re-post never opens a second order — but it will keep answering `EIN0052` for as long as the reservation stays open. **Step 3 — stop, and surface it.** There is no client route that clears this reservation. Stop retrying in a loop, show the `request_id` to the operator, and let them reconcile it directly — a retry loop only produces more `EIN0052`s to sort through. **Step 4 — an EVM-gasless-only shortcut (advisory, not contract).** On that lane specifically, the Permit2 `nonce` inside the `signPayload` the user signed being spent on-chain is proof the order opened. Until you can check that, do not ask the user to sign for the same payment again. **Tip.** Check the code for `EIN0052` **before** consulting `HubError.retryable` — `EIN0052` sits in the availability block (`EIN0050`–`EIN0079`) and so reads as retryable, but looping on it is exactly the wrong instinct; it needs an operator, not a retry. See [Submit order (openFor)](/infinity-interop/api/order-openfor), [Errors — the blocks](/infinity-interop/api/errors#the-blocks-and-when-to-retry) and [SDK errors — Retrying](/infinity-interop/sdk/errors#retrying). ### 3. `EIN0005` — quote expired **Step 1 — compare the times.** The quote from [`POST /quote`](/infinity-interop/api/quote) carried: ```json { "issuedAt": "2026-09-22T12:00:15Z", "expiresAt": "2026-09-22T12:00:45Z" } ``` Thirty seconds of validity, never refreshed. **Step 2 — which rule applied.** On a **signed** lane, a submission at or after `expiresAt` is `EIN0005`, even while the quoting process still holds the template. On a **proxy** lane, the rule is 20 minutes after the quote was issued, not `expiresAt` itself — but the aggregator already refuses the registration once `expiresAt` has passed (`EIN0007`), and the deposit then completes unregistered ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). A **prepared** quote's stored expiry is never extended by a retry. A **non-prepared** quote submitted to a different replica than the one that quoted it looks expired there too, even before its real `expiresAt`. Had the user signed too late, the **first** `openFor` for this quote, sent after `12:00:45Z`, would have answered as below. (In the [quickstart](/infinity-interop/api/quickstart) this quote was accepted at `12:00:20Z`, so posting it now replays that ack instead: see step 4.) ```bash curl curl -sS -X POST https://proxy-api.infinimesh.net/v1/order/openfor \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"quoteRequestId":"01M34FSW4R9F2C4E7A1B3D5F80","quoteId":"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58","signature":"0x00<65-byte-ecdsa-hex>"}' ``` ```json Response { "success": false, "request_id": "c605be15-a0c1-47cf-9b6c-2cca0b3016e4", "message": "You have errors in your request. Please fix them and retry", "errors": [ { "error_reference": "quote expired", "error_code": "EIN0005", "error_message": "Quote expired" } ] } ``` **Step 3 — recover.** Re-quote (a **new** `preparationId` if you were preparing), have the user sign the new disclosure, and submit that. The old signature is dead — do not resend it. On a proxy lane whose transaction is already sent, don't re-quote: a new quote means a second deposit. Follow the first one on chain instead. **Step 4 — check first if you already submitted.** A recorded submission is still replayed after `expiresAt` (the idempotency check runs before the expiry check). If you're not sure whether an earlier attempt landed, check before you re-quote: read the status by the `hubOrderId` alias ([walk-through 1](#1-the-answer-to-post-orderopenfor-never-arrived)) and look for the order in [`GET /orders`](/infinity-interop/api/orders-list). A re-quote is a **new** quote with its own `quoteId`, so submitting it opens a second order, and the user pays twice. See [Submit order (openFor)](/infinity-interop/api/order-openfor), [Get quote](/infinity-interop/api/quote) and [Limits](/infinity-interop/api/limits). ### 4. `EIN0080` — rate limited **Step 1 — the body.** At the defaults (`tenantRps: 5`) every shed on every route carries the same back-off: ```bash curl curl -sS -X POST https://proxy-api.infinimesh.net/v1/quote \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"sourceChain":"eip155-1","destChain":"eip155-56","sourceAsset":"weth","destAsset":"wbnb","amount":"1000000000000000","recipient":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}' ``` ```json Response { "success": false, "request_id": "71c1a658-5df1-48dd-86b4-dcaae2bd1b07", "errors": [ { "error_reference": "rate limit shed, retry after 200ms", "error_code": "EIN0080", "error_message": "Too many requests", "error_params": ["200"] } ] } ``` `error_params[0]` is `floor(1000 / tenantRps)` milliseconds, at least 1 — the same value on every shed, on `/quote`, `/quote/preview` and `/quote/prepare` alike (they share one bucket), and separately on `POST /order`/`/order/openfor`. **Step 2 — which bucket.** The three quote routes share a bucket; `POST /order` and `/order/openfor` share another; both are per **organization**: every call of your organization, with a key or with its project id, draws on them. A `429` with an `EPX` code (`EPX0080`, `EPX0082`) is the gateway's limit instead, in front of the hub: the same rule applies, and its back-off also comes in a `Retry-After` header. **Step 3 — recover.** On the order routes nothing was reserved or forwarded by a shed, so re-posting the identical body after `error_params[0]` ms (`HubError.retryAfterMs`) is exactly right — it is not the `EIN0052` situation. **Step 4 — stay under it.** Debounce `POST /quote/preview` while the user is still typing and pause it while a wallet prompt is open; call `POST /quote/prepare` once per confirmation, not once per render; poll the status views every 2–3 s (both are cached 2 s, so polling faster buys nothing). Behind your own backend, meter your users yourself: they all share your organization's buckets. See [Get quote](/infinity-interop/api/quote), [Limits](/infinity-interop/api/limits) and [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits). ## See also - [Conventions — the response envelope](/infinity-interop/api/conventions#the-response-envelope), [Errors](/infinity-interop/api/errors) - [Handling errors](/infinity-interop/api/concepts/handling-errors), [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits), [Execution errors](/infinity-interop/api/concepts/execution-errors), [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) - [Authentication](/infinity-interop/api/authentication), [Limits](/infinity-interop/api/limits) - [`POST /order/openfor`](/infinity-interop/api/order-openfor), [`POST /order`](/infinity-interop/api/order-create) - SDK: [Errors](/infinity-interop/sdk/errors) — `HubError`, `retryable`, `retryAfterMs`, `isCredentialRefused`, `adviseError` - API quickstart — [Quickstart](/infinity-interop/api/quickstart) --- # API overview URL: https://docs.c8ntinuum.com/infinity-interop/api The complete reference for the HTTP API the Infinity hub exposes under `/v1`: every route, the credential it takes, its parameters, validation rules, response fields, and the errors it returns. The hub is the orchestration service in front of the Open Intents Framework aggregator and the solver engines. A client asks it for a **quote** (it runs one round across the configured providers, vets the answers through its own firewall and discloses the winner), then **submits an order** against that quote, then **follows the order** through three status views. It is an exposure layer: it lets a frontend or an integrator's backend start flows and see order state. It is not what makes the system safe — the contracts, the engines and the attestation committee are — so a hub-side check is defence in depth for the paths the hub serves. **Base URL.** `https://proxy-api.infinimesh.net/v1`: the public test hub, which runs on test networks only ([Testing](/infinity-interop/sdk/testing#the-test-hub)). Every example in this reference uses it, for example `https://proxy-api.infinimesh.net/v1/catalog`; your production address comes with your integration access. The API is served by the hub's gateway, which checks your credential and passes the hub's answers back unchanged. **A secret key never goes in a web page.** A page calls either your backend, which holds the key and forwards the page's calls (a proxy server: [Partner backend](/infinity-interop/sdk/partner-backend)), or the gateway directly with your public project id ([Authentication — project ids](/infinity-interop/api/authentication#project-ids-for-web-pages)). The gateway never takes a key from a browser. The API's machine contract is the gateway's OpenAPI document ([Get OpenAPI document](/infinity-interop/api/openapi)): the routes under `/v1`, the two credentials and the `EPX` codes, with the hub's bodies, which the gateway passes through unchanged. This reference describes the hub's contract version `2026-09-23`. ## Credentials Every call carries one credential header: your secret API key (`x-api-key`) from a server, or your public project id (`x-project-id`) from a web page. There is no sign-in and no session. A key opens every route below; a project id opens every route but `GET /orders`, the order history. The seven routes that read `X-Infinity-Subject` require it: the user of yours, or the page's visitor, a call is for ([Authentication — your users](/infinity-interop/api/authentication#your-users)). See [Authentication](/infinity-interop/api/authentication), and [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits). ## Discovery - [`GET /catalog`](/infinity-interop/api/catalog) — the deployment's declared chains and assets - [`GET /openapi.yaml`](/infinity-interop/api/openapi) — the API's contract, as raw YAML ## Quotes - [`POST /quote`](/infinity-interop/api/quote) — run one quote round and disclose the winner - [`POST /quote/preview`](/infinity-interop/api/quote-preview) — indicative offers for browsing - [`POST /quote/prepare`](/infinity-interop/api/quote-prepare) — prepare one chosen provider under a stable id ## Orders - [`POST /order/openfor`](/infinity-interop/api/order-openfor) — submit a gasless order, or register a proxy-lane order - [`POST /order`](/infinity-interop/api/order-create) — register acceptance of an open-flow winner ## Status - [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) — the aggregator-backed projection (openFor) - [`GET /status/{orderId}`](/infinity-interop/api/hub-status) — the hub's own event-derived lifecycle - [`GET /orders`](/infinity-interop/api/orders-list) — every openFor submission of your organization, or of one of your users ## On-chain views - [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) — observed fill facts - [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx) — resolve a transaction hash - [`GET /onchain/{orderId}/order`](/infinity-interop/api/onchain-order) — the escrow's raw order tuple (EVM refunds) - [`GET /onchain/{orderId}/refund`](/infinity-interop/api/onchain-refund) — a Solana order's refund, as an unsigned transaction ## Environments Partners test against the public test hub, `https://proxy-api.infinimesh.net`, which runs on test networks only: see [Testing](/infinity-interop/sdk/testing). [c8ntinuum Testnet](/developer-essentials/network-information/testnet) is reachable as the chain `eip155-8888` on whichever hub deployment declares it. ## More - [Quickstart](/infinity-interop/api/quickstart) — a gasless order in five calls and one signature - [Conventions](/infinity-interop/api/conventions) — base URL, headers, the response envelope and its statuses, timeouts, value formats, id namespaces, unknown fields - [Flows](/infinity-interop/api/flows) — the sequences at a glance; the explanations are under [Concepts](/infinity-interop/api/concepts) - [Errors](/infinity-interop/api/errors) — the full `EIN`/`EPX`/`EMC` catalogue - [Limits](/infinity-interop/api/limits) — rate limits, timeouts and other operational behaviour - [Concepts](/infinity-interop/api/concepts) — step execution, trade types, input validation, handling errors, rate limits, execution errors, refunds, order lifecycle - [Troubleshooting](/infinity-interop/api/troubleshooting) — symptom → cause → fix, by phase - SDK: [`@c8ntinuum/infinity-interop-sdk`](/infinity-interop/sdk) covers every partner route with exact types, and runs the flows end to end **Route summary** (all under `/v1`): | Group | Routes | Credential | |---|---|---| | Discovery | `GET /catalog` | API key or project id | | Discovery | `GET /openapi.yaml` | none (public) | | Quotes | `POST /quote`, `/quote/preview`, `/quote/prepare` | API key or project id | | Orders | `POST /order`, `/order/openfor` | API key or project id | | Status | `GET /order/{orderId}/status`, `GET /status/{orderId}` | API key or project id | | Status | `GET /orders` | API key only | | On-chain | `GET /onchain/{orderId}/fill`, `GET /onchain/tx/{chainDomain}/{txHash}/order`, `GET /onchain/{orderId}/order`, `GET /onchain/{orderId}/refund` | API key or project id | The hub's operator routes are not part of the partner API. --- # Get catalog URL: https://docs.c8ntinuum.com/infinity-interop/api/catalog `GET /v1/catalog` The deployment's declared corridor registry — build pickers from it rather than from a hand-kept table. Open to your project id, so a page's pickers render before your user signs in to anything. Built once at boot from configuration; it changes only on restart. The external aggregator lanes are not listed (quoting is their discovery). | Item | Value | |---|---| | Route key | `infinityCatalog` | | Credential | API key or project id | ## Request No path, query or body parameters. ## Response **`data`** (`Catalog`): `{chains[], assets[]}` — both always arrays. `chains[]`, sorted by domain: | Field | Type | Meaning | |---|---|---| | `chainDomain` | string | e.g. `eip155-8888` | | `family` | string | `evm`, `solana`, `sui` or `aptos` — the signing family, which decides the whole rail | | `userProxy` | string | the chain's UserProxy (the wrap-native / swap-token contract); absent when none | | `swapRouter` | string | present means the chain accepts an **undeclared token as a source** through the token-in lane | | `nativeKey` | string | present means the chain's native coin can be a **source** under this asset key (wrapped by the deployment). Absent means the native coin is a destination only | | `wrappedKey` | string | the wrapped token the escrow actually holds on the native-coin lane — and what a refund returns. Absent when the chain declares no native-coin lane | | `inputSettlers[]` | string[] | the chain's input settlers (escrows), canonical form — the `settler` of `GET /onchain/{id}/order` is one of them. Absent when the chain declares none | `assets[]`, sorted by chain then key: | Field | Type | Meaning | |---|---|---| | `chainDomain` | string | the asset's chain | | `assetKey` | string | the wire key `sourceAsset`/`destAsset` accept — the lowercase symbol | | `assetRef` | string | the same asset as `:` — accepted wherever `assetKey` is | | `symbol` | string | display symbol | | `address` | string | canonical form: lowercase `0x` hex, 40 characters on EVM, 64 on Solana/Sui/Aptos | | `decimals` | integer | | | `native` | boolean | the chain's native coin (the all-zero address): it receives fills and cannot fund an escrow directly — offer it as a destination, or as a source only where the chain has a `nativeKey` | ## Behavior Open to a project id — a page's pickers render before your user signs in to anything. Built once at boot from configuration; it changes only on restart. The external aggregator lanes are not listed (quoting is their discovery mechanism). ## Errors The handler itself never fails, so this route answers no `EIN` code, not even `EIN0049`. The gateway's own refusals — a refused credential or origin, a rate limit, the hub unreachable or too slow — can come back, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)). ## Example ```bash curl -sS https://proxy-api.infinimesh.net/v1/catalog \ -H "x-api-key: $HUB_API_KEY" ``` ### Response Illustrative values; formats follow the [Response](#response) table. ```json { "success": true, "request_id": "6f7a8b9c-0d1e-4f6a-bb4c-5d6e7f8a9b0c", "data": { "chains": [ { "chainDomain": "eip155-1", "family": "evm", "userProxy": "0xb8e661486bd859fb5656c07c459c3617f6853b85", "swapRouter": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", "nativeKey": "eth", "wrappedKey": "weth", "inputSettlers": ["0xf439808b5a80e3c355645b62310502817000cd1d"] }, { "chainDomain": "eip155-56", "family": "evm", "inputSettlers": ["0xf439808b5a80e3c355645b62310502817000cd1d"] } ], "assets": [ { "chainDomain": "eip155-1", "assetKey": "eth", "assetRef": "eip155-1:0x0000000000000000000000000000000000000000", "symbol": "ETH", "address": "0x0000000000000000000000000000000000000000", "decimals": 18, "native": true }, { "chainDomain": "eip155-1", "assetKey": "weth", "assetRef": "eip155-1:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "symbol": "WETH", "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "decimals": 18, "native": false }, { "chainDomain": "eip155-56", "assetKey": "wbnb", "assetRef": "eip155-56:0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "symbol": "WBNB", "address": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "decimals": 18, "native": false } ] } } ``` `eip155-56` omits `userProxy`/`swapRouter`/`nativeKey`/`wrappedKey` (none declared, `omitempty`); the `eth` row is the chain-native zero-address declaration, flagged illustrative. The values above are illustrative fixtures. Read the live set from your deployment; how to read the rows is on [Supported chains and assets](/infinity-interop/api/supported-chains-and-assets). ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const catalog = await client.getCatalog(); // cached for 5 minutes const { chains, assets } = catalog; ``` ## See also - SDK: [`client.getCatalog()`](/infinity-interop/sdk/quotes#the-catalog), a `CatalogIndex` with lookups for pickers and the checks - [Quickstart](/infinity-interop/api/quickstart) --- # Get OpenAPI document URL: https://docs.c8ntinuum.com/infinity-interop/api/openapi `GET /v1/openapi.yaml` The API's machine contract, served by the hub's gateway: the routes under `/v1`, the two credentials (`x-api-key` and `x-project-id`) with the routes each one opens, and the gateway's `EPX` codes with their HTTP statuses. The request and response bodies are the hub's own, which the gateway passes through unchanged. Generate your types from the served document rather than from a copy, and watch `info.version` to notice a contract change. | Item | Value | |---|---| | Credential | none: the document is public | ## Request No path, query or body parameters, and no credential header. ## Response The raw YAML document: **no envelope**, unlike every other route. ## Example ```bash curl -sS https://proxy-api.infinimesh.net/v1/openapi.yaml ``` ```ts // client: an InteropClient over hubTransport (the browser transports do not reach this route) const { data: yaml } = await client.api.call("openapi"); ``` ## See also - [Authentication](/infinity-interop/api/authentication) — the two credentials - [Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx) — every `EPX` code - SDK: [`client.api.call`](/infinity-interop/sdk/reference#hubapi) reaches any route by name - [API overview](/infinity-interop/api) — the contract version this reference describes --- # Get quote URL: https://docs.c8ntinuum.com/infinity-interop/api/quote `POST /v1/quote` Run one quote round across the configured providers and disclose the winner. | Item | Value | |---|---| | Route key | `infinityQuote` | | Credential | API key or project id | | `X-Infinity-Subject` | required: the quote is made for that user of yours; submit it with the same subject ([Authentication — your users](/infinity-interop/api/authentication#your-users)) | ## Request ### Request body `QuoteRequest`: | Field | Type | Required | Rules | Meaning | |---|---|---|---|---| | `sourceChain` | string | yes | chain-domain shape, ≤ 64 | the chain the user pays on | | `destChain` | string | yes | chain-domain shape, ≤ 64 | the chain the recipient is paid on | | `sourceAsset` | string | yes | asset key or `:
`, ≤ 128; a bare `0x` address is refused | the asset the user pays with | | `destAsset` | string | yes | as `sourceAsset` | the asset the recipient receives | | `amount` | string | yes | decimal base units, > 0, ≤ 80 characters | the input the user spends (default), or with `swapType: "exact-output"` what the recipient receives | | `recipient` | string | yes | ≤ 128; an address of the **destination** chain's family | who is paid on the destination | | `sourceAccount` | string | no | ≤ 128; an address of the **source** chain's family | the user's funding account. Optional on EVM↔EVM; **needed for any non-EVM leg** (the solver compiles the transaction around it; a Solana source without it is `EIN0008` with no reference, bare `EIN0050` on preview) and for a signed `POST /order` (the signer must equal it). When it is absent, the hub quotes the `recipient` as the payer (`signablePayload.payer` and the signed `witness.user`): a signer other than the recipient then pays, with its own Permit2 nonce and tokens, for an order whose user and refund beneficiary is the recipient | | `swapType` | string | no | `exact-input` (default) or `exact-output`, case-sensitive | which side is fixed | | `maxAmountIn` | string | no | decimal base units; **exact-output only** | the most the user will pay | | `slippageBps` | integer | no | 1–1000; absent or `0` means 50 | token-in lane tolerance; ignored on every other lane | Validation and resolution rules, in the order they apply: - A **non-EVM** `recipient` or `sourceAccount` that does not decode is `EIN0001` with that field as reference. EVM addresses are not format-checked at this point — a malformed one fails later, not as `EIN0001`. - An asset **reference** whose chain is not the request's chain is `EIN0001` (reference `sourceAsset` / `destAsset`, `error_params` = `[reference chain, request chain]`). - A reference to a **declared** asset behaves exactly as its key. - An **undeclared destination** reference is forwarded as its address. - An **undeclared source** reference is admitted only on a chain that declares a swap router and a wrapped native — the **token-in lane** (the proxy swaps it into the wrapped native). An address with no code there (its `decimals()` call answers empty) is `EIN0002`, reference `no ERC-20 at :
`. A contract that is not an ERC-20 passes that probe, and the round then fails at the provider, typically `EIN0008` `providerRefused`. On any other chain an undeclared source is `EIN0002` ("accepts no undeclared source"). - `maxAmountIn` on an exact-input request is `EIN0001` (reference `maxAmountIn`). - `exact-output` with a token-in source is `EIN0001` (reference `swapType`): that lane's price is known only from what the user spends. - Source chains are EVM and Solana. Sui and Aptos are supported as destination chains only: a quote whose source is a Sui or Aptos chain is refused. The catalog's `family` tells you which rail a chain is on. ## Response **`data`** (`QuoteResponse`): | Field | Type | Meaning | |---|---|---| | `quoteRequestId` | string | the round id (a ULID; on prepare, derived from your organization and `preparationId`) | | `quoteId` | string | the winning quote (64 hex) — the input to `POST /order` and `POST /order/openfor` | | `issuedAt` | timestamp | the round's clock | | `expiresAt` | timestamp | when the quote stops being submittable; never refreshed ([Limits](/infinity-interop/api/limits)) | | `executionClass` | string | `INFINITY_SOLVER`, `PROVIDER_NATIVE_COVERED` or `PROVIDER_NATIVE_UNCOVERED` | | `amountIn` | string | base units the winner charges (on the token-in lane: the escrow's wrapped native — see `swapInput`) | | `amountOut` | string | base units the recipient receives (on an auction: the guaranteed floor) | | `legs[]` | array | `{provider, product, version, sourceChain, destChain, sourceAsset, destAsset, amountIn, amountOut}` per leg | | `score` | object | `{value, scoringVersion, candidatesEvaluated, providersMissedRound}`; `value` is an exact decimal string | | `policy` | object | `{policyVersion, pathEpoch, scoringVersion}` — the configuration the round ran under | | `disclosure` | object | `{signableHash, signablePayload}` — see below | | `signPayload` | object | **openFor flow only:** what the user signs, verbatim ([Step execution — gasless openFor](/infinity-interop/api/concepts/step-execution#gasless-openfor)) | | `executionTx` | object | **the user sends it:** `{approvals[], steps[]}` ([Step execution — proxy lanes](/infinity-interop/api/concepts/step-execution#proxy-lanes), [Step execution — open flow](/infinity-interop/api/concepts/step-execution#open-flow)) | | `swapInput` | object | **token-in lane only:** `{assetRef, amountMax, spender}` — the token the user pays with and the most that can be taken. Show this, not `amountIn`, as the cost | | `signIntent` | string | the order-intent text to `personal_sign` for `POST /order` (scheme `eip191`); frozen as v1 — sign it verbatim, never derive it. Absent when the hub could not build it (it discloses none rather than a wrong one); a signed `POST /order` is then impossible, so re-quote | | `etaSeconds` | integer | the provider's own fill-time estimate; omitted when none was disclosed | | `auction` | object | present on a decaying winner, and also on a `limit` / `exclusiveLimit` winner, which does not decay (`slope` `"0"`, `ceiling` = `floor`) and ranks as firm ([Flows — auctions](/infinity-interop/api/flows#auctions); what the fields mean: [Trade types — firm and auctioned winners](/infinity-interop/api/concepts/trade-types#firm-and-auctioned-winners)) | `disclosure.signableHash` is the value to echo as `artifactHash` on `POST /order`. `disclosure.signablePayload` is the canonical signable form. Its keys a client reads: | Key | Meaning | |---|---| | `kind` | the winner kind, which decides the submission flow: `oif-escrow-gasless`, `oif-escrow-nonevm`, `aggregator-gasless` (signed, openFor); `oif-wrap-native`, `oif-swap-token` (proxy lanes, unsigned openFor); `aggregator-bridge` (open flow); `transfer` (discloses neither `signPayload` nor `executionTx`; not completable); `solver-swap` (every `INFINITY_SOLVER` candidate; not completable) | | `orderId` | the recomputed **on-chain** order id (proxy lanes, Solana, the external gasless lane; absent on Permit2) | | `wrapsNative` / `wrappedAsset` | Solana native-coin quotes: the user pays the native coin; the escrow holds (and a refund returns) the wrapped token `wrappedAsset` | | `signatureEnvelope` | `ed25519-97` on Solana | `executionTx`: - `approvals[]`: `{chainId, token, spender, amount}` — send first. - `steps[]`: `{chainId, to, data, value, description}` — then send in order. - `chainId` is an **integer** on the hub's proxy lanes and a **chain-domain string** on the external aggregator lane; accept both. - `value` is decimal wei. `description` names the proxy call (`wrapAndOpen`, `swapAndOpenV2`). ## Behavior A winner discloses at most one of `signPayload` and `executionTx`; which one, what to do with it, and the solver-swap and `transfer` cases that disclose neither: [Step execution](/infinity-interop/api/concepts/step-execution#at-most-one-of-signpayload-and-executiontx). The hub caches a quote round for about 30 seconds: the same request body inside that window answers with the **same** `quoteId` and its **original** `expiresAt`. Time a quote from when you first received its `quoteId`, not from your latest request. ## Errors | Code | When | |---|---| | `EIN0001` | a form field (reference = the field), `body` (well-formed JSON of the wrong shape, or a body sent without a JSON `Content-Type`; a body that is not JSON at all is the framework's `EMC0028`, reference `Body`), the asset-reference chain rule (`error_params` = `[reference chain, request chain]`), `maxAmountIn`, `swapType`, `x-infinity-subject` (a subject outside the hub's rule) | | `EIN0002` | an asset that does not resolve: no code at a token-in reference (`no ERC-20 at …`; a contract that is not an ERC-20 passes and typically fails as `EIN0008`), a chain with no swap router, an `eip155` chain the deployment doesn't configure (`chain has no corridor assets`), or a provider reporting the asset unresolved | | `EIN0007` | the hub's own fail-closed checks refused the round | | `EIN0008` | no viable route — terminal for this request. `error_reference` `providerRefused` (a provider answered and refused — typically the amount is below its economic floor: offer another **amount**), `notFillable` with `error_params[0]` = the destination chain (no solver delivers there: offer another **destination**), or empty. A `maxAmountIn` under the price is `providerRefused` | | `EIN0050` | availability — retry later. `error_params[0]` may carry the cause: `responseRejected` (answers discarded by the hub's vetting), `solverUnavailable`, `providerMissed` (a provider missed the deadline), `upstreamAuth` (the aggregator refused the hub's own credential — configuration; retrying does not help until an operator renews it). Also bare when the deployment has paused exposure, and `responseRejected` for a chain domain outside `eip155` that the deployment doesn't configure (an unconfigured `eip155` chain is `EIN0002`) | | `EIN0051` | the quote store was unavailable | | `EIN0080` | the hub's rate limit, charged to your organization ([Limits](/infinity-interop/api/limits)) — `error_params[0]` is a suggested back-off in milliseconds: the time one request's token takes to refill, `floor(1000 / infinity.auth.tenantRps)` and at least 1 (200 at the defaults), the same value on every shed | | `EIN0081` | the hub is at its in-flight quote cap — back off | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS -X POST https://proxy-api.infinimesh.net/v1/quote \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"sourceChain":"eip155-1","destChain":"eip155-56","sourceAsset":"weth","destAsset":"wbnb","amount":"1000000000000000","recipient":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}' ``` ### Response Illustrative values; formats follow the [Response](#response) table. `signPayload` is the fixture typed data verbatim; `signablePayload.quoteId` is the provider's own quote id, not the hub's `quoteId` above it. The deadlines are the fixture's too: `witness.expires` and `orderExpiresAt` (4102445200) fall in the year 2100. A live quote carries an `orderExpiresAt` about 1 800 s after `issuedAt`, and the firewall refuses one more than 24 h ahead, so a live hub would not serve this sample at its `issuedAt`. ```json { "success": true, "request_id": "8b9c0d1e-2f30-416b-9c6d-7e8f9a0b1c2d", "data": { "quoteRequestId": "01M34FSW4R9F2C4E7A1B3D5F80", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "issuedAt": "2026-09-22T12:00:15Z", "expiresAt": "2026-09-22T12:00:45Z", "executionClass": "PROVIDER_NATIVE_UNCOVERED", "amountIn": "1000000000000000", "amountOut": "32548366596169380000", "legs": [ { "provider": "c8-solver", "product": "oif-escrow-v0", "version": "v0", "sourceChain": "eip155-1", "destChain": "eip155-56", "sourceAsset": "weth", "destAsset": "wbnb", "amountIn": "1000000000000000", "amountOut": "32548366596169380000" } ], "score": { "value": "30920948266360911000.000000", "scoringVersion": "score:v3", "candidatesEvaluated": 1, "providersMissedRound": 0 }, "policy": { "policyVersion": "pv1", "pathEpoch": "pe1", "scoringVersion": "score:v3" }, "disclosure": { "signableHash": "leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40", "signablePayload": { "amountIn": "1000000000000000", "amountOut": "32548366596169380000", "deadline": "4102444900", "dependencyNodes": [ { "destChain": "eip155-56", "family": "oif-escrow", "permit2": "0x000000000022d473030f116ddee9f6b43ac78ba3", "provider": "c8-solver", "sourceChain": "eip155-1", "spender": "0xf439808b5a80e3c355645b62310502817000cd1d" } ], "destAsset": "wbnb", "destChain": "eip155-56", "expires": 4102445200, "integrityChecksum": "5f2c8a1db06e4bb0a2f37e5d9c8b1a4e6f0d2c3b5a79881726354493827160aa", "kind": "oif-escrow-gasless", "nonce": "1786146443149", "oifSemanticVersion": 1, "orderExpiresAt": 4102445200, "payer": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "permit2": "0x000000000022d473030f116ddee9f6b43ac78ba3", "productKey": "c8-solver:oif-escrow-v0:v0:eip155-1:std", "provider": "c8-solver", "quoteId": "e2e-quote-1", "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "schemaVersion": 1, "signPayload": { "…": "the top-level signPayload below, verbatim — put it back here to reproduce signableHash" }, "sourceAsset": "weth", "sourceChain": "eip155-1", "spender": "0xf439808b5a80e3c355645b62310502817000cd1d", "validUntil": 4102444800 } }, "signPayload": { "signatureType": "eip712", "domain": { "name": "Permit2", "chainId": 1, "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3" }, "primaryType": "PermitBatchWitnessTransferFrom", "message": { "permitted": [ { "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "amount": "1000000000000000" } ], "spender": "0xF439808b5A80E3C355645B62310502817000CD1D", "nonce": "1786146443149", "deadline": "4102444900", "witness": { "user": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "expires": 4102445200, "inputOracle": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", "outputs": [ { "oracle": "0x0000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe6", "settler": "0x0000000000000000000000008a791620dd6260079bf849dc5567adc3f2fdc318", "chainId": 56, "token": "0x000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "amount": "32548366596169380000", "recipient": "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", "callbackData": "0x", "context": "0x" } ] } }, "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "MandateOutput": [ { "name": "oracle", "type": "bytes32" }, { "name": "settler", "type": "bytes32" }, { "name": "chainId", "type": "uint256" }, { "name": "token", "type": "bytes32" }, { "name": "amount", "type": "uint256" }, { "name": "recipient", "type": "bytes32" }, { "name": "callbackData", "type": "bytes" }, { "name": "context", "type": "bytes" } ], "Permit2Witness": [ { "name": "user", "type": "address" }, { "name": "expires", "type": "uint32" }, { "name": "inputOracle", "type": "address" }, { "name": "outputs", "type": "MandateOutput[]" } ], "PermitBatchWitnessTransferFrom": [ { "name": "permitted", "type": "TokenPermissions[]" }, { "name": "spender", "type": "address" }, { "name": "nonce", "type": "uint256" }, { "name": "deadline", "type": "uint256" }, { "name": "witness", "type": "Permit2Witness" } ], "TokenPermissions": [ { "name": "token", "type": "address" }, { "name": "amount", "type": "uint256" } ] } }, "signIntent": "{\"amountIn\":\"1000000000000000\",\"amountOut\":\"32548366596169380000\",\"kind\":\"infinity-signable-intent:dev-v1\",\"leaf\":\"leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40\",\"productKey\":\"c8-solver:oif-escrow-v0:v0:eip155-1:std\",\"quoteId\":\"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58\",\"recipient\":\"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\",\"schemaVersion\":1}", "etaSeconds": 30 } } ``` No `executionTx` or `swapInput` (the escrow lane discloses neither); no `auction` (this is a firm quote, not a decaying winner). ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const q = await client.getQuote({ sourceChain: "eip155-1", destChain: "eip155-56", sourceAsset: "weth", destAsset: "wbnb", amount: "1000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", sourceAccount: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", // the payer }); q.response; // the answer above, unchanged q.lane; // what executing it involves ``` ## See also - SDK: [`client.getQuote()`](/infinity-interop/sdk/quotes), and the checks before signing ([The pre-signing checks](/infinity-interop/sdk/quotes#the-pre-signing-checks)) - [Step execution](/infinity-interop/api/concepts/step-execution) - [Trade types](/infinity-interop/api/concepts/trade-types) - [Input validation](/infinity-interop/api/concepts/input-validation) - [Quickstart](/infinity-interop/api/quickstart) --- # Preview quotes URL: https://docs.c8ntinuum.com/infinity-interop/api/quote-preview `POST /v1/quote/preview` Indicative offers for browsing. Reserves nothing, appends nothing, and returns nothing signable. | Item | Value | |---|---| | Route key | `infinityQuotePreview` | | Credential | API key or project id | | `X-Infinity-Subject` | required: the preview is for that user of yours ([Authentication — your users](/infinity-interop/api/authentication#your-users)) | ## Request ### Request body The same `QuoteRequest` as [`POST /quote`](/infinity-interop/api/quote#request-body), with the same validation. ## Response **`data`** (`QuotePreviewResponse`): | Field | Type | Meaning | |---|---|---| | `signable` | boolean | always `false` | | `offers[]` | array | `{provider, previewId, expiresAt, amountIn, amountOut}` — estimates, **ordered by provider id, not by price**. `previewId` is informational, never a quote id | ## Behavior - **An empty `offers` array with success** means no provider call failed and none produced an offer: for each provider asked, the aggregator either answered that no solver serves the request, or answered without a usable offer (any it sent were discarded, below) and without reporting a solver unavailable. It also comes back when no provider was asked at all, because every provider's circuit breaker is open, so it can hide an outage. - **Any provider error turns a round without offers into `EIN0050`**, and refusals of the request as sent count as errors: an amount below the provider's minimum, an exact-output request whose input would exceed `maxAmountIn`, a Solana source without `sourceAccount`. Such a request gets `EIN0050`, not an empty success, and retrying doesn't change it; `POST /quote` answers it with `EIN0008` ([known issue](/infinity-interop/integration-notes#ki-preview-no-offer)). A provider that cannot preview at all counts as failed on every round. - On a token-in lane an offer's `amountIn` is the slippage-widened ceiling, rounded up — the figure the wallet will be asked to approve. - Offers with invalid amounts, a changed fixed-side amount, or an exact-output input above `maxAmountIn` are discarded. ## Errors | Code | When | |---|---| | (`POST /quote` validation codes) | see [`POST /quote`](/infinity-interop/api/quote#errors) | | `EIN0050` | bare, when there are **zero offers and at least one provider call failed**, a refusal of the request as sent included ([known issue](/infinity-interop/integration-notes#ki-preview-no-offer)); or when the deployment has paused exposure | | `EIN0080` | your organization's quote bucket, shared by the three quote routes; `error_params[0]` is the same suggested back-off as on `POST /quote`, `floor(1000 / tenantRps)` ms and at least 1 | | `EIN0081` | busy — back off | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS -X POST https://proxy-api.infinimesh.net/v1/quote/preview \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"sourceChain":"eip155-1","destChain":"eip155-56","sourceAsset":"weth","destAsset":"wbnb","amount":"1000000000000000","recipient":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}' ``` ### Response Illustrative values; formats follow the [Response](#response) table. ```json { "success": true, "request_id": "9c0d1e2f-3041-427c-ad7e-8f9a0b1c2d3e", "data": { "signable": false, "offers": [ { "provider": "c8-solver", "previewId": "indicative-reference", "expiresAt": "2026-09-22T12:00:45Z", "amountIn": "1000000000000000", "amountOut": "32548366596169380000" } ] } } ``` ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const preview = await client.previewQuotes({ sourceChain: "eip155-1", destChain: "eip155-56", sourceAsset: "weth", destAsset: "wbnb", amount: "1000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", }); ``` ## See also - SDK: [`client.previewQuotes()`](/infinity-interop/sdk/quotes#preview-and-prepare) - [Step execution — preview, then prepare](/infinity-interop/api/concepts/step-execution#preview-then-prepare) - [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) --- # Prepare quote URL: https://docs.c8ntinuum.com/infinity-interop/api/quote-prepare `POST /v1/quote/prepare` Prepare one chosen provider under a stable `preparationId`. The result is a normal, signable `QuoteResponse`; a retry with the same id returns the same stored quote. | Item | Value | |---|---| | Route key | `infinityQuotePrepare` | | Credential | API key or project id | | `X-Infinity-Subject` | required: the quote is prepared for that user of yours; submit it with the same subject ([Authentication — your users](/infinity-interop/api/authentication#your-users)) | ## Request ### Request body The [`QuoteRequest`](/infinity-interop/api/quote#request-body) fields plus: | Field | Type | Required | Rules | Meaning | |---|---|---|---|---| | `provider` | string | yes | 1–128 characters | the provider picked from a preview offer | | `preparationId` | string | yes | `^[A-Za-z0-9_-]{16,128}$` | one id per deliberate signing attempt — a UUID works | ## Response **`data`:** `QuoteResponse`, as [`POST /quote`](/infinity-interop/api/quote#response). ## Behavior - **Replay:** a stored preparation under this id is returned as-is, **with its original `expiresAt`** — a retry never extends it. The inputs, the organization and the provider must match the stored one, or it is `EIN0001` (reference `preparationId`). A stored preparation that has expired (or whose configuration moved on) is `EIN0005`: create a new id. - Only the named provider is asked. No winner is `EIN0008` with an empty reference. - The prepared transaction template is stored with the quote, so a prepared quote can be submitted through **any** hub replica sharing the event store. ## Errors | Code | When | |---|---| | (`POST /quote` codes) | see [`POST /quote`](/infinity-interop/api/quote#errors) | | `EIN0001` | reference `preparationId/provider` (a malformed id or provider) | | `EIN0001` | reference `preparationId` (the id reused with different inputs) | | `EIN0005` | the stored preparation expired | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS -X POST https://proxy-api.infinimesh.net/v1/quote/prepare \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"sourceChain":"eip155-1","destChain":"eip155-56","sourceAsset":"weth","destAsset":"wbnb","amount":"1000000000000000","recipient":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","provider":"","preparationId":"0d5b1f44-64aa-44ad-8ffb-f576b55e9ac2"}' ``` ### Response Illustrative values; formats follow the [Response](#response) table. Identical shape to `POST /quote`; the ids differ because prepare derives the round id from your organization and `preparationId`. `signablePayload.quoteId` is the provider's own quote id, not the hub's `quoteId` above it. The year-2100 deadlines are the test fixture's, which a live hub would not serve ([`POST /quote` — example](/infinity-interop/api/quote#example)). ```json { "success": true, "request_id": "0d1e2f30-4152-438d-be8f-9a0b1c2d3e4f", "data": { "quoteRequestId": "ba09213b767a4060c13fa044c2cd07881f91586253231b9f418bf6b4ace2b155", "quoteId": "d81346ecccae20db0ae167581128091e3d6cad874626d601c0716d5725ba8e28", "issuedAt": "2026-09-22T12:00:15Z", "expiresAt": "2026-09-22T12:00:45Z", "executionClass": "PROVIDER_NATIVE_UNCOVERED", "amountIn": "1000000000000000", "amountOut": "32548366596169380000", "legs": [ { "provider": "c8-solver", "product": "oif-escrow-v0", "version": "v0", "sourceChain": "eip155-1", "destChain": "eip155-56", "sourceAsset": "weth", "destAsset": "wbnb", "amountIn": "1000000000000000", "amountOut": "32548366596169380000" } ], "score": { "value": "30920948266360911000.000000", "scoringVersion": "score:v3", "candidatesEvaluated": 1, "providersMissedRound": 0 }, "policy": { "policyVersion": "pv1", "pathEpoch": "pe1", "scoringVersion": "score:v3" }, "disclosure": { "signableHash": "leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40", "signablePayload": { "amountIn": "1000000000000000", "amountOut": "32548366596169380000", "deadline": "4102444900", "dependencyNodes": [ { "destChain": "eip155-56", "family": "oif-escrow", "permit2": "0x000000000022d473030f116ddee9f6b43ac78ba3", "provider": "c8-solver", "sourceChain": "eip155-1", "spender": "0xf439808b5a80e3c355645b62310502817000cd1d" } ], "destAsset": "wbnb", "destChain": "eip155-56", "expires": 4102445200, "integrityChecksum": "5f2c8a1db06e4bb0a2f37e5d9c8b1a4e6f0d2c3b5a79881726354493827160aa", "kind": "oif-escrow-gasless", "nonce": "1786146443149", "oifSemanticVersion": 1, "orderExpiresAt": 4102445200, "payer": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "permit2": "0x000000000022d473030f116ddee9f6b43ac78ba3", "productKey": "c8-solver:oif-escrow-v0:v0:eip155-1:std", "provider": "c8-solver", "quoteId": "e2e-quote-1", "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "schemaVersion": 1, "signPayload": { "…": "the top-level signPayload below, verbatim — put it back here to reproduce signableHash" }, "sourceAsset": "weth", "sourceChain": "eip155-1", "spender": "0xf439808b5a80e3c355645b62310502817000cd1d", "validUntil": 4102444800 } }, "signPayload": { "signatureType": "eip712", "domain": { "name": "Permit2", "chainId": 1, "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3" }, "primaryType": "PermitBatchWitnessTransferFrom", "message": { "permitted": [ { "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "amount": "1000000000000000" } ], "spender": "0xF439808b5A80E3C355645B62310502817000CD1D", "nonce": "1786146443149", "deadline": "4102444900", "witness": { "user": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "expires": 4102445200, "inputOracle": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", "outputs": [ { "oracle": "0x0000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe6", "settler": "0x0000000000000000000000008a791620dd6260079bf849dc5567adc3f2fdc318", "chainId": 56, "token": "0x000000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "amount": "32548366596169380000", "recipient": "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", "callbackData": "0x", "context": "0x" } ] } }, "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "MandateOutput": [ { "name": "oracle", "type": "bytes32" }, { "name": "settler", "type": "bytes32" }, { "name": "chainId", "type": "uint256" }, { "name": "token", "type": "bytes32" }, { "name": "amount", "type": "uint256" }, { "name": "recipient", "type": "bytes32" }, { "name": "callbackData", "type": "bytes" }, { "name": "context", "type": "bytes" } ], "Permit2Witness": [ { "name": "user", "type": "address" }, { "name": "expires", "type": "uint32" }, { "name": "inputOracle", "type": "address" }, { "name": "outputs", "type": "MandateOutput[]" } ], "PermitBatchWitnessTransferFrom": [ { "name": "permitted", "type": "TokenPermissions[]" }, { "name": "spender", "type": "address" }, { "name": "nonce", "type": "uint256" }, { "name": "deadline", "type": "uint256" }, { "name": "witness", "type": "Permit2Witness" } ], "TokenPermissions": [ { "name": "token", "type": "address" }, { "name": "amount", "type": "uint256" } ] } }, "signIntent": "{\"amountIn\":\"1000000000000000\",\"amountOut\":\"32548366596169380000\",\"kind\":\"infinity-signable-intent:dev-v1\",\"leaf\":\"leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40\",\"productKey\":\"c8-solver:oif-escrow-v0:v0:eip155-1:std\",\"quoteId\":\"d81346ecccae20db0ae167581128091e3d6cad874626d601c0716d5725ba8e28\",\"recipient\":\"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\",\"schemaVersion\":1}", "etaSeconds": 30 } } ``` No `executionTx` or `swapInput` (the escrow lane discloses neither); no `auction` (firm quote). ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const req: QuoteRequest = { sourceChain: "eip155-1", destChain: "eip155-56", sourceAsset: "weth", destAsset: "wbnb", amount: "1000000000000000", recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", }; const preview = await client.previewQuotes(req); const offer = preview.offers?.[0]; if (offer?.provider) { const q = await client.prepareQuote(req, offer.provider); // a fresh preparationId unless you pass one } ``` ## See also - SDK: [`client.prepareQuote()`](/infinity-interop/sdk/quotes#preview-and-prepare) - [Step execution — preview, then prepare](/infinity-interop/api/concepts/step-execution#preview-then-prepare) - [Handling rate limits](/infinity-interop/api/concepts/handling-rate-limits) --- # Submit order (openFor) URL: https://docs.c8ntinuum.com/infinity-interop/api/order-openfor `POST /v1/order/openfor` Submit a gasless order (the user signed `signPayload`; the solver submits and pays gas), or register a proxy-lane order (the user already sent `executionTx`; nothing is signed). | Item | Value | |---|---| | Route key | `infinityOrderOpenFor` | | Credential | API key or project id | | `X-Infinity-Subject` | required: the subject the quote was made for ([Authentication — your users](/infinity-interop/api/authentication#your-users)) | ## Request ### Request body `OpenForRequest`: | Field | Type | Required | Rules | Meaning | |---|---|---|---|---| | `quoteRequestId` | string | yes | ≤ 128 | from the quote | | `quoteId` | string | yes | ≤ 128 | from the quote | | `signature` | string | per lane | ≤ 512 | **required** on the signed lanes; **absent or empty** on the proxy lanes | The signature's wire encoding depends on the winner's family: | Lane (`signablePayload.kind`) | Signature | |---|---| | EVM escrow (`oif-escrow-gasless`, Permit2 EIP-712) | the scheme byte `0x00` + the 65-byte ECDSA signature → 66 bytes as `0x` hex | | Solana escrow (`oif-escrow-nonevm`) | `0x00` ‖ 64-byte ed25519 signature ‖ 32-byte public key (the order's user) → 97 bytes as **standard base64**, 132 characters. The hub also reads hex but forwards the string unchanged, and the aggregator accepts only base64. The SDK's `solanaEnvelope(signature, publicKey)` builds it | | External gasless (`aggregator-gasless`, EIP-3009) | the raw 65-byte ECDSA signature as `0x` hex (a 66-byte prefixed form is accepted and normalised) | | Proxy lanes (`oif-wrap-native`, `oif-swap-token`) | none | ## Response **`data`** (`OpenForAck`): | Field | Type | Meaning | |---|---|---| | `orderId` | string | the aggregator's id — the primary status handle | | `hubOrderId` | string | the hub's durable alias, derivable from `quoteId` (see [Conventions — order-id namespaces](/infinity-interop/api/conventions#the-three-order-id-namespaces)) | | `quoteId` | string | echoed | | `state` | string | the upstream's initial state, typically `created` — not a fill guarantee | | `submittedAt` | timestamp | when the hub recorded the submission | ## Behavior - **The lane decides.** A signed lane with no signature, or a proxy lane with one, is `EIN0010`. A winner that completes through the open flow (a bridge winner) is `EIN0001` pointing at [`POST /order`](/infinity-interop/api/order-create). The same `EIN0010` covers an unknown quote, another organization's quote, and a malformed signature on the external gasless lane (`aggregator-gasless`) — one code on purpose. - **The hub does not verify an EVM escrow signature.** On `oif-escrow-gasless` it forwards the signature as sent, and the upstream judges it: a bad one comes back as `EIN0021`, reference `… (SOLVER_REJECTED)`, or `http 400 (VALIDATION_ERROR)` for a signature sent without the `0x00` scheme byte. A bad Solana envelope is `EIN0007` (below). - **Rate limit.** Every request whose body validates is charged to your organization's order bucket ([Limits](/infinity-interop/api/limits)) before the quote is read or anything is reserved. A shed is `EIN0080` and has reserved and forwarded nothing, so re-posting the same body after `error_params[0]` milliseconds is safe. - **Re-posting is safe.** A quote the hub has recorded a submission for is answered with the **original ack**, with no second forward and no time limit — provided the re-post passes the lane's signature-presence rule above. The replay is keyed on the quote, not the body: a re-post carrying a different signature, even an invalid one, gets the original ack too. A re-post never opens a second order. - **Lost the answer?** Derive `hubOrderId` and read the status before re-posting — [Handling errors](/infinity-interop/api/concepts/handling-errors#a-lost-answer-is-not-a-lost-order). The gateway's availability answers (`EPX0050`, `EPX0051`, and `EPX0052` when the hub took longer than the gateway's 10 s) are lost answers too, with an unknown outcome. The gateway keeps this call running at the hub when your client times out or disconnects, but cuts it at those 10 s; if the hub was forwarding then, the forward is cancelled with it, since it runs on the request's context: the reservation stays pending, the status read answers `EIN0009`, and every re-post answers `EIN0052` until an operator reconciles it ([known issue](/infinity-interop/integration-notes#ki-openfor-cancel)). - **The forward reservation.** Before forwarding, the hub reserves the quote. An accepted forward records the submission (the re-post above). A forward the upstream definitively refused (`EIN0021`, a refused proxy-lane registration, a refused hub credential) releases the reservation, and so does a forward the hub's own outbound admission stopped **before sending** (`EIN0081` from its limiter, `EIN0050` from an open circuit breaker): back off and re-post the same body, which forwards again. Don't re-post a **declined** signed order, though: the aggregator answers the repeat with a `502`, which the hub serves as `EIN0050` (`upstream unavailable: http 502`) and then treats as a forward that went unanswered, so every later re-post answers `EIN0052` ([known issue](/infinity-interop/integration-notes#ki-redecline)). Re-quote and sign again instead. A forward that was **sent** and not answered (`EIN0050` after a timeout or a 5xx, or `EIN0081` for the upstream's own 429), or answered in a way the hub cannot read (`EIN0052` at once), keeps the reservation, because the upstream may have opened the order: every re-post then answers `EIN0052` until an operator reconciles the reservation — the hub never releases it on its own. Whether an upstream 429 proves that no order was opened is an open question; until it is settled, a 429 is treated as unknown. - **Solana** envelopes are checked by the hub before forwarding: 97 bytes, flag `0x00`, the public key equal to the order's user, the signature valid over the disclosed `transactionMessage`, and the message's order id equal to the quote's. A failure is `EIN0007`. - **Sui and Aptos** are destination-only chains; no Move escrow is disclosed. - **Expiry.** On the **signed lanes** (`oif-escrow-gasless`, `oif-escrow-nonevm`, `aggregator-gasless`) a submission at or after `expiresAt` is `EIN0005`, and nothing is reserved or forwarded, even while the quoting process still holds the template. The replay of a recorded submission and the `EIN0052` check come first, so both still answer after expiry. On the **proxy lanes** the hub keeps a quote registrable for **20 minutes** after it was issued, prepared or not, but the aggregator refuses a registration once `expiresAt` has passed: `EIN0007`, reference `fail-closed rejected: upstream refused: http 400 (QUOTE_EXPIRED)` ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). So register as soon as the transaction is sent, well before `expiresAt`. Past the hub's 20 minutes the answer is `EIN0005`. - **Once a proxy-lane transaction is out, never re-quote.** The user's own transaction opened the order in escrow before this call, so a new quote would mean a second deposit, whatever this call answers. A refused registration (`EIN0005`, `EIN0007`, `EIN0050` `upstreamAuth`) leaves the deposit where it is: it completes on chain anyway — the solver still fills it — but the hub never records the order, so it is missing from [`GET /orders`](/infinity-interop/api/orders-list) and the status routes. Follow it by the send transaction's hash on [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx), then [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill), and withdraw after the escrow expires if it is not filled ([Refunds](/infinity-interop/api/concepts/refunds)). - **A registration does not prove the deposit.** Neither the hub nor the aggregator checks that the transaction was sent: a proxy-lane quote registered without it is acked `created` and stays `created` upstream ([known issue](/infinity-interop/integration-notes#ki-proxy-unsent)). Register only after sending; the `open` fact on [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) is the proof. - A non-prepared quote must be submitted to the replica that quoted it (its template is held in that process for 30 s, 20 min on the proxy lanes); a prepared quote works on any replica. - **Not configured.** A deployment with no OIF aggregator (`infinity.upstream.oif.baseUrl` unset) answers this route `EIN0007` (reference `fail-closed rejected: openFor route not wired`). Give this call a client timeout longer than the gateway's 10 s, so that you get the gateway's answer (`EPX0052` at worst) rather than none. Your client giving up does not stop the submission: the gateway keeps it running at the hub. The gateway's own 10 s cut does: a call cut while the hub is still forwarding cancels the forward, and the quote then answers `EIN0052` until an operator reconciles it ([Integration notes](/infinity-interop/integration-notes#ki-openfor-cancel)). The SDK waits 15 s on submissions by default (35 s through a partner proxy): don't lower it. ## Errors | Code | When | |---|---| | `EIN0001` | form fields, `body` (well-formed JSON of the wrong shape, or a body sent without a JSON `Content-Type`; a body that is not JSON at all is the framework's `EMC0028`, reference `Body`); the winner completes via `POST /order` | | `EIN0005` | the quote expired: at or after `expiresAt` on a signed lane, past the 20-minute window on a proxy lane. Or its template is gone: the quoting process no longer holds it (on `aggregator-gasless`: the round or the provider's quote). On a signed lane, re-quote and re-sign. On a proxy lane the user's transaction is already out: never re-quote — follow the deposit on chain (see [Behavior](#behavior)) | | `EIN0007` | the hub's own checks refused it: a Solana envelope that fails, a prepared-template mismatch, or the aggregator refusing a **proxy-lane** registration — after `expiresAt`, reference `fail-closed rejected: upstream refused: http 400 (QUOTE_EXPIRED)` ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). A refused proxy-lane registration comes after the user's transaction: never re-quote — follow the deposit on chain (see [Behavior](#behavior)). Also the route itself when no OIF aggregator is configured | | `EIN0010` | unknown or foreign quote; a signature present or missing against the lane; a malformed signature on `aggregator-gasless`. A bad signature on `oif-escrow-gasless` is forwarded and comes back as `EIN0021`; a bad Solana envelope is `EIN0007` | | `EIN0021` | **signed lane** (all three, `aggregator-gasless` included): the solver or provider declined the order when the hub forwarded it (a 400, 409 or 422) — a bad `oif-escrow-gasless` signature included. Nothing was opened, the reservation is released, and the same signed bytes cannot succeed later: re-quote. Don't re-post the declined body: the repeat answers `EIN0050` and then `EIN0052` ([known issue](/infinity-interop/integration-notes#ki-redecline)). `error_reference` carries the upstream's status and code, e.g. `http 409 (SOLVER_REJECTED)` | | `EIN0050` | bare: the forward was sent and went unanswered (re-posting answers `EIN0052` until an operator reconciles the reservation) — including the aggregator's `502` to a re-post of a declined order (`upstream unavailable: http 502`) — or the hub's circuit breaker for the upstream was open and nothing was sent (back off and re-post the same body). `error_params` `["upstreamAuth"]`: the upstream refused the hub's own credential and accepted nothing; the reservation is released, but only an operator renewing the credential clears it — not retryable. On a proxy lane the user's deposit is already in escrow: never re-quote | | `EIN0051` | a store was unavailable | | `EIN0052` | **outcome unknown:** a submission of this quote may have opened the order — an earlier one that went unanswered (a call cut mid-forward included), or this one, answered in a way the hub cannot read. **Never re-quote or ask the user to sign again.** Re-posting the same request is safe, and keeps answering `EIN0052` until an operator reconciles the reservation | | `EIN0080` | your organization's rate limit on order submissions ([Limits](/infinity-interop/api/limits)); nothing was reserved or forwarded — re-post the same body after `error_params[0]` milliseconds | | `EIN0081` | busy, three causes: the forward-reservation table is full (store-less assemblies only); the hub's own outbound rate limiter for the upstream, which quote traffic shares, did not admit the forward in time — nothing was sent, re-post after a back-off; or the upstream aggregator answered 429 — the reservation is kept, so a re-post answers `EIN0052` | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS -X POST https://proxy-api.infinimesh.net/v1/order/openfor \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"quoteRequestId":"01M34FSW4R9F2C4E7A1B3D5F80","quoteId":"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58","signature":"0x00<65-byte-ecdsa-hex>"}' ``` ### Response Illustrative values; formats follow the [Response](#response) table. ```json { "success": true, "request_id": "1e2f3041-5263-449e-8f9a-0b1c2d3e4f50", "data": { "orderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "hubOrderId": "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "state": "created", "submittedAt": "2026-09-22T12:00:20Z" } } ``` ```ts // client: your InteropClient. submitOpenFor recovers a lost answer: it looks the order up by its // derived hub order id, then re-posts the SAME body once. const submitted = await client.submitOpenFor({ quoteRequestId: q.quoteRequestId, quoteId: q.quoteId, signature: evmEscrowEnvelope(sig), // "0x00" + the 65-byte signature }); ``` ## See also - SDK: `client.submitOpenFor()` ([Lost answers](/infinity-interop/sdk/execution#lost-answers)); `client.executeQuote()` signs, submits and tracks in one call ([Executing orders](/infinity-interop/sdk/execution)) - [Step execution](/infinity-interop/api/concepts/step-execution) - [Handling errors](/infinity-interop/api/concepts/handling-errors) - [Execution errors](/infinity-interop/api/concepts/execution-errors) - [Quickstart](/infinity-interop/api/quickstart) --- # Create order URL: https://docs.c8ntinuum.com/infinity-interop/api/order-create `POST /v1/order` Register acceptance of an **open-flow** winner, after the user sent the disclosed `executionTx` themselves (paying the gas). One order per quote. Only for an open-flow winner (the SDK's lane is `open-flow`): every OIF-lane quote is refused `EIN0007` at the re-approval step ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)). | Item | Value | |---|---| | Route key | `infinityOrderCreate` | | Credential | API key or project id | | `X-Infinity-Subject` | required: the subject the quote was made for ([Authentication — your users](/infinity-interop/api/authentication#your-users)) | ## Request ### Request body `OrderCreateRequest`: | Field | Type | Required | Rules | Meaning | |---|---|---|---|---| | `quoteRequestId` | string | yes | ≤ 128 | from the quote | | `quoteId` | string | yes | ≤ 128 | from the quote | | `artifactHash` | string | yes | ≤ 256 | echo of `disclosure.signableHash`; the hub recomputes its own and compares (a cross-check, never authority) | | `signature` | string | together | ≤ 512 | `personal_sign` over `signIntent` | | `signer` | string | together | ≤ 128 | must equal the quote's `sourceAccount` (0x addresses compare case-insensitively) | | `scheme` | string | together | `eip191` (for `personal_sign`), `eip712` or `ed25519` | the signature scheme | ## Response **`data`** (`OrderCreateAck`): | Field | Type | Meaning | |---|---|---| | `orderId` | string | 64 hex, `hex(sha256("infinity:v1:order:" + quoteId))` | | `quoteId` | string | echoed | | `createdAt` | timestamp | the first acceptance | | `status` | string | always `CREATED`; everything after is asynchronous — read `GET /status/{orderId}` | ## Behavior - **`signature`, `signer` and `scheme` go together — all three or none.** Any one present makes all three required; a partial set is `EIN0010`. A quote requested without `sourceAccount` therefore cannot be signed. - **None** is admitted only when the deployment enables unsigned registrations (`infinity.flags.orderSignatureOptional`); otherwise `EIN0010`. - The hub re-approves the winner (product enabled, class admissibility, fingerprint) before the `artifactHash` and signature checks; a failure is `EIN0007`. `POST /order` registers `create`-flow winners only. Every OIF-lane quote answers `EIN0007` here; complete it through `POST /order/openfor`. Decide with the SDK's `detectLane(q)` ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)). - A **proxy-lane** `executionTx` is not registered here (`EIN0007`) — it completes through [`POST /order/openfor`](/infinity-interop/api/order-openfor), unsigned. - **Register before `expiresAt`, and never re-quote after the transaction.** The user's transaction is already sent when this call is made, and a first registration at or after `expiresAt` (about 30 s after issue) is `EIN0005`. A refused registration doesn't undo the transaction: it completes on chain anyway, unregistered, and a new quote would mean a second transaction. The same holds for `EIN0006`, `EIN0007` and `EIN0010` below; proxy-lane registrations follow the same rule ([known issue](/infinity-interop/integration-notes#ki-proxy-window)). - **Rate limit.** Every request whose body validates, a duplicate included, is charged to your organization's order bucket ([Limits](/infinity-interop/api/limits)) before the quote is read; a shed is `EIN0080`. - **Idempotent:** a repeat for a quote that already has an order — including one created through `POST /order/openfor` — gets that order's ack (same `orderId` and `quoteId`, and the first `createdAt`). It is answered right after the quote's own checks (known, yours, quoted) and before the expiry, configuration, re-approval, `artifactHash` and signature checks, so it holds after `expiresAt`, after a configuration change (no `EPOCH_SUPERSEDED` is recorded), and whatever `artifactHash` or signature fields the repeat carries. A lost ack is recovered by re-posting. ## Errors | Code | When | |---|---| | `EIN0001` | form, `body` (well-formed JSON of the wrong shape, or a body sent without a JSON `Content-Type`; a body that is not JSON at all is the framework's `EMC0028`, reference `Body`) | | `EIN0010` | unknown or foreign quote | | `EIN0051` | a store read, the existing-order lookup included, was unavailable | | `EIN0080` | rate limit | For a quote that has no order yet, checked in this order: | Code | When | |---|---| | `EIN0005` | quote expired: the first registration came at or after `expiresAt` | | `EIN0006` | the configuration the quote ran under was superseded | | `EIN0007` | re-approval or artifact build failed; every OIF-lane quote fails re-approval ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)) | | `EIN0010` | a `create` winner's `artifactHash` mismatch, or any signature problem | All four come after the user's transaction was sent: never re-quote because of one (see [Behavior](#behavior)). The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS -X POST https://proxy-api.infinimesh.net/v1/order \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" \ -H 'Content-Type: application/json' \ -d '{"quoteRequestId":"01M34FSW4R9F2C4E7A1B3D5F80","quoteId":"b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58","artifactHash":"leaf:v1:5c70e8deaaac78819757d29073bcc8dbdb299da6016a35e1e3017569a1202a40","signature":"","signer":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","scheme":"eip191"}' ``` ### Response Illustrative values; formats follow the [Response](#response) table. The ids are the example world's quote, an `oif-escrow-gasless` quote that completes through [`POST /order/openfor`](/infinity-interop/api/order-openfor): sent here first, it would be refused `EIN0007` ([known issue](/infinity-interop/integration-notes#ki-order-create-oif)). The answer below is the idempotent repeat once its openFor order exists — that order's ack (`orderId` is the openFor `hubOrderId`, `createdAt` its `submittedAt`), whatever signature fields the repeat carries. A `create` winner's own registration answers in the same shape. ```json { "success": true, "request_id": "2f304152-6374-45af-9a0b-1c2d3e4f5061", "data": { "orderId": "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "createdAt": "2026-09-22T12:00:20Z", "status": "CREATED" } } ``` ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const created = await client.api.createOrder({ quoteRequestId: q.quoteRequestId, quoteId: q.quoteId, artifactHash: q.disclosure.signableHash, signature: sigIntent, signer: address, scheme: "eip191", }); ``` ## See also - SDK: `client.api.createOrder()`; `client.executeQuote()` runs the whole open flow ([Lanes — open-flow](/infinity-interop/sdk/lanes#open-flow)) - [Step execution — open flow](/infinity-interop/api/concepts/step-execution#open-flow) - [Handling errors](/infinity-interop/api/concepts/handling-errors) --- # Get upstream status URL: https://docs.c8ntinuum.com/infinity-interop/api/order-upstream-status `GET /v1/order/{orderId}/status` The aggregator-backed projection of an **openFor** order. | Item | Value | |---|---| | Route key | `infinityOrderUpstreamStatus` | | Credential | API key or project id; **your organization's orders only** | | `X-Infinity-Subject` | required: only the orders placed under that subject are answered ([Authentication — your users](/infinity-interop/api/authentication#your-users)) | One of the hub's three status views — the advisory one. Which view answers which question: [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle#the-three-status-views). ## Request ### Path parameters | Param | Rules | Meaning | |---|---|---| | `orderId` | the aggregator `orderId` or the `hubOrderId`, `^[A-Za-z0-9_-]{1,256}$` | anything else, an unknown id, and an order that is not yours (another organization's, or one placed under another subject) all answer the same `EIN0009`. Open-flow (`POST /order`) orders are not served here; use [`GET /status/{orderId}`](/infinity-interop/api/hub-status) | ## Response **`data`** (`UpstreamOrderStatus`): | Field | Type | Meaning | |---|---|---| | `orderId` | string | the upstream order id | | `state` | string | `created`, `pending`, `executing`, `executed`, `settling`, `settled`, `finalized`, `refunded`, `failed` | | `failedAt` | string | `Prepare`, `Fill`, `PostFill`, `PreClaim` or `Claim` — failures only | | `failureReason` | string | the upstream's message — failures only | | `history[]` | array | `{state, at}` — the distinct states the hub has read, in order, each stamped when the hub first read it: `history[0].at` is the hub's first read, not `submittedAt`, and a state the upstream passed through between two reads is absent. It lives in the hub's memory: a restart, or 10 minutes without a read, can start it over | | `fillTransaction` | object | the upstream's fill reference — unverified; resolve it with [`GET /onchain/tx/…`](/infinity-interop/api/onchain-fill-by-tx) | | `upstreamFillReported` | boolean | the upstream supplied a fill reference (sticky); not chain proof | | `filled` | boolean | legacy alias of `upstreamFillReported` | | `stateSource` | string | always `upstream-advisory` | | `finalityVerified` | boolean | always `false` | | `latestUpstreamState` | string | the latest reported state, including one that was rejected | | `rejectedUpstreamTransition` | boolean | the latest report could not replace the accepted state | | `lastUpstreamSyncAt` | timestamp | the last successful upstream read | | `upstreamStale` | boolean | the accepted state could not be refreshed | ## Behavior - **This is advisory.** Neither `finalized`, `settled` nor `filled` proves settlement; use the on-chain views for observed facts. - States only move forward: `created` < `pending` < `executing` < `executed` < `settling` < `settled` < `finalized`, with `failed` and `refunded` reachable from any non-terminal state. The terminal states are `finalized`, `failed` and `refunded`: `settled` can still become any of them. A backwards, unknown or contradictory report is not applied: it sets `rejectedUpstreamTransition` and `upstreamStale` and keeps the previous state. - Served from a 2-second cache; when a refresh fails the last good state is served with `upstreamStale: true`. - **Stop polling at `finalized`, `failed` or `refunded`**, and do not present any of them as verified finality. - **Give the loop a stall rule.** An EVM gasless order can stay `executed` long after it filled and settled on chain, with fresh syncs, `upstreamStale: false` and no `fillTransaction`; `settled` may never be reported ([known issue](/infinity-interop/integration-notes#ki-upstream-stall)). Once the order reaches `executed`, read [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) with its `onchainOrderId` (from [`GET /orders`](/infinity-interop/api/orders-list)): its `filled` and `settled` facts are the chain's answer. Hand off to it after a few minutes without progress. The chain facts are open to your project id too; a page, which cannot list orders, takes the on-chain id from the order it submitted: on the escrow lanes the openFor ack's `orderId` is the on-chain id, and the SDK's `executeQuote` finds it for you. - **Open to a project id**, like every route but `GET /orders`. Anyone who holds your project id and one of your order ids can read that order's status. Nothing lists the ids to a page. ## Errors | Code | When | |---|---| | `EIN0007` | an upstream answer the hub refused and no earlier state to fall back to | | `EIN0009` | unknown, not yours, malformed, or unknown upstream | | `EIN0050` / `EIN0051` | availability, with no earlier state — `EIN0050` with `error_params` `["upstreamAuth"]` when the aggregator refused the hub's own credential, which only an operator can clear | | `EIN0081` | the projection table is full | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS https://proxy-api.infinimesh.net/v1/order/0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9/status \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" ``` ### Response Illustrative values; formats follow the [Response](#response) table. ```json { "success": true, "request_id": "30415263-7485-46b0-8b1c-2d3e4f506172", "data": { "stateSource": "upstream-advisory", "finalityVerified": false, "latestUpstreamState": "executed", "rejectedUpstreamTransition": false, "upstreamFillReported": true, "orderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "state": "executed", "history": [ { "state": "created", "at": "2026-09-22T12:00:23Z" }, { "state": "executed", "at": "2026-09-22T12:00:52Z" } ], "fillTransaction": { "hash": "0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d" }, "filled": true, "lastUpstreamSyncAt": "2026-09-22T12:01:00Z", "upstreamStale": false } } ``` `failedAt`/`failureReason` are absent: this order is not on the failure arm. The first `at` is the hub's first read, three seconds after the ack's `submittedAt`. ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const st = await client.api.getOrderStatus(ack.orderId); ``` ## See also - SDK: `client.api.getOrderStatus()`; `client.trackOrder()` polls it together with the chain facts ([Tracking](/infinity-interop/sdk/execution#tracking)) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) - [Execution errors](/infinity-interop/api/concepts/execution-errors) --- # Get hub status URL: https://docs.c8ntinuum.com/infinity-interop/api/hub-status `GET /v1/status/{orderId}` The hub's own lifecycle view, from its event projection. The status door for open-flow orders; it also answers for an openFor order by its `hubOrderId`. | Item | Value | |---|---| | Route key | `infinityOrderStatus` | | Credential | API key or project id; **your organization's orders only** | | `X-Infinity-Subject` | not read: the hub does not scope this view to one of your users | One of the hub's three status views — the hub's own projection. Which view answers which question: [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle#the-three-status-views). ## Request ### Path parameters | Param | Rules | Meaning | |---|---|---| | `orderId` | exactly **64 lowercase hex characters, no `0x`** (the `POST /order` `orderId` or an openFor `hubOrderId`) | anything else is `EIN0001` reference `orderId` | ## Response **`data`** (`OrderStatus`): | Field | Type | Meaning | |---|---|---| | `orderId` | string | echoed | | `domains` | object | eight fixed keys, each `{state, terminal, updatedAt?, finality?}` — below | | `lastEventSeq` | integer | monotonic poll cursor (0 for an order the projection has not reached yet) | | `asOf` | timestamp | when the hub read the view, not when it answered: the view is cached for 2 seconds, and reads inside that window repeat the same `asOf` | | `watermarks` | object | per chain domain, `{blockNumber, blockHash?, updatedAt}` — the block up to which the projection is **complete** for that chain. A chain with none published is absent (never block 0). A chain's watermark moves only when the reconciler processes a new observation on that chain, about 1–2 minutes (one sweep window) after it, so on a quiet chain it can be far behind the observer watermarks of [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill); one chain's `updatedAt` has also been seen 2 hours off ([known issue](/infinity-interop/integration-notes#ki-hubstatus-frozen)). Omitted when empty | The eight domains and their states (an empty `state` means not reached): | Domain | States | Terminal | |---|---|---| | `intent` | `CREATED` | `CREATED` | | `sourceTx` | `DEPOSIT_FINALIZED` | `DEPOSIT_FINALIZED` | | `providerCustody` | `DISPATCHED` | — | | `receiver` | `OPEN`, `USER_PAID`, `BACKSTOP_PAID`, `RECOVERY_ONLY` | `USER_PAID`, `RECOVERY_ONLY` | | `delivery` | `DETECTED`, `FINALIZED` | `FINALIZED` | | `guarantee` | `RESERVED`, `ACTIVE`, `RELEASED`, `EXPIRED`, `BACKSTOP_PAID` | `RELEASED`, `EXPIRED`, `BACKSTOP_PAID` | | `recovery` | `ACTIVE`, `ESCALATED`, `RECOVERING`, `SURPLUS`, `CLOSED` | — | | `solverSettlement` | `FILLED`, `SETTLED`, `REFUNDED` | `SETTLED`, `REFUNDED` | `updatedAt` can appear on `intent` (only while `lastEventSeq` is 0) and on `delivery`; `finality` (`PENDING`, `FINALIZED`, `ORPHANED`) on `sourceTx` and `delivery`. ## Behavior **Owner-scoped.** The owner is the organization named on the order's creation record. An order another organization created answers `EIN0009` with the same reference as an unknown id, including when the view is served from the cache. An id with no creation record answers `EIN0009` to every caller. The view is not scoped by `X-Infinity-Subject`: any call of your organization reads any of its orders, so a backend that serves several users answers this route for the user's own orders only. An on-chain order id is refused before that, as `EIN0001` `orderId`, because it carries `0x`: use the on-chain views ([Get on-chain fill](/infinity-interop/api/onchain-fill), [Get escrow order](/infinity-interop/api/onchain-order)) for those. The view is cached for 2 seconds; staleness can under-report progress and never gates money. **Uncovered orders do not progress.** An order whose quote carries `executionClass` `PROVIDER_NATIVE_UNCOVERED`, the class of every lane `POST /order/openfor` accepts, has no admissible domain transitions after creation. Its view reads `intent` `CREATED` with the other seven domains empty for the order's whole life, even after the order has filled and settled on chain ([known issue](/infinity-interop/integration-notes#ki-hubstatus-frozen)). Follow openFor orders with [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) and the on-chain views; this route gives the intent record. ## Errors | Code | When | |---|---| | `EIN0001` | `orderId` | | `EIN0009` | no such order, or not yours | | `EIN0051` | projection, creation-record or watermark read unavailable | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS https://proxy-api.infinimesh.net/v1/status/a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2 \ -H "x-api-key: $HUB_API_KEY" ``` ### Response Illustrative values; formats follow the [Response](#response) table. Same quote as the openFor pages for id-consistency: one quote completes through one flow, so this order's `orderId` is the `hubOrderId` [`POST /order/openfor`](/infinity-interop/api/order-openfor) returned for it. ```json { "success": true, "request_id": "41526374-8596-47c1-9c2d-3e4f50617283", "data": { "orderId": "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2", "domains": { "delivery": { "state": "", "terminal": false }, "guarantee": { "state": "", "terminal": false }, "intent": { "state": "CREATED", "terminal": true }, "providerCustody": { "state": "", "terminal": false }, "receiver": { "state": "", "terminal": false }, "recovery": { "state": "", "terminal": false }, "solverSettlement": { "state": "", "terminal": false }, "sourceTx": { "state": "", "terminal": false } }, "lastEventSeq": 1, "asOf": "2026-09-22T12:01:00Z", "watermarks": { "eip155-1": { "blockNumber": 7031, "blockHash": "0xa820bbc72094336fc3cc07d3e9bb087f78edcdd2476bc4823b9ec53ad0d13e96", "updatedAt": "2026-09-22T11:59:40Z" }, "eip155-56": { "blockNumber": 7036, "blockHash": "0xcd1e34bf5a23730c5eae2971908e272f8a93aaa28025283b201f8b439ff05658", "updatedAt": "2026-09-22T11:59:40Z" } } } } ``` This order's quote carries `executionClass` `PROVIDER_NATIVE_UNCOVERED`, so the view stays like this for the order's whole life, fill and settlement included ([Behavior](#behavior)). `intent` has no `updatedAt` because the projection already holds the order (`lastEventSeq` 1). The `watermarks` are the projection's: at the same moment the observers are at block 7061 on `eip155-56` ([`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill#response)). ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const view = await client.api.getHubOrderStatus(created.orderId); ``` ## See also - SDK: `client.api.getHubOrderStatus()`; `client.executeQuote()` follows open-flow orders on this view ([Tracking](/infinity-interop/sdk/execution#tracking)) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) - [Execution errors](/infinity-interop/api/concepts/execution-errors) --- # List orders URL: https://docs.c8ntinuum.com/infinity-interop/api/orders-list `GET /v1/orders` The openFor submissions your organization made under the `X-Infinity-Subject` you send — one of your users', or all of them when every call shares one subject — each joined to what the hub's observers recorded on chain. | Item | Value | |---|---| | Route key | `infinityOrdersList` | | Credential | API key only (a project id is refused `EPX0003`) | | `X-Infinity-Subject` | required: only the orders placed under it are listed | When every call of yours shares one subject (the SDK's `ownership` scope sends `default`), the listing under it holds **every** order of your organization, whichever of your users placed it. Never hand it to a browser unfiltered. To show one user their history, name each user in the subject ([Authentication — your users](/infinity-interop/api/authentication#your-users)), or build it on your server from your own record of who placed which order; the SDK's `createInteropProxy` does either ([Partner backend](/infinity-interop/sdk/partner-backend#keeping-your-users-apart)). ## Request ### Headers | Header | Rules | Meaning | |---|---|---| | `X-Infinity-Subject` | required; `^[A-Za-z0-9._@+~-]{1,128}$` | the subject to list: only the orders placed under it are listed, and the page echoes it in `subject` | ### Query parameters | Param | Type | Rules | Meaning | |---|---|---|---| | `since` | RFC 3339 timestamp | optional | strictly-after cursor: pass the previous page's last `submittedAt`. Absent = from the beginning | | `limit` | integer | optional, > 0 | page size, default 100; values above 500 are capped at 500 | Pages are ordered by `submittedAt`, ascending. Records are keyed by `orderId` — deduplicate by id if you choose an overlapping cursor. Open-flow registrations (`POST /order`) are not listed. ## Response **`data`** (`OrdersList`): | Field | Type | Meaning | |---|---|---| | `orders[]` | array | always present (possibly empty); entries below | | `subject` | string | the `X-Infinity-Subject` this page was scoped to, verbatim. Refuse a page whose `subject` is not exactly the one you sent | Each entry (`OrderListEntry`): | Field | Type | Meaning | |---|---|---| | `orderId` | string | the aggregator id — the status handle | | `hubOrderId` | string | the hub's alias | | `quoteId`, `quoteRequestId` | string | the quote it was submitted against | | `state` | string | the **initial** upstream state at submission — never updated here | | `submittedAt` | timestamp | the paging cursor | | `createdAt` | timestamp | the same instant as `submittedAt` | | `onchainOrderId` | string | the on-chain id — the key `chain` was joined by and the key for the on-chain views. Recorded at submission, so it is present from the first listing, before anything is observed; a row older than the field carries the `orderId` | | `onchainOrderIdSource` | string | `verified` (the hub's own recompute agreed with the upstream) or `upstream` (the upstream's answer alone); absent on older rows | | `swapInput` | object | token-in orders only: `{assetRef, amountMax}` — what the user paid with | | `quote` | object | the terms as quoted: `{sourceChain, destChain, sourceAsset, destAsset, amountIn, amountOut, recipient, swapType}` — present from the first listing on orders submitted since the field was added; absent on older rows | | `chain` | object | the on-chain facts — **absent until something is observed** (a fact, not an error) | `chain` (`OrderChainFacts`): | Field | Type | Meaning | |---|---|---| | `status` | string | derived at read time: `AWAITING_OPEN`, `OPEN`, `PARTIALLY_FILLED`, `FILLED`, `SETTLED`, `REFUNDED`, `EXPIRED`. Terminal: `SETTLED`, `REFUNDED`. `EXPIRED` (an output missed its fill deadline; render it as failed) is not: no fill can complete the order any more, but the escrow can be refunded once `refundableAfter` passes, and the entry then reads `REFUNDED` | | `refundEligible` | boolean | advisory estimate: the open was observed, the order is neither settled nor refunded, and now is past `refundableAfter` | | `refundEligibilitySource` | string | always `stored_projection` — undated stored observations, not live contract state | | `refundRequiresOnchainCheck` | boolean | always `true` — verify the live contract before offering or sending a refund | | `expires` | timestamp | the escrow's expiry; absent when the open was disclosed id-only | | `fillDeadline` | timestamp | the escrow's fill deadline; absent likewise | | `refundableAfter` | timestamp | the deadline in force: `expires`, or a later deadline a refund extender set; absent when `expires` is unknown | | `refundDeadline` | timestamp | the deadline an extender last set; absent when nobody extended the order | | `user` | string | the escrow's user — who a refund pays. Absent until the Open is observed (a fill can be observed first) | | `sourceChain`, `sourceTxHash` | string | the origin escrow's chain and Open transaction. Absent until the Open is observed | | `inputs[]`, `outputs[]` | array | always present: `{chainDomain, token, symbol?, decimals?, amount, recipient?, fillTxHash?, paidAmount?}`. `symbol`/`decimals` come from the catalog; `recipient` is on outputs; `fillTxHash` and `paidAmount` (what the fill actually paid — differs from `amount` on an auctioned output) appear once a fill is observed | ## Errors | Code | When | |---|---| | `EIN0001` | `since`, `limit`, `x-infinity-subject` (missing, or outside the rule) | | `EIN0050` / `EIN0051` | availability — the whole page fails | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS "https://proxy-api.infinimesh.net/v1/orders?limit=100" \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: default" ``` ### Response Illustrative values; formats follow the [Response](#response) table. The call used one shared subject, `default`, which the page echoes. ```json { "success": true, "request_id": "52637485-96a7-48d2-ad3e-4f5061728394", "data": { "orders": [ { "orderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "hubOrderId": "a1c662328ee36c035986071fdca2997c167288c9da362f7d540ff102dfedf4c2", "quoteId": "b9760d500009ec356eb2fc720c32cb77b8b0420a8e2b2deb6a97ce63ba64fb58", "quoteRequestId": "01M34FSW4R9F2C4E7A1B3D5F80", "state": "created", "submittedAt": "2026-09-22T12:00:20Z", "onchainOrderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "onchainOrderIdSource": "upstream", "createdAt": "2026-09-22T12:00:20Z", "quote": { "sourceChain": "eip155-1", "destChain": "eip155-56", "sourceAsset": "weth", "destAsset": "wbnb", "amountIn": "1000000000000000", "amountOut": "32548366596169380000", "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "swapType": "exact-input" }, "chain": { "status": "FILLED", "refundEligible": false, "refundEligibilitySource": "stored_projection", "refundRequiresOnchainCheck": true, "expires": "2100-01-01T00:06:40Z", "fillDeadline": "2100-01-01T00:01:40Z", "refundableAfter": "2100-01-01T00:06:40Z", "user": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "sourceChain": "eip155-1", "sourceTxHash": "0x7e3a9c1b5d2f8e4a0c6b3d9f1e7a5c2b8d4f0a6e3c9b1d7f5a2e8c4b0f6d3a9e", "inputs": [ { "chainDomain": "eip155-1", "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "symbol": "WETH", "decimals": 18, "amount": "1000000000000000" } ], "outputs": [ { "chainDomain": "eip155-56", "token": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", "symbol": "WBNB", "decimals": 18, "amount": "32548366596169380000", "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "fillTxHash": "0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d", "paidAmount": "32548366596169380000" } ] } } ], "subject": "default" } } ``` `swapInput` is absent (not a token-in order); `refundDeadline` is absent (nobody extended the deadline); `inputs[]` never carries `recipient`/`fillTxHash`/`paidAmount`. For one of your users, send their subject instead. The page then lists only their orders, and its `data` carries `"subject": "user-42"`: ```bash curl -sS "https://proxy-api.infinimesh.net/v1/orders?limit=100" \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: user-42" ``` ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const page = await client.api.listOrders({ limit: 100 }); // under hubTransport's subject, "default" unless set const theirs = await client.api.listOrders({ limit: 100 }, { subject: "user-42" }); ``` ## See also - SDK: `client.walkOrders()` pages with the cursor rule, and `client.summarizeOrder()` turns an entry into a row ([Order history](/infinity-interop/sdk/orders)) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) - [Execution errors](/infinity-interop/api/concepts/execution-errors) --- # Get on-chain fill URL: https://docs.c8ntinuum.com/infinity-interop/api/onchain-fill `GET /v1/onchain/{orderId}/fill` What the hub's own observers saw on the chains, keyed by the **on-chain** order id. Unlike the status reads, these routes are **not scoped to your organization** (chain data is public, and a refund may be sent from another device than the one that ordered). They take your secret key or your project id, so a web page can follow the orders it submits on chain itself. | Item | Value | |---|---| | Route key | `infinityOnchainFill` | | Credential | API key or project id | **The id format matters, and it is not validated.** Observations are stored under `0x` + 64 **lowercase** hex. Any other spelling (uppercase, no `0x`) is not an error: it answers `200` with no facts and a watermark for every known chain, which reads exactly like "not filled yet", so the withdraw rule would offer a withdraw for an order that is in fact filled ([known issue](/infinity-interop/integration-notes#ki-onchain-id)). Pass `onchainOrderId` exactly as the hub serves it, and check the format before calling. Never send an empty id: `/onchain//fill` is not a route. ## Request ### Path parameters | Param | Rules | Meaning | |---|---|---| | `orderId` | `0x` + 64 lowercase hex (not validated) | the on-chain order id | ## Response **`data`** (`OnchainFill`): | Field | Type | Meaning | |---|---|---| | `onchainOrderId` | string | echoed; always present | | `open` | EventFact | the escrow Open — absent while unobserved | | `filled` | EventFact | the destination fill — absent while unobserved | | `settled` | EventFact | the settlement — gated on chain by the attestation, so an observed settle is the attesters' approval made visible | | `refunded` | EventFact | the escrow refund — absent while unobserved | | `watermarks` | object | per chain domain, `{blockNumber, blockHash?, updatedAt}` — how far the observers have ingested (finalized head), on the chains the answer is dated against ([Behavior](#behavior)): once a usable Open is observed, its output chains only; before that, every chain the deployment knows. Among those, absent chains have none published; omitted when no chain has one | `EventFact` is `{chainDomain, txHash, blockNumber, observedAt}`. `observedAt` is when the **hub stored** the observation, not the block time. No amounts are served here (the `GET /orders` legs carry them). The first observation of each event wins. ## Behavior **The dating rule.** An absent fact means "not observed (yet)", never an error — with one guard: **an absent `filled` is only returned if the hub can date it.** - The hub finds the order's destination chains from its observed Open (each output's chain). With no usable Open, it uses every chain the deployment knows. - For each of those chains it needs a published observer watermark no older than `infinity.chains.readMaxStalenessMs` (default 30 000 ms). - If a destination chain cannot be resolved, or any watermark is missing or stale, the answer is refused with **`EIN0051`** — which means "cannot tell", never "not filled". - When `filled` is present the watermarks are disclosure only. The hub compares nothing with a deadline; the client's withdraw rule is in [Refunds](/infinity-interop/api/concepts/refunds#how-do-i-know-it-was-not-filled). ## Errors | Code | When | |---|---| | `EIN0051` | an absent fill that cannot be dated, or a store outage | This route never returns `EIN0009`. The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS https://proxy-api.infinimesh.net/v1/onchain/0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9/fill \ -H "x-api-key: $HUB_API_KEY" ``` ### Response Illustrative values; formats follow the [Response](#response) table. ```json { "success": true, "request_id": "96a7b8c9-d0e1-4c16-a172-839405162738", "data": { "onchainOrderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "open": { "chainDomain": "eip155-1", "txHash": "0x7e3a9c1b5d2f8e4a0c6b3d9f1e7a5c2b8d4f0a6e3c9b1d7f5a2e8c4b0f6d3a9e", "blockNumber": 7054, "observedAt": "2026-09-22T12:00:35Z" }, "filled": { "chainDomain": "eip155-56", "txHash": "0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d", "blockNumber": 7059, "observedAt": "2026-09-22T12:00:50Z" }, "watermarks": { "eip155-56": { "blockNumber": 7061, "blockHash": "0x24b46941e1b961c61d47168c30d44f60560924e9709fac3a26cd3c2e2bc13b1b", "updatedAt": "2026-09-22T12:01:00Z" } } } } ``` `settled` and `refunded` are absent: unobserved at this snapshot. `watermarks` holds only `eip155-56`, the Open's output chain. ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const oc = await client.api.getOnchainFill(onchainOrderId); ``` ## See also - SDK: [`client.api.getOnchainFill()`](/infinity-interop/sdk/orders#following-one-order); `client.trackOrder()` reads it for you ([Tracking](/infinity-interop/sdk/execution#tracking)) - [Refunds](/infinity-interop/api/concepts/refunds) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) --- # Resolve fill by transaction URL: https://docs.c8ntinuum.com/infinity-interop/api/onchain-fill-by-tx `GET /v1/onchain/tx/{chainDomain}/{txHash}/order` Resolve a transaction hash — typically the `fillTransaction` from [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) — into the order's observed lifecycle. | Item | Value | |---|---| | Route key | `infinityOnchainFillByTx` | | Credential | API key or project id | Not scoped to your organization. See [Get on-chain fill](/infinity-interop/api/onchain-fill) for the id-format note. ## Request ### Path parameters | Param | Rules | Meaning | |---|---|---| | `chainDomain` | not validated | the canonical domain of the chain the transaction is on (e.g. `eip155-56`); an unknown or miscased domain finds nothing and then has no watermark, so it answers `EIN0051` | | `txHash` | lowercased by the hub except on Solana and Sui (case-sensitive base58, used as given) | an EVM hash must include `0x` | ## Response **`data`:** `OnchainFill`, as [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill#response). ## Behavior - The hub looks for any tracked settler event (Open, Filled, Settled, Refunded) at the first nine event positions (0–8) of that transaction on that chain: its receipt's logs on EVM chains, where every log counts, settler or not; its instructions on Solana, inner ones included. An event further in is not found. - **A Solana Open signature does not resolve**, while the same order's settle signature does. Follow a Solana-origin order by `onchainOrderId` on [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) ([known issue](/infinity-interop/integration-notes#ki-solana-open-tx)). - **Hit:** the answer is the full [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) view of that order, with its dating rule. - **Miss:** `onchainOrderId` is `""` (present, empty) with no facts, and `watermarks` holds the named chain — "not observed yet, as of this position", poll again. If that chain has no fresh watermark the miss is refused with `EIN0051`. ## Errors | Code | When | |---|---| | `EIN0051` | see behaviour above | Never send an empty segment: the path is then not a route. The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS https://proxy-api.infinimesh.net/v1/onchain/tx/eip155-56/0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d/order \ -H "x-api-key: $HUB_API_KEY" ``` ### Response Illustrative values; formats follow the [Response](#response) table. This is the **hit** case — the same object [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill#response) returns for this order's on-chain id. A **miss** returns the same shape with no facts: `{"onchainOrderId": "", "watermarks": {"eip155-56": {"blockNumber": 7061, "blockHash": "0x24b46941e1b961c61d47168c30d44f60560924e9709fac3a26cd3c2e2bc13b1b", "updatedAt": "2026-09-22T12:01:00Z"}}}`. ```json { "success": true, "request_id": "a7b8c9d0-e1f2-4d27-b283-940516273849", "data": { "onchainOrderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "open": { "chainDomain": "eip155-1", "txHash": "0x7e3a9c1b5d2f8e4a0c6b3d9f1e7a5c2b8d4f0a6e3c9b1d7f5a2e8c4b0f6d3a9e", "blockNumber": 7054, "observedAt": "2026-09-22T12:00:35Z" }, "filled": { "chainDomain": "eip155-56", "txHash": "0x4b8d2f6a1e9c3b7d5f0a8e2c6b4d9f1a3e7c5b0d8f2a6e4c1b9d3f7a5e0c8b2d", "blockNumber": 7059, "observedAt": "2026-09-22T12:00:50Z" }, "watermarks": { "eip155-56": { "blockNumber": 7061, "blockHash": "0x24b46941e1b961c61d47168c30d44f60560924e9709fac3a26cd3c2e2bc13b1b", "updatedAt": "2026-09-22T12:01:00Z" } } } } ``` ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const oc = await client.findOrderByTx("eip155-56", st.fillTransaction!.hash!); ``` ## See also - SDK: [`client.findOrderByTx()`](/infinity-interop/sdk/orders#following-one-order) - [Refunds](/infinity-interop/api/concepts/refunds) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) - [Quickstart](/infinity-interop/api/quickstart) --- # Get escrow order URL: https://docs.c8ntinuum.com/infinity-interop/api/onchain-order `GET /v1/onchain/{orderId}/order` The escrow's **raw order tuple** — what `refund(order)` needs from a device that holds only the on-chain id. | Item | Value | |---|---| | Route key | `infinityOnchainOrder` | | Credential | API key or project id | Not scoped to your organization. See [Get on-chain fill](/infinity-interop/api/onchain-fill) for the id-format note. ## Request ### Path parameters | Param | Rules | Meaning | |---|---|---| | `orderId` | `0x` + 64 lowercase hex (not validated) | the on-chain order id. The hub records an openFor order's `orderId` as its `onchainOrderId`, so the ack's `orderId` works here too | ## Response **`data`** (`OnchainOrder`): | Field | Type | Meaning | |---|---|---| | `onchainOrderId` | string | the id | | `chainDomain` | string | the origin escrow's chain | | `settler` | string | the escrow contract to send `refund(order)` to, in the origin chain's form — check it is one of the chain's `inputSettlers` in [`GET /catalog`](/infinity-interop/api/catalog) | | `rawOrder` | string | the ABI-encoded `StandardOrder` exactly as the Open log carried it (`0x` hex), verified against the id when stored | ## Behavior - The settler honours a refund only for a tuple that hashes to the order id, and a refund pays only the order's own user — so the blob needs availability, not confidentiality. - **EVM origins only.** Non-EVM order bodies are not served: such an order answers `EIN0009` permanently, even once its Open, fill and settle are observed ([known issue](/infinity-interop/integration-notes#ki-nonevm-tuple)). A Solana-origin order refunds through [Get Solana refund](/infinity-interop/api/onchain-refund) instead. ## Errors | Code | When | |---|---| | `EIN0009` | no verified tuple under this id: an unknown or miscased id, a hub order id (`hubOrderId`, or a `POST /order` `orderId`), a non-EVM origin (permanently), or an order whose Open has not been observed yet (poll again) | | `EIN0051` | store unavailable | Never send an empty id: `/onchain//order` is not a route. The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS https://proxy-api.infinimesh.net/v1/onchain/0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9/order \ -H "x-api-key: $HUB_API_KEY" ``` ### Response Illustrative values; formats follow the [Response](#response) table. `rawOrder` is elided here: a real ABI-encoded `StandardOrder` is not derivable from the documented sources. ```json { "success": true, "request_id": "b8c9d0e1-f2a3-4e38-8394-051627384950", "data": { "onchainOrderId": "0x8c7f293ae9851d2919e1543d0268483a0636bef9417240b0aa15258c508e32b9", "chainDomain": "eip155-1", "settler": "0xf439808b5a80e3c355645b62310502817000cd1d", "rawOrder": "0x…" } } ``` ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const tuple = await client.api.getOnchainOrder(onchainOrderId); ``` ## See also - SDK: `client.api.getOnchainOrder()`; `client.prepareEvmRefund()` and `client.refundEvmOrder()` build and check the refund from it ([Refunds](/infinity-interop/sdk/refunds#evm-origin-orders)) - [Get Solana refund](/infinity-interop/api/onchain-refund): the Solana counterpart - [Refunds](/infinity-interop/api/concepts/refunds) - [Order lifecycle](/infinity-interop/api/concepts/order-lifecycle) --- # Get Solana refund URL: https://docs.c8ntinuum.com/infinity-interop/api/onchain-refund `GET /v1/onchain/{orderId}/refund` The refund of an order opened on a **Solana** escrow, built by the hub as a serialized, **unsigned** transaction for the order's own user, who is its fee payer and its only signer. The user's wallet signs it and sends it. | Item | Value | |---|---| | Route key | `infinityOnchainRefund` | | Credential | API key or project id | Not scoped to your organization. An EVM refund needs no such route: `refund(order)` is one call, built from [Get escrow order](/infinity-interop/api/onchain-order). A Solana refund names ten accounts plus three per input, in an order the program defines, so the hub builds it and the client only signs. ## Request ### Path parameters | Param | Rules | Meaning | |---|---|---| | `orderId` | `0x` + 64 lowercase hex | the on-chain order id: `onchainOrderId` in [`GET /orders`](/infinity-interop/api/orders-list) | ### Query parameters | Param | Rules | Meaning | |---|---|---| | `unwrap` | `true` or `false`, default `false`; any other value is `EIN0001` (reference `unwrap`) | also close the user's wrapped-SOL account, so the refund arrives as SOL. Ignored for an order with no wrapped-SOL input | ## Response **`data`** (`OnchainRefund`): | Field | Type | Meaning | |---|---|---| | `onchainOrderId` | string | the id | | `chainDomain` | string | the origin escrow's chain, e.g. `solana-localnet` | | `program` | string | the escrow program the refund calls (base58). Check it is one of the chain's `inputSettlers` in [`GET /catalog`](/infinity-interop/api/catalog) | | `transaction` | string | the serialized transaction, in `encoding`: one empty signature slot, then the legacy message. The wallet signs it as it is | | `encoding` | string | `base64` | | `feePayer` | string | the order's user (base58): it pays the fee and is the only signer. **The signing wallet must be this account** | | `signers` | string[] | the accounts that must sign: `[feePayer]` | | `recentBlockhash` | string | the blockhash the transaction is valid under (base58) | | `lastValidBlockHeight` | integer | past this block height the cluster refuses the transaction: ask again | | `refundableAfter` | integer | unix seconds: the deadline the refund had to be past, the later of the order's expiry and any extension | | `unwrap` | boolean | `true` when the transaction also closes the user's wrapped-SOL account | | `computeUnits` | integer | what the hub's simulation of the transaction consumed | | `inputs[]` | object[] | what the refund pays back, in the order's input order: `token` (the mint, base58), `amount` (base units, decimal string) and `tokenAccount` (the user's token account it is paid to, base58; the refund creates it when missing) | ## Behavior - **Built from the chain when you ask.** The hub reads the escrow's order record, the cluster's clock and each input's mint at request time, and **simulates** the transaction before serving it, so it never hands out one the program would refuse. - **Short-lived.** The transaction carries a recent blockhash and lives about a minute: send it before `lastValidBlockHeight`, or ask again. Fetch it right before the user signs. - **The cluster's clock decides.** The refund needs the cluster's clock **strictly** past `refundableAfter`. Before that, the route answers `EIN0007` `NOT_YET_REFUNDABLE` with both values, so you can show when the refund opens. - **`unwrap=true` closes the whole wrapped-SOL account**, so wrapped SOL the user already held there becomes SOL too. - **Anyone may fetch it.** A refund is permissionless on chain and pays only the order's user, so the transaction needs availability, not confidentiality: the same reasoning as [Get escrow order](/infinity-interop/api/onchain-order). - **Check before signing:** `feePayer` is the user's connected wallet; `program` is a declared escrow; and the transaction holds only the escrow's `refund` for this order, plus, with `unwrap`, one SPL Token `CloseAccount` that pays the user. The TypeScript SDK runs these checks ([SDK — Refunds](/infinity-interop/sdk/refunds#solana-origin-orders)). ## Errors | Code | When | |---|---| | `EIN0007` | the hub will not build this refund: `error_reference` is the reason (below) and `error_params` its values | | `EIN0009` | an id the hub has not observed, or one the escrow holds no record of (a just-opened order: poll again) | | `EIN0001` | `unwrap` is not `true` or `false` (reference `unwrap`) | | `EIN0050` | the Solana node was unavailable: retry | The `EIN0007` reasons: | `error_reference` | `error_params` | Meaning, and what to do | |---|---|---| | `NOT_YET_REFUNDABLE` | `[refundableAfter, clusterNow]`, unix seconds | the deadline has not passed on the cluster's clock: show "refundable from …" and try again after it | | `ALREADY_REFUNDED` | — | the order was refunded already: nothing to do | | `ALREADY_CLAIMED` | — | the order was filled and claimed: the recipient was paid | | `NOT_OPEN` | — | the escrow holds no open order under this id: nothing to refund | | `FEE_PAYER_UNFUNDED` | `[feePayer]` | the user needs a little SOL, for the fee or the rent of a token account the refund recreates | | `ORIGIN_NOT_SUPPORTED` | `[family]` | not a Solana-origin order: refund an EVM order from [Get escrow order](/infinity-interop/api/onchain-order) | | `CHAIN_NOT_WIRED` | `[chainDomain]` | this hub cannot refund on that chain: report it | | `SIMULATION_FAILED` | `[the node's error, as JSON]` | the program would refuse the refund: report the order id | | `UNBUILDABLE`, `UNREADABLE` | `[detail]` | hub side (`UNREADABLE`: what the chain holds for the order cannot be trusted): report the order id | The gateway's own refusals — a refused credential, a route not open to it, a rate limit, the hub unreachable or too slow — can come back from every route, with an `EPX` code and a real HTTP status ([Errors — gateway codes](/infinity-interop/api/errors#gateway-codes-epx)); `EIN0049` (internal, scrubbed) can come back from any route. ## Example ```bash curl -sS -G https://proxy-api.infinimesh.net/v1/onchain/0x5a8d3c61f0b24e97a1c6d8e2b4f7093a6c1e5d9b2f8a4c7e0d3b6a9f1c2e5d80/refund \ --data-urlencode 'unwrap=true' \ -H "x-api-key: $HUB_API_KEY" ``` ### Response Illustrative values; formats follow the [Response](#response) table. The program, the transaction, the blockhash and the token account are elided: real ones are not derivable from the documented sources. ```json { "success": true, "request_id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "data": { "onchainOrderId": "0x5a8d3c61f0b24e97a1c6d8e2b4f7093a6c1e5d9b2f8a4c7e0d3b6a9f1c2e5d80", "chainDomain": "solana-localnet", "program": "…", "transaction": "AQAAAA…", "encoding": "base64", "feePayer": "CCckTaUo6jc8Q6HydkjKjvGX3MHJS77uwis2DN7UFoqZ", "signers": ["CCckTaUo6jc8Q6HydkjKjvGX3MHJS77uwis2DN7UFoqZ"], "recentBlockhash": "…", "lastValidBlockHeight": 1204517, "refundableAfter": 1790000000, "unwrap": true, "computeUnits": 41250, "inputs": [ { "token": "So11111111111111111111111111111111111111112", "amount": "250000000", "tokenAccount": "…" } ] } } ``` ```ts // client: your InteropClient (see /infinity-interop/sdk/installation) const refund = await client.api.getOnchainRefund(onchainOrderId, { unwrap: true }); // or fetch it, check it, and have the user's wallet sign and send it, in one call: await client.refundSolanaOrder({ onchainOrderId, wallet: solana, unwrap: true }); ``` ## See also - SDK: `client.api.getOnchainRefund()`; `client.refundSolanaOrder()` fetches, checks, signs and sends the refund ([Refunds — Solana-origin orders](/infinity-interop/sdk/refunds#solana-origin-orders)) - [Get escrow order](/infinity-interop/api/onchain-order): the EVM refund's source - [Refunds](/infinity-interop/api/concepts/refunds), [Solana support](/infinity-interop/api/concepts/solana-support) --- # SDK overview URL: https://docs.c8ntinuum.com/infinity-interop/sdk `@c8ntinuum/infinity-interop-sdk` is the TypeScript SDK for the Infinity hub's partner API. It runs the whole order flow for you: quote, the user's wallet steps, submission, tracking, history and refunds. It checks every quote against what the user asked for before anything is signed. It is built only on the partner contract (the [API reference](/infinity-interop/api)), and calls the hub's gateway under `/v1` with your secret API key or your public project id. ## How a partner uses it Your **secret API key stays on your backend**. A web page reaches the hub one of two ways: through your backend's proxy server (`createInteropProxy`), which forwards its calls with your key, or straight to the hub's gateway with your public **project id** (`projectTransport`), which opens every route but the order history: ```mermaid flowchart LR subgraph Browser UI[Your app] -->|proxyTransport| P UI -.->|projectTransport + project id| H UI -->|signs with| W[User's wallet] end subgraph Your backend P[createInteropProxy] -->|hubTransport + secret key| H end H[Infinity hub] --> S[Solvers] W -->|transactions, signatures| C[(Chains)] ``` - **Backend only** (bots, treasury moves, scripts): `hubTransport` + `createInteropClient`, with keys the server holds. See the [quickstart](/infinity-interop/sdk/quickstart). - **Browser app with your backend**: your backend mounts `createInteropProxy`; the browser uses `proxyTransport` and the user's own wallet. Every route is open this way, the order history and refunds included, and your backend keeps each user's orders to that user: by its own records, or by naming the user to the hub (`X-Infinity-Subject`). See [Partner backend](/infinity-interop/sdk/partner-backend) and [Browser apps](/infinity-interop/sdk/frontend). - **Browser app with a project id**: no server of yours; the browser uses `projectTransport`, and follows and refunds the orders it submits itself. Only the order history is not open to a project id. See [Browser apps](/infinity-interop/sdk/frontend#with-a-project-id-projecttransport). ## What it handles | | | |---|---| | **Every lane** | Native wrap-and-open, Permit2 gasless, token-in, Solana gasless, EIP-3009, the open flow: one `executeQuote` call runs whichever the quote needs. See [Lanes](/infinity-interop/sdk/lanes) | | **Pre-signing checks** | Every field that moves money is compared with the request and the catalog, and proxy calldata is decoded, before a wallet is prompted. See [Quotes and checks](/infinity-interop/sdk/quotes) | | **Quote timing** | A quote lives about 30 s; the SDK re-quotes when too little is left to sign, and asks the user again only if the new one is worse | | **Tracking** | Follows an order by the hub's chain facts, not only the advisory aggregator state. See [Executing orders](/infinity-interop/sdk/execution) | | **Lost answers** | A submission whose answer was lost is found again by its derived hub order id, or re-posted once, never duplicated | | **History and refunds** | Paged history with readable rows, and EVM and Solana refunds proved on chain first. See [Order history](/infinity-interop/sdk/orders) and [Refunds](/infinity-interop/sdk/refunds) | | **Credentials and the gateway** | Your key in `x-api-key` on every server call, your project id from a page; timeouts a little longer than the gateway's, and its refusals as typed errors with their status and back-off | ## Entry points | Import | For | Contains | |---|---|---| | `@c8ntinuum/infinity-interop-sdk` | browser and Node | the client, `proxyTransport`, `projectTransport`, quotes, checks, execution, history, refunds, errors, the EIP-1193 wallet adapter | | `@c8ntinuum/infinity-interop-sdk/server` | your backend (Node) | `hubTransport`, `createInteropProxy`, `toNodeListener`, `memoryOwnershipStore`, `verifySubjectScoping` | | `@c8ntinuum/infinity-interop-sdk/evm` | browser and Node, with `viem` | wallet adapters for a viem wallet client or a server-held account | | `@c8ntinuum/infinity-interop-sdk/solana` | browser and Node | Wallet Standard adapter, keypair wallet, a minimal Solana RPC | Requirements, the package and its entry points. Quote, check and execute a bridge from Node with your secret key. Hold the secret key and serve your frontend 1:1 with `createInteropProxy`. `proxyTransport` or a project id with `projectTransport`, the user's wallet, and a bridge screen step by step. EIP-1193, viem, Wallet Standard and server-held keys, with chain-instance pins. What the user does on each lane, and what the SDK checks. Requests, reverse quotes, preview and prepare, timing, and the pre-signing checks. `executeQuote`, its events and result, tracking and lost answers. Paging, readable rows and tracking an order later. When an order is refundable, and refunding it on EVM and Solana. Error classes, what to tell the user, and what to retry. Every export by entry point, and the route each low-level call maps to. The test hub, keys from the hub dashboard, safe test wallets, and the runnable examples. --- # Installation URL: https://docs.c8ntinuum.com/infinity-interop/sdk/installation ## Requirements | Item | Value | |---|---| | Node | `>= 20`: the SDK uses the global `fetch`, `crypto.randomUUID` and Web Crypto (Ed25519 for Solana keypairs) | | Browsers | any current browser, in a secure context (`https` or `localhost`) | | Modules | ESM and CommonJS builds, with type declarations for both | | Runtime dependencies | none | | Optional peer | `viem >= 2.21.0`, only for the `/evm` entry point | ## Install ```bash npm npm install @c8ntinuum/infinity-interop-sdk ``` ```bash pnpm pnpm add @c8ntinuum/infinity-interop-sdk ``` ```bash yarn yarn add @c8ntinuum/infinity-interop-sdk ``` Add `viem` if you use the `/evm` adapters (a viem wallet client, or a key your server holds): ```bash npm install viem ``` ## Entry points | Import | Use it in | Main exports | |---|---|---| | `@c8ntinuum/infinity-interop-sdk` | browser and Node | `createInteropClient`, `proxyTransport`, `projectTransport`, `executeQuote`, `vetQuote`, `trackOrder`, refunds, errors, `evmWalletFromEip1193` | | `@c8ntinuum/infinity-interop-sdk/server` | Node only | `hubTransport`, `createInteropProxy`, `toNodeListener`, `memoryOwnershipStore`, `verifySubjectScoping` | | `@c8ntinuum/infinity-interop-sdk/evm` | browser and Node | `evmWalletFromViem`, `evmWalletFromAccount` (needs `viem`) | | `@c8ntinuum/infinity-interop-sdk/solana` | browser and Node | `solanaWalletFromStandard`, `solanaWalletFromKeypair`, `solanaRpc` | Keep `/server` out of your production browser code, and never ship your secret API key to a browser: a key in a page is a key anyone can use, and the hub's gateway refuses a key from a browser anyway. A page reaches the hub through your backend ([Partner backend](/infinity-interop/sdk/partner-backend)) or with your public project id ([Browser apps](/infinity-interop/sdk/frontend)). The entry points share one build, so an error class thrown by `/server` is the same class you catch from the root import (`instanceof` works across them). ## First call A backend sends its secret key with every call; here it reads the catalog: ```ts const client = createInteropClient({ transport: hubTransport({ baseUrl: "https://proxy-api.infinimesh.net", // the hub's origin: the SDK adds /v1 apiKey: process.env.HUB_API_KEY!, // c8n_sk_…, made for your project in the hub dashboard }), }); const catalog = await client.getCatalog(); console.log(catalog.chains.map((chain) => chain.chainDomain)); ``` There is no sign-in: the key goes in `x-api-key` on every call. On the routes where the hub requires `X-Infinity-Subject`, a call that names none of your users goes under the transport's `subject`, `default` unless you set another. Next: the [quickstart](/infinity-interop/sdk/quickstart) runs a full bridge, and [Testing](/infinity-interop/sdk/testing) shows where the test hub's keys come from. --- # Quickstart URL: https://docs.c8ntinuum.com/infinity-interop/sdk/quickstart This runs a complete bridge from Node with keys your server holds: the shape of a bot, a treasury move or a test script. A browser app does the same steps through your backend, or with your project id: see [Browser apps](/infinity-interop/sdk/frontend). Server-held keys are for test networks and for your own funds. Never ask users for their keys; they sign in their own wallets. Make a secret API key (`c8n_sk_…`) in the [hub dashboard](https://console.infinimesh.net). Keep it in your server's secret store, or in a git-ignored `.env` file as `HUB_API_KEY`. See [Testing](/infinity-interop/sdk/testing#keys-and-project-ids). ```ts import { createInteropClient, hubTransport } from "@c8ntinuum/infinity-interop-sdk/server"; const client = createInteropClient({ transport: hubTransport({ baseUrl: process.env.HUB_BASE_URL!, // the hub's origin, e.g. https://proxy-api.infinimesh.net apiKey: process.env.HUB_API_KEY!, // c8n_sk_… }), }); ``` The transport sends the key in `x-api-key` on every call, under `/v1`: there is no sign-in and no session to renew. It refuses, before any call, a value that is not a secret key (a project id, for example) with a message that says so. ```ts import { toBaseUnits } from "@c8ntinuum/infinity-interop-sdk"; const catalog = await client.getCatalog(); const eth = catalog.resolveAsset("eip155-1", "eth"); if (eth?.kind !== "declared" || eth.asset.decimals === undefined) throw new Error("ETH is not on this hub"); const amount = toBaseUnits("0.05", eth.asset.decimals); // "50000000000000000" ``` Amounts are always decimal strings in base units. Build your pickers from the catalog rather than hard-coding chains, assets or contract addresses: they move when the hub is redeployed. ```ts const me = "0x…"; // the account that pays and receives here const quote = await client.getQuote({ sourceChain: "eip155-1", sourceAsset: "eth", destChain: "eip155-8888", destAsset: "ctm", amount, sourceAccount: me, recipient: me, }); const report = await client.vetQuote(quote); console.log(quote.lane.kind, quote.amountOut, report.ok); ``` `quote.lane.kind` says what the user will do (here `wrap-native`: one transaction). `vetQuote` compares every field that moves money with your request and the catalog. `executeQuote` runs the same checks again right before signing, so this step is for showing them, not a requirement. ```ts import { privateKeyToAccount } from "viem/accounts"; import { evmWalletFromAccount } from "@c8ntinuum/infinity-interop-sdk/evm"; const evm = evmWalletFromAccount({ account: privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`), rpcUrls: { 1: process.env.RPC_EIP155_1!, 8888: process.env.RPC_EIP155_8888! }, }); const result = await client.executeQuote({ quote, wallets: { evm }, onEvent: (event) => { if (event.type === "step") console.log(event.step, event.status, event.transactionHash ?? ""); if (event.type === "filled") console.log("the recipient is paid"); }, }); console.log(result.orderId, result.outcome); // "success" once the chain shows it settled ``` The SDK re-quotes if too little time is left to sign, sends the transaction, registers the order with the hub, and follows it until the chain shows it settled. ```ts import { formatUnits } from "@c8ntinuum/infinity-interop-sdk"; for (const entry of await client.listAllOrders()) { const row = await client.summarizeOrder(entry); const { amount, decimals, symbol } = row.pay; const paid = decimals === undefined ? `${amount} (base units)` : formatUnits(amount ?? 0n, decimals); console.log(row.phase, paid, symbol); } ``` With your key and no subject of your own, this is the history of `hubTransport`'s one subject (`default`): every order made that way, whoever placed it. A service that acts for users of its own names each one with `subject`, and then lists only that user's orders ([Executing on a server for your users](/infinity-interop/sdk/execution#executing-on-a-server-for-your-users)). `decimals` is never guessed: a token the catalog does not list can come back without it. See [Order history](/infinity-interop/sdk/orders). ## When something fails Every error the SDK throws has a user-facing message and an action: ```ts try { await client.executeQuote({ quote, wallets: { evm } }); } catch (error) { const advice = adviseError(error); console.error(advice.action, advice.message); // e.g. "change-amount", "No provider will take this amount…" } ``` A refused key throws a `HubError` whose `isCredentialRefused` is `true` (`EPX0001`: missing or malformed; `EPX0002`: unknown, revoked or expired). Fix the key before calling again: 20 refused credentials within a minute lock your IP out for the rest of that minute (`EPX0081`). See [Errors](/infinity-interop/sdk/errors) for the full list. ## The runnable version The SDK repository's `examples/node-quickstart` has this as scripts, configured by a `.env` with `HUB_BASE_URL` and `HUB_API_KEY`: `pnpm smoke` (a read-only check of a hub, starting with one call that checks your key), `pnpm catalog`, `pnpm quote`, `pnpm bridge`, `pnpm orders` and `pnpm refund`. See [Testing](/infinity-interop/sdk/testing#the-examples). --- # Partner backend URL: https://docs.c8ntinuum.com/infinity-interop/sdk/partner-backend Your secret API key never reaches a web page. A page can call the hub's gateway itself with your public project id ([Browser apps](/infinity-interop/sdk/frontend#with-a-project-id-projecttransport)), which opens every route but the order history. For the history, and to know your users, the page calls **your backend**: it holds the key and forwards the page's calls one to one, after checking who the user is. `createInteropProxy` is that forwarder, ready-made. ## Mount the proxy ```ts const transport = hubTransport({ baseUrl: process.env.HUB_BASE_URL!, // the hub's origin: https://proxy-api.infinimesh.net apiKey: process.env.HUB_API_KEY!, // c8n_sk_…, made for your project in the hub dashboard }); const proxy = createInteropProxy({ transport, basePath: "/api/interop", userScope: "subject", // the hub keeps each user apart, by X-Infinity-Subject (default: "ownership") resolveUser: async (request) => { const session = await yourSessionFrom(request); // your cookie, JWT… if (session === null) return null; // → 401 return { id: session.userId, wallets: session.verifiedWallets }; }, onError: (error, context) => alertOperators(error, context?.route), }); const handle = toNodeListener(proxy); createServer((request, response) => { if (request.url?.startsWith("/api/interop")) return handle(request, response); // …the rest of your server }).listen(8787); ``` `hubTransport` sends the key in `x-api-key` on every call, under `/v1`: there is no sign-in and no session to keep alive. `proxy` is a fetch-style handler (`(request: Request) => Promise`), so it also mounts directly in any runtime that speaks `Request`/`Response` (a Next.js route handler, Hono, Bun, Deno, a Cloudflare Worker). `toNodeListener` adapts it to Node's `http` module, Express or Connect. ## What it does on every call | Route | Rule | |---|---| | `GET /catalog` | served to anyone, signed in or not (`publicCatalog`, default on), so pickers render before sign-in; cached for five minutes, and served stale while the hub is unreachable | | `POST /quote`, `/quote/preview`, `/quote/prepare` | the funding account and the recipient must be among `user.wallets`; the quote is recorded as the user's, and with `userScope: "subject"` it is made under the user's subject | | `POST /order/openfor`, `POST /order` | only the user who received the quote may submit it (with `"subject"`, the hub itself refuses another user's quote); a lost openFor answer is re-posted once (the hub replays the original acknowledgement) | | `GET /order/{id}/status` | only for the user's own orders: by the proxy's records, or with `"subject"` by the hub | | `GET /status/{id}` | only for the user's own orders, by the proxy's records either way: the hub does not scope this view by subject | | `GET /orders` | only the user's own orders, filtered to their wallets: built from your organization's listing, or with `"subject"` the hub's listing for the user's subject, served only while the subject canary passes | | `GET /onchain/…` | forwarded; the hub's chain facts are the same for everyone | Only the routes a browser needs are exposed (`BROWSER_ROUTES`); the OpenAPI document and everything else stay on the server. The hub's own answers come back unchanged (its `200`/`400` envelope), and so do the gateway's rate limits and outages: a `429` or `5xx` with its `EPX` code and its `Retry-After`. The proxy's own refusals come back in the same envelope with `PXY` codes: | Code | Status | When | |---|---|---| | `PXY0400` | 400 | the body is not a JSON object, or a submission has no `quoteId` | | `PXY0401` | 401 | sign in first: `resolveUser` returned `null` | | `PXY0403` | 403 | not the user's quote, or a funding account or recipient outside the user's wallets | | `PXY0404` | 404 | an unknown route, or an order that is not the user's | | `PXY0413` | 413 | the body is too large | | `PXY0500` | 500 | the proxy is misconfigured (a gateway refusal about the integration itself, or a subject outside the hub's rule), or it failed | | `PXY0502` | 502 | the hub could not be reached, or (with `"subject"`) a history page came back for another subject | | `PXY0503` | 503 | the gateway refused your key, or (with `"subject"`) history while the subject canary fails: the user sees "temporarily unavailable", and `onError` tells you | ### The user and their wallets `resolveUser` is your authentication. Return `null` for a signed-out request. `wallets` are the addresses you have **verified** the user controls (for example by a signed message at sign-in). With them, the proxy refuses a quote that pays from or to anyone else, and filters the history to rows that touch those wallets. Without them, those checks are off: only do that when your own backend already enforces them. ## Keeping your users apart The key tells the hub your organization, never which of your users a call is for. `userScope` picks how the proxy keeps your users apart ([Authentication — your users](/infinity-interop/api/authentication#your-users)): | `userScope` | Who keeps them apart | How | |---|---|---| | `"ownership"` (default) | the proxy, from its own records | every call goes under `hubTransport`'s one subject (`default` unless you set another), so the hub keeps one history for all your users. The proxy records which user received each quote and submitted each order (the `OwnershipStore`), refuses to submit a quote for anyone but the user it was made for, answers status reads for the user's own orders only, and builds each user's history from that shared listing (`GET /orders`, up to `historyMaxPages` pages of 500, default 40) | | `"subject"` | the hub, by `X-Infinity-Subject` | every call on the seven routes that read it carries the user's subject, `subjectOf(user)` (default `user.id`), and the hub keeps each user's quotes, orders and history apart | With `"subject"`: - the subject is your stable id for the person, never reused for someone else, in the hub's alphabet (`^[A-Za-z0-9._@+~-]{1,128}$`); a `subjectOf` that returns anything else is a misconfiguration (`PXY0500`, and `onError`); - before it serves any history the proxy runs the **subject canary**: it lists orders for a subject none of your users has, which must come back empty and echo that subject. While the canary fails it refuses history (`PXY0503`), reports it to `onError`, and tries again at most once a minute. Run it yourself at start-up with `proxy.checkSubjectScoping()`, or with `verifySubjectScoping(client.api)` (from `/server`) on a client over `hubTransport`; - every history page must echo the user's subject (`data.subject`), or the proxy refuses it (`PXY0502`); - `GET /status/{orderId}`, which the hub does not scope by subject, is still answered for the user's own orders only, from the proxy's records. Either way the proxy records who submitted each order. The default `memoryOwnershipStore()` is in memory and lost on restart; pass your own `OwnershipStore` (four methods: `recordQuote`, `ownerOfQuote`, `recordOrder`, `ownerOfOrder`) backed by your database when that matters. ## Relaying RPC calls Your frontend also reads chains: balances, allowances, receipts. Some RPCs answer no CORS; others need a key you would rather not publish. Relay them through the same backend, for the chains you configure only: ```ts // POST /api/rpc/ → your RPC for that chain const upstreams = { "eip155-1": process.env.RPC_EIP155_1!, "eip155-8888": process.env.RPC_EIP155_8888! }; ``` The SDK repository's `examples/proxy-server` has a complete relay with a size cap and a timeout. In production, also allow-list the JSON-RPC methods your frontend uses and rate-limit them. ## Hardening - **`beforeForward(context)`**: your last word before a call is forwarded. Return a `Response` to answer it yourself (a per-user rate limit, a deny list), or nothing to continue. Every user of your backend shares your organization's pool at the gateway and its buckets at the hub, so meter them here. - **`onError(error, context)`**: a key the gateway refused, misconfiguration, a failed subject canary and proxy bugs land here: page an operator. Users only ever see a generic "temporarily unavailable". - **Timeouts**: `hubTransport` waits a little longer than the gateway (25 s on quotes, 12 s on reads, 15 s on submissions), so the gateway's own answer ends a slow call; change them with `timeouts` only upwards. - **Pages on other origins**: answer CORS for exactly those origins, never `*`: your backend spends your API key. From an https page your backend must be https too (or on the user's own computer, `http://localhost`): browsers block other `http://` calls from https pages, bare IPs included. The example sets this with `CORS_ORIGINS`. - **The example has no login**: `examples/proxy-server` believes whatever user id and wallets the page sends, so anyone who can reach it spends your key and can read any of your users' history through it. It listens on `127.0.0.1` unless `HOST` says otherwise, and warns at start-up once `HOST` or `CORS_ORIGINS` opens it up. Put your own authentication in `resolveUser` before anyone else can reach it. It keeps users apart by subject unless `HUB_USER_SCOPE=ownership`, and runs the subject canary at start-up. ## Keys Keys are made, listed and revoked in the hub dashboard, never through the API. Keep the key in your secret store, give each environment its own, and rotate by making the new key, deploying it, then revoking the old one. `hubTransport` refuses at start-up a value that is not in a secret key's format (`c8n_sk_` and 43 characters), with a message that says so. When the gateway refuses the key (`EPX0001`, `EPX0002`), fix it before calling again: 20 refused credentials within a minute lock your server's IP out of every call for the rest of that minute (`EPX0081`), valid keys included. `hubTransport` refuses to start in a web page. Its `dangerouslyAllowBrowser: true` lifts that only for a test key on a hub that accepts browser calls; the hub's gateway does not. See [Browser apps](/infinity-interop/sdk/frontend) and [API keys and rate limits](/infinity-interop/api/api-keys-and-rate-limits). --- # Browser apps URL: https://docs.c8ntinuum.com/infinity-interop/sdk/frontend In the browser, the SDK talks either to **your** backend (`proxyTransport`, see [Partner backend](/infinity-interop/sdk/partner-backend)) or straight to the hub's gateway with your public **project id** (`projectTransport`), and the user signs in **their** wallet. Everything else is the same client as on a server. Never put a secret API key in a web page: any script there can read it, and whoever holds it acts as your organization. The hub's gateway does not take a key from a browser anyway (its CORS pre-flight never allows `x-api-key`), and `hubTransport` refuses to start in a page. A page uses one of the two transports below. ## Two ways to reach the hub | | `proxyTransport` | `projectTransport` | |---|---|---| | Calls go to | your backend (`createInteropProxy`), which forwards them with your secret key | the hub's gateway, with your project id | | Credential in the page | your own session (a cookie or a token of yours) | the project id, public by design | | Routes | every route: catalog, quotes, submission, status, history, chain facts, refunds | every route but the order history (`PROJECT_ROUTES`): catalog, quotes, submission, status, chain facts, refunds | | Your users | your backend knows them, and keeps each one's quotes, orders and history to them: by its own records, or by naming each user to the hub (`X-Infinity-Subject`) | the gateway knows none: the page names each visitor by a random id (`X-Infinity-Subject`), which keeps visitors apart and proves nothing (the project id is public) | | Rate limits | your backend's own, then your keys' pool | per visitor IP, plus your project id's pool | ## Through your backend: `proxyTransport` ```ts const client = createInteropClient({ transport: proxyTransport({ url: "/api/interop", // where your backend mounts createInteropProxy headers: () => ({ authorization: `Bearer ${yourSessionToken()}` }), // YOUR auth, not the hub's }), }); ``` `proxyTransport` sends no hub credential: only your own authentication (`headers`), which your backend's `resolveUser` reads. Use `credentials: "include"` when your backend authenticates by cookie on another origin. From an https page, the proxy's URL must be https too, or on this computer (`http://localhost`): the transport refuses, before any call, an address the browser would block as mixed content. It waits longer than your proxy does: 15 s on reads, 30 s on quotes, 35 s on submissions. ## With a project id: `projectTransport` ```ts const client = createInteropClient({ transport: projectTransport({ baseUrl: "https://proxy-api.infinimesh.net", // the gateway's origin: the SDK adds /v1 projectId: "c8n_pk_…", // your project id, from its page in the hub dashboard }), }); ``` - **List your pages' origins** on the project in the hub dashboard: the exact scheme, host and port of each page, and for development, for example, `http://localhost:3000`. While the list is empty, any origin is accepted; once it is set, a page on an unlisted origin gets `EPX0010` (403). - **Every route but the order history is open** (`PROJECT_ROUTES`): the catalog, the quotes, order submission, both status views, the chain facts and the refund routes. `GET /orders` would show every visitor all of your orders: a call to it throws an `InteropError` (`invalid-input`) before any request. - **Keep the ids of the orders the page submits** (the `submitted` event). The order history is not open to a project id, so list those ids yourself. - **Tracking and refunds work as on a server.** The chain facts, the open flow's lifecycle view and the refund routes are open to a project id, so `executeQuote` follows an order to its end, the `filled` event included, and the page can refund an order itself ([History and refunds](#history-and-refunds)). - **It names the visitor.** The hub requires `X-Infinity-Subject` on the quotes, order submission and `GET /order/{orderId}/status`, so the transport sends one random id per browser and project there: `visitor-` and a UUID, kept in `localStorage` (for the life of the transport when storage is unavailable). Each visitor's quotes and statuses stay apart, and a reload keeps the same id. A `subject` on the transport, the client or a call sends yours instead. The project id is public, so anyone can send any subject: it labels a visitor and proves nothing. Scope your signed-in users from your server ([Authentication — your users](/infinity-interop/api/authentication#your-users)). - The transport sends `x-project-id`, `Content-Type` on a POST, the subject where the hub requires it and the standard `Accept`, nothing more: the gateway sets the rest. It refuses a secret key in `projectId`, with a message that says so. - The gateway limits each visitor IP, plus a pool shared by every visitor of your project; a `429` is a `HubError` whose `retryAfterMs` says how long to wait. Its timeouts are 25 s on quotes, 12 s on reads and 15 s on submissions, a little longer than the gateway's own. ## The user's wallets ```ts const evm = evmWalletFromEip1193(window.ethereum, { // Reads (allowances, receipts, block time) through YOUR RPC, for any chain, not only the wallet's current one. readProvider: (chainId) => jsonRpcProvider(`/api/rpc/eip155-${chainId}`), // Refuse to send unless the wallet's chain contains this block: a test fork answers chain id 1 too. chainInstances: { 1: { blockNumber: 26_090_310n, blockHash: "0x1345…6864" } }, }); ``` `window.ethereum` belongs to whichever extension loaded last: with MetaMask and Phantom both installed, it may not be the wallet your user wants. List the installed wallets instead, each of which announces itself ([EIP-6963](https://eips.ethereum.org/EIPS/eip-6963)), and pass the provider of the one they pick: ```ts type Announced = { info: { uuid: string; name: string; icon: string; rdns: string }; provider: Eip1193Provider }; const installed = new Map(); // by rdns: MetaMask is "io.metamask" window.addEventListener("eip6963:announceProvider", (event) => { const detail = (event as CustomEvent).detail; installed.set(detail.info.rdns, detail); // show detail.info.name, and detail.info.icon in an }); window.dispatchEvent(new Event("eip6963:requestProvider")); // Once the user picks one: const evm = evmWalletFromEip1193(picked.provider, { readProvider, chainInstances }); ``` For Solana, any Wallet Standard wallet (Phantom, Solflare, Backpack…): ```ts const wallet = getWallets().get().find((w) => "solana:signTransaction" in w.features)!; const account = wallet.accounts[0]!; // after the wallet's standard:connect const solana = solanaWalletFromStandard(wallet, account, { chain: "solana:mainnet", // the Wallet Standard chain id of the cluster you use rpc: solanaRpc("/api/rpc/solana-mainnet"), }); ``` See [Wallets](/infinity-interop/sdk/wallets) for every adapter and option. ## A bridge screen, step by step `await client.getCatalog()` returns a `CatalogIndex`: chains, their assets, which assets can be a source (`canBeSource`), which are native (`isNativeSource`), and the token-in chains (`acceptsTokenIn`). It is cached for 5 minutes, and readable before your user signs in: through your backend's proxy, or with your project id. `client.previewQuotes(request)` returns non-binding offers per provider, and nothing to sign. An empty `offers` array is a normal answer: no provider takes this route or amount right now. `client.getQuote(request)` returns a `Quote`. Show the user: - what they pay: `quote.payAmount` (a ceiling when `quote.payAmountIsCeiling`, on the token-in lane); - what the recipient gets: `quote.amountOut`, **a floor** (a fill usually pays more, as the order is an auction); - the time left: `quoteTimeLeftMs(quote)`, a client-clock countdown from when this quote was first received; - the checks: `await client.vetQuote(quote)`, one row per check, all of which must pass. The funding account and the recipient must be wallets the user connected; through your backend, the proxy refuses others. ```ts const result = await client.executeQuote({ quote, wallets: { evm, solana }, onEvent: (event) => timeline.push(event), // steps, checks, re-quotes, submission, status, fill }); ``` The wallet prompts once or twice, depending on the lane: an approval, then a transaction or a signature. If less than 15 s is left on the quote, the SDK fetches a fresh one first, and continues only if it is not worse (same lane, no lower output, no higher cost). A worse one throws `ReviewRequiredError` with both quotes: show the new one and ask again. A `filled` event means the chain shows the recipient paid, usually seconds after submission. `result.outcome` is `success` once the order has settled. See [Executing orders](/infinity-interop/sdk/execution) for every event. ## History and refunds Through your backend, `client.listAllOrders()` pages through the user's orders (your backend scopes them to the user, by its records or by the user's subject), and `client.summarizeOrder(entry)` turns each into a row with readable sides and a phase. A row whose order was never filled offers a refund once the order's deadline has passed: see [Refunds](/infinity-interop/sdk/refunds). With a project id, the history needs your backend, but refunds do not: show the orders this browser submitted, from the ids it kept, follow each with `trackOrder`, and refund one from the page with `refundEvmOrder` or `refundSolanaOrder`. ## A complete example The SDK repository's `examples/bridge-demo` is a bridge page built this way; a hosted copy runs at [demo.infinimesh.net](https://demo.infinimesh.net). It has two views, switched from its header (and remembered; `?face=dev` or `?face=simple` in a link picks one). Both share one connection, the wallets and the browser's order list: - **Simple**, a swap page with the essentials in the c8ntinuum look: the two tokens and the amount, a quote that refreshes on its own, one button that says what to do next, the route as the order moves (signed, submitted, filled), and your orders with their state and a refund button. It connects with your project id. - **Dev**, the walk-through: a Connection card for your project id (App ID, straight to the gateway) or your proxy server (`examples/proxy-server`), the step-by-step timeline, the check table and the raw wire calls. Both offer test-network dev wallets for trying every lane without a browser extension, and list the installed browser wallets by name. --- # Wallets URL: https://docs.c8ntinuum.com/infinity-interop/sdk/wallets `executeQuote` takes the user's wallets as `{ evm?, solana? }` and only calls the one the quote's source chain needs. The SDK ships adapters for the common cases. Anything else implements a small interface (see [Your own adapter](#your-own-adapter)). ## EVM | Adapter | Entry point | Use it for | |---|---|---| | `evmWalletFromEip1193(provider, options?)` | root | any injected or embedded wallet (MetaMask, Rabby, Privy, WalletConnect…); no dependencies | | `evmWalletFromViem({ walletClient, publicClient?, chains? })` | `/evm` | an app already built on viem or wagmi | | `evmWalletFromAccount({ account, rpcUrls, chainInstances? })` | `/evm` | a key your server holds (bots, treasury, tests); signs locally, sends through your RPCs | ### `evmWalletFromEip1193` options | Option | Meaning | |---|---| | `account` | the account to use (default: the wallet's first) | | `readProvider(chainId)` | an EIP-1193 provider for reads on any chain (`eth_call`, receipts, block time). Without it, reads go through the wallet, which only answers for the chain it is currently on. `jsonRpcProvider(url)` makes one from a URL | | `chains` | `wallet_addEthereumChain` parameters, per chain id, for chains the wallet does not know (error 4902) | | `chainInstances` | a block number and hash per chain id, checked through the wallet's own provider before its first transaction on that chain | | `receiptPollMs` | how often to poll for a receipt (default 1.5 s) | ### Chain-instance pins Two chains can share a chain id. A test "Ethereum" that is a mainnet fork answers `eth_chainId 0x1`, exactly like real Ethereum. A wallet pointed at a public RPC would then sign and send to **real mainnet**, and a signature made for the fork is valid on mainnet too. A pin names a block that exists only on the chain you mean. Before signing on that chain, the adapter reads the block and refuses (`WalletError`, reason `wrong-chain`) unless the hash matches. `evmWalletFromAccount` checks it through its own RPC before the first transaction or typed-data signature, and `evmWalletFromEip1193` through the wallet's provider: ```ts // The test fork as reset on 2026-09-29. const pins = { 1: { blockNumber: 26_090_310n, blockHash: "0x1345c747eec5ea470112194686e42ab7b21b1397bbf23aace1962105c5b06864" } }; ``` The pin changes whenever the hub team resets their fork: ask them for a block, or pick one the fork mined after it forked, whose hash differs from real mainnet's at that height (or that real mainnet has not reached yet). On a real network you do not need a pin: its chain id is unique. ## Solana | Adapter | Entry point | Use it for | |---|---|---| | `solanaWalletFromStandard(wallet, account, { chain, rpc? })` | `/solana` | any Wallet Standard wallet (Phantom, Solflare, Backpack…) | | `await solanaWalletFromKeypair({ secretKey, rpc? })` | `/solana` | a key your server holds: a 64-byte secret (the Solana CLI's JSON array, base58, or bytes) or a 32-byte seed. Signs with Web Crypto Ed25519 | | `solanaRpc(url)` | `/solana` | a minimal JSON-RPC client (broadcast, confirmation, blockhash, genesis) for the adapters above | `chain` is the Wallet Standard chain id of the cluster the hub serves, for example `solana:mainnet`. ### How a Solana order is signed On the Solana gasless lane the user signs the escrow's **message**; a solver pays the fees and submits it. Wallets differ in what they will sign, so `executeQuote` takes a `solanaSigning` strategy: | Strategy | What happens | |---|---| | `"transaction"` | the wallet signs the transaction as a normal Solana transaction; the SDK lifts the user's signature from it and checks the message came back unchanged | | `"message"` | the wallet signs the raw message bytes (`solana:signMessage`) | | `"auto"` (default) | `transaction` when the wallet supports it, else `message` | Either way, the SDK verifies the Ed25519 signature against the user's key before submitting it. Pass `solanaGenesisHash` to `executeQuote` to refuse signing on another cluster: one key signs for every cluster alike. ## Your own adapter Every method is one JSON-RPC call or one wallet prompt: ```ts interface EvmWallet { readonly family: "evm"; getAddress(): Promise; getChainId(): Promise; switchChain(chainId: number): Promise; // throw WalletError("wrong-chain") if it cannot signTypedData(typedData: Eip712TypedData): Promise; // eth_signTypedData_v4, domain/types/message VERBATIM signMessage(message: string): Promise; // personal_sign sendTransaction(tx: { chainId: number; to: string; data: string; value: bigint }): Promise; waitForReceipt(hash: string, options: { chainId: number; timeoutMs?: number; signal?: AbortSignal }): Promise; call(request: { chainId: number; to: string; data: string }): Promise; // eth_call getBlockTimestamp(chainId: number): Promise; // the source chain's clock, for refunds } interface SolanaWallet { readonly family: "solana"; getAddress(): Promise; // base58 signTransaction(transaction: Uint8Array): Promise; // wire bytes in, wire bytes out signMessage?(message: Uint8Array): Promise; sendTransaction?(signed: Uint8Array): Promise; signAndSendTransaction?(transaction: Uint8Array): Promise; confirmTransaction?(signature: string, options: { lastValidBlockHeight?: number; signal?: AbortSignal }): Promise<"confirmed" | "failed" | "expired">; getGenesisHash?(): Promise; } ``` Pass the hub's typed data to the wallet exactly as disclosed: the signature is over those objects, and a re-encoded domain or message signs something else. --- # Testing URL: https://docs.c8ntinuum.com/infinity-interop/sdk/testing ## The test hub The public test hub is **`https://proxy-api.infinimesh.net`**, with its API under `https://proxy-api.infinimesh.net/v1`. Pass the origin as `baseUrl`: the SDK adds `/v1` (a trailing `/v1` is accepted too). It runs on test networks only. Call it from your backend with your secret key, or from a page with your project id. Its gateway never takes a secret key from a browser. | Chain | RPC | Explorer | |---|---|---| | `eip155-8888` (Continuum public testnet) | `https://public-testnet-evm.c8ntinuum.link` | [testnet.ctmscan.io](https://testnet.ctmscan.io) | | `eip155-1` (a mainnet fork: chain id 1) | `https://eth-testnet.infinimesh.net` | none: query the RPC | | `solana-localnet` | `https://sol-testnet.infinimesh.net` (WebSocket `wss://sol-testnet.infinimesh.net`) | none: query the RPC | - **Test funds:** CTM from the [Continuum testnet faucet](https://testnet-faucet.c8ntinuum.io/). For the other test assets, swap CTM for them through the test hub itself, or ask the hub team. - **Lanes:** the test hub offers `wrap-native`, `evm-permit2`, `swap-token` and `solana-escrow`; not `evm-3009` or `open-flow` yet. The test hub has its own catalog, and its contract addresses change when it is redeployed. Build everything from the catalog. ## Keys and project ids Your organization, its project and its keys live in the hub dashboard, [console.infinimesh.net](https://console.infinimesh.net) for the test network: the API has no sign-up and no key routes. - **A secret API key** (`c8n_sk_…`) for your servers and scripts: make one for your project, and keep it in your secret store or in a git-ignored `.env` file as `HUB_API_KEY`. - **The project id** (`c8n_pk_…`) for your web pages: it is on the project page, public by design. List the origins of the pages that use it (`http://localhost:5173`, for example, while you develop). Check a key with one call that needs it: ```bash curl -sS "https://proxy-api.infinimesh.net/v1/orders?limit=1" \ -H "x-api-key: $HUB_API_KEY" \ -H "X-Infinity-Subject: default" ``` A `200` means the key is accepted. A `401` means it is not: `EPX0001` for a value that is not in the key's format (a typo, a stray space, a truncated copy), `EPX0002` for an unknown, revoked or expired key. Fix it before trying again: 20 refused credentials within a minute lock your IP out of every call for the rest of that minute (`EPX0081`). The node quickstart's `pnpm smoke` starts with the same check. To try the flows in a browser without a server, use the project id: list the page's origin on the project (`http://localhost:5173` for the bridge demo's `pnpm dev`; the hosted demo, `https://demo.infinimesh.net`, too if you connect it with your project id), or leave the list empty while you test ([Browser apps](/infinity-interop/sdk/frontend)). ## Test wallets Use keys made for testing, holding nothing on any real network. A test "Ethereum" that is a **mainnet fork** answers chain id 1, exactly like real Ethereum. A transaction or a Permit2 signature made for the fork is valid on real mainnet too: anyone can replay it there. Never test with a key that holds real funds, and pin the fork (below) so the SDK refuses to sign if your RPC points at the real network. On a server, or in a script: ```ts const evm = evmWalletFromAccount({ account: privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`), rpcUrls: { 1: process.env.RPC_EIP155_1!, 8888: process.env.RPC_EIP155_8888! }, chainInstances: { 1: { blockNumber: 26_090_310n, blockHash: "0x…" } }, // a block of the fork }); const solana = await solanaWalletFromKeypair({ secretKey: process.env.SOLANA_SECRET_KEY!, rpc: solanaRpc(process.env.RPC_SOLANA_LOCALNET!), // to send refunds; orders are sent by a solver }); ``` ## Chain-instance pins A pin names one block, by number and hash, that only the chain you mean contains. Before the first signature or transaction on that chain, the wallet adapter reads that block and refuses unless the hash matches (`WalletError`, reason `wrong-chain`). Ask the hub team for a block of their fork: it changes whenever they reset it. The examples read pins from `.env` as `PIN_EIP155_=:`. See [Chain-instance pins](/infinity-interop/sdk/wallets#chain-instance-pins). ## The examples The SDK repository has three runnable examples, each configured by a git-ignored `.env` copied from its `.env.example`: | Example | What it is | |---|---| | `examples/node-quickstart` | scripts: `pnpm smoke`, `catalog`, `quote`, `bridge`, `orders`, `refund`. Its `.env` holds `HUB_BASE_URL` and `HUB_API_KEY`, and test-network wallet keys for `bridge` and `refund` | | `examples/proxy-server` | a partner backend: holds your secret key (`HUB_API_KEY`), serves the demo through the proxy, and relays the demo's RPC calls. It keeps users apart by `X-Infinity-Subject`, checked by the subject canary at start-up, unless `HUB_USER_SCOPE=ownership`. It has no login of its own: it listens on this computer only unless you set `HOST`, and answers other origins only if you list them in `CORS_ORIGINS` | | `examples/bridge-demo` | a bridge screen in the browser, with two views switched from its header: **Simple**, a swap page with the essentials (tokens and amount, the quote, one button, the route as the order moves, your orders and refunds), and **Dev**, the walk-through (quote, the checks, execute, track, history, refunds, the wire calls). It connects with your project id (App ID) or, on the dev view, through your proxy server. A hosted copy runs at [demo.infinimesh.net](https://demo.infinimesh.net) | Start with the smoke test. It checks your key with one call, reads the catalog, then asks for a preview and a quote on every route and runs the pre-signing checks on each quote. It needs no funds, and signs nothing: ```bash pnpm smoke --recipient-evm 0x… --recipient-solana ``` Then one order per lane you offer, with small amounts: ```bash pnpm bridge --from eip155-8888:ctm --to eip155-1:weth --amount 3 ``` To run the browser demo through the proxy, start the proxy first (`pnpm --filter @c8ntinuum-examples/proxy-server dev`, then `pnpm --filter @c8ntinuum-examples/bridge-demo dev`). Through the proxy, the demo reaches the RPCs through its relay, `POST /api/rpc/`, so a keyed RPC URL stays on the server. The demo's Connection card can also call the test hub directly, with your project id: every route but the order history is open that way. ## Testing a refund There is no way to force a refund: an order becomes refundable only once its deadline has passed with no fill (see [Refunds](/infinity-interop/sdk/refunds#when-an-order-is-refundable)). On a test hub: 1. Get an order **nobody will fill**. Ask the hub team, or open one on the escrow yourself with an output no solver will fill (an absurd amount) and a short deadline. 2. **Wait in real time**, until the source chain's clock is past the deadline and the hub's destination observer has read past it too. `checkEvmRefund` says `not-yet` until then. 3. Refund it with `refundEvmOrder` (or `pnpm refund --id `), and check that the escrow paid the wrapped coin back. Never move a shared test chain's clock (`evm_increaseTime`, `evm_mine` with a timestamp) to reach a deadline: every other user of that fork, and the hub's own observers, run on the same clock. ## Before going live - Use the production hub's origin, and a secret key made for production, kept on your servers only. - List your production pages' origins on your project, if your pages use the project id. - Keep pins only for test chains: a real network's chain id is unique. - Put your own authentication in `resolveUser` if you serve pages through `createInteropProxy`. - Run the smoke test against production before you open the flow to users. --- # Lanes URL: https://docs.c8ntinuum.com/infinity-interop/sdk/lanes A quote's winner discloses **how** the order must be opened. The SDK reads that into a lane (`quote.lane.kind`), and `executeQuote` runs it. You never branch on lanes to execute, but your UI can use them to tell the user what is coming. | Lane | Source asset | The user | Order reaches the hub by | Gas | |---|---|---|---|---| | `wrap-native` | a chain's native coin (ETH, CTM…) | sends one transaction to the chain's UserProxy, which wraps the coin and opens the escrow | `POST /order/openfor`, no signature | the user | | `evm-permit2` | an ERC-20 the catalog lists (WETH, WCTM…) | approves Permit2 once, then signs an EIP-712 order | `POST /order/openfor` with the signature | none: a solver opens it | | `swap-token` | any ERC-20 on a token-in chain (a launchpad token, by address) | approves the UserProxy for exactly the amount, then sends one transaction that swaps into the wrapped native and opens the escrow | `POST /order/openfor`, no signature | the user | | `solana-escrow` | SOL, wSOL or an SPL token | signs the escrow's Solana message | `POST /order/openfor` with the signature | none: a solver pays the fees | | `evm-3009` | a token supporting EIP-3009 | signs a `ReceiveWithAuthorization` | `POST /order/openfor` with the signature | none | | `open-flow` | an aggregator bridge or transfer | sends the disclosed transaction, then signs its intent | `POST /order` | the user | | `informational` | — | nothing: the winner is priced but discloses nothing to execute | — | — | | `unsupported` | — | nothing: the answer is malformed or names an unknown lane (`quote.lane.reason` says why) | — | — | Which lanes a hub offers depends on its catalog and its providers. `executeQuote` refuses `informational` and `unsupported` quotes before any wallet prompt. ## `wrap-native` The quote carries one transaction (`executionTx`): `wrapAndOpen(order)` on the chain's UserProxy, with the coin as `value`. Before the wallet sees it, the SDK checks: - the target is the catalog's UserProxy for the source chain, on the source chain; - `value` is the amount asked for, and there are no approvals; - the calldata is `wrapAndOpen`, and **decoded**: the order's user is the funding account, its input is the wrapped native in the quoted amount, its output is the quoted amount of the asked-for asset, on the destination chain, to the recipient. As soon as the wallet returns the transaction's hash, the SDK registers the order with `POST /order/openfor` (no signature: the chain proves the deposit), then waits for the receipt. Registering at once matters: the aggregator refuses a registration once the quote's `expiresAt` has passed, and a deposit registered too late is filled but never recorded. A reverted deposit throws `WalletError` (`reverted`): nothing was deposited. An unfilled order refunds the **wrapped** coin. ## `evm-permit2` The user signs an EIP-712 Permit2 order; a solver opens it on chain. The token needs an allowance to Permit2 (`0x000000000022D473030F116dDEE9F6B43aC78BA3`) first: `executeQuote` sends that approval when it is missing (`permit2Approval: "unlimited"` by default, or `"exact"`), before signing. An approval mined while a quote is open can leave the signature on an expired quote. The SDK handles it by re-quoting, but a UI that approves before quoting avoids the extra round. The checks: the typed data is a Permit2 batch witness whose domain is on the source chain; the permitted token and amount are the input asked for; the witness's user, recipient, output token, amount and destination chain match the request; its nonce, deadline and expiry are present; and on an auction, the signed amount is the auction's floor. The signature goes to the hub with the one-byte `0x00` scheme prefix (66 bytes). ## `swap-token` (token-in) For a token the escrow does not take directly: the UserProxy swaps it into the wrapped native, then opens the escrow, in one transaction (`swapAndOpenV2(order, tokenIn, amountInMax)`). The quote's pay amount is a **ceiling**: `amountInMax`, the most the swap may spend (`slippageBps` in the request, 1–1000). The SDK approves the UserProxy for exactly that ceiling, decodes the calldata like `wrap-native`, and also checks that `tokenIn` is the asked-for token and the ceiling is within the tolerance. Reverse (exact-output) quotes are refused on this lane: its price is only known from what the user spends. ## `solana-escrow` The hub discloses the escrow transaction's **message**. The user signs it; a solver adds its fee-payer signature and submits it. Before signing, the SDK checks: - the message parses (a legacy Solana message), its fee payer is the disclosed solver, and the user is one of its signers; - the disclosed digest is the SHA-256 of those bytes; - the sender is the funding account, and the fee payer is not the user. The signature goes to the hub as `0x00 ‖ signature ‖ public key`, base64. See [Wallets](/infinity-interop/sdk/wallets#how-a-solana-order-is-signed) for the signing strategies. ## `evm-3009` The user signs an EIP-3009 `ReceiveWithAuthorization` for a token that supports it; the aggregator settles it. The SDK checks that the typed data is an EIP-3009 authorization on the source chain, that its payer is the funding account and its value the quoted input, and submits the raw 65-byte signature. ## `open-flow` An aggregator's own bridge or transfer: the user sends the disclosed transaction, then signs its intent (EIP-191), and the order is registered with `POST /order`. The SDK follows it on the hub's lifecycle view (`GET /status/{id}`) until the recipient is paid or the order goes to recovery. Open-flow orders are not listed in `GET /orders`. --- # Quotes and checks URL: https://docs.c8ntinuum.com/infinity-interop/sdk/quotes ## A quote request ```ts const quote = await client.getQuote({ sourceChain: "eip155-8888", sourceAsset: "wctm", // a catalog key, or ":
" for a token the catalog does not list destChain: "solana-localnet", destAsset: "spl", amount: "500000000000000000000", // base units, a decimal string sourceAccount: "0x…", // who pays recipient: "4vUS…", // who is paid, in the destination chain's format }); ``` | Field | Meaning | |---|---| | `sourceChain`, `destChain` | chain domains from the catalog (`eip155-1`, `solana-localnet`…) | | `sourceAsset`, `destAsset` | a catalog asset key, or `:
` for any token by address (the token-in lane, a launchpad token as destination). A bare `0x…` is refused | | `amount` | base units, as a decimal string, never a number. What the user spends, or with `exact-output`, what the recipient receives | | `sourceAccount` | the paying account. Always send it: without it the hub quotes the recipient as the payer | | `recipient` | the paid account, in the destination chain's address format | | `swapType` | `exact-input` (default) or `exact-output` | | `maxAmountIn` | `exact-output` only: the most the user will pay | | `slippageBps` | the token-in lane only: how far the swap may spend above the quote (1–1000) | `getQuote`, `previewQuotes` and `prepareQuote` check both accounts before any call. A `recipient` that cannot be an address on `destChain`, or a `sourceAccount` that cannot be one on `sourceChain`, throws an `InteropError` (`invalid-input`) naming the field and the chain, for example `recipient must be a Solana address (base58, or 0x and 64 hex digits) on solana-localnet`. An EVM account is `0x` and 40 hex digits (any casing); a Solana account is base58, or `0x` and 64 hex digits. Without the check the hub answers `EIN0001`, with the field as its reference. A common cause: an address typed for one destination, still in the form after the user switched the destination chain. `toBaseUnits("0.05", 18)` and `formatUnits(50000000000000000n, 18)` convert between readable and base-unit amounts. ## The quote `getQuote` returns a `Quote`: | Field | Meaning | |---|---| | `lane` | what executing it involves: see [Lanes](/infinity-interop/sdk/lanes) | | `payAmount` | what the user pays, in the source asset. A ceiling when `payAmountIsCeiling` (token-in) | | `amountOut` | what the recipient receives. On an auction this is the **floor**: fills usually pay more | | `quoteId`, `quoteRequestId` | the winning quote and its round | | `firstReceivedAt`, `validityMs` | when this quote was first received (client clock), and how long it lives | | `requestId` | the answer's request id, for support | | `request`, `response` | what was asked, and the hub's answer unchanged | ## How long a quote lives A quote lives about **30 seconds**. The hub also answers an identical request within that window with the **same** quote and its original expiry. The SDK therefore anchors the countdown on when a `quoteId` was first received, using the client's own clock only: ```ts const secondsLeft = Math.floor(quoteTimeLeftMs(quote) / 1000); ``` `executeQuote` wants at least **15 s** left before it asks the user to sign (`minSigningTimeMs`). With less, it fetches a fresh quote for the same request, and continues only if the new one is **not worse**: the same lane, no lower output, no higher cost. A worse one throws `ReviewRequiredError` carrying both quotes, so you can show the new terms and ask again. Pass `acceptRequote(fresh, reviewed)` to decide yourself. ## Reverse quotes With `swapType: "exact-output"`, `amount` is what the recipient must receive, and the quote's `payAmount` is the price: ```ts const quote = await client.getQuote({ sourceChain: "eip155-1", sourceAsset: "eth", destChain: "eip155-8888", destAsset: "ctm", amount: "100000000000000000000", // exactly 100 CTM to the recipient swapType: "exact-output", maxAmountIn: "10000000000000000", // optional: pay at most 0.01 ETH sourceAccount: me, recipient: me, }); ``` A `maxAmountIn` under the price answers `EIN0008 providerRefused`: raise the cap rather than retry. The token-in lane refuses `exact-output` (`EIN0001`, reference `swapType`). ## Preview and prepare For a UI that shows live estimates as the user types, then commits to one provider: ```ts const preview = await client.previewQuotes(request); // non-binding, nothing to sign const offer = preview.offers?.[0]; // { provider, amountOut, … } if (offer) { const quote = await client.prepareQuote(request, offer.provider!); // a signable quote from that provider await client.executeQuote({ quote, wallets }); } ``` An empty `offers` array is a normal answer: no provider takes this route or amount right now. A prepared quote is bound to a `preparationId` (a fresh UUID unless you pass one), so repeating the same preparation is safe. When a prepared quote runs low on time, the SDK prepares it again. ## The pre-signing checks The hub routes and prices; it does not decide what the user signs. Before any wallet prompt, `executeQuote` compares every field that moves money with the request and the catalog, and refuses with a `VetError` if one row fails. Run the same checks yourself to show them: ```ts const report = await client.vetQuote(quote); report.ok; // every row passed report.rows; // { check, status: "pass" | "fail" | "skip", detail }[] report.failed; // the failed rows ``` | Lane | What is checked | |---|---| | every lane | the lane is executable; both chains are in the catalog; the amounts are readable | | `wrap-native` | one step, on the source chain, to the catalog's UserProxy, with `value` = the amount and no approvals; the decoded `wrapAndOpen` order: user, source chain, wrapped-native input and amount, output token, amount, chain and recipient | | `swap-token` | the token is the one asked for; one approval, of the UserProxy, capped at the quote's ceiling; the ceiling within the slippage tolerance; the decoded `swapAndOpenV2` order as above, plus `tokenIn` and `amountInMax` | | `evm-permit2` | a Permit2 batch witness on the source chain; the permitted token and amount; the witness's user, recipient, output token, amount and destination chain; nonce, deadline and expiry present; on an auction, the signed amount is its floor | | `solana-escrow` | the message parses, its fee payer is the disclosed solver, and the user signs it; the digest is the message's hash; the sender is the user and the fee payer is not; with a trusted clock, not expired | | `evm-3009` | an EIP-3009 authorization on the source chain, from the user, for the quoted input | | `open-flow` | the disclosed steps are on the source chain and target the aggregator the quote names | `vetQuote` also takes a context: `sourceAccounts` and `destinationAccounts` (every account the user may pay from or to, default the request's), `permit2Address`, `maxSlippageBps`, and `nowSec`, a unix time from a clock you trust (a chain head), which turns on the Solana expiry check. ## The catalog `await client.getCatalog()` returns a `CatalogIndex` over one snapshot, cached for 5 minutes (the last good copy is kept when the hub is unreachable): | Method | Answers | |---|---| | `chains`, `assets` | the lists, as the hub declares them | | `chain(domain)`, `family(domain)`, `evmChainId(domain)` | one chain; `evm` or `solana`; its EVM chain id | | `assetsOn(domain)`, `resolveAsset(domain, keyOrRef)` | a chain's assets; one asset by key or reference | | `canBeSource(domain, key)`, `isNativeSource(domain, key)`, `acceptsTokenIn(domain)` | what the user may pay with | | `userProxy(domain)`, `inputSettlers(domain)`, `wrappedKey(domain)` | the contracts the checks compare against | Build pickers from it rather than hard-coding: contract addresses, keys and proxies move when the hub is redeployed. --- # Executing orders URL: https://docs.c8ntinuum.com/infinity-interop/sdk/execution ```ts const result = await client.executeQuote({ quote, wallets: { evm, solana }, onEvent }); ``` One call runs the whole order for the quote's [lane](/infinity-interop/sdk/lanes): wallet checks, approvals, re-quoting when time is short, the pre-signing checks, the signature or transaction, the submission to the hub, and tracking until the order ends. ## Options | Option | Meaning | |---|---| | `quote` | the quote the user reviewed | | `wallets` | `{ evm?, solana? }`: the one the source chain needs is used. See [Wallets](/infinity-interop/sdk/wallets) | | `onEvent(event)` | progress, to drive a timeline (below) | | `signal` | an `AbortSignal`. Before anything is signed, aborting leaves nothing behind | | `subject` | the user of yours this order is for, sent in `X-Infinity-Subject` on the submission, a re-quote and the status reads ([below](#executing-on-a-server-for-your-users)). Without it, the transport's own: `hubTransport`'s `subject`, or a page's visitor id | | `minSigningTimeMs` | re-quote when less than this is left (default 15 000) | | `acceptRequote(fresh, reviewed)` | continue with a fresh quote? Default: only when it is not worse, else `ReviewRequiredError` | | `permit2Approval` | `"unlimited"` (default) or `"exact"`, when a Permit2 allowance is missing | | `solanaSigning` | `"auto"` (default), `"transaction"` or `"message"` | | `solanaGenesisHash` | refuse to sign on another Solana cluster | | `waitForCompletion` | `false` to return right after submission, and track later | | `statusPollMs`, `receiptTimeoutMs` | polling cadence (default 3 s) and how long to wait for a receipt | | `vet` | extra check context (other accounts the user owns, a trusted clock) | ## Events | `type` | Fields | When | |---|---|---| | `step` | `step`, `status` (`started`, `awaiting-wallet`, `confirming`, `done`, `skipped`), `transactionHash?`, `detail?` | each step: `check-wallet`, `switch-chain`, `approve-permit2`, `approve-token`, `requote`, `vet`, `sign-order`, `send-transaction`, `sign-intent`, `submit-order`, `track-order` | | `vet` | `report` | the checks ran (all passed, or the call throws `VetError`) | | `requoted` | `quote`, `previous`, `reason` | a fresh quote replaced the reviewed one | | `submitted` | `orderId`, `hubOrderId`, `recovered` | the hub registered the order (`recovered` after a lost answer) | | `status` | `status` | each tracking poll (the aggregator's view, or the hub's lifecycle view on the open flow) | | `filled` | `chain` | the hub has seen the fill on chain: **the recipient is paid** | `awaiting-wallet` is the moment to tell the user to look at their wallet. On the `wrap-native` and `swap-token` lanes, `submit-order` runs while the deposit is still `confirming`: the order is registered as soon as the transaction is broadcast, and the SDK waits for the receipt afterwards. ## The result | Field | Meaning | |---|---| | `outcome` | `success`, `failure`, `refunded`, or `pending` (not waited for, or still running when the wait stopped) | | `decidedBy` | `chain` when the hub's chain facts ended the wait, `upstream` when the aggregator's state did | | `orderId`, `hubOrderId`, `onchainOrderId` | the order's ids: the status handle, the hub's durable alias, the on-chain id | | `transactions` | every transaction the user sent: `{ step, chainId, hash }` | | `quote` | the quote actually executed (a re-quote if one happened) | | `submission`, `finalStatus`, `chainFacts`, `vet` | the hub's acknowledgement, the last status and chain facts, the check report | ## Tracking The aggregator's status (`GET /order/{id}/status`) is **advisory**. It can lag, or stop at `executed`, long after the order settled. The SDK therefore also reads the hub's own chain facts (`GET /onchain/{id}/fill`) on every round: - a **fill** on chain emits `filled`: the recipient has been paid; - **settled** or **refunded** on chain ends the wait (`decidedBy: "chain"`); - a terminal aggregator state (`settled`, `finalized`, `failed`, `refunded`) also ends it. Fills usually show up seconds after submission. A wait gives up after 30 minutes with a `timeout` error, whose advice is to check the order's status later; the order itself is unaffected. To follow an order outside `executeQuote` (after `waitForCompletion: false`, or from your history): ```ts const progress = await client.trackOrder(orderId, { onchainOrderId, // enables the chain facts onFill: () => showPaid(), onStatus: (status) => showStatus(status.state), }); progress.outcome; // "success" | "failure" | "refunded" ``` Open-flow orders are followed on the hub's lifecycle view instead (`GET /status/{id}`): done when the recipient is paid (`USER_PAID`) or delivery is final, and a failure when the order goes to recovery (`RECOVERY_ONLY`). Tracking works the same with a project id (`projectTransport`): the chain facts and the lifecycle view are open to it. ## Lost answers A submission can succeed while its answer is lost (a timeout, a dropped connection, or the gateway's `EPX0050`–`EPX0053`, among them `EPX0052` when the hub took longer than the gateway's 10 s). The gateway keeps a submission running at the hub when the caller disconnects, but not past its own 10 s. The hub records one submission per quote and answers a repeat with the original acknowledgement, so the SDK recovers without ever asking the user to sign again: 1. it looks the order up by its **derived hub order id**, `hex(sha256("infinity:v1:order:" + quoteId))`; 2. if the hub has not seen it, it re-posts the **same** body once; 3. if every answer is still lost, it throws `OutcomeUnknownError` with the `hubOrderId` to follow, and, on the transaction lanes, the `transactionHash` already on chain. After an `OutcomeUnknownError`, never quote again or ask the user to sign again: the order may exist. Follow it with `trackOrder(error.hubOrderId)`, or find it with `client.findOrderByTx(sourceChain, error.transactionHash)`. ## Executing on a server for your users A call that names no user goes under `hubTransport`'s own subject (`default` unless you set another), so an order your backend executes for one of your users lands in that shared history like every other, and the hub does not know which user it was for. Two ways to keep your users apart ([Authentication — your users](/infinity-interop/api/authentication#your-users)): - **Name the user.** Get the quote with their subject, and pass the same `subject` to `executeQuote`, which sends it on the submission, a re-quote and the status reads. The hub then keeps that user's quote, order and history apart: ```ts const quote = await client.getQuote(request, { subject: user.id }); const result = await client.executeQuote({ quote, wallets, subject: user.id }); ``` A client made for one user, `createInteropClient({ transport, subject })`, sends it on every call that reads it. The SDK checks the subject against the hub's rule (`^[A-Za-z0-9._@+~-]{1,128}$`) before any call. - **Record the user yourself**, next to `result.orderId` and `result.hubOrderId`. Calls your frontend makes through `createInteropProxy` are kept apart for you ([Partner backend](/infinity-interop/sdk/partner-backend#keeping-your-users-apart)). --- # Order history URL: https://docs.c8ntinuum.com/infinity-interop/sdk/orders `GET /orders` lists the orders submitted through `POST /order/openfor`: every lane except the open flow. Each entry joins what was asked (the quote) with what the hub has seen on chain. ## Paging ```ts for await (const page of client.walkOrders()) { for (const entry of page) render(await client.summarizeOrder(entry)); } // or everything at once (newest first with sortOrdersNewestFirst): const all = await client.listAllOrders({ maxPages: 5 }); ``` `walkOrders` follows the guide's cursor rule: the last entry's `submittedAt` is the next `since`. It removes duplicates by `orderId` and stops on a short page, on a page that added nothing, or after `maxPages` (default 20). `pageSize` defaults to the hub's maximum, 500. What the list holds depends on the transport: - **On your server, with your key** (`hubTransport`), it is the history of the transport's one subject (`default` unless you set another): every order made under it, whichever of your users placed it. Never hand it to a browser unfiltered. With a user's `subject` (`client.walkOrders({ subject })`, or a client made with `createInteropClient({ transport, subject })`), it is only that user's orders ([Authentication — your users](/infinity-interop/api/authentication#your-users)). - **Through your proxy** (`proxyTransport`), it is already the signed-in user's own orders: the proxy scopes it to the user, by the user's subject or from your organization's listing ([Partner backend](/infinity-interop/sdk/partner-backend#keeping-your-users-apart)). - **With a project id** (`projectTransport`), the history is not open: keep the ids of the orders the page submits (the `submitted` event), and follow them with `trackOrder`. ## A readable row `summarizeOrder(entry)` resolves assets through the catalog and returns an `OrderSummary`: | Field | Meaning | |---|---| | `phase` | `submitted`, `open`, `filling`, `filled`, `settled`, `refunded` or `expired`, from the chain facts | | `pay` | what the user paid: `symbol`, `decimals`, `amount` (base units), `isCeiling` (token-in), `paidInNative` | | `receive` | what the recipient gets: the quoted `amount`, and `paidAmount` once a fill is seen; `recipient` | | `orderId`, `hubOrderId`, `onchainOrderId`, `quoteId` | the order's ids | | `submittedAt` | when it was submitted | | `openTxHash`, `fillTxHashes` | the escrow's open, and the fills | | `refundEstimate`, `refundableAfter` | the hub's **advisory** refund estimate and the deadline in force | ```ts const row = await client.summarizeOrder(entry); const paid = row.pay.decimals === undefined ? `${row.pay.amount} (base units)` : formatUnits(row.pay.amount ?? 0n, row.pay.decimals); console.log(`${row.phase}: ${paid} ${row.pay.symbol}`); ``` `decimals` is known for catalog assets and for legs the hub joined; it is never guessed. A token the catalog does not list (a launchpad token by address) may come back without it. `expired` means the fill deadline passed, which comes **before** refunds open. An expired order is not yet refundable: see [Refunds](/infinity-interop/sdk/refunds). ## Following one order | Call | Returns | |---|---| | `client.trackOrder(orderId, { onchainOrderId })` | waits until the order ends, by the chain facts and the aggregator's state. See [Tracking](/infinity-interop/sdk/execution#tracking) | | `client.waitForOrder(orderId)` | waits on the aggregator's state only | | `client.api.getOrderStatus(orderId)` | one read of the aggregator's (advisory) status, by `orderId` or `hubOrderId` | | `client.api.getOnchainFill(onchainOrderId)` | one read of the chain facts: open, filled, settled, refunded, with the observers' watermarks | | `client.findOrderByTx(chainDomain, txHash)` | which order a settler transaction belongs to: after a lost answer, from a transaction hash | On a server that names your users, pass the order's `subject` to `trackOrder`, `waitForOrder` and `getOrderStatus` too: the aggregator's status view reads it. The chain facts and the open flow's lifecycle view do not. ## Open-flow orders Open-flow orders (`POST /order`) are not listed in `GET /orders`. Keep their ids when you submit them, and follow them on the hub's lifecycle view with `waitForHubOrder` (or `executeQuote`, which does it for you). --- # Refunds URL: https://docs.c8ntinuum.com/infinity-interop/sdk/refunds An order nobody fills stays in the source chain's escrow until its deadline. After that, the user can take it back. The escrow pays back **what it holds**: on the transaction lanes (`wrap-native`, `swap-token`) that is the **wrapped** native (WETH, WCTM…), never the coin or the token the user paid with. A refund only ever pays the order's own user. The underlying rules are on [Refunds](/infinity-interop/api/concepts/refunds) in the API reference. This page covers what the SDK does with them. The refund calls read the hub's on-chain views, which take your secret key or your project id: they run on your server, from a page through your backend's proxy, or from a page with your project id. ## When an order is refundable Two clocks decide it, and the wall clock is neither of them: 1. **The destination chain's watermark.** The hub's observer on the destination chain must have read at least up to the deadline. Before that, "not filled" is not known yet, and the hub answers `EIN0051` rather than guess. 2. **The source chain's own clock.** The source chain's latest block must be **strictly** past the deadline in force, `max(expires, refundableAfter)`. Before that, the escrow reverts the refund, and the user still pays the gas. `refundableAfter` comes from the order's history row (`chain.refundableAfter` in `GET /orders`). Pass it on when you have it, so a deadline the hub extended counts. An order's `expired` phase starts at the **fill** deadline, which comes before refunds open. Expired does not mean refundable yet. ## EVM-origin orders ### Checking ```ts const check = await client.checkEvmRefund({ onchainOrderId, entry, // the GET /orders row, if you have it wallet: evm, // optional: adds the live on-chain checks }); ``` | `status` | Fields | Meaning | |---|---|---| | `refundable` | `verifiedOnchain`, `refundableAfter` | both clocks are past the deadline. `verifiedOnchain` is `true` when a wallet was given and the escrow's own state was checked too | | `not-yet` | `reason`, `retryAfterSec?` | `watermark`: the destination has not been read up to the deadline yet. `deadline`: the source chain's clock has not passed it (`retryAfterSec` is how long, by that clock) | | `filled`, `settled`, `refunded` | `fact` | the hub has seen it filled, settled or already refunded: nothing to refund | | `cannot-tell` | `reason` | the hub cannot date an answer yet (its observers are catching up), or the destination chain has no dated watermark. Never read this as "not filled" | Without a history row (an order your organization did not submit, or one found by transaction hash), the check reads the deadline and the destination chains from the escrow's own order. ### Sending the refund ```ts const { transactionHash } = await client.refundEvmOrder({ onchainOrderId, wallet: evm, refundableAfter: entry.chain?.refundableAfter, onTransaction: (hash) => showPending(hash), }); ``` Before the wallet sees anything, `refundEvmOrder`: 1. reads the escrow's order from the hub (`GET /onchain/{id}/order`) and refuses a settler the catalog does not declare for that chain, an order opened on another chain, and bytes that do not decode as an order; 2. switches the wallet to the source chain; 3. checks on chain that the order hashes to this id **on this settler** (`orderIdentifier`), that the escrow still holds the deposit (`orderStatus`), and that the chain's clock is past the deadline in force. It then sends `refund(order)` to the settler from the user's wallet (the user pays the gas) and waits for the receipt. A reverted refund throws `WalletError` with reason `reverted`. `client.prepareEvmRefund(onchainOrderId)` returns the same plan without sending anything: the settler, the decoded order and the transaction. ## Solana-origin orders ```ts const { signature, confirmation } = await client.refundSolanaOrder({ onchainOrderId, wallet: solana, unwrap: true, // also turn wrapped SOL back into SOL }); ``` The hub **builds** the Solana refund ([`GET /onchain/{id}/refund`](/infinity-interop/api/onchain-refund)): an unsigned transaction whose fee payer and only signer is the order's user. It reads the escrow and the cluster's clock when you ask, and simulates the transaction before serving it. The transaction lives about a minute (`lastValidBlockHeight`), so `refundSolanaOrder` fetches it right before signing. `unwrap: true` also closes the user's wrapped-SOL account in the same transaction, so SOL comes back as SOL. It closes the **whole** account: wrapped SOL the user already held there becomes SOL too. Before the wallet sees it, the SDK checks that the transaction: - names this order, on a Solana chain, through an escrow program the catalog declares; - is unsigned, with one signature slot, and its fee payer is the order's user and the only signer; - carries the disclosed blockhash and no address lookup tables; - starts with the escrow's `refund` instruction for **this** order, and holds nothing else, except with `unwrap`, exactly one SPL Token `CloseAccount` that pays the user. It also checks that the connected wallet **is** the order's user. The wallet needs a little SOL for the fee, or for the rent of a token account the refund recreates. `client.prepareSolanaRefund(onchainOrderId, unwrap)` fetches and checks the transaction without signing it. There is no separate check step on Solana: the hub checks the escrow and the cluster's clock when it builds the transaction, and says why when it will not. ## When a refund is refused Every refusal is a `RefundError`, before any wallet prompt unless noted: | `reason` | Meaning | What to do | |---|---|---| | `not-yet-refundable` | the deadline has not passed on the source chain's clock | wait `retryAfterSec` (when known), then try again | | `not-observed` | the hub has not seen the escrow opening yet | ask again in a moment | | `retry-later` | the hub or the Solana node could not answer | retry | | `already-refunded` | the order was refunded already | nothing to do | | `already-claimed` | the order was filled and settled | nothing to do: the recipient was paid | | `not-deposited` | the escrow holds no deposit under this id | nothing to refund | | `wallet-mismatch` | the connected Solana wallet is not the order's user | connect the order's wallet | | `fee-payer-unfunded` | the user's Solana wallet cannot pay the fee or the rent | add a little SOL | | `origin-not-supported` | the order was opened on the other chain family | use the other refund call | | `settler-not-declared` | the hub named an escrow the catalog does not declare | refuse, and report it | | `transaction-mismatch` | the hub's order or transaction is not what it claims to be | refuse, and report it | | `unbuildable` | the hub could not build the refund | report it with the order id | `adviseError(error)` turns the first three into a `retry-later` advice, with `retryAfterMs` when the wait is known. See [Errors](/infinity-interop/sdk/errors). ## In a UI - Offer a refund only from the check: `refundable` enables the button, and `not-yet` shows how long is left. `summarizeOrder`'s `refundEstimate` is the hub's advisory estimate, not a check. - Pass the history row's `refundableAfter` to both calls. - Tell the user what they get back: the wrapped coin on the transaction lanes, and on Solana, SOL or wSOL depending on `unwrap`. - On EVM, the user needs gas on the source chain. On Solana, a little SOL. The [bridge demo](/infinity-interop/sdk/frontend#a-complete-example)'s Orders tab implements this flow. To make an order refundable on a test hub, see [Testing](/infinity-interop/sdk/testing#testing-a-refund). --- # Errors URL: https://docs.c8ntinuum.com/infinity-interop/sdk/errors Every failure the SDK raises is an `InteropError` with a `kind`. Branch on the class, the `kind` or the hub's `code`, never on a message. ## The error classes | Class | `kind` | Thrown when | Fields | |---|---|---|---| | `HubError` | `hub` | the hub refused the call with its error envelope (HTTP 400), or the gateway in front of it refused the call in the same envelope, with a real HTTP status and an `EPX` code | see [below](#huberror) | | `HubUnavailableError` | `unavailable` | no usable answer: a network failure, a timeout, or an answer that is not the envelope (an HTML page, a bare status). **A POST may still have been accepted** | `reason` (`network`, `timeout`, `http-status`, `not-json`, `bad-envelope`), `status`, `requestId` | | `ProxyError` | `proxy` | your own proxy refused the call (browser only) | `status`, `code` (`PXY…`, see [Partner backend](/infinity-interop/sdk/partner-backend#what-it-does-on-every-call)) | | `WalletError` | `wallet` | the wallet refused or failed, or is on the wrong account or chain | `reason`: `rejected`, `unsupported`, `failed`, `wrong-account`, `wrong-chain`, `reverted` | | `VetError` | `vet` | the [pre-signing checks](/infinity-interop/sdk/quotes#the-pre-signing-checks) refused the quote. Nothing was signed or sent | `report` | | `QuoteExpiredError` | `quote-expired` | the quote ran out of time before it could be used | `quoteId` | | `ReviewRequiredError` | `review-required` | a fresh quote is worse than the one the user reviewed. Nothing was signed | `fresh`, `reviewed` | | `OutcomeUnknownError` | `outcome-unknown` | an order submission's answer was lost, even after the re-post. The order may exist | `quoteId`, `hubOrderId`, `transactionHash` | | `RefundError` | `refund` | a refund cannot go ahead, or not yet (see [Refunds](/infinity-interop/sdk/refunds#when-a-refund-is-refused)) | `reason`, `retryAfterSec` | | `InteropError` | `invalid-input` | you passed something the contract cannot accept: a value that is not a secret key given to `hubTransport`, a route not open to a project id, a `subject` outside the hub's rule, a quote's `recipient` or `sourceAccount` that cannot be an address on its chain, … | — | | `InteropError` | `timeout` | a wait (for an order's end, a receipt) ran out of time. Nothing is known to have failed | — | | `InteropError` | `aborted` | your `AbortSignal` fired | — | All entry points share one copy of these classes, so `instanceof` works whichever entry point threw. `isAbortError(error)` is also true for a platform `AbortError`. ## `HubError` | Field | Meaning | |---|---| | `code` | the hub's or the gateway's code: `EIN0008`, `EPX0080`… | | `status` | the HTTP status: `400` for the hub's own answers; `401`–`504` for the gateway's refusals | | `reference` | the field or the reason, when the hub names one: `amount`, `providerRefused`, `NOT_YET_REFUNDABLE`… | | `params` | the code's values (`error_params`), always an array | | `hubMessage` | the English message: for logs, never for program logic | | `requestId` | the answer's request id. Quote it when you contact the hub team | | `entries` | every entry of the answer's `errors[]`; the first one decides | | `category` | from the code's block, the same for `EIN` and `EPX` codes: `terminal` (`0001`–`0049`), `availability` (`0050`–`0079`), `busy` (`0080`–`0099`); `framework` (`EMC…`) or `unknown` | | `retryable` | `true` for the availability and busy blocks, except `EIN0050` with `params[0]` `upstreamAuth` (the hub's own credential at its aggregator: only an operator clears it) | | `retryAfterMs` | a rate limit's back-off: `error_params[0]` of `EIN0080` and of the gateway's `EPX0080` and `EPX0082`, else the gateway's `Retry-After` header (the only back-off `EPX0081` carries) | Two getters sort the codes that are not about the request: - `isCredentialRefused`: the gateway refused your credential (`401`: `EPX0001` a missing or malformed key, `EPX0002` an unknown, revoked or expired one, `EPX0008` or `EPX0009` a malformed or unknown project id), or a page's origin that is not on the project's list (`403`, `EPX0010`). An operator matter: retrying cannot fix it, and refused credentials count toward the gateway's lockout of the caller's IP (`EPX0081`). - `isClientBug`: a framework code that means the request itself was malformed (`CLIENT_BUG_CODES`). Every code is listed in the [Error reference](/infinity-interop/api/errors). ## What to tell the user `adviseError(error)` sorts any error the SDK throws into an action, who it is for, and a default message: ```ts try { await client.executeQuote({ quote, wallets }); } catch (error) { const advice = adviseError(error); if (advice.audience === "operator") reportToYourTeam(error); showMessage(advice.message); // or your own copy, keyed by advice.action } ``` | `action` | From | What to do | |---|---|---| | `fix-request` | `EIN0001`, `EIN0003` | let the user correct the input | | `change-amount` | `EIN0008` `providerRefused` | ask for another amount | | `change-destination` | `EIN0008` `notFillable` | suggest another network or token | | `change-pair` | `EIN0002` | offer only what the catalog lists | | `no-route` | any other `EIN0008` | say there is no route right now | | `requote` | `EIN0004`–`EIN0007`, `EIN0010`, `EIN0021`, `QuoteExpiredError`, `ReviewRequiredError` | get a new quote, and have the user review it | | `resubmit` | a lost submission: `OutcomeUnknownError`, or an availability answer (`EIN` or `EPX`) while submitting | follow the order; never ask for a new signature | | `check-status` | a `timeout` while following an order | point the user to the order's status | | `retry-later` | availability and busy codes, the gateway's `EPX0050`–`EPX0079`, `HubUnavailableError`, a `ProxyError` other than 401 or 403, a refund that is not possible yet | retry in a moment (`retryAfterMs` when known) | | `wait` | `EIN0080`, and the gateway's rate limits `EPX0080`–`EPX0099` | wait `retryAfterMs`, then retry | | `not-found` | `EIN0009` | the order may just not be observed yet: ask again shortly | | `operator` | the gateway's other refusals (`EPX0001`–`EPX0049`: a refused key, project id or origin, a route not open to the credential), client bugs and other `EMC` codes, `EIN0049`, `upstreamAuth`, a `ProxyError` 401 or 403 | show a generic message, and alert your team | | `wallet` | `WalletError` | the wallet did not complete it (`rejected`: the user cancelled) | | `none` | anything else | a generic message | `audience` is `operator` for the codes that describe your key, your project, your origins or your client: the user only needs to hear that the service is unavailable. `DEFAULT_ERROR_MESSAGES` holds the default English copy. Pass the route when you know it: `adviseError(error, { route: "orderOpenFor" })` reads availability answers and a `HubUnavailableError` as `resubmit`, because on a submission they say nothing about whether the order exists. ## Retrying ### What the SDK already does - **Timeouts.** Every transport waits a little longer than the gateway, so the gateway's own answer ends a slow call: `hubTransport` and `projectTransport` wait 25 s on quotes, 12 s on reads and 15 s on submissions; `proxyTransport` waits 30 s, 15 s and 35 s, longer than your proxy. - **Submissions.** When an order submission's answer is lost (a timeout, a dropped connection, the gateway's `EPX0050`–`EPX0053`), the SDK looks the order up by its derived hub order id, then re-posts the **same** body once. See [Lost answers](/infinity-interop/sdk/execution#lost-answers). - **Tracking.** Status polls ride out availability and busy answers, waiting at least `retryAfterMs`. - **Quotes.** `executeQuote` re-quotes when too little time is left to sign. See [How long a quote lives](/infinity-interop/sdk/quotes#how-long-a-quote-lives). - **Catalog.** When the hub cannot be reached, the last good copy is used. Everything else throws once, and the rest is your call. ### Your rules - `retryable` is `true`: back off and retry. On a rate limit (`EIN0080`, the gateway's `429`), wait `retryAfterMs` first. - `isCredentialRefused` is `true`: stop, and alert your operators. Check the key or project id, and the project's origin list, in the hub dashboard. Don't retry meanwhile: 20 refused credentials within a minute lock the caller's IP out of every call for the rest of that minute (`EPX0081`). - A terminal code (`0001`–`0049`): never retry unchanged. Fix the input, or quote again. - Once the user has **sent a transaction** for a quote (the `wrap-native` and `swap-token` lanes), never quote again or ask them to sign again for it: the deposit is on chain. Follow the order instead, with `client.findOrderByTx(sourceChain, transactionHash)` if you lost its id. - A `HubUnavailableError` on a POST may have been accepted. For quotes, just ask again. For submissions, the SDK's recovery has already run: an `OutcomeUnknownError` means follow the order. - Your own abort: do not retry. ## In the browser Through your proxy, the browser gets: - `HubError` for the hub's refusals about the request, relayed unchanged, and for the gateway's rate limits and outages (`429`, `5xx`), with their status and `Retry-After`; - `ProxyError` for everything about your key or your proxy: the proxy never relays a refused key. It reports it to you (`onError`) and answers `PXY0503`, as it does for history while its subject canary fails; a client bug or another refusal about the integration is `PXY0500`; - `HubUnavailableError` when the proxy cannot be reached, and a `ProxyError` `PXY0502` when the proxy cannot reach the hub. With a project id, the browser gets the gateway's refusals directly, as `HubError`s with an `EPX` code and a real status: `EPX0010` (403), for one, means the page's origin is not on your project's list. `GET /orders`, the one route not open to a project id, never gets that far: `projectTransport` refuses it before calling, with an `InteropError` (`invalid-input`). --- # Reference URL: https://docs.c8ntinuum.com/infinity-interop/sdk/reference The SDK's type declarations are the complete reference: every export below has a doc comment in your editor. This page is the map. ## Root: `@c8ntinuum/infinity-interop-sdk` Browser-safe: runs in a browser and in Node 20+. ### Client | Export | What it is | |---|---| | `createInteropClient({ transport, subject?, catalogTtlMs? })` | the client: every flow on this site, over one transport. `subject` is the `X-Infinity-Subject` sent by default on the seven routes that read it; a per-call `subject` overrides it. Without either, the transport's own goes out: `hubTransport`'s `subject` (`default`), or `projectTransport`'s visitor id | | `proxyTransport({ url, headers?, credentials?, fetch?, timeouts? })` | the browser's transport through your proxy. `headers` carry **your** session, never a hub credential. From an https page it refuses, before any call, an `http://` url other than `localhost` (the browser would block it as mixed content). Waits 15 s on reads, 30 s on quotes, 35 s on submissions | | `projectTransport({ baseUrl, projectId, subject?, apiPrefix?, fetch?, timeouts? })` | the browser's transport with your public project id (`c8n_pk_…`), straight to the gateway: `x-project-id` on every call, and `X-Infinity-Subject` on the seven routes that require it. `baseUrl` is the gateway's origin (a trailing `/v1` is accepted). The subject is `subject`, or else one random id per browser and project (`visitor-` and a UUID, kept in `localStorage`): it labels a visitor and proves nothing. It refuses a secret key and a route outside `PROJECT_ROUTES`, before any call | | `PROJECT_ROUTES` | the routes open to a project id: every business route but `GET /orders` (the catalog, the three quote routes, `POST /order`, `POST /order/openfor`, both status views and the four on-chain views) | | `createHubApi(transport, { subject? })` | one typed method per hub route, answers unchanged ([below](#hubapi)); `subject` is the default `X-Infinity-Subject`, as on the client | | `CallOptions` | `subject?`, `signal?`, `timeoutMs?` on any call. `subject` goes out as `X-Infinity-Subject` on the seven routes that read it, and must match the hub's rule (`^[A-Za-z0-9._@+~-]{1,128}$`) | | `Transport`, `TransportRequest`, `TransportResult`, `Timeouts`, `FetchLike` | the transport contract, to write your own | ### Quotes and checks | Export | What it is | |---|---| | `Quote`, `toQuote`, `QuoteClock` | a quote with its lane, amounts and countdown; the clock that anchors countdowns on first receipt | | `quoteTimeLeftMs(quote, now?)` | milliseconds left, by the client's clock | | `baseQuoteRequest(request)` | a prepared quote's request, without the prepare-only fields | | `MAX_QUOTE_VALIDITY_MS` | 30 000 | | `detectLane(response)` | the lane a quote answer discloses; never throws (`unsupported`, with a reason) | | `disclosureKindOf`, `signPayloadShapeOf`, `typedDataOf`, `parseExecutionTx` | readers for a disclosure's parts | | `vetQuote(quote, context)`, `VetError`, `VetReport` | the [pre-signing checks](/infinity-interop/sdk/quotes#the-pre-signing-checks) | | `PERMIT2_ADDRESS`, `MAX_TOKEN_IN_SLIPPAGE_BPS`, `TOKEN_IN_ROUNDING_UNITS` | the checks' constants | | `CatalogIndex` | the [catalog](/infinity-interop/sdk/quotes#the-catalog) over one snapshot | ### Execution | Export | What it is | |---|---| | `executeQuote(context, options)` | what `client.executeQuote` runs ([Executing orders](/infinity-interop/sdk/execution)) | | `ReviewRequiredError`, `isQuoteNotWorse(fresh, reviewed)` | the re-quote rule | | `DEFAULT_MIN_SIGNING_TIME_MS` | 15 000 | | `ensurePermit2Allowance`, `ensureExactAllowance`, `readAllowance` | the approval steps, on their own | | `evmEscrowEnvelope(signature)` | a Permit2 order's signature as the hub takes it: `0x00` followed by the 65-byte signature | | `eip3009Envelope(signature)` | an EIP-3009 signature as the hub takes it: the raw 65 bytes | | `solanaEnvelope(signature, publicKey)` | a Solana order's signature as the hub takes it: `0x00 ‖ signature ‖ public key`, base64 | | `signSolanaOrderMessage(wallet, message, strategy)` | signs a Solana order's message with a [strategy](/infinity-interop/sdk/wallets#how-a-solana-order-is-signed) | | `verifyEd25519(publicKey, signature, message)` | checks an Ed25519 signature with Web Crypto (`null` where the runtime lacks Ed25519) | ### Orders | Export | What it is | |---|---| | `submitOpenFor(api, body, options?)` | `POST /order/openfor` with the [lost-answer recovery](/infinity-interop/sdk/execution#lost-answers). Its options, like those of `trackOrder`, `waitForOrder`, `walkOrders` and `listAllOrders`, take `CallOptions`, `subject` included | | `deriveHubOrderId(quoteId)` | the hub's durable alias for the order submitted against a quote ([below](#derivehuborderid)) | | `isLostSubmission(error)` | whether an error says nothing about the submission's outcome (a transport failure, or an availability or busy code, the gateway's included) | | `trackOrder`, `waitForOrder`, `waitForHubOrder` | following an order ([Tracking](/infinity-interop/sdk/execution#tracking)) | | `TERMINAL_ORDER_STATES`, `isTerminalOrderState`, `orderOutcome` | the aggregator's terminal states: `settled`, `finalized`, `failed`, `refunded` | | `isHubOrderSettled`, `hubOrderOutcome` | the same for the open flow's lifecycle view | | `walkOrders`, `listAllOrders`, `summarizeOrder`, `sortOrdersNewestFirst` | [order history](/infinity-interop/sdk/orders) | | `ORDERS_PAGE_LIMIT` | 500 | ### Refunds | Export | What it is | |---|---| | `checkEvmRefund`, `prepareEvmRefund`, `verifyEvmRefundOnchain`, `refundEvmOrder` | [EVM refunds](/infinity-interop/sdk/refunds#evm-origin-orders) | | `prepareSolanaRefund`, `refundSolanaOrder`, `solanaRefundRefusal` | [Solana refunds](/infinity-interop/sdk/refunds#solana-origin-orders) | | `ESCROW_STATUS` | the settler's `orderStatus` values: `none` 0, `deposited` 1, `claimed` 2, `refunded` 3 | | `SOLANA_REFUND_DISCRIMINATOR`, `SPL_TOKEN_PROGRAM` | what the Solana refund check compares against | ### Wallets | Export | What it is | |---|---| | `evmWalletFromEip1193(provider, options?)`, `jsonRpcProvider(url)` | an EIP-1193 wallet; a read provider from a URL ([Wallets](/infinity-interop/sdk/wallets)) | | `EvmWallet`, `SolanaWallet`, `Wallets` | the [adapter interfaces](/infinity-interop/sdk/wallets#your-own-adapter) | | `typedDataJson(typedData)` | the JSON the wallet signs: the disclosed members unchanged, plus `EIP712Domain` when absent | ### Errors `InteropError`, `HubError` (with `status`, `isCredentialRefused` and `isClientBug`), `HubUnavailableError`, `ProxyError`, `WalletError`, `QuoteExpiredError`, `OutcomeUnknownError`, `RefundError`, `isAbortError`, `classifyHubCode`, `CLIENT_BUG_CODES`, `adviseError`, `adviseHubError` and `DEFAULT_ERROR_MESSAGES`: see [Errors](/infinity-interop/sdk/errors). ### Bytes, units and addresses | Export | What it is | |---|---| | `toBaseUnits(text, decimals)`, `formatUnits(amount, decimals)`, `isWireAmount(value)` | readable amounts ↔ base units (`bigint`, or the wire's decimal strings) | | `parseAssetRef`, `familyOfChainDomain`, `evmChainIdOf` | `:
` references and chain domains | | `sameAddress(family, a, b)`, `isEvmAddress`, `evmAddressOf`, `solanaKeyBytes`, `toBase58Key`, `toHexKey` | address comparison and conversion, per chain family | | `decodeStandardOrder`, `decodeProxyCall`, `encodeApprove`, `encodeAllowance`, `SELECTORS`, `PROXY_SELECTORS`, `MAX_UINT256` | the EVM encodings the SDK reads and writes | | `parseSolanaMessage`, `parseSolanaTransaction`, `unsignedTransaction`, `feePayerOf`, `requiredSignersOf`, `instructionFactsOf`, `encodeCompactU16` | Solana message and transaction bytes | | `base58Encode`/`Decode`, `bytesToBase64`, `base64ToBytes`, `bytesToHex`, `hexToBytes`, `sha256`, `sha256Hex`, `newRequestId` | encodings, hashing, and a UUID v4 (a fresh `preparationId`, for one) | ### The wire contract | Export | What it is | |---|---| | `ROUTES`, `RouteName`, `buildRoutePath`, `matchRoute` | every hub route: method, path under `/v1`, timeout class, answer type, and whether the hub reads `X-Infinity-Subject` there (`subject`) | | `API_PREFIX` | `/v1` | | `BROWSER_ROUTES` | the routes a browser may reach through a proxy: every business route, never the OpenAPI document | | `DEFAULT_TIMEOUTS`, `TimeoutClass` | `read` 12 s, `quote` 25 s, `submit` 15 s: a little longer than the gateway's 10 s and 20 s | | the wire types (`QuoteRequest`, `QuoteResponse`, `OpenForRequest`, `OrderListEntry`, `OnchainFill`…) | the hub's request and answer shapes, with the OpenAPI-generated `paths`, `components` and `operations` | ## Server: `@c8ntinuum/infinity-interop-sdk/server` For your backend: it holds your secret key. | Export | What it is | |---|---| | `hubTransport({ baseUrl, apiKey, subject?, apiPrefix?, fetch?, timeouts?, dangerouslyAllowBrowser? })` | the server's transport: your secret key (`c8n_sk_` and 43 characters) in `x-api-key` on every call, under `/v1`. No sign-in, no session. `baseUrl` is the hub's origin (a trailing `/v1` is accepted). `subject` (default `default`) is the `X-Infinity-Subject` of the calls that name none of your users, on the seven routes where the hub requires one. It refuses a value that is not a secret key (a project id, an old `cif_…` key, a key of the wrong shape) with a message that says so, and refuses to start in a web page unless `dangerouslyAllowBrowser: true`: for a test key on a hub that accepts browser calls (the hub's gateway does not); it then warns in the console | | `HubTransport`, `HubTransportOptions` | its types; `transport.baseUrl` is the hub's origin | | `createInteropProxy(options)`, `toNodeListener(handler)` | the [partner proxy](/infinity-interop/sdk/partner-backend), and its Node `http` adapter | | `InteropProxyOptions` | `transport`, `resolveUser`, `basePath?`, `userScope?` (`"ownership"`, the default, or `"subject"`), `subjectOf?` (the subject for a user, default `user.id`), `routes?`, `ownership?`, `enforceWalletOwnership?`, `filterOrdersByWallet?`, `recoverOpenFor?`, `openForRetryDelayMs?`, `publicCatalog?`, `catalogTtlMs?`, `maxBodyBytes?`, `historyMaxPages?`, `beforeForward?`, `onError?` ([Keeping your users apart](/infinity-interop/sdk/partner-backend#keeping-your-users-apart)) | | `InteropProxy`, `ProxyContext`, `ProxyUser`, `FetchHandler` | the handler (with `checkSubjectScoping()`, the subject canary on demand), what `beforeForward` and `onError` receive, the user `resolveUser` returns | | `verifySubjectScoping(api)`, `SubjectScopingCheck` | the subject canary: lists orders for a never-used subject, which must come back empty and echo it; `{ ok, detail }` | | `memoryOwnershipStore(options?)`, `OwnershipStore` | which user owns which quote and order: the proxy's own record (`userScope: "ownership"`, and `GET /status/{orderId}` either way) | | `createInteropClient` | also exported here, for scripts that use only this entry point | ## EVM: `@c8ntinuum/infinity-interop-sdk/evm` Needs `viem` (2.21 or later). | Export | What it is | |---|---| | `evmWalletFromViem({ walletClient, publicClient?, chains? })` | an `EvmWallet` over a viem or wagmi wallet client | | `evmWalletFromAccount({ account, rpcUrls, chainInstances? })` | an `EvmWallet` over a viem local account, sending through your RPCs | ## Solana: `@c8ntinuum/infinity-interop-sdk/solana` No dependencies. | Export | What it is | |---|---| | `solanaWalletFromStandard(wallet, account, { chain, rpc? })` | a `SolanaWallet` over a Wallet Standard wallet | | `solanaWalletFromKeypair({ secretKey, rpc? })` | a `SolanaWallet` over a key you hold (async) | | `solanaRpc(url)` | a minimal JSON-RPC client: broadcast, confirmation, blockhash, genesis | ## `HubApi` `client.api` (or `createHubApi(transport)`) has one method per hub route. Answers come back as the hub sent them, and every method takes `CallOptions` last. | Method | Route | |---|---| | `getCatalog()` | [`GET /catalog`](/infinity-interop/api/catalog) | | `quote(body)` | [`POST /quote`](/infinity-interop/api/quote) | | `previewQuotes(body)` | [`POST /quote/preview`](/infinity-interop/api/quote-preview) | | `prepareQuote(body)` | [`POST /quote/prepare`](/infinity-interop/api/quote-prepare) | | `openFor(body)` | [`POST /order/openfor`](/infinity-interop/api/order-openfor) | | `createOrder(body)` | [`POST /order`](/infinity-interop/api/order-create) | | `getOrderStatus(orderId)` | [`GET /order/{orderId}/status`](/infinity-interop/api/order-upstream-status) | | `getHubOrderStatus(orderId)` | [`GET /status/{orderId}`](/infinity-interop/api/hub-status) | | `listOrders({ since?, limit? })` | [`GET /orders`](/infinity-interop/api/orders-list): the orders of the call's subject; under `hubTransport`'s shared one (`default`), every order made that way, so never hand it to a browser unfiltered; with a user's `subject`, only that user's | | `getOnchainFill(onchainOrderId)` | [`GET /onchain/{orderId}/fill`](/infinity-interop/api/onchain-fill) | | `getOnchainOrder(onchainOrderId)` | [`GET /onchain/{orderId}/order`](/infinity-interop/api/onchain-order) | | `getOnchainRefund(onchainOrderId, { unwrap? })` | [`GET /onchain/{orderId}/refund`](/infinity-interop/api/onchain-refund) | | `findOrderByTx(chainDomain, txHash)` | [`GET /onchain/tx/{chainDomain}/{txHash}/order`](/infinity-interop/api/onchain-fill-by-tx) | | `call(route, { params?, query?, body? })` | any route by name, returning `{ data, requestId }`; `call("openapi")` returns [`GET /openapi.yaml`](/infinity-interop/api/openapi) as text, with `hubTransport` only | `call` keeps the answer's `requestId`, which the typed methods drop. Use it when you want to log it, for example next to a quote. ## `deriveHubOrderId` ```ts const hubOrderId = await deriveHubOrderId(quote.quoteId); // hex(sha256("infinity:v1:order:" + quoteId)): 64 lowercase hex characters, no 0x ``` The hub files every submission under this id, and `GET /order/{id}/status` answers it like the aggregator's order id. It is how you find an order whose submission answer was lost, without asking the user to sign again. --- # Learn URL: https://docs.c8ntinuum.com/learn Learn pages explain how c8ntinuum works and why its design differs from a standard EVM chain. If you need exact network values or deployment commands, use [Developer Essentials](/developer-essentials). If you are building a mental model of the system, start here. ## Choose a path | Reader | Start with | Then read | |--------|------------|-----------| | EVM developer | [Execution and EVM](/learn/execution-evm) | [EVM Compatibility](/core-concepts/evm-compatibility), [Transaction Lifecycle](/learn/transaction-lifecycle), [Modules & Precompiles](/modules/overview) | | Cosmos or IBC developer | [Interoperability and IBC](/learn/interoperability) | [IBC](/core-concepts/ibc), [Cross-chain Verification](/modules/cross-chain-verification), [ICS20](/modules/cosmos/ics20) | | Protocol reader | [Chain Architecture](/learn/chain-architecture) | [Consensus](/learn/consensus), [Transactions](/core-concepts/transactions), [Precompile Families](/modules/overview) | | Integrator or custodian | [Transaction Lifecycle](/learn/transaction-lifecycle) | [Accounts](/core-concepts/accounts), [Tokens](/core-concepts/tokens), [Exchange and Custody Integration](/guides/exchange-integration) | ## Concept map Accounts, tokens, transactions, IBC, and EVM compatibility. The high-level shape of c8ntinuum's EVM, Cosmos, precompile, and interoperability layers. How Ethereum-compatible execution fits with native module access. Trust-minimized cross-chain communication, IBC, and light-client verification. Native precompile families exposed to Solidity contracts. Submission, validation, mempool handling, execution, receipts, and finality. ## What Learn is for Learn pages are intentionally conceptual. They explain relationships, trust boundaries, and developer implications. Exact ABI signatures, RPC method availability, endpoint limits, and operational runbooks live in Reference, Developer Essentials, and Node Operations pages. --- # Accounts URL: https://docs.c8ntinuum.com/core-concepts/accounts Accounts on c8ntinuum are compatible with both Ethereum and Cosmos SDK tooling. Every account has dual representation for cross-ecosystem compatibility. ## Address Formats ### Hex (Ethereum) ```text 0x91defC7fE5603DFA8CC9B655cF5772459BF10c6f ``` ### Bech32 (Cosmos) ```text c81j800cll9vq7l4rxfke2u74mjgkdlzrr0nfjdlj ``` Both formats reference the **same underlying account**. Use whichever is appropriate for your context — `0x` for EVM interactions, `c8` for Cosmos SDK operations. ## Key Derivation c8ntinuum uses Ethereum-compatible HD key derivation: | Property | Value | |----------|-------| | **Curve** | ECDSA secp256k1 | | **Default HD Path (`ctmd`)** | `m/44'/60'/0'/0/0` | | **Ledger Ethereum HD Path** | `m/44'/60'/0'/0/0` | | **Default Coin Type (`ctmd`)** | 60 (Ethereum) | | **Ledger Ethereum Coin Type** | 60 (Ethereum) | | **Key Type** | `eth_secp256k1` | `ctmd keys add` uses coin type `60` by default, so a mnemonic recovered with `ctmd` derives the same `0x` address as the first MetaMask account for that mnemonic. Coin type `118` (`m/44'/118'/0'/0/0`) is used only when you pass `--coin-type 118`, for example with the Ledger Cosmos app. c8ntinuum supports `eth_secp256k1` account keys. Ledger coin type `60` uses the Ethereum app and creates an `eth_secp256k1` public key; Ledger coin type `118` uses the Cosmos app and creates a Cosmos `secp256k1` public key. ## Account Types | Type | Prefix | Key | |------|--------|-----| | **User accounts** | `c8` / `0x` | `eth_secp256k1` | | **Validator operators** | `c8valoper` | `eth_secp256k1` | | **Consensus nodes** | `c8valcons` | `ed25519` | ## EIP-7702 Status EIP-7702 Set Code is fully supported for EOA code delegation on c8ntinuum. ## Querying Accounts **JSON-RPC** (standard Ethereum): ```bash curl -X POST https://public-evm-rpc.c8ntinuum.com \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x...","latest"],"id":1}' ``` **Cosmos REST API**: ```bash # Mainnet REST/LCD endpoint: curl "https://public-comet-grpc-web.c8ntinuum.io/cosmos/auth/v1beta1/accounts/{address}" # On local nodes, REST uses tcp://127.0.0.1:1317 when enabled: curl "http://127.0.0.1:1317/cosmos/auth/v1beta1/accounts/{address}" ``` --- # Tokens URL: https://docs.c8ntinuum.com/core-concepts/tokens ## Native Token c8ntinuum's native token is **CTM**. It uses 18-decimal precision to match Ethereum's denomination: ```text 1 CTM = 10^18 ctm (base denom) ``` | Field | Value | |-------|-------| | **Official name** | `c8ntinuum` | | **Symbol** | `CTM` | | **Base denom** | `ctm` | | **Decimals** | `18` | This mirrors Ethereum's: ```text 1 ETH = 10^18 wei ``` ## Token Types c8ntinuum supports two categories of assets: ### Cosmos Coins Tokens managed by the Cosmos SDK `x/bank` module. Used for staking, IBC transfers, governance deposits, and gas fees. ### EVM Tokens Standard Ethereum token contracts: - **ERC-20**: Fungible tokens - **ERC-721**: Non-fungible tokens (NFTs) - **ERC-1155**: Multi-token standard All Ethereum token standards work identically on c8ntinuum. ## Wrapped Native Token (WETH9) c8ntinuum ships with a canonical wrapped native token (WETH9-equivalent) pre-deployed at: ```text 0xc8Fb80fCc03f699C70ff0CC08C09106288888888 ``` On c8ntinuum (chain ID `8`) this contract is wrapped CTM: symbol `WCTM`, name `c8ntinuum`, 18 decimals. This provides an ERC-20 interface for CTM, enabling use in DeFi protocols that require ERC-20 tokens. The same address is a different contract on other chains. On Ethereum (chain ID `1`) it is the CTM ERC-20 token, and on BNB Chain (chain ID `56`) it is the CTM BEP-20 token, not wrapped CTM. Always identify the token by chain ID and address. ## Native-to-ERC20 Surfaces c8ntinuum has three related but distinct surfaces: | Surface | Address model | Use | |---------|---------------|-----| | WETH9 | Fixed pre-deployed contract | Standard wrapped CTM for most DeFi integrations. | | Native CTM ERC-20 | Registered token pair for `ctm` at `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (symbol `CTM`) | ERC-20 methods over native CTM balances, with no wrapping. `balanceOf` returns the same amount as the native balance. | | ERC20/WERC20 extensions | Dynamic registered token-pair addresses | Cosmos-native token pairs exposed through ERC-20-compatible methods. | Native CTM can move through a `transfer()` call on `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. Such a transfer appears as an ERC-20 `Transfer` log from that address, not as transaction value, so deposit scanners should track both native value transfers and `Transfer` logs from this address. The [ERC20 extension](/modules/cosmos/erc20) and [WERC20 extension](/modules/cosmos/werc20) depend on registered token-pair addresses. Do not treat the fixed WETH9 address as the address for every WERC20 token-pair surface. ## Token Decimals in Precompiles Cosmos module precompiles take amounts in the base denom `ctm`, the smallest unit of CTM: 1 CTM = 10^18 `ctm`, the same scale as wei. Do not convert to 6 decimals as on other Cosmos chains; check each precompile's expected amount format before interacting with it. --- # Transactions URL: https://docs.c8ntinuum.com/core-concepts/transactions ## Transaction Types c8ntinuum supports two transaction types: ### Ethereum Transactions Standard EVM transactions are submitted through Ethereum-compatible JSON-RPC. | Type | EIP | Description | |------|-----|-------------| | **Legacy** | EIP-155 | Chain ID replay protection | | **Access List** | EIP-2930 | Pre-declared storage access | | **Dynamic Fee** | EIP-1559 | Base fee + priority fee | | **Set Code** | EIP-7702 | EOA code delegation (type 4) | ### Cosmos Transactions Cosmos SDK transactions using Protobuf-encoded messages. Can contain multiple messages in a single transaction (e.g., delegate + vote in one tx). ## Transaction Lifecycle ### 1. Submission Transaction is submitted via JSON-RPC (`eth_sendRawTransaction`) or Cosmos SDK. ### 2. Validation (CheckTx) - Signature verification - Fee and balance checks - Nonce validation - Gas limit verification ### 3. Mempool Valid transactions enter the mempool and are broadcast to validators. c8ntinuum supports nonce gap queuing — transactions with future nonces are queued locally and promoted automatically. ### 4. Execution (FinalizeBlock) - EVM bytecode execution with gas metering - State changes committed to the Cosmos SDK store - Receipt generation with bloom filters and logs ### 5. Finality After a transaction is included in a committed block, c8ntinuum has 1-block finality under standard CometBFT. Expected block time is 4s. ## Transaction Ordering Transactions are ordered by effective tip: - **Ethereum**: `min(gas_tip_cap, gas_fee_cap - base_fee)` - **Cosmos**: `(fee_amount / gas_limit) - base_fee` Higher tips = higher priority, regardless of transaction type. ## Receipts c8ntinuum generates Ethereum-compatible transaction receipts with standard fields: `transactionHash`, `blockHash`, `blockNumber`, `from`, `to`, `gasUsed`, `effectiveGasPrice`, `contractAddress`, `logs`, `logsBloom`, `status` --- # IBC URL: https://docs.c8ntinuum.com/core-concepts/ibc ## Overview c8ntinuum uses IBC (Inter-Blockchain Communication) for **trust-minimized cross-chain messaging**. Unlike bridge protocols that rely on trusted intermediaries, IBC uses light-client verification — safety reduces to the security assumptions of the underlying chains' consensus. c8ntinuum implements IBC v2 (Eureka) and exposes IBC functionality directly to Solidity via the [ICS02](/modules/cross-chain-verification) and [ICS20](/modules/cosmos/ics20) precompiles. IBC v2/Eureka support is a network and release-level capability. Before building production cross-chain logic, confirm the target channel, client type, and enabled precompile behavior against the target c8ntinuum release and the [Precompile ABI Reference](/reference/precompile-abi#ics02-and-ics20). ## Key Components Verifies the state of a counterparty chain using cryptographic proofs. Each client maintains a `ClientState` (parameters) and evolving `ConsensusState` snapshots. c8ntinuum registers the Tendermint light client (`07-tendermint`); the allowed client types are `07-tendermint` and `09-localhost`. Access from Solidity via the `ics02` precompile. Standardized cross-chain token transfer protocol. Supports composable cross-chain payments and asset movement without application-specific bridging logic. Access from Solidity via the `ics20` precompile. 1. **SendPacket** — Application data committed on source chain 2. **RecvPacket** — Destination verifies proof, executes `OnRecvPacket` 3. **Acknowledgement** — Source verifies ack, finalizes or reverts 4. **Timeout** — If deadline passes, source triggers rollback ## IBC from Solidity c8ntinuum's precompiles enable smart contracts to participate directly in IBC flows: ```solidity // Pseudocode: import ICS20I and construct Height timeoutHeight per the ABI reference. ICS20I(0x0000000000000000000000000000000000000802).transfer( "transfer", sourceChannel, denom, amount, address(this), // sender: must be the direct caller of the precompile (this contract holds the tokens) receiver, timeoutHeight, timeoutTimestamp, // channel-N (IBC v1): Unix nanoseconds; IBC v2 client ID: Unix seconds (see below) memo ); ``` The `timeoutTimestamp` rules depend on what you pass as `sourceChannel`. For a channel ID (`channel-N`, IBC v1), it is an absolute Unix time in nanoseconds, and `0` disables the timestamp timeout only when `timeoutHeight` is set, because both cannot be zero. For an IBC v2 client ID, it is an absolute Unix time in seconds that must be non-zero and no more than 24 hours after the current block time; set `timeoutHeight` to zero. The `sender` argument must equal the immediate caller of the precompile: an EOA calling it directly, or the calling contract itself. Otherwise the call reverts with `msg.sender address ... does not match the requester address ...`. Use [Precompile ABI Reference](/reference/precompile-abi#ics02-and-ics20) for the exact `Height` struct, return value, events, and revert behavior. ## Trust Model c8ntinuum's IBC design avoids adding a privileged correctness third party. The trust model is: 1. **Source chain consensus** — validators agree on the packet 2. **Light client verification** — destination chain verifies source state This eliminates the single point of failure that bridge operator committees represent. --- # EVM Compatibility URL: https://docs.c8ntinuum.com/core-concepts/evm-compatibility ## Compatibility Matrix | Feature | Ethereum | c8ntinuum | Status | |---------|----------|-----------|--------| | **EVM bytecode execution** | All opcodes | All opcodes up to the Prague fork; Osaka is not active, so opcodes such as `CLZ` are rejected | Compatible up to Prague | | **Transaction ordering** | Gas price + nonce | Fee priority + nonce | Compatible | | **Nonce management** | Sequential | Sequential + gap queuing | Compatible | | **EIP-1559 fees** | Base fee + tip | Base fee + tip; both are burned | Compatible | | **EIP-7702 (Set Code)** | EOA code delegation | Fully supported | Fully supported | | **EIP-2935 (Block Hashes)** | Historical block hashes | Supported by the EVM stack | Compatible | | **Event logs** | Ethereum event system | Ethereum-compatible logs | Compatible | | **JSON-RPC API** | Standard methods | Ethereum-compatible endpoint | Compatible | | **Smart contract standards** | ERC-20, 721, 1155, etc. | All standards | Compatible | | **Developer tools** | Hardhat, Foundry, Remix | Compatible through standard EVM RPC | Compatible | | **Block time** | ~12 seconds | Expected 4s | Different | | **Finality** | Multiple confirmations are common | 1-block finality under standard CometBFT | Different | | **Reorganizations** | Possible | Governed by CometBFT consensus finality | Different | | **Cross-chain** | Bridge protocols | Native IBC | Enhanced | | **Precompiles** | Standard crypto only | Standard + advanced | Enhanced | The matrix reflects the current c8ntinuum documentation status. For production wallet policy or protocol compatibility gates, verify precompile behavior, gas formulas, and node release version against the target network. ## Forward Compatibility c8ntinuum is **forward-compatible** with Ethereum: - It can run any valid Ethereum smart contract that targets the Prague EVM version or earlier. Compile with `evmVersion` set to `prague` or lower. - It can implement features not yet available on standard Ethereum EVM (zk verification, post-quantum signatures) ## What's Enhanced ### Additional Precompiles Beyond standard Ethereum precompiles (ecrecover, sha256, etc.), c8ntinuum provides: - **Cross-chain**: IBC light clients, ICS-20 transfers, SP1 zk proof verification - **Cryptography**: Ed25519, P-256, Schnorr, Schnorrkel, FROST, ML-DSA, SLH-DSA, VRF - **Hashing**: SHA-3, BLAKE2b, Poseidon, gnark-crypto aligned hashes - **Cosmos modules**: Bank, Staking, Distribution, Governance, Slashing, ERC-20 See [Modules](/modules/overview) for the full catalogue. ### Pre-deployed Contracts 13 widely-used contracts deployed at genesis (Multicall3, Permit2, WETH9, the Safe singleton factory, CreateX, etc.). See [Pre-deployed Contracts](/modules/predeployed-contracts). ### Fee Burn c8ntinuum follows EIP-1559-style fee behavior but burns the whole fee: the base fee and the priority fee are both burned by [`x/feeburn`](/modules/custom/feeburn), so validators receive no fee revenue. The priority fee still sets transaction priority. See [Gas on c8ntinuum](/developer-essentials/gas) for configured fee-market parameters. ## JSON-RPC Notes - Public endpoint method coverage is provider-dependent; see [RPC Reference](/rpc-reference/json-rpc-api) - Mining-related methods (`eth_mining`, `eth_hashrate`, `eth_coinbase`, `eth_getWork`, `eth_submitWork`, `eth_submitHashrate`) are not available and return JSON-RPC error `-32601` (CometBFT consensus, no mining) - `txpool_*` methods require compatible node configuration - Debug tracing is intended for full archive debug surfaces, not the default public RPC --- # Chain Architecture URL: https://docs.c8ntinuum.com/learn/chain-architecture c8ntinuum combines an Ethereum-compatible execution surface with Cosmos-native modules and trust-minimized interoperability. The important idea is not that these layers are separate products; it is that each layer has a clear responsibility and contracts can cross the boundary through precompiles. ## Layer model | Layer | Role | Developer-facing surface | |-------|------|--------------------------| | EVM execution | Runs Solidity bytecode, Ethereum transaction types, logs, receipts, and contract standards. | JSON-RPC, wallets, Foundry, Hardhat, Remix, ethers, viem | | Cosmos-native modules | Maintain chain-native state for bank, staking, distribution, governance, slashing, IBC, and custom policy modules. | Cosmos SDK messages, queries, REST/gRPC, module keepers | | Precompile bridge | Exposes selected native module and cryptographic capabilities to Solidity at stable addresses. | Fixed-address or token-pair precompiles called from EVM contracts | | Interoperability | Verifies and transports cross-chain state and packets through IBC and light-client semantics. | ICS02 and ICS20 precompiles, IBC modules, relayer-facing flows | ## Request flow | Flow | Path through the system | Output | |------|-------------------------|--------| | Standard contract call | EVM JSON-RPC -> EVM execution -> state update | Ethereum-compatible receipt and logs | | Native module call from Solidity | EVM transaction -> precompile -> Cosmos module keeper -> state update | Ethereum-compatible receipt plus native module effects | | Cosmos SDK transaction | Cosmos transaction path -> ante/validation -> module keeper -> state update | Cosmos transaction response and indexed events | | IBC transfer from Solidity | EVM transaction -> ICS20 precompile -> IBC transfer keeper -> packet lifecycle | EVM receipt plus IBC packet handling | ## Trust boundaries - EVM bytecode follows Ethereum execution semantics for contract logic, gas metering, logs, and receipts. - Native modules own their own state and validation rules; precompiles expose controlled entry points into that state. - Cross-chain safety depends on source-chain consensus, light-client verification, packet commitments, acknowledgements, and timeout handling. - Operational controls such as the circuit breaker and IBC breaker are native modules with defined enforcement scope. ## What this means for app developers - Existing EVM applications can start with the standard EVM surface and only use precompiles when they need native functionality. - Contracts that call precompiles should treat them as system interfaces with versioned behavior, not ordinary deployable contracts. - Integrations that read historical state should decide whether JSON-RPC, an indexer, or an archive node is the right source. - Cross-chain applications should design around packet acknowledgement and timeout paths, not only the happy path. ## Next reading - [Execution and EVM](/learn/execution-evm) - [Interoperability and IBC](/learn/interoperability) - [Modules & Precompiles](/modules/overview) - [Circuit Breaker](/modules/custom/circuit) --- # Consensus URL: https://docs.c8ntinuum.com/learn/consensus c8ntinuum uses CometBFT-style Byzantine fault tolerant consensus. Blocks become final after validator agreement rather than waiting through probabilistic proof-of-work confirmations. The expected block time is `4s`. ## Protocol finality vs operational confirmations | Concept | Meaning | Typical use | |---------|---------|-------------| | Protocol finality | A committed block is final after one block under standard CometBFT assumptions. | Smart contracts, wallets, and normal dapp UX can treat included transactions as final. | | Operational confirmations | A business policy that waits additional blocks even after protocol finality. | Exchanges and custodians may wait 60 confirmations to absorb scanner lag, provider delay, and operational risk. | Both statements can be true: the protocol provides one-block finality, while high-value business workflows may choose a larger confirmation policy. ## Developer-facing effects | Area | Effect | |------|--------| | Finality | Transactions are final after one committed block under standard CometBFT assumptions. | | Expected block time | `4s` | | Reorganizations | Application logic should not expect Ethereum-style probabilistic reorg handling. | | Validators | Native staking and validator state are exposed through Cosmos modules and selected precompiles. | | JSON-RPC | Mining-related Ethereum RPC methods such as `eth_mining`, `eth_hashrate`, and `eth_coinbase` are not available and return JSON-RPC error `-32601`. | | Indexing | Indexers should still use cursors, receipt checks, and retry logic because infrastructure can lag even when consensus has finalized. | ## Failure model CometBFT finality assumes less than one-third Byzantine voting power and a network that can eventually make progress. The Learn pages describe developer-facing behavior, not validator operations or incident handling. Operator procedures belong in [Node Operations](/node-ops). ## Related pages - [Transactions](/core-concepts/transactions) - [Transaction Lifecycle](/learn/transaction-lifecycle) - [Differences from Ethereum](/developer-essentials/differences-from-ethereum) - [Deposit Processing](/guides/exchange-integration/deposit-processing) --- # Execution and EVM URL: https://docs.c8ntinuum.com/learn/execution-evm This page is a conceptual overview of how EVM and native execution interact. For the canonical compatibility matrix and JSON-RPC notes, see [EVM Compatibility](/core-concepts/evm-compatibility). For exact precompile signatures, see [Precompile ABI Reference](/reference/precompile-abi). c8ntinuum is a standard EVM chain from the perspective of Solidity contracts and Ethereum tooling. The extension point is that contracts can call native precompiles when they need Cosmos module access, cross-chain verification, cryptographic verification, hashing, or data utilities. ## What stays familiar | Area | Behavior | |------|----------| | Contracts | Solidity bytecode deploys and executes through the EVM. | | Wallets | EVM-compatible wallets sign and submit transactions for chain ID `8` on mainnet. | | Tooling | Foundry, Hardhat, Remix, ethers, viem, and web3.js use standard EVM RPC flows. | | Receipts and logs | EVM transactions produce Ethereum-compatible receipts, status fields, logs, and bloom filters. | | Token standards | ERC-20, ERC-721, ERC-1155, and ordinary Solidity interfaces work as expected. | ## What is extended | Extension | Why it exists | Learn more | |-----------|---------------|------------| | Native module access | Contracts can query or invoke selected Cosmos module behavior without reimplementing it in bytecode. | [Modules & Precompiles](/modules/overview) | | Cross-chain verification | Contracts can participate in IBC and proof-carrying cross-chain flows. | [Cross-chain Verification](/modules/cross-chain-verification) | | Cryptographic verification | Contracts can verify signatures, proofs, and randomness more efficiently than pure Solidity. | [Cryptographic Verification](/modules/cryptographic-verification) | | Hashing and data utilities | Contracts can use native hashing and structured-data helpers. | [Hashing](/modules/hashing), [Data Ergonomics](/modules/data-ergonomics) | ## EVM and Cosmos transaction paths | Transaction path | Submitted through | Executes in | Typical reader | |------------------|-------------------|-------------|----------------| | EVM transaction | `eth_sendRawTransaction` or wallet RPC | EVM, with optional precompile calls into native modules | Solidity developers and dapps | | Cosmos SDK transaction | Cosmos transaction broadcast | Cosmos SDK module handlers | Operators, module integrators, Cosmos tooling | | EVM precompile call | EVM transaction calling a fixed precompile address | Native implementation behind an EVM-compatible call boundary | Hybrid EVM/Cosmos applications | ## Practical rules - Treat precompiles as system interfaces: use the documented addresses and ABI reference for signatures. - Keep exact method signatures in [Precompile ABI Reference](/reference/precompile-abi); Learn pages should explain when and why to call a precompile. - Estimate gas against the target network because native execution and SDK metering can affect costs. - Use [EVM Compatibility](/core-concepts/evm-compatibility) for compatibility differences and [JSON-RPC API](/rpc-reference/json-rpc-api) for method availability. --- # Interoperability and IBC URL: https://docs.c8ntinuum.com/learn/interoperability This page covers the mental model and application-design checklist. For the canonical IBC component overview, packet lifecycle, and trust model, see [IBC](/core-concepts/ibc). c8ntinuum treats interoperability as authenticated communication between replicated state machines. Instead of depending on privileged bridge operators, c8ntinuum emphasizes light-client verification, packet semantics, acknowledgements, and timeout paths. ## Mental model | Concept | Meaning | Developer implication | |---------|---------|-----------------------| | Light client | Verifies another chain's state from cryptographic proofs and consensus updates. | Cross-chain correctness should reduce to consensus and proof verification, not an operator committee. | | Packet | A cross-chain message committed on a source chain and verified on a destination chain. | Applications should model the message as asynchronous. | | Acknowledgement | A response proving the destination chain handled the packet. | Source-chain logic may need an acknowledgement branch. | | Timeout | A safety path when a packet is not completed before its deadline. | User-facing flows need refund or recovery behavior. | | Relayer | Off-chain actor that submits packet data and proofs between chains. | Relayer liveness affects completion time, not the underlying verification model. | ## Solidity access c8ntinuum exposes IBC functionality to smart contracts through precompiles: | Precompile | Role | |------------|------| | ICS02 | Client updates and membership or non-membership verification against a verified counterparty state. | | ICS20 | Fungible token transfer flows that use IBC packet semantics. | Exact signatures live in [Precompile ABI Reference](/reference/precompile-abi#ics02-and-ics20). For a conceptual overview of the precompile family, see [Cross-chain Verification](/modules/cross-chain-verification). ## Application design checklist - Decide whether the contract only initiates a transfer or also tracks acknowledgement/timeout outcomes. - Make timeout values explicit and visible to users. - Treat relayer delay as a normal asynchronous condition. - Keep source and destination address formats clear in UI and logs. - Use the IBC breaker and rate-limiter pages to understand operational safety controls. ## Beyond IBC: Infinity Interop For cross-chain swaps and bridges that leave the IBC trust model — a quote from competing solvers, an order signed or sent by the user, and three status views to follow it — c8ntinuum runs the Infinity hub. Its HTTP API and TypeScript SDK are documented under [Infinity Interop](/infinity-interop). ## Next reading - [IBC](/core-concepts/ibc) - [ICS20](/modules/cosmos/ics20) - [IBC Breaker](/modules/custom/ibc-breaker) - [IBC Rate-Limiter Extension](/modules/custom/ibc-rate-limiter-extension) - [Infinity Interop](/infinity-interop) --- # Modules & Precompiles URL: https://docs.c8ntinuum.com/modules/overview Precompiles are **natively executed system interfaces** exposed at fixed addresses. They are used when computation is too costly in EVM bytecode (e.g., SNARK verification) or when contracts must interface with native chain modules without re-implementing them in Solidity. For canonical addresses, ABI artifacts, import guidance, function signatures, events, reverts, and gas behavior, see the [Precompile ABI Reference](/reference/precompile-abi). c8ntinuum's precompiles are organized into five families. c8ntinuum also includes custom Cosmos modules for validator incentives, validator creation policy, chain availability controls, IBC safety controls, and fee burning. When to use ICS02, ICS20, SP1 verifiers, and heterogeneous verification helpers Bank, Staking, Distribution, Governance, Slashing, ERC-20 surfaces (catalogued below) Validator rewards, minimum self-delegation, circuit breaker, IBC controls, fee burn (catalogued below) Signature, VRF, threshold, and post-quantum verification guidance Native hashing surfaces and encoding guidance JSON, address conversion, and address-compression concepts --- ## Cross-Chain Verification & Interoperability Trust-minimized cross-chain communication via light-client semantics, zkVM verification, and heterogeneous chain support. | Precompile | Purpose | Details | |------------|---------|---------| | **ics02** | Light-client verification | Enables contracts to update verified remote consensus views and validate proof-carrying statements about remote state | | **ics20** | IBC fungible token transfers | Standardized cross-chain token transfer logic, composable payments and asset movement | | **sp1verifiergroth16** | Groth16 proof verification (SP1) | Verifies Groth16 proofs from SP1 programs — "prove off-chain, verify on-chain" | | **sp1verifierplonk** | Plonk proof verification (SP1) | Verifies Plonk proofs from SP1 programs, offering flexibility in proof size and verification cost | --- ## Native Module Access Cosmos-native modules and chain economics accessible from Solidity for hybrid Cosmos-EVM applications. | Precompile | Purpose | Details | Reference | |------------|---------|---------|-----------| | **bank** | Native asset accounting | Balance, supply, and denomination logic as first-class EVM operations | [Details](/modules/cosmos/bank) | | **erc20** | ERC-20 surface for native assets | Bridges Ethereum's token interface into the native token model | [Details](/modules/cosmos/erc20) | | **werc20** | Wrapped native token | WETH-like wrapper for the native token supporting ERC-20-only flows | [Details](/modules/cosmos/werc20) | | **staking** | Delegation state interface | Staking operations and state queries for liquid staking, vaults, automation | [Details](/modules/cosmos/staking) | | **distribution** | Rewards accounting | Programmatic reward handling and auto-compounding strategies | [Details](/modules/cosmos/distribution) | | **slashing** | Fault observability | Slashing and jailing semantics for validator risk assessment | [Details](/modules/cosmos/slashing) | | **gov** | Governance interface | Proposal and voting workflows, governance-driven automation | [Details](/modules/cosmos/governance) | | **valrewards** | Custom incentive surface | Specialized validator reward logic and incentive design | [Details](/modules/custom/validator-rewards) | | **bech32** | Address bridge | Conversion between bech32 and EVM address representations | [Details](/modules/cosmos/bech32) | --- ## Custom Modules c8ntinuum modules add chain-specific policy and operational controls around the standard Cosmos/EVM stack. | Module | Purpose | Details | |--------|---------|---------| | **x/valrewards** | Validator reward points and epoch payouts | [Validator Rewards](/modules/custom/validator-rewards) | | **x/msdcheck** | Minimum self-delegation floor across staking entry points | [Minimum Self-Delegation Check](/modules/custom/minimum-self-delegation) | | **x/circuit** | Global chain availability gate for native and EVM transaction routes | [Circuit Breaker](/modules/custom/circuit) | | **x/ibcbreaker** | Operator-controlled IBC availability gate for restricted outbound IBC initiation paths | [IBC Breaker](/modules/custom/ibc-breaker) | | **x/ibcratelimiterext** | Governance-managed operator whitelist for IBC rate-limiter mutation APIs | [IBC Rate-Limiter Extension](/modules/custom/ibc-rate-limiter-extension) | | **x/feeburn** | Burns all transaction fees, base fee and priority fee, every block | [Fee Burn](/modules/custom/feeburn) | --- ## Cryptographic Constructions Advanced signature schemes, threshold protocols, and verifiable randomness. | Precompile | Purpose | Details | |------------|---------|---------| | **ed25519** | EdDSA verification | Ed25519 signatures for Solana-style and EdDSA-based ecosystems | | **p256** | NIST P-256 verification | Mainstream curve for enterprise crypto and modern authentication | | **schnorr** | Schnorr over secp256k1 | Aggregation-friendly signatures in a widely deployed curve | | **schnorrkel** | Schnorrkel / sr25519 | Verification for Schnorrkel-style ecosystem signatures | | **frost** | FROST signature verification | Verifies a complete FROST signature (single-signer or aggregated) against a caller-supplied key; the caller enforces any threshold or roster policy | | **ecvrf** | Verifiable Random Functions | Publicly verifiable randomness for lotteries, leader election, and randomness beacons | | **pqmldsa** | Post-quantum (ML-DSA) | Lattice-based PQ signature verification (NIST standardized) | | **pqslhdsa** | Post-quantum (SLH-DSA) | Hash-based PQ signatures as complementary design point | --- ## Hashing Surfaces Standardized and ZK-efficient hashing aligned with modern proof systems. | Precompile | Purpose | Details | |------------|---------|---------| | **sha3hash** | SHA-3 family | Standardized hashing for commitments and cryptographic constructions | | **blake2bhash** | BLAKE2b | High-performance hashing for modern protocols | | **poseidonhash** | Poseidon (ZK-efficient) | Optimized for zero-knowledge circuits — matches off-chain proving systems | | **gnarkhash** | gnark-crypto aligned | Practical hash utilities for proof systems and zk engineering | --- ## Data Handling & Ergonomics Developer ergonomics and UX improvements for multi-domain applications. | Precompile | Purpose | Details | |------------|---------|---------| | **json** | Structured payload parsing | Interpret structured messages without brittle custom parsers | | **addresstable** | Address compression | Short-hands for frequently used addresses, reducing calldata and gas | | **bech32** | Address conversion | Conversion between EVM `0x...` and Cosmos `c8...` address formats (also listed under Native Module Access) | --- ## Design Notes - Precompiles are implemented as **native code** for gas-efficiency and correctness - Address assignments are **stable by design** to preserve composability across upgrades - When building protocols that depend on specific precompile behavior (zk verification, PQ signatures), refer to versioned specifications and tests ## Usage in Smart Contracts ```solidity // Pseudocode: import the canonical interfaces for the target release. IBank constant bank = IBank(IBANK_PRECOMPILE_ADDRESS); StakingI constant staking = StakingI(STAKING_PRECOMPILE_ADDRESS); // Query native token-pair balances bank.balances(msg.sender); // Delegate the contract's own funds to a validator. // The delegator must be the direct caller of the precompile (here, the contract). staking.delegate(address(this), validatorAddr, amount); ``` For exact return structs, imports, events, reverts, and gas behavior, use [Precompile ABI Reference](/reference/precompile-abi). Cosmos module precompiles take amounts in the base denom `ctm`, the smallest unit of CTM: 1 CTM = 10^18 `ctm`, the same scale as wei. Do not convert to 6 decimals as on other Cosmos chains; check each precompile's expected amount format before interacting with it. --- # Cross-chain Verification URL: https://docs.c8ntinuum.com/modules/cross-chain-verification Cross-chain verification precompiles let contracts reason about state or messages from other systems without trusting a bridge committee. They are for applications that need proof-carrying cross-chain behavior, not just ordinary local contract calls. ## Surfaces | Surface | Address | Use it for | Status in docs | |---------|---------|------------|----------------| | ICS02 client | `0x0000000000000000000000000000000000000807` | Client updates, membership proofs, and non-membership proofs. | ABI reference available | | ICS20 transfer | `0x0000000000000000000000000000000000000802` | IBC fungible token transfer flows. | Learn page and ABI reference available | | SP1 Groth16 verifier | `0x0000000000000000000000000000000000000600` | Verifying Groth16 proofs from SP1 programs. | ABI and gas schedule available | | SP1 Plonk verifier | `0x0000000000000000000000000000000000000700` | Verifying Plonk proofs from SP1 programs. | ABI and gas schedule available | ## When to use - Use ICS02 when a contract needs to verify a statement about remote chain state. - Use ICS20 when the application flow is token movement over IBC. - Use SP1 verifiers when the expensive computation or state transition is proven off-chain and checked on-chain. ## Design notes | Concern | Guidance | |---------|----------| | Trust model | Safety should reduce to source-chain consensus, proof soundness, and light-client verification. | | Liveness | Relayers or off-chain provers may affect completion time even when verification is trust-minimized. | | Timeouts | Cross-chain flows need explicit timeout and acknowledgement handling. | | Gas | Proof verification can be large; use [Precompile ABI Reference](/reference/precompile-abi#gas-schedule) for formulas. | ## Related pages - [Interoperability and IBC](/learn/interoperability) - [IBC](/core-concepts/ibc) - [ICS20](/modules/cosmos/ics20) - [Precompile ABI Reference](/reference/precompile-abi#ics02-and-ics20) --- # Cryptographic Verification URL: https://docs.c8ntinuum.com/modules/cryptographic-verification Cryptographic verification precompiles expose native implementations for primitives that would be expensive, awkward, or unavailable in ordinary EVM bytecode. They are verification surfaces: contracts pass structured inputs and receive success, failure, or decoded output according to the relevant ABI. ## Surfaces | Surface | Address | Use it for | |---------|---------|------------| | P256 / secp256r1 | `0x0000000000000000000000000000000000000100` | Raw-call P-256 verification for enterprise and passkey-style signatures. | | Ed25519 | `0x0000000000000000000000000000000000000500` | Ed25519 signature verification. | | Schnorr | `0x0000000000000000000000000000000000000703` | Schnorr verification over secp256k1-style ecosystems. | | Schnorrkel | `0x0000000000000000000000000000000000000704` | Schnorrkel / sr25519-style verification. | | ECVRF | `0x0000000000000000000000000000000000000708` | Verifying ECVRF proofs and returning the VRF output. An off-chain key holder produces each proof and sees the output first. | | FROST | `0x0000000000000000000000000000000000000709` | Verifying a complete FROST signature (single-signer or aggregated) against a caller-supplied verification key. The calling contract enforces any threshold, roster, or group-key policy. | | PQMLDSA | `0x0000000000000000000000000000000000000712` | ML-DSA post-quantum signature verification. | | PQSLHDSA | `0x0000000000000000000000000000000000000713` | SLH-DSA post-quantum signature verification. | ## When to use - Use these precompiles when a protocol must verify signatures or proofs from ecosystems that are not covered by standard Ethereum precompiles. - Prefer native precompile verification over pure Solidity implementations for gas-heavy cryptographic operations. - For P256, use a raw low-level call because the ABI reference documents no Solidity ABI for that address. ## Safety notes | Concern | Guidance | |---------|----------| | Input encoding | Follow the exact ABI or raw-call input shape from the canonical reference. | | Failure behavior | Verification failures may return `false`, empty bytes, or revert depending on the interface. | | Domain separation | Contracts remain responsible for message framing, replay protection, and domain separation. | | Upgrade boundary | Treat address, ABI, revert, and gas schedule changes as compatibility-sensitive. | ## Related pages - [Precompile ABI Reference](/reference/precompile-abi) - [Opcode Pricing](/developer-essentials/opcode-pricing) - [Oracles](/tooling-and-infrastructure/oracles) --- # Hashing URL: https://docs.c8ntinuum.com/modules/hashing Hashing precompiles provide native implementations for hash functions used by commitments, proof systems, interoperability protocols, and data structures. They are useful when Solidity-only hashing would be too expensive or when the application needs a hash family that is not built into the EVM. ## Surfaces | Surface | Address | Use it for | |---------|---------|------------| | Gnark hash | `0x0000000000000000000000000000000000000705` | gnark-crypto aligned hash utilities for proof-system integrations. | | SHA3 hash | `0x0000000000000000000000000000000000000706` | SHA-3 family hashing for standardized commitments. | | BLAKE2b hash | `0x0000000000000000000000000000000000000707` | BLAKE2b hashing for high-performance protocol integrations. | | Poseidon hash | `0x0000000000000000000000000000000000000711` | ZK-efficient hashing aligned with proving systems. | ## When to use - Use native hashing when a contract needs a hash family that Ethereum does not expose as a standard opcode or precompile. - Use Poseidon or gnark-aligned utilities when the off-chain proof system expects those hash semantics. - Use SHA-3 or BLAKE2b when integrating with external protocols that commit data using those functions. ## Implementation notes | Concern | Guidance | |---------|----------| | ABI details | Use [Precompile ABI Reference](/reference/precompile-abi) for method signatures and return values. | | Gas | Gnark hash costs `6000 + ceil(input bytes / 32) * 30`. SHA3 and BLAKE2b cost `3000 + ceil(input bytes / 32) * 30`. Poseidon costs a flat `3000`. Input bytes are the full ABI-encoded call data. | | Test vectors | Applications should keep test vectors for every hash function used in a protocol boundary. | | Encoding | Hashes are only as stable as the byte encoding passed into them; define serialization explicitly. | ## Related pages - [Modules & Precompiles](/modules/overview) - [Precompile ABI Reference](/reference/precompile-abi) - [Opcode Pricing](/developer-essentials/opcode-pricing) --- # Data Ergonomics URL: https://docs.c8ntinuum.com/modules/data-ergonomics Data ergonomics precompiles help contracts work with structured payloads and repeated address data. They are not core consensus concepts, but they can reduce glue code and make cross-domain applications easier to build. ## Surfaces | Surface | Address | Use it for | |---------|---------|------------| | JSON | `0x0000000000000000000000000000000000000701` | Reading structured JSON payloads through a native parser. | | Address table | `0x0000000000000000000000000000000000000710` | Short-hands for frequently used addresses, reducing repeated calldata in supported flows. | | Bech32 | `0x0000000000000000000000000000000000000400` | Converting between EVM `0x...` and Cosmos `c8...` address formats. | ## When to use - Use JSON parsing when structured payloads are part of a protocol boundary and native parsing is safer than brittle byte slicing. - Use address conversion when a contract must translate between EVM and Cosmos account representations. - Use address compression when a protocol passes the same addresses repeatedly: `register` an address once, after which `compress` encodes it as its table index and `decompress` reads it back. The interface is `AddressTableI`. ## Design notes | Concern | Guidance | |---------|----------| | Canonical encoding | Define the exact payload shape before relying on parser output. | | Address UX | Label `0x...`, `c8...`, `c8valoper...`, and `c8valcons...` formats clearly in interfaces. | | Failure behavior | Treat parse failure, unknown paths, and unsupported payload shapes as expected error cases. | | Reference boundary | Keep exact ABI signatures in [Precompile ABI Reference](/reference/precompile-abi). | ## Related pages - [Accounts](/core-concepts/accounts) - [Bech32](/modules/cosmos/bech32) - [Precompile ABI Reference](/reference/precompile-abi) --- # Staking URL: https://docs.c8ntinuum.com/modules/cosmos/staking ## Overview The Staking precompile provides a Solidity interface to the Cosmos SDK `x/staking` module. Smart contracts can manage their own delegations and query staking state. `createValidator` and `editValidator` must be called directly by the validator operator's account (an EOA or an EIP-7702 delegated account); calls from a smart contract revert. **Precompile Address**: `0x0000000000000000000000000000000000000800` ## Gas Costs | Method Type | Gas Cost | |-------------|----------| | **Transactions** | `2000 + (30 x bytes of input)` | | **Queries** | `1000 + (3 x bytes of input)` | ## Transaction Methods | Method | Description | |--------|-------------| | `createValidator` | Create a new validator with description, commission, and self-delegation | | `editValidator` | Update validator description and commission | | `delegate` | Delegate tokens to a validator | | `undelegate` | Undelegate tokens (begins unbonding period) | | `redelegate` | Move delegation from one validator to another | | `cancelUnbondingDelegation` | Cancel an in-progress unbonding | ## Query Methods | Method | Description | |--------|-------------| | `delegation` | Get delegation details for a delegator-validator pair | | `unbondingDelegation` | Get unbonding delegation details | | `validator` | Get validator information | | `validators` | List all validators with pagination | | `redelegations` | Get redelegation entries | ## Example ```solidity // Pseudocode: import StakingI and Coin from the canonical interface. StakingI constant staking = StakingI(0x0000000000000000000000000000000000000800); // Delegate 1000 CTM (1000 * 10^18 ctm) from this contract to a validator staking.delegate(address(this), "c8valoper1...", 1000 ether); // Query delegation (uint256 shares, Coin memory balance) = staking.delegation(msg.sender, "c8valoper1..."); ``` In transaction methods, the delegator address must be the direct caller of the precompile. A contract delegates its own tokens by passing `address(this)`; an EOA calls the precompile directly with its own address. Any other address reverts with `msg.sender address ... does not match the requester address ...`. Queries such as `delegation` accept any address. Cosmos module precompiles take amounts in the base denom `ctm`, the smallest unit of CTM: 1 CTM = 10^18 `ctm`, the same scale as wei. Do not convert to 6 decimals as on other Cosmos chains; check each precompile's expected amount format before interacting with it. For exact signatures, structs, return values, and events, see [Precompile ABI Reference](/reference/precompile-abi#staking). --- # Distribution URL: https://docs.c8ntinuum.com/modules/cosmos/distribution ## Overview The Distribution precompile provides access to the Cosmos SDK `x/distribution` module. Smart contracts can claim rewards, query reward balances, and interact with the community pool. On mainnet, `x/distribution` currently receives no automatic rewards. Inflation is 0, `x/feeburn` burns all transaction fees every block before `x/distribution` can allocate them, and community tax and proposer rewards are 0. Unless tokens are deposited with `depositValidatorRewardsPool`, the reward and commission queries return empty amounts, `withdrawDelegatorRewards`, `claimRewards` and `withdrawValidatorCommission` pay nothing, and delegators earn nothing. Validators are paid through the `x/valrewards` subsidy instead; see [Validator Rewards](/modules/custom/validator-rewards). **Precompile Address**: `0x0000000000000000000000000000000000000801` ## Gas Costs | Method Type | Gas Cost | |-------------|----------| | **Transactions** | `2000 + (30 x bytes of input)` | | **Queries** | `1000 + (3 x bytes of input)` | ## Transaction Methods | Method | Description | |--------|-------------| | `setWithdrawAddress` | Set the address that receives staking rewards | | `withdrawDelegatorRewards` | Withdraw rewards from a specific validator | | `withdrawValidatorCommission` | Withdraw validator commission earnings | | `fundCommunityPool` | Send tokens to the community pool | | `claimRewards` | Claim rewards for a delegator with a max retrieval count | | `depositValidatorRewardsPool` | Add tokens to one validator's `x/distribution` rewards (commission first, the rest to its delegators). Takes the validator operator address. Not related to the `x/valrewards` pool | ## Query Methods | Method | Description | |--------|-------------| | `validatorDistributionInfo` | Get a validator's distribution state | | `validatorOutstandingRewards` | Get unclaimed validator rewards | | `validatorCommission` | Get accumulated validator commission | | `delegationTotalRewards` | Get total rewards across all delegations | | `delegatorValidators` | List validators a delegator has staked with | ## Example ```solidity // Pseudocode: import DistributionI from the canonical interface. DistributionI constant dist = DistributionI(0x0000000000000000000000000000000000000801); // Withdraw this contract's rewards from a validator dist.withdrawDelegatorRewards(address(this), "c8valoper1..."); ``` The delegator address must be the direct caller of the precompile. A contract withdraws its own rewards by passing `address(this)`; an EOA calls the precompile directly with its own address. Any other address reverts with `msg.sender address ... does not match the requester address ...`. For exact signatures, return values, and events, see [Precompile ABI Reference](/reference/precompile-abi#distribution). --- # Bank URL: https://docs.c8ntinuum.com/modules/cosmos/bank ## Overview The Bank precompile exposes native Cosmos SDK token supply and balance views. ERC-20-style transfers happen through registered ERC20 extension/token-pair addresses, not through the static Bank precompile. **Precompile Address**: `0x0000000000000000000000000000000000000804` ## Gas Costs The Bank precompile has query methods only. Each method has a fixed gas cost: | Method | Gas Cost | |--------|----------| | `balances` | `2851` | | `supplyOf` | `2477` | | `totalSupply` | `2477` | ## Transfer behavior The Bank precompile has no transfer method. For ERC-20-style transfers, see [ERC20](/modules/cosmos/erc20). ## Query Methods | Method | Signature | Returns | |--------|-----------|---------| | `balances` | `balances(address account)` | `(tuple[](address contractAddress,uint256 amount) balances)` | | `supplyOf` | `supplyOf(address erc20Address)` | `(uint256 totalSupply)` | | `totalSupply` | `totalSupply()` | `(tuple[](address contractAddress,uint256 amount) totalSupply)` | ## Example ```solidity // Pseudocode: import the canonical IBank interface for the target release. IBank constant bank = IBank(0x0000000000000000000000000000000000000804); // Query native token-pair balances exposed through the bank precompile. bank.balances(msg.sender); ``` Cosmos module precompiles take amounts in the base denom `ctm`, the smallest unit of CTM: 1 CTM = 10^18 `ctm`, the same scale as wei. Do not convert to 6 decimals as on other Cosmos chains; check each precompile's expected amount format before interacting with it. For the ABI catalog, see [Precompile ABI Reference](/reference/precompile-abi#bank). --- # Governance URL: https://docs.c8ntinuum.com/modules/cosmos/governance ## Overview The Governance precompile provides a Solidity interface to the Cosmos SDK `x/gov` module. Smart contracts can submit proposals, vote, and query governance state. **Precompile Address**: `0x0000000000000000000000000000000000000805` ## Gas Costs | Method Type | Gas Cost | |-------------|----------| | **Transactions** | `2000 + (30 x bytes of input)` | | **Queries** | `1000 + (3 x bytes of input)` | ## Transaction Methods | Method | Description | |--------|-------------| | `submitProposal` | Submit a JSON proposal with deposit | | `cancelProposal` | Cancel a proposal | | `vote` | Cast a vote on an active proposal | | `voteWeighted` | Cast a weighted vote with split options | | `deposit` | Deposit tokens to a proposal | ## Query Methods | Method | Description | |--------|-------------| | `getConstitution` | Get the active constitution text | | `getProposal` | Get proposal details by ID | | `getProposals` | List proposals with optional status, voter, depositor, and pagination filters | | `getVote` | Get a specific vote record | | `getVotes` | List votes for a proposal | | `getDeposit` | Get a specific deposit record | | `getDeposits` | List deposits for a proposal | | `getTallyResult` | Get current tally for a proposal | | `getParams` | Get governance parameters | ## Example ```solidity // Pseudocode: import IGov and VoteOption from the canonical interface. IGov constant gov = IGov(0x0000000000000000000000000000000000000805); // Vote YES on proposal #1 with this contract's account gov.vote(address(this), 1, VoteOption.Yes, ""); ``` The voter address must be the direct caller of the precompile. A contract votes with its own account by passing `address(this)`; an EOA calls the precompile directly with its own address. Any other address reverts with `msg.sender address ... does not match the requester address ...`. For exact signatures, return structs, and events, see [Precompile ABI Reference](/reference/precompile-abi#governance). --- # ICS20 URL: https://docs.c8ntinuum.com/modules/cosmos/ics20 ## Overview The ICS20 precompile exposes standardized IBC fungible token transfer logic to EVM contracts. Initiate cross-chain token transfers, query IBC denominations (`denom`, `denoms`, `denomHash`), and build composable cross-chain payment flows — all from Solidity. **Precompile Address**: `0x0000000000000000000000000000000000000802` ## Gas Costs | Method Type | Gas Cost | |-------------|----------| | **Transactions** | `2000 + (30 x bytes of input)` | | **Queries** | `1000 + (3 x bytes of input)` | ## Transaction Methods | Method | Signature | Returns | |--------|-----------|---------| | `transfer` | `transfer(string sourcePort,string sourceChannel,string denom,uint256 amount,address sender,string receiver,Height timeoutHeight,uint64 timeoutTimestamp,string memo)` | `(uint64 nextSequence)` | ## Query Methods | Method | Signature | Returns | |--------|-----------|---------| | `denom` | `denom(string hash)` | `(Denom denom)` | | `denomHash` | `denomHash(string trace)` | `(string hash)` | | `denoms` | `denoms(PageRequest pageRequest)` | `(Denom[] denoms, PageResponse pageResponse)` | ## Example ```solidity // Pseudocode: import ICS20I and construct Height timeoutHeight per the ABI reference. ICS20I constant ics20 = ICS20I(0x0000000000000000000000000000000000000802); // Transfer this contract's tokens to another chain ics20.transfer( "transfer", // source port "channel-0", // source channel "ctm", // denomination 1000000, // amount address(this), // sender: must be the direct caller receiver, // receiver on destination chain timeoutHeight, uint64((block.timestamp + 600) * 1e9), // timeout: absolute Unix time in nanoseconds (IBC v1 channel) "" // memo ); ``` `sender` must be the direct caller of the precompile. A contract can send only its own tokens, so it passes `address(this)`; an EOA calls the precompile directly with its own address. Any other address reverts with `msg.sender address ... does not match the requester address ...`. For a `channel-N` (IBC v1) source channel, `timeoutTimestamp` is an absolute Unix time in nanoseconds, not seconds. For an IBC v2 client ID, the precompile passes the value through unchanged and IBC v2 reads it in seconds. ## Trust Model ICS20 transfers inherit IBC's trust-minimized model: - No trusted intermediary required - Safety backed by light-client verification on both chains - Timeouts refund the sender after a relayer submits a timeout proof. Refunds depend on a live relayer and an active counterparty light client; if the client expires or is frozen, the refund waits until the client is recovered (for example through a governance client-recovery proposal) For exact params, events, and return values, see [Precompile ABI Reference](/reference/precompile-abi#ics02-and-ics20). --- # ERC20 URL: https://docs.c8ntinuum.com/modules/cosmos/erc20 ## Overview The ERC20 extension bridges Ethereum's de facto token interface into the native Cosmos token model. This allows existing DeFi contracts and tooling to interoperate with Cosmos-native assets seamlessly. Each native Cosmos denomination registered as a token pair gets a dynamically assigned ERC-20 address. Standard ERC-20 methods (`balanceOf`, `transfer`, `approve`, `transferFrom`, `allowance`) work as expected. **Address**: Dynamic per token pair ## Standard ERC-20 Interface | Method | Description | |--------|-------------| | `name()` | Token name | | `symbol()` | Token symbol | | `decimals()` | Token decimal precision | | `totalSupply()` | Total bank supply of the denom, including tokens held by module accounts | | `balanceOf(address)` | Account balance | | `transfer(address, uint256)` | Transfer tokens | | `approve(address, uint256)` | Approve spender | | `transferFrom(address, address, uint256)` | Transfer from approved allowance | | `allowance(address, address)` | Check approval amount | ## How It Works Native Cosmos tokens are registered as token pairs linking a Cosmos denomination to an ERC-20-compatible address. When you interact with the ERC-20 interface, the extension translates operations to the underlying `x/bank` module, so balances remain consistent across both interfaces. For exact signatures, events, and dynamic address behavior, see [Precompile ABI Reference](/reference/precompile-abi#erc20-and-werc20-extensions). --- # WERC20 URL: https://docs.c8ntinuum.com/modules/cosmos/werc20 ## Overview The WERC20 extension exposes the native CTM bank balance through an ERC-20 interface. It is enabled by the ERC20 keeper for the native token pair rather than by a single fixed precompile address. It holds no separate wrapped balance: `balanceOf` returns the account's native CTM balance. **Address**: `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (mainnet and testnet token pair for the native denom `ctm`; name `c8ntinuum`, symbol `CTM`, 18 decimals) ## Methods Standard ERC-20 methods plus: | Method | Description | |--------|-------------| | `deposit()` | Payable. Sends the attached CTM straight back to the caller and emits `Deposit`; no balance changes | | `withdraw(uint256)` | Reverts if the caller's spendable CTM is below the amount, otherwise emits `Withdrawal`; no tokens move | Plus all standard ERC-20 methods: `balanceOf`, `transfer`, `approve`, `transferFrom`, `allowance`. `deposit()` and `withdraw()` exist only for WETH interface compatibility. Neither wraps nor unwraps anything. ## Usage ```solidity IWERC20 constant wctm = IWERC20(payable(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)); // Read the caller's native CTM balance through the ERC-20 interface uint256 balance = wctm.balanceOf(msg.sender); // WETH-compatible calls: deposit() returns the value to this contract, // withdraw() only checks the balance and emits an event wctm.deposit{value: 1 ether}(); wctm.withdraw(1 ether); ``` For most wrapped-CTM application flows, use the pre-deployed [WETH9](/modules/predeployed-contracts) contract at `0xc8Fb80fCc03f699C70ff0CC08C09106288888888`. WETH9 is a fixed genesis contract; WERC20 addresses are dynamic token-pair surfaces. For exact signatures, events, and dynamic token-pair address behavior, see [Precompile ABI Reference](/reference/precompile-abi#erc20-and-werc20-extensions). --- # Slashing URL: https://docs.c8ntinuum.com/modules/cosmos/slashing ## Overview The Slashing precompile provides access to the Cosmos SDK `x/slashing` module. Query validator signing info, check jailed status, and integrate slashing risk into contract logic. **Precompile Address**: `0x0000000000000000000000000000000000000806` ## Gas Costs | Method Type | Gas Cost | |-------------|----------| | **Transactions** | `2000 + (30 x bytes of input)` | | **Queries** | `1000 + (3 x bytes of input)` | ## Transaction Methods | Method | Description | |--------|-------------| | `unjail` | Unjail a validator after the jail duration has passed and the underlying issue is fixed | ## Query Methods | Method | Description | |--------|-------------| | `getSigningInfo` | Get signing info for a validator consensus address | | `getSigningInfos` | List signing info for validators with pagination | | `getParams` | Get slashing module parameters | ## Use Cases - **Risk assessment**: Check validator jail/slash history before delegating - **Liquid staking**: Monitor validator health and trigger rebalancing - **Insurance**: Build slashing insurance products using on-chain slashing data For exact signatures, return values, and events, see [Precompile ABI Reference](/reference/precompile-abi#slashing). --- # Bech32 URL: https://docs.c8ntinuum.com/modules/cosmos/bech32 ## Overview The Bech32 precompile provides address format conversion between Ethereum hex (`0x...`) and Cosmos Bech32 (`c8...`) representations. Essential for cross-domain UX and contract logic that bridges both address systems. **Precompile Address**: `0x0000000000000000000000000000000000000400` ## Methods | Method | Description | |--------|-------------| | `hexToBech32(address, string)` | Convert hex address to Bech32 with given prefix | | `bech32ToHex(string)` | Convert Bech32 address to hex | ## Example ```solidity // Pseudocode: import Bech32I from the canonical interface. Bech32I constant bech32 = Bech32I(0x0000000000000000000000000000000000000400); // Convert hex to Bech32 string memory cosmosAddr = bech32.hexToBech32( msg.sender, "c8" ); // Convert Bech32 to hex (returns 0x742D35cC6644c068532fDDB11b4c36A58D6d3EAB) address evmAddr = bech32.bech32ToHex("c81wskntnrxgnqxs5e0mkc3knpk5kxk604tum0jdk"); ``` For exact signatures and ABI artifacts, see [Precompile ABI Reference](/reference/precompile-abi#bech32). --- # Validator Rewards URL: https://docs.c8ntinuum.com/modules/custom/validator-rewards `x/valrewards` awards validators using per-epoch points. Rewards are not share-based: each validator listed in the previous block's commit info earns points in proportion to its voting power (whether or not it signed), proposers receive a fixed bonus, and epoch rewards are allocated proportionally to points. The module pays validators only; delegators receive no `x/valrewards` rewards. ## Runtime model - `current_reward_settings` are used for the epoch currently accumulating points. - `next_reward_settings` are staged values that become active at the next epoch boundary. - Changes made during epoch `N` do not alter calculations already in progress for epoch `N`. - Whitelisted accounts can submit runtime setter messages. - Whitelist changes are governance-only through `MsgUpdateParams`. - `PROPOSER_BONUS_POINTS` remains hardcoded and is currently `1`. If `rewarding_paused` is switched to `true` during an epoch, the current epoch still completes and pays normally. The next epoch starts paused, records no new validator points, and creates no new outstanding reward rows. ## Core logic | Area | Behavior | |------|----------| | Points | Every validator listed in the previous block's commit info receives points proportional to voting power, whether it signed, was absent, or voted nil; proposer receives a fixed bonus. | | Epoch rewards | At epoch end, total points split configured `rewards_per_epoch` across validators. | | Claiming | Claims are one-time and single-validator. Outstanding rewards are zeroed after success. | | Sponsor calls | Cosmos msg and EVM precompile claim paths are sponsor-callable; payout always goes to the target validator operator. | | Funding | Claims are paid from the module rewards pool and fail if the pool has insufficient balance. | | Epoch transitions | Payout uses completed epoch settings; only after payout are next settings promoted to current settings. | ## Mainnet values On mainnet (`c8ntinuum_8-1`) the epoch length, reward per epoch, and whitelist differ from the default genesis below, and the rewards pool was funded at genesis: | Setting | Mainnet value | |---------|---------------| | `blocks_in_epoch` (current and next) | `21600` | | `rewards_per_epoch` (current and next) | `45000000000000000000000` (45,000 CTM) | | `rewarding_paused` | `false` | | `params.whitelist` | Three team-controlled operator accounts | | Rewards pool funded at genesis | `16425000000000000000000000ctm` (16,425,000 CTM) | The whitelisted accounts can change `blocks_in_epoch`, `rewards_per_epoch`, and `rewarding_paused` without a governance proposal. Their changes are staged in `next_reward_settings` and take effect at the next epoch boundary. To see the current values, run `ctmd query valrewards params` and `ctmd query valrewards rewards-pool`. ## Default genesis These are the module defaults for a newly generated genesis, not the mainnet values. ```json { "valrewards": { "params": { "whitelist": [] }, "current_reward_settings": { "blocks_in_epoch": 17280, "rewards_per_epoch": "45004521205479450000000", "rewarding_paused": false }, "next_reward_settings": { "blocks_in_epoch": 17280, "rewards_per_epoch": "45004521205479450000000", "rewarding_paused": false }, "epoch_state": { "current_epoch": 0, "blocks_into_current_epoch": 0 }, "epoch_to_pay": 0, "validator_points": [], "validator_outstanding_rewards": [] } } ``` The rewards pool balance is not duplicated inside `app_state.valrewards`; it remains part of the module account balance in auth/bank state. ## Validation rules | Field | Rule | |-------|------| | `blocks_in_epoch` | `int64`, minimum `20`, maximum `6500000` | | `rewards_per_epoch` | Required decimal integer string, minimum `1000000000000000000`, maximum `25000000000000000000000000` | | `rewarding_paused` | Boolean | | `params.whitelist` | Valid bech32 account addresses only; duplicates rejected | | Deposit amount | Valid positive coin amount | | Outstanding rewards | Must use `evmtypes.DefaultEVMDenom`; sum cannot exceed the funded module account balance | | `epoch_to_pay` | Cannot exceed `epoch_state.current_epoch` | | Points/rewards entry epochs | Cannot exceed `epoch_state.current_epoch` | ## Exposed methods ### Cosmos SDK messages - `MsgDepositRewardsPool(depositor, amount)` - `MsgClaimRewards(validator_operator, epoch, requester)` - `MsgSetBlocksInEpoch(signer, blocks_in_epoch)` - `MsgSetRewardsPerEpoch(signer, rewards_per_epoch)` - `MsgSetRewardingPaused(signer, rewarding_paused)` - `MsgUpdateParams(authority, params)` `MsgClaimRewards` is sponsor-callable: any valid requester may submit the transaction, but rewards are still paid only to the target validator account. Runtime setter messages require the signer to be present in `params.whitelist`; `MsgUpdateParams` is authority-only and intended for governance. ### Queries - `Query/RewardsPool` - `Query/ValidatorOutstandingRewards` - `Query/DelegationRewards` - `Query/Params` `Query/ValidatorOutstandingRewards` expects a canonical Bech32 validator operator address. Hex `0x...` addresses are rejected. `Query/DelegationRewards` does not return delegator rewards. Its `delegator` field must be a validator operator's account address (the `c8...` or `0x...` account that owns the `c8valoper...` address). It returns that validator's own outstanding reward for the epoch, the same value as `Query/ValidatorOutstandingRewards`, and fails with `target address must be a validator operator` for any other address. ### EVM precompile Address: `0x0000000000000000000000000000000000000714` - `rewardsPool()` - `validatorOutstandingRewards(epoch, validatorAddress)` - `delegationRewards(delegatorAddress, epoch)` (`delegatorAddress` must be a validator operator's account; see `Query/DelegationRewards` above) - `depositValidatorRewardsPool(depositor, amount)` - `claimRewards(validatorOperatorAddress, epoch)` For exact ABI signatures, see [Precompile ABI Reference](/reference/precompile-abi#validator-rewards). ## CLI ```bash ctmd query valrewards rewards-pool ctmd query valrewards validator-outstanding-rewards ctmd query valrewards delegation-rewards ctmd query valrewards params ctmd tx valrewards deposit ctmd tx valrewards claim ctmd tx valrewards set-blocks-in-epoch ctmd tx valrewards set-rewards-per-epoch ctmd tx valrewards set-rewarding-paused ``` `claim` and `delegation-rewards` take the validator operator's account address (`c8...` or `0x...`), for example the output of `ctmd keys show -a`, not the `c8valoper...` form. `validator-outstanding-rewards` takes the `c8valoper...` form. --- # Minimum Self-Delegation Check URL: https://docs.c8ntinuum.com/modules/custom/minimum-self-delegation `x/msdcheck` enforces a minimum self-delegation floor for validator creation and proposed `MinSelfDelegation` updates across all transaction entry points: Cosmos SDK transactions, API routes, and the EVM staking precompile. The enforced minimum is: ```text 888888000000000000000000 ``` ## Scope and semantics - The rule applies to the validator operator's self-delegation only. - It does not depend on delegations from other users. - A validator can only set or raise `MinSelfDelegation` through create/edit flows. - `EditValidator` is not blocked solely because current validator tokens fell below the threshold after creation; the restriction applies to the proposed `MinSelfDelegation` value. - If an operator later reduces self-delegation below the minimum, for example through undelegation, the staking module's native behavior jails the validator and removes it from the active set. - `x/msdcheck` does not block undelegate or redelegate. ## Default genesis `x/msdcheck` does not have an independent module genesis section in `app_state`. Practical implications: - There is no `app_state.msdcheck` object to configure. - Staking genesis (`app_state.staking`) still applies as usual. - The minimum self-delegation check is enforced by module logic, not by msdcheck-specific genesis params. ## Covered scenarios | Scenario | Expected result | |----------|-----------------| | `MsgCreateValidator` below minimum | Reject | | `MsgCreateValidator` at exact minimum | Accept | | `MsgCreateValidator` above minimum | Accept | | `MsgEditValidator` below current stake threshold but without invalid `MinSelfDelegation` change | Accept | | `MsgEditValidator` proposed `MinSelfDelegation` below minimum | Reject | | EVM staking precompile `createValidator` below minimum | Reject | | EVM staking precompile `createValidator` at exact minimum | Accept | | EVM staking precompile `createValidator` above minimum | Accept | ## Test commands Run the tests from the root of the c8ntinuum source repository (see [Build the Node](/node-ops/build-the-node) for prerequisites). The tests live in the `evmd` Go module and link against the Rust SP1 verifier library, so run them through the top-level Makefile, which builds that library and sets the library path: ```bash make test GO_PKGS=./tests/integration/staking GO_TEST_FLAGS="-count=1 -run TestMSDCheck" ``` The command runs `TestMSDCheckIntegration` (Cosmos SDK messages) and `TestMSDCheckPrecompile` (EVM staking precompile). --- # Circuit Breaker URL: https://docs.c8ntinuum.com/modules/custom/circuit `x/circuit` is a global circuit breaker for the chain. It exposes a `system_available` flag and a whitelist of accounts authorized to toggle it. Whitelist management is governance-controlled. ## Core logic - `system_available` is a single boolean stored in module state. - Only whitelisted accounts can submit `MsgUpdateCircuit`. - Repeating the current `system_available` value succeeds without rewriting state. - An empty whitelist is allowed while `system_available=true`. - An empty whitelist is rejected while `system_available=false`, so at least one operator remains able to restore availability. - `MsgUpdateParams` updates the whitelist and requires the governance authority address. - gRPC query handlers reject nil request objects explicitly. ## Enforcement scope - The circuit gate is enforced in ante for native Cosmos SDK transactions and EVM extension transactions. - `eth_sendRawTransaction` submissions become `MsgEthereumTx` and pass through ante checks, so they are also gated. - When `system_available=false`, only transactions containing `MsgUpdateCircuit` messages are allowed through the circuit gate. - Any transaction containing non-`MsgUpdateCircuit` messages is rejected with `ErrUnauthorized` and `system unavailable`. ## Default genesis ```json { "circuit": { "params": { "whitelist": [] }, "state": { "system_available": true } } } ``` On mainnet (`c8ntinuum_8-1`) the whitelist is not empty. It holds three team-controlled operator accounts, which can halt and resume transactions chain-wide by setting `system_available` without a governance proposal. They cannot move funds or change the whitelist through this module. To see the current list, run `ctmd query circuit whitelist`. ## Exposed methods ### Cosmos SDK messages - `MsgUpdateCircuit(signer, system_available)` - `MsgUpdateParams(authority, params)` ### Queries - `Query/SystemAvailable` - `Query/Whitelist` ### CLI ```bash ctmd query circuit system-available ctmd query circuit whitelist ctmd tx circuit update-circuit ``` ## Test coverage The source test set covers CLI flow, governance whitelist updates, no-op state-write behavior, keeper nil-request checks, empty whitelist recovery guard, native and EVM ante gate behavior, JSON-RPC route behavior, and query correctness. --- # IBC Breaker URL: https://docs.c8ntinuum.com/modules/custom/ibc-breaker `x/ibcbreaker` provides an operator-controlled IBC breaker with one global `ibc_available` flag, a whitelist of operator accounts that can toggle the flag, and governance-controlled whitelist updates. The practical goal is to stop selected outbound IBC initiation paths, including ICS20 transfer initiation (`MsgTransfer`), when the breaker is disabled. ## Non-scope `x/ibcbreaker` is not a full transfer freeze. Internal non-IBC transfers, for example bank `MsgSend`, remain available. ## State and params ```json { "ibcbreaker": { "params": { "whitelist": [] }, "state": { "ibc_available": true } } } ``` - `state.ibc_available` is stored as a single byte and defaults to `true` if missing. - `params.whitelist` is a list of bech32 addresses allowed to submit `MsgUpdateIbcBreaker`. - Whitelist entries are validated for address format and duplicates. On mainnet (`c8ntinuum_8-1`) the whitelist is not empty. It holds three team-controlled operator accounts, which can halt and resume the restricted IBC paths by setting `ibc_available` without a governance proposal. They cannot move funds or change the whitelist through this module. To see the current list, run `ctmd query ibcbreaker whitelist`. ## Authorization model - `MsgUpdateIbcBreaker` signer must be bech32 and present in `params.whitelist`. - Repeated requests for the current `ibc_available` value succeed as a no-op. - `MsgUpdateParams` is authority-only and is the only path to update the whitelist. - `MsgUpdateParams.ValidateBasic()` rejects nil params before handler execution. ## Enforcement logic Enforcement is implemented in the Cosmos ante decorator `IbcAvailableDecorator` and in transfer keeper execution for `MsgTransfer`. When `ibc_available=false`: - Cosmos ante scans transaction messages and rejects restricted IBC message types with `ErrUnauthorized`. - Ante errors include the offending restricted message type URL. - `authz.MsgExec` contents are recursively scanned. - Recursion depth `>= 7` is rejected as unauthorized. - Transfer keeper rejects `MsgTransfer` with `ErrUnauthorized` and `ibc unavailable`. Restricted message types include: - `/ibc.core.client.v1.MsgCreateClient` - `/ibc.core.connection.v1.MsgConnectionOpenInit` - `/ibc.core.channel.v1.MsgChannelOpenInit` - `/ibc.applications.transfer.v1.MsgTransfer` - `/ibc.applications.interchain_accounts.controller.v1.MsgRegisterInterchainAccount` - `/ibc.applications.interchain_accounts.controller.v1.MsgSendTx` - `/ibc.core.client.v2.MsgRegisterCounterparty` - `/ibc.core.client.v2.MsgUpdateClientConfig` - `/ibc.core.channel.v2.MsgSendPacket` ## EVM and ICS20 scope Cosmos IBC ante decoration is not applied to the EVM extension transaction route. However, ICS20 precompile transfer still routes into the transfer keeper, so `MsgTransfer` is blocked when `ibc_available=false`. Non-restricted IBC messages can still pass this decorator. ## Exposed methods ```bash ctmd query ibcbreaker ibc-available ctmd query ibcbreaker whitelist ctmd tx ibcbreaker update-ibcbreaker ``` Cosmos SDK messages: - `MsgUpdateIbcBreaker(signer, ibc_available)` - `MsgUpdateParams(authority, params)` Queries: - `Query/IbcAvailable` - `Query/Whitelist` --- # IBC Rate-Limiter Extension URL: https://docs.c8ntinuum.com/modules/custom/ibc-rate-limiter-extension `x/ibcratelimiterext` adds an independent, governance-managed operator whitelist for IBC rate-limiter operations. Packet enforcement remains the same as upstream ratelimiting middleware. The extension stores its own whitelist, lets governance update that whitelist through `MsgUpdateParams`, and lets whitelisted operators call rate-limiter mutation APIs. ## Operation authorization An address can operate the rate limiter if it can execute: - `MsgAddRateLimit` - `MsgUpdateRateLimit` - `MsgRemoveRateLimit` - `MsgResetRateLimit` Authorization passes when either: 1. The signer is the ratelimiting authority, normally governance authority. 2. The signer is in the `ibcratelimiterext` whitelist. Authority authorization is evaluated on the canonical decoded bech32 address, not raw input string casing. ## Architecture | Area | Behavior | |------|----------| | Independent whitelist module | `x/ibcratelimiterext` owns `Params.whitelist`, `MsgUpdateParams`, and `Query/Whitelist`. | | Ratelimiter service override | Keeps ratelimiting data plane, queries, and genesis, but overrides `ratelimit.v1.Msg` authorization. | | Middleware stack v1 | `transfer -> erc20 -> callbacks -> ratelimiting -> channel` | | Middleware stack v2 | `transferv2 -> erc20v2 -> ratelimitingv2` | ## Default genesis ```json { "ibcratelimiterext": { "params": { "whitelist": [] } } } ``` `params.whitelist` contains bech32 account addresses allowed to execute rate-limiter write operations through this extension. Default is empty, so only ratelimiter authority can operate those commands until whitelist entries are added. On mainnet (`c8ntinuum_8-1`) the whitelist is not empty. It holds three team-controlled operator accounts, which can add, update, remove, or reset IBC rate limits without a governance proposal. To see the current list, run `ctmd query ibcratelimiterext whitelist`. The upstream `ratelimit` module has its own separate genesis in `app_state.ratelimit`, including `hour_epoch`. `ibcratelimiterext` does not store or initialize those fields. ## Query CLI The extension adds only the `whitelist` query. Rate-limit queries come from the upstream `ratelimit` module: ```bash ctmd query ibcratelimiterext whitelist --node --chain-id ctmd query ratelimit list-rate-limits --node --chain-id ctmd query ratelimit rate-limit channel-0 --denom ibc/ABCDEF123 --node --chain-id ctmd query ratelimit rate-limits-by-chain osmosis-1 --node --chain-id ``` ## Transaction CLI The transaction examples set `--gas-prices` to `222000000000ctm` (222 gwei), the chain's minimum gas price. The gas price must also be at least the current base fee, so use a higher value when the base fee is above 222 gwei. See [Gas on c8ntinuum](/developer-essentials/gas). Whitelist updates are authority-only. The authority is the `x/gov` module account (`c810d07y265gmmuvt4z0w9aw880jnsr700j335k0a` on mainnet), which has no key, so submit the update as a governance proposal. `MsgUpdateParams` replaces the whole whitelist, so list every address that should stay on it. Set `deposit` to at least the governance minimum deposit (see `ctmd query gov params`). ```json { "messages": [ { "@type": "/cosmos.evm.ibcratelimiterext.v1.MsgUpdateParams", "authority": "c810d07y265gmmuvt4z0w9aw880jnsr700j335k0a", "params": { "whitelist": ["c8...", "c8..."] } } ], "metadata": "", "deposit": "ctm", "title": "Update IBC rate-limiter operator whitelist", "summary": "" } ``` ```bash ctmd tx gov submit-proposal proposal.json \ --from \ --chain-id \ --node \ --gas auto --gas-adjustment 1.4 \ --gas-prices 222000000000ctm ``` Rate-limit write commands require either governance authority or a whitelisted operator. They live under the upstream `ratelimit` module and take flags, not positional arguments. The extension only decides whether the signer is authorized: ```bash ctmd tx ratelimit add-rate-limit \ --channel-or-client-id channel-0 \ --denom ibc/ABCDEF123 \ --max-percent-send 10 \ --max-percent-recv 20 \ --duration-hours 24 \ --from \ --chain-id \ --node \ --gas auto --gas-adjustment 1.4 \ --gas-prices 222000000000ctm ctmd tx ratelimit update-rate-limit \ --channel-or-client-id channel-0 \ --denom ibc/ABCDEF123 \ --max-percent-send 15 \ --max-percent-recv 25 \ --duration-hours 24 \ --from \ --chain-id \ --node \ --gas auto --gas-adjustment 1.4 \ --gas-prices 222000000000ctm ctmd tx ratelimit remove-rate-limit \ --channel-or-client-id channel-0 \ --denom ibc/ABCDEF123 \ --from \ --chain-id \ --node \ --gas auto --gas-adjustment 1.4 \ --gas-prices 222000000000ctm ctmd tx ratelimit reset-rate-limit \ --channel-or-client-id channel-0 \ --denom ibc/ABCDEF123 \ --from \ --chain-id \ --node \ --gas auto --gas-adjustment 1.4 \ --gas-prices 222000000000ctm ``` --- # Fee Burn URL: https://docs.c8ntinuum.com/modules/custom/feeburn `x/feeburn` burns every transaction fee the chain collects. At the start of each block it takes the whole `fee_collector` balance, which holds the fees paid in the previous block, and destroys it. This covers the full fee of EVM and Cosmos SDK transactions: the EIP-1559 base fee and the priority fee (tip) alike. Validators and delegators receive no fee revenue; validators are paid through [Validator Rewards](/modules/custom/validator-rewards) instead. ## Core logic - Fees are collected into the standard `fee_collector` module account during the block. For EVM transactions, unused gas is refunded from `fee_collector` before the block ends, so what remains is the fee actually charged (see [Gas on c8ntinuum](/developer-essentials/gas)). - In `BeginBlock` of the next block, `x/feeburn` reads all balances held by `fee_collector`, moves them to the `feeburn` module account, and burns them from there. The `feeburn` module account holds the `Burner` permission. - Every denom held by `fee_collector` is burned in one pass. On mainnet, fees can only be paid in CTM, so in practice this is the CTM fee total. - If `fee_collector` is empty, the module does nothing for that block. - The burn runs after the `x/feemarket` and `x/vm` begin blockers and before `x/distribution`, so `x/distribution` always finds an empty fee pot and allocates nothing to validators or the community pool. - The priority fee still sets transaction priority in the mempool, but it is burned together with the base fee rather than paid to the block proposer. Because the burn happens at the start of the following block, querying `fee_collector` at the latest height can show a nonzero balance: those are the fees of the latest block, which are burned when the next block begins. ## Parameters and genesis `x/feeburn` is stateless. It has no parameters, no store, no transactions, and no queries, so there is no setting that governance can change. ```json { "feeburn": {} } ``` Mainnet genesis contains this empty `app_state.feeburn` object. ## Module accounts | Account | Address | Role | |---------|---------|------| | `fee_collector` | `c817xpfvakm2amg962yls6f84z3kell8c5lm2ynjn` | Collects transaction fees during a block | | `feeburn` | `c81d99pujdz48wkmn3uesfxnrctn44uuf5c5g7mys` | Receives the `fee_collector` balance and burns it (`Burner` permission) | ## Verify on mainnet Query the fee pot and the total CTM supply through the public REST endpoint: ```bash curl -s "https://public-comet-grpc-web.c8ntinuum.io/cosmos/bank/v1beta1/balances/c817xpfvakm2amg962yls6f84z3kell8c5lm2ynjn" curl -s "https://public-comet-grpc-web.c8ntinuum.io/cosmos/bank/v1beta1/supply/by_denom?denom=ctm" ``` Mainnet inflation is `0`, so no CTM is minted, and the total supply falls by the amount of fees burned. The `feeburn` module account balance is always empty between blocks. ## Test commands Run the test from the root of the c8ntinuum source repository (see [Build the Node](/node-ops/build-the-node) for prerequisites). The tests live in the `evmd` Go module and link against the Rust SP1 verifier library, so run them through the top-level Makefile: ```bash make test GO_PKGS=./tests/integration GO_TEST_FLAGS="-count=1 -run TestFeeBurnIntegration" ``` The test seeds `fee_collector` with CTM and a second denom, advances one block, and checks that `fee_collector` is empty, that the second denom's total supply is back to zero, and that `x/distribution` outstanding rewards and the community pool did not grow. --- # Pre-deployed Contracts URL: https://docs.c8ntinuum.com/modules/predeployed-contracts c8ntinuum ships with a curated set of **pre-deployed ecosystem contracts** at well-known addresses. In source and genesis these are called `preinstalls`: contract runtime bytecode placed at fixed addresses during chain initialization. This removes the bootstrap tax of deploying basic infrastructure and ensures immediate compatibility with existing wallets, SDKs, and protocol tooling. These contracts are **identical or bytecode-equivalent** to widely used deployments on Ethereum and major rollups, unless otherwise noted. ## Contract Directory | Name | Address | Reference | Notes | |------|---------|-----------|-------| | **Create2** | `0x4e59b44847b379578588920cA78FbF26c0B4956C` | Bytecode only | Deterministic deployment proxy; Foundry's default CREATE2 factory | | **Create2Deployer** | `0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2` | Bytecode only | Deterministic deployment helper with `deploy`, `deployERC1820Implementer`, `computeAddress`, and `computeAddressWithDeployer`. It has no owner or pause functions, so its bytecode differs from the Ethereum contract at this address | | **Multicall3** | `0xcA11bde05977b3631167028862bE2a173976CA11` | Bytecode only | Batched read/write calls | | **Permit2** | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | Bytecode only | Uniswap shared approvals and permit flows | | **Safe singleton factory** | `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` | Bytecode only | Safe canonical singleton factory | | **EIP-2935 history storage** | `0x0000F90827F1C53a10cb7A02335B175320002935` | Go-ethereum `params.HistoryStorageCode` | Historical block hash storage | | **ERC1820 registry** | `0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24` | ABI committed under preinstalls | Interface registry and pseudo-introspection | | **EIP2470 singleton factory** | `0xce0042B868300000d44A59004Da54A005ffdcf9f` | ABI committed under preinstalls | Deterministic singleton factory | | **CreateX** | `0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed` | Source and ABI committed under preinstalls | Universal CREATE/CREATE2/CREATE3 deployer | | **MultiSend** | `0x998739BFdAAdde7C933B942a68053933098f9EDa` | Source and ABI committed under preinstalls | Safe batch executor using delegatecall | | **MultiSendCallOnly** | `0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B` | Source and ABI committed under preinstalls | Safe batch executor without delegatecall | | **UniversalSigValidator** | `0x7DD271fA79df3a5Feb99F73BebFA4395b2E4F4Be` | Source and ABI committed under preinstalls | EIP-6492 / ERC-1271 signature validator | | **WETH9** | `0xc8Fb80fCc03f699C70ff0CC08C09106288888888` | Bytecode only | Wrapped CTM with the WETH9 ABI (name `c8ntinuum`, symbol `WCTM`, 18 decimals). Its bytecode differs from Ethereum's WETH9 | ## Source of truth | Surface | Source | |---------|--------| | Mainnet preinstall list | `app_state.evm.preinstalls` in the [published mainnet genesis](https://resources.c8ntinuum.io/mainnet/genesis.json) | | VM genesis schema | `proto/cosmos/evm/vm/v1/genesis.proto` and `proto/cosmos/evm/vm/v1/evm.proto` | | Runtime registration | `x/vm/keeper/preinstalls.go` | | EVM genesis initialization | `x/vm/genesis.go` | | Go default preinstalls | `x/vm/types/preinstall.go` | | Governance registration message | `proto/cosmos/evm/vm/v1/tx.proto` | | Local contracts package | `contracts/package.json` | `x/vm/types.DefaultPreinstalls` and the published mainnet genesis list the same 13 entries. Treat the published mainnet genesis as the canonical list for the current chain configuration. The other paths in this table are paths inside the `ctmd` node source, which will be published in a public GitHub repository (repository TBA). ## Genesis and runtime behavior The `Preinstall` schema has three fields: | Field | Meaning | |-------|---------| | `name` | Human-readable contract name. It is not part of EVM execution. | | `address` | Ethereum hex address where code will be installed. `0x` prefixes and mixed case are accepted. | | `code` | Hex-encoded deployed runtime bytecode. `0x` prefixes are accepted. Empty code and empty-code hashes are rejected. | During genesis initialization, `x/vm/genesis.go` calls `Keeper.AddPreinstalls`. For each entry the keeper converts the address, computes the Keccak-256 code hash, rejects empty or conflicting code, creates an account, stores the code hash, and stores the code bytes. No storage is initialized for preinstalls. These contracts execute as ordinary EVM bytecode after genesis, so calls use normal EVM gas accounting and normal Solidity revert data. There is no special precompile-style gas schedule. On genesis export, preinstalls are exported as ordinary EVM contract accounts and the `preinstalls` array is empty. Governance can register more preinstalls through `MsgRegisterPreinstalls`, but only the configured VM authority, normally the Cosmos SDK governance module account, may execute that message. ## Package and import guidance The local contracts package is named `cosmos-evm-contracts`, but `contracts/package.json` does not define `main`, `files`, `exports`, or `types`. Treat this tree as repo-local contract assets, not as a stable npm subpath API. Do not promise npm imports like `cosmos-evm-contracts/preinstalls/createx/abi/abi-createx.json` unless the package is formalized later. Current consumers should vendor or copy the files they need, or import them by relative path from a checkout of the node source repository once it is published on GitHub (repository TBA). The preinstall ABI JSON files are raw ABI arrays, not Hardhat artifact objects with an `abi` wrapper field. No generated TypeScript binding package is committed for preinstalls; consumers who want typed ethers factories should generate TypeChain bindings from the committed ABI JSON files or Solidity sources. ## Tooling examples ### Hardhat and ethers ```ts const CREATEX_ADDRESS = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed"; const signer = await ethers.provider.getSigner(); const createX = new ethers.Contract(CREATEX_ADDRESS, createXAbi, signer); const predicted = await createX.computeCreate2Address(salt, initCodeHash); ``` ### viem ```ts const CREATEX_ADDRESS = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed"; const createXAbi = createXAbiJson as Abi; const publicClient = createPublicClient({ transport: http("https://public-evm-rpc.c8ntinuum.com"), }); declare const salt: Hex; declare const initCodeHash: Hex; await publicClient.readContract({ address: CREATEX_ADDRESS, abi: createXAbi, functionName: "computeCreate2Address", args: [salt, initCodeHash], }); ``` ### Foundry Foundry can compile Solidity sources once the preinstall tree is vendored or remapped into the project: ```text continuum-preinstalls/=lib/continuum/contracts/solidity/preinstalls/ ``` ```solidity contract UsesCreateX { CreateX constant CREATEX = CreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); } ``` For bytecode-only entries such as Multicall3, Permit2, and WETH9, use the canonical external ABI expected by the tooling integration, or add a local interface in the consuming project. ## Design Considerations - **Bytecode parity** — Where possible, exact on-chain bytecode from Ethereum mainnet is reused to maximize compatibility with existing tools and audits - **Stable addresses** — These addresses are stable across network upgrades. Breaking changes require migration tooling - **Safety** — Contracts like `MultiSendCallOnly` provide safer defaults (no DELEGATECALL) - **Versioning** — Address changes, bytecode changes, removed ABI entries, changed ABI entries, or changed genesis import semantics are breaking release changes ## Usage Notes - You can **assume the presence** of these contracts when writing dApps and tooling for c8ntinuum - A fresh `local_node.sh` chain also includes these preinstalls: `ctmd init` writes the default set into the EVM genesis state, and the script does not remove it - If you rely on specific behavior (gas costs, revert semantics), refer to upstream source repositories and c8ntinuum's pinned test expectations --- # Transaction Lifecycle URL: https://docs.c8ntinuum.com/learn/transaction-lifecycle This page is a side-by-side EVM/Cosmos lifecycle walkthrough with operational guidance for senders. For the canonical reference on transaction types, ordering, and receipt fields, see [Transactions](/core-concepts/transactions). c8ntinuum accepts both Ethereum-style EVM transactions and Cosmos SDK transactions. For EVM users, the normal path is familiar: sign, submit, wait for inclusion, read the receipt. The difference is that an EVM transaction can also call precompiles that execute native chain logic. ## Lifecycle | Step | EVM transaction | Cosmos SDK transaction | |------|-----------------|------------------------| | Signing | Wallet or signer creates an Ethereum transaction for the EVM chain ID. | Cosmos signer creates a Protobuf-encoded transaction with one or more messages. | | Submission | JSON-RPC, usually `eth_sendRawTransaction` or wallet RPC. | Cosmos transaction broadcast endpoint or CLI. | | Validation | Signature, nonce, balance, fee, gas, and route checks. | Signature, account, fee, gas, and message route checks. | | Mempool | Valid transactions wait for ordering and block inclusion. | Valid transactions wait for ordering and block inclusion. | | Execution | EVM bytecode runs; precompile calls may invoke native modules. | Native module message handlers run. | | Output | Ethereum-compatible receipt, logs, status, and gas fields. | Cosmos transaction response and indexed events. | | Finality | Final after one committed block under standard CometBFT assumptions. | Final after one committed block under standard CometBFT assumptions. | ## Ordering and nonces Transactions are ordered by effective tip while preserving nonce semantics for each EVM account. c8ntinuum supports nonce gap queuing, but applications should still manage nonces carefully for high-throughput senders. | Concern | Guidance | |---------|----------| | Sequential senders | Track the latest pending nonce and serialize retries. | | Parallel senders | Allocate nonces centrally or shard sender accounts. | | Replacement transactions | Use current fee data and preserve the same nonce. | | Custody crediting | Confirm receipt status and logs; do not credit from pending transaction data. | ## Finality and confirmations Protocol finality is one block under standard CometBFT assumptions. Business workflows may wait longer for operational reasons. For example, exchange deposit processing recommends 60 confirmations to absorb RPC, log, receipt, and scanner lag. ## Related pages - [Transactions](/core-concepts/transactions) - [Consensus](/learn/consensus) - [Gas on c8ntinuum](/developer-essentials/gas) - [Best Practices](/developer-essentials/best-practices) - [Deposit Processing](/guides/exchange-integration/deposit-processing) --- # Node Operations URL: https://docs.c8ntinuum.com/node-ops This section is for operators running c8ntinuum infrastructure: installation and requirements first, day-to-day operations next, then advanced data and event surfaces. Minimum and recommended hardware profiles for c8ntinuum operators. Build and install the `ctmd` binary from source once the repository is published. Installation checklist for a c8ntinuum full node. Recommended bootstrap path for new non-archive nodes. Manual mainnet install, validator creation, and links to staking concepts. Routine sync, peer, log, disk, RPC, and alerting checks for running nodes. Canonical upgrade runbook location. Recovery paths for stuck, lagged, corrupted, or out-of-disk nodes. Peer health, block lag, and network exposure checks for block delivery. WebSocket endpoints, event subscriptions, and connection planning. Archive and historical-data operator guidance. --- # Hardware Requirements URL: https://docs.c8ntinuum.com/node-ops/hardware-requirements These are conservative operator defaults, not benchmark-certified capacity limits. Validate public RPC throughput and storage headroom against live traffic before committing production SLOs. ## How to read these requirements These profiles assume Linux `x86_64` hosts, production-grade SSD or NVMe storage, unmetered or high-capacity bandwidth, NTP or chrony time sync, process supervision, and monitoring for disk, CPU, memory, peer count, block lag, and service restarts. Do not place chain data on HDD-backed storage. Network block devices should be used only after validating sustained read/write latency and IOPS during sync, compaction, upgrades, and high RPC load. The older `500 GB SSD, NVMe recommended` bootstrap value remains useful as a state-sync floor for short-lived or test infrastructure. Production nodes should use the node-profile minimums below. ## Hardware matrix | Node profile | Minimum | Recommended | Notes | |--------------|---------|-------------|-------| | Full node, pruned/private | 4 vCPU, 16 GB RAM, 1 TB NVMe, 100 Mbps | 8 vCPU, 32 GB RAM, 2 TB NVMe, 1 Gbps | Default for private application infrastructure and state-sync bootstrap. | | Validator node | 4 vCPU, 16 GB RAM, 1 TB NVMe, 100 Mbps low-latency | 8 vCPU, 32 GB RAM, 2 TB NVMe, 1 Gbps | No public RPC. Connect only to sentries and the signer or KMS. | | Sentry node | 4 vCPU, 16 GB RAM, 1 TB NVMe, 100 Mbps | 8 vCPU, 32 GB RAM, 2 TB NVMe, 1 Gbps | Public P2P. Enable protected RPC only when the sentry is intentionally serving that surface. | | Seed/seeder node | 2 vCPU, 8 GB RAM, 100 GB SSD, 100 Mbps | 4 vCPU, 16 GB RAM, 250 GB SSD/NVMe, 1 Gbps | Dedicated peer-discovery role only. If deployed as a full `ctmd` node, use full-node storage. | | Private RPC node | 4 vCPU, 16 GB RAM, 1 TB NVMe, 100 Mbps | 8 vCPU, 32 GB RAM, 2 TB NVMe, 1 Gbps | Internal wallets and applications, protected by firewall rules or a private gateway. | | Public RPC node | 8 vCPU, 32 GB RAM, 2 TB NVMe, 1 Gbps | 16 vCPU, 64 GB RAM, 4 TB NVMe, 1 Gbps+ | Place behind reverse proxy, rate limits, authentication where applicable, and horizontal scaling. | | Archive node | 8 vCPU, 64 GB RAM, 4 TB NVMe, 1 Gbps | 16 vCPU, 128 GB RAM, 8 TB expandable NVMe, 1 Gbps | Replay from genesis with `pruning = "nothing"` and monitor storage growth. Disk figures are the disk to provision; current archive data is roughly 1 TB. | | Archive RPC/debug node | 16 vCPU, 64 GB RAM, 8 TB NVMe, 1 Gbps | 32 vCPU, 128 GB RAM, 12 TB expandable NVMe, 1-10 Gbps | Private debug and trace surface only. Do not expose `debug`, `txpool`, or `personal` publicly. | | Event/indexer node | 8 vCPU, 32 GB RAM, 2 TB NVMe plus separate DB storage, 1 Gbps | 16 vCPU, 64 GB RAM, 4 TB NVMe plus separately sized DB/index storage, 1 Gbps+ | For WebSocket/event consumers, scanners, and exchange or indexer pipelines. | ## Storage and growth Plan storage for the selected data mode, not just initial sync. Pruned full nodes can bootstrap with state sync and default pruning, while archive nodes must replay from genesis and retain complete historical state. Archive profiles should use expandable storage and growth monitoring. Keep enough headroom for chain growth, database compaction, upgrades, logs, and backup staging. For archive configuration, use `pruning = "nothing"`, keep ABCI responses, enable the transaction index, and enable the EVM indexer as described in [Archive Data Setup](/node-ops/archive-data). ## Archive sizing The current archive data footprint is roughly `1 TB` and grows about `10 GB/month`. That is the data, not the disk to buy: provision `4 TB` minimum and `8 TB` recommended for an archive node, as in the hardware matrix, to leave room for growth, compaction, upgrades, logs, and backup staging. Archive RPC/debug nodes need extra CPU, RAM, and disk I/O when serving traces, long historical scans, or backfills. Run archive backups monthly and resize from observed query load. ## RPC scaling Public RPC is an infrastructure tier, not a single-node promise. Put public HTTP, WebSocket, REST, and gRPC surfaces behind a reverse proxy with TLS, rate limits, request-size limits, authentication where applicable, and IP allowlists for private or paid endpoints. Use the default public EVM namespace shape `eth,net,web3` for external RPC. Enable `debug`, `txpool`, or `personal` only on private operations or archive RPC nodes. For published endpoint limits, see [RPC Providers](/tooling-and-infrastructure/rpc-providers) and [Events and WebSockets](/node-ops/events-and-websockets). ## Validator and sentry separation Run validator nodes as private infrastructure. They should not expose public RPC, REST, gRPC, WebSocket, or metrics endpoints. Connect validators only to trusted sentries and the signer or KMS path. Sentries absorb public P2P exposure and can be rotated or scaled without moving the validator consensus key. Recommended validator-side settings include `pex = false`; recommended sentry-side settings include `pex = true`, `persistent_peers` including the validator, and `private_peer_ids` including the validator node ID. For the full topology notes, see [Validator Installation](/node-ops/validator-installation). ## Reference baselines These defaults are informed by c8ntinuum's node runbooks, [CometBFT production guidance](https://docs.cosmos.network/cometbft/v0.38/docs/core/Running-in-production#hardware), [Cosmos validator sentry guidance](https://docs.cosmos.network/hub/latest/validators/validator-faq#technical-requirements), and [Geth hardware requirements](https://geth.ethereum.org/docs/getting-started/hardware-requirements). ## Related pages - [Full Node Installation](/node-ops/full-node-installation) - [Validator Installation](/node-ops/validator-installation) - [Archive Data Setup](/node-ops/archive-data) - [RPC Providers](/tooling-and-infrastructure/rpc-providers) - [Events and WebSockets](/node-ops/events-and-websockets) --- # Build the Node URL: https://docs.c8ntinuum.com/node-ops/build-the-node c8ntinuum builds the `ctmd` node binary from source. Use this page when you need a local developer build, an installed binary, or a self-contained distribution bundle. The `ctmd` source (Apache 2.0) will be published in a public GitHub repository; the repository is to be announced. The steps on this page assume a checkout of that repository. Until it is published, use the release binaries listed in [metadata.json](https://resources.c8ntinuum.io/metadata.json): install them with `deploy.sh` as described in [Full Node Installation](/node-ops/full-node-installation), or, for a validator, with the [manual mainnet install](/node-ops/validator-installation#manual-mainnet-install). For local chain startup after the binary is built, see [Local Development](/tooling-and-infrastructure/local-development). ## What gets built The top-level Makefile builds `ctmd` from `evmd/cmd/evmd`. Builds use CGO and first build the Rust SP1 verifier dynamic library from `rust/sp1verifier`. The usual developer targets are: ```bash make doctor make build make install ``` `make build` creates a development binary under `build/`. `make install` installs the binary into `BINDIR`, which defaults to `$(go env GOPATH)/bin` or `$HOME/go/bin` if `GOPATH` is not available. ## Prerequisites - Go matching `go.mod` and `evmd/go.mod` - `make` - Rust and Cargo for `rust/sp1verifier` - A working C toolchain for CGO builds - macOS dynamic-library tooling: `otool` and `install_name_tool` - Linux dynamic-library tooling: `ldd`; `patchelf` is recommended Ledger support is enabled by default and requires `gcc`. To build without ledger support: ```bash LEDGER_ENABLED=false make build ``` RocksDB support is enabled by default. If RocksDB is not detected, the build falls back to `goleveldb` unless `ROCKSDB_REQUIRED=true` is set. The build expects RocksDB `10.9.1`, the version the `grocksdb` v1.10.7 bindings target. The Makefile checks only that RocksDB headers and libraries are present, not their version. ## macOS setup Install the Xcode Command Line Tools: ```bash xcode-select --install ``` For a local RocksDB-backed build, install RocksDB and companion libraries with Homebrew: ```bash brew install rocksdb snappy zstd lz4 bzip2 zlib gflags ``` Check the installed RocksDB version with `brew info rocksdb`; the build expects `10.9.1`. The build system uses Homebrew prefixes when available and uses `install_name_tool` to fix local dynamic-library paths. ## Linux setup Install the core build tools and dynamic-library helpers: ```bash sudo apt-get update sudo apt-get install -y --no-install-recommends \ build-essential \ pkg-config \ patchelf \ curl \ ca-certificates \ libsnappy-dev \ zlib1g-dev \ libbz2-dev \ liblz4-dev \ libzstd-dev \ libgflags-dev \ libnuma-dev ``` If your distribution ships a RocksDB `10.9.1` development package, install it. Otherwise, build RocksDB `10.9.1` from source and set `ROCKSDB_PREFIX`, `ROCKSDB_INCLUDE_DIR`, or `ROCKSDB_LIB_DIR` so the Makefile can find it, or build with `ROCKSDB_ENABLED=false`. Distribution packages may be older than `10.9.1`, and the Makefile accepts an older version without warning, so check the package version first. ## Build commands Check the local environment: ```bash make doctor ``` Build a development binary: ```bash make build ``` Install the binary: ```bash make install ``` Install into a custom directory: ```bash BINDIR=/path/to/bin make install ``` Create a self-contained distribution bundle under `build/dist`: ```bash make dist make verify-dist ``` Useful build overrides: ```bash ROCKSDB_ENABLED=false make build ROCKSDB_REQUIRED=true make build RUST_SP1_PROFILE=dev make build RUSTUP_TOOLCHAIN= make build ``` ## Outputs `make build` writes: ```text build/ctmd build/libsp1verifier.dylib # macOS build/libsp1verifier.so # Linux ``` `make install` writes: ```text $BINDIR/ctmd $BINDIR/Frameworks/libsp1verifier.dylib # macOS $BINDIR/lib/libsp1verifier.so # Linux ``` `make dist` writes: ```text build/dist/bin/ctmd build/dist/lib/libsp1verifier.* ``` When RocksDB is enabled, install and dist targets also bundle the required RocksDB dynamic libraries that the build system can detect. ## Verify the binary For a development build: ```bash ./build/ctmd version ``` For an installed build: ```bash ctmd version ``` If `ctmd` is not found after `make install`, confirm that `BINDIR` is on your `PATH`: ```bash export PATH="$(go env GOPATH)/bin:$PATH" ``` ## Troubleshooting | Issue | Fix | |-------|-----| | `cargo` not found | Install Rust and Cargo, then rerun `make doctor`. | | `gcc not installed for ledger support` | Install a C toolchain or build with `LEDGER_ENABLED=false make build`. | | RocksDB is missing | Let the build fall back to `goleveldb`, set `ROCKSDB_REQUIRED=true` to fail fast, or set `ROCKSDB_PREFIX=/path/to/rocksdb`. | | `patchelf` is missing on Linux | Install `patchelf`; development builds can still run with `LD_LIBRARY_PATH=build ./build/ctmd version`. | | `ctmd` is not on `PATH` | Use the full binary path or install with `BINDIR="$HOME/.local/bin" make install`. | --- # Full Node Installation URL: https://docs.c8ntinuum.com/node-ops/full-node-installation This page covers deployment of a non-validator c8ntinuum `ctmd` node with the `deploy.sh` lifecycle script. For hosted deployment scripts and peer-list resources, see [Deployment Scripts](/reference/deployment-scripts). Validators do not use `deploy.sh`: install a validator node with the [manual mainnet install](/node-ops/validator-installation#manual-mainnet-install), which mirrors what the script does, and follow the signing-key, TMKMS, sentry, and double-sign protection procedures in [Validator Installation](/node-ops/validator-installation). ## Network defaults Use this table as an operator quick reference. For the canonical network records, see [Network Information - Mainnet Beta](/developer-essentials/network-information/mainnet) and [Network Information - Testnet](/developer-essentials/network-information/testnet). | Item | Mainnet | Testnet | | --- | --- | --- | | Cosmos chain ID | `c8ntinuum_8-1` | `c8ntinuum_8888-1` | | EVM chain ID | `8` | `8888` | | Native currency | `CTM` | `CTM` | | Public EVM RPC | `https://public-evm-rpc.c8ntinuum.com` | `https://public-testnet-evm.c8ntinuum.link` | | Public CometBFT RPC | `https://public-comet-rpc.c8ntinuum.com` | `https://public-testnet-comet.c8ntinuum.link` | | Genesis URL | `https://resources.c8ntinuum.io/mainnet/genesis.json` | `https://resources.c8ntinuum.io/testnet/genesis.json` | | Genesis SHA-256 | `ebe6292aa5885b508b1d455c93564683c92e9cc3f80592ee615b1cffbc74a653` | `844f4322877d19e538d7ce584af92a2ab3a7eb825757b6b956068a4d255c6d43` | | Peers URL | `https://resources.c8ntinuum.io/peers.json` | Same schema, selected by chain ID | ## Operating model | OS | Node home | Process model | |----|-----------|---------------| | Linux | `/var/lib/ctmd` | `systemd`, via `ctmd.service` | | macOS | `$HOME/.ctmd` | `deploy.sh start` background process with PID/log files | On Linux, `deploy.sh install` creates a `ctmd` service user/group, installs a hardened `ctmd.service`, enables the service, and leaves it stopped until you run `deploy.sh state-sync` or `deploy.sh start`. On macOS, it installs the binary and bootstraps the user-managed `$HOME/.ctmd` home without installing a service. ## Recommended operational configuration Use the node profile that matches the workload. A normal application node should prioritize fast bootstrap, protected RPC surfaces, and bounded historical reads. An exchange, custodian, or indexer that must rebuild history from first principles should run archive infrastructure or consume a dedicated indexer. | Workload | Recommended node shape | Data mode | Notes | |----------|------------------------|-----------|-------| | Wallets and applications | Full node behind a private RPC gateway | State sync bootstrap, default pruning | Best for current blocks, balances, receipts, and recent logs. | | Exchange deposits | Full node plus indexed log pipeline | State sync bootstrap plus durable scanner database | Use bounded RPC scans and idempotent crediting. Add archive/indexer access for long backfills. | | Complete historical replay | Archive node | Replay from genesis, `pruning = "nothing"` | State sync does not provide pre-snapshot history. | | Debug tracing | Archive node with private debug RPC | Replay from genesis, EVM indexer enabled | Keep `debug`, `txpool`, and `personal` off public endpoints. | Recommended baseline: - Install Linux production nodes with `deploy.sh install --moniker ` and manage them with `deploy.sh start`, `deploy.sh stop`, and `systemd`. - Bootstrap new non-archive nodes with [State Sync](/node-ops/state-sync); replay archive nodes from genesis. - Keep RPC, REST, gRPC, WebSocket, and metrics endpoints on localhost or private interfaces unless a reverse proxy enforces TLS, authentication, rate limits, request size limits, and IP allowlists. - Use the default public EVM namespace shape `eth,net,web3` for externally exposed RPC. Enable `txpool` or `debug` only on private archive or operations nodes. - For archive nodes, follow [Archive Data Setup](/node-ops/archive-data): set `pruning = "nothing"`, keep ABCI responses, enable the transaction index, and enable the EVM indexer. - Monitor local height, catching-up state, peer count, process restarts, disk growth, RPC latency, HTTP `429` rates, and WebSocket reconnect storms. ## Deploy script To install or update `ctmd`, run the `deploy.sh` script. You can download and run the script manually: ```bash curl -fsSL https://resources.c8ntinuum.io/deploy.sh -o deploy.sh chmod +x deploy.sh sudo ./deploy.sh install --moniker ``` This download-first flow is the preferred operator path because it gives you a local script to inspect, rerun, and use for lifecycle commands. ## Install from release Recommended Linux install: ```bash sudo ./deploy.sh install --moniker ``` Recommended macOS install (Apple silicon only; `deploy.sh` does not support Intel Macs): ```bash ./deploy.sh install --moniker ``` `deploy.sh install` reads [metadata.json](https://resources.c8ntinuum.io/metadata.json), resolves `.ctmd.latest_version`, selects the current artifact and SHA-256 URL for the host platform (`linux-amd64`, `linux-arm64`, or `darwin-arm64`), downloads the tarball, verifies SHA-256, and installs the `ctmd` binary with bundled shared libraries. It then initializes the node home, downloads genesis, repairs chain ID settings, and patches seeds and persistent peers from `https://resources.c8ntinuum.io/peers.json`. For mainnet, the script checks both the downloaded and the local `genesis.json` against the SHA-256 published at the URL in the `metadata.json` field `genesis.mainnet.sha256_url` (`https://resources.c8ntinuum.io/mainnet/genesis.json.sha256`). If you pass a non-default `--chain-id`, the script skips the mainnet SHA-256 check. The installer creates these managed paths: | Path | Purpose | |------|---------| | `/opt/ctmd-/bin/ctmd` | Versioned binary | | `/opt/ctmd-/lib/` | Bundled shared libraries | | `/opt/ctmd` | Symlink to the active version | | `/usr/local/bin/ctmd` | Symlink to the active binary | | `/etc/systemd/system/ctmd.service` | Linux systemd unit | | `/var/lib/ctmd/` | Linux node home | | `/var/cache/ctmd/` | Linux release download cache | | `$HOME/.ctmd/releases/ctmd-/` | macOS versioned install | | `$HOME/.ctmd/current` | macOS symlink to the active version | | `$HOME/.ctmd/bin/ctmd` | macOS CLI symlink | | `$HOME/.ctmd/cache/downloads/` | macOS release download cache | ## Linux service unit `deploy.sh install` generates `/etc/systemd/system/ctmd.service`. The file carries a `do not edit` header, and both `install` and `state-sync` rewrite it, so local changes belong in a drop-in: ```bash sudo systemctl edit ctmd.service sudo systemctl restart ctmd.service ``` `systemctl edit` writes `/etc/systemd/system/ctmd.service.d/override.conf` and reloads systemd; a drop-in you place by hand needs `systemctl daemon-reload`. Drop-ins survive reinstall and state sync, but `deploy.sh reset` deletes the unit and the entire `ctmd.service.d` directory, so keep overrides in configuration management rather than only on the host. Read the unit on the host you are working on rather than from documentation: ```bash systemctl cat ctmd.service ``` A host built by hand from a flat `/usr/local` install carries a different unit at the same path: a non-versioned `LD_LIBRARY_PATH`, and a real binary at `/usr/local/bin/ctmd` instead of a symlink. Use one deployment model per host and do not mix them. `deploy.sh start` and `deploy.sh stop` drive this unit through `systemctl`. Logs go to the journal under the `ctmd-node` syslog identifier. The fields that matter to an operator: | Field | Value | Implication | |-------|-------|-------------| | `ExecStart` | `/usr/local/bin/ctmd start --home /var/lib/ctmd` | Runs through the CLI symlink, which points at the active `/opt/ctmd` install. Repointing the active install takes effect on the next restart, with no unit edit and no `daemon-reload`. | | `User` / `Group` | `ctmd` / `ctmd` | Unprivileged system account created by `install`. `install` and `state-sync` set ownership across the node home; anything you copy in by hand, such as a restored `data/` directory, must be owned by `ctmd:ctmd`. | | `Environment=LD_LIBRARY_PATH` | `/opt/ctmd/lib` | Bundled libraries resolve through the active symlink rather than the `ldconfig` cache or the binary RPATH, so the binary and its libraries always move together. | | `Environment=TMPDIR` | `/var/lib/ctmd/tmp` | `ProtectSystem=strict` makes `/tmp` read-only for the service, and CometBFT writes state-sync chunks through the Go temp directory. `install` creates this directory owned by `ctmd` with mode `0700`; state sync fails if it is missing or not writable. | | `Restart` / `RestartSec` | `on-failure` / `10s` | A crash restarts after ten seconds. A clean `deploy.sh stop` or `systemctl stop` stays stopped. | | `KillSignal` / `TimeoutStopSec` | `SIGINT` / `600` | `SIGINT` is the clean CometBFT shutdown, and a busy node can take minutes to close its database. Do not shorten the timeout: an earlier `SIGKILL` lands mid-write. | | `LimitNOFILE` | `1000000` | Headroom for database file handles and peer sockets. Do not lower it in a drop-in. | | `TasksMax` | `infinity` | Removes the default per-unit task cap of roughly 15% of the system maximum, which otherwise limits a busy node unexpectedly. | Two hardening settings need care. `CapabilityBoundingSet=` and `AmbientCapabilities=` are both empty, so the service runs with no capabilities. That is correct only while every listener binds above 1024, as the Ports table below shows. If you move a listener below 1024, add `CAP_NET_BIND_SERVICE` to both directives in a drop-in; setting one without the other still fails the bind. Prefer a reverse proxy or a firewall redirect over lowering the node's own bind ports. The generated unit sets `MemoryDenyWriteExecute=no`; keep that value unless you have tested the node with it changed. macOS installs no service. `deploy.sh start` runs `ctmd` in the background under your own user, records the PID in `$HOME/.ctmd/ctmd.pid`, and writes `$HOME/.ctmd/node.log`; `deploy.sh stop` sends `SIGINT` to that PID. There is no unit, no drop-in, and no journal. To return a host to a previously installed version, repoint the active symlink as described in [Rollback](/node-ops/upgrade-instructions#rollback). The unit does not change, so no `daemon-reload` is needed. ## Build from source The `ctmd` source will be published in a public GitHub repository; the repository is to be announced. Until then, install from the release binaries above. Once the source is available, follow [Build the Node](/node-ops/build-the-node), then verify the binary: ```bash ctmd version ctmd version --long | grep -i rocksdb ``` ## Start sync State sync is recommended for new nodes because it avoids replaying from genesis: ```bash sudo ./deploy.sh state-sync ``` On macOS: ```bash ./deploy.sh state-sync ``` To replay from genesis instead: ```bash sudo ./deploy.sh start sudo journalctl -u ctmd.service -f ``` On macOS: ```bash ./deploy.sh start tail -f "$HOME/.ctmd/node.log" ``` ## Ports | Port | Service | Exposure | |------|---------|----------| | `26656/tcp` | P2P | Public or peer-restricted | | `26657/tcp` | CometBFT RPC | Localhost or reverse proxy only | | `1317/tcp` | Cosmos REST/LCD | Localhost or reverse proxy only | | `9090/tcp` | gRPC | Localhost or reverse proxy only | | `8545/tcp` | EVM JSON-RPC HTTP | Localhost or reverse proxy only | | `8546/tcp` | EVM JSON-RPC WebSocket | Localhost or reverse proxy only | | `26660/tcp` | CometBFT Prometheus metrics | Firewall-restricted | | `6065/tcp` | EVM JSON-RPC metrics | Firewall-restricted | Do not expose RPC, REST, gRPC, or EVM JSON-RPC directly to the public internet without a reverse proxy, firewall, and rate limiting. ## Health checks Check local status: ```bash curl -s http://127.0.0.1:26657/status | jq ``` Check install and sync status: ```bash ./deploy.sh status ``` `status` reports the active install, binary version, node home, service state when present, latest height, catching-up state, and peer count. ## Monitoring Enable CometBFT metrics in `config.toml`: ```toml [instrumentation] prometheus = true prometheus_listen_addr = "127.0.0.1:26660" namespace = "ctm" ``` Optional EVM JSON-RPC metrics require `--metrics` on startup and this app config: ```toml [json-rpc] metrics-address = "127.0.0.1:6065" ``` Scrape `http://127.0.0.1:26660/metrics` for CometBFT metrics and `http://127.0.0.1:6065/debug/metrics/prometheus` for EVM JSON-RPC metrics. Run `node_exporter` alongside `ctmd` for disk, CPU, memory, and network metrics. ## Common recovery Use the managed lifecycle commands for normal start, stop, and inspection: ```bash sudo ./deploy.sh stop sudo ./deploy.sh start ./deploy.sh status ``` For a clean reinstall, use the destructive reset command: ```bash sudo ./deploy.sh reset ``` `reset` deletes the managed install, node home, service, cache, and Linux system user/group so a later `install --moniker ` can start from scratch. Do not run it unless you intend to delete local chain data and key material in the managed node home. On macOS: ```bash ./deploy.sh reset ``` --- # State Sync URL: https://docs.c8ntinuum.com/node-ops/state-sync State sync lets a node bootstrap from a recent application snapshot instead of replaying every block from genesis. Use `deploy.sh state-sync` for normal c8ntinuum installs; the hosted script and peer-list source are listed in [Deployment Scripts](/reference/deployment-scripts). The script resolves trusted RPC endpoints and snapshot peers, patches the node config, resets local chain data, starts the node, waits for catch-up, then disables state sync again. State sync wipes the local `data/` directory. Back up anything you need before confirming the reset. ## Prerequisites - **Managed install and node home** created by `deploy.sh install --moniker ` - `curl`, `jq`, and `sed` - Network access to trusted CometBFT RPC endpoints and P2P snapshot peers Default homes: | OS | Node home | Process model | |----|-----------|---------------| | Linux | `/var/lib/ctmd` | `ctmd.service` managed by systemd | | macOS | `$HOME/.ctmd` | `deploy.sh start` background process with PID/log files | ## Quick start Linux: ```bash sudo ./deploy.sh install --moniker sudo ./deploy.sh state-sync ``` macOS: ```bash ./deploy.sh install --moniker ./deploy.sh state-sync ``` The default flow targets mainnet: | Item | Value | | --- | --- | | Chain ID | `c8ntinuum_8-1` | | Genesis URL | `https://resources.c8ntinuum.io/mainnet/genesis.json` | | Genesis SHA-256 | `ebe6292aa5885b508b1d455c93564683c92e9cc3f80592ee615b1cffbc74a653` | | Peers URL | `https://resources.c8ntinuum.io/peers.json` | | Trust-height lag | `2000 blocks` | | Trust period | `168h0m0s` | | P2P port | `26656` | ## Testnet Install the node with the testnet chain ID first. `deploy.sh state-sync` reads the chain ID from the initialized home, so the chain selection happens during `install`, not as a `state-sync` flag. The peers file is shared across networks and selected by the installed chain ID. Linux: ```bash sudo env CTMD_GENESIS_URL=https://resources.c8ntinuum.io/testnet/genesis.json \ ./deploy.sh install --moniker --chain-id c8ntinuum_8888-1 ``` macOS: ```bash CTMD_GENESIS_URL=https://resources.c8ntinuum.io/testnet/genesis.json \ ./deploy.sh install --moniker --chain-id c8ntinuum_8888-1 ``` Use the same genesis URL override when running state sync: Linux: ```bash sudo env CTMD_GENESIS_URL=https://resources.c8ntinuum.io/testnet/genesis.json \ ./deploy.sh state-sync ``` macOS: ```bash CTMD_GENESIS_URL=https://resources.c8ntinuum.io/testnet/genesis.json \ ./deploy.sh state-sync ``` ## Custom endpoints `deploy.sh state-sync` reads RPCs and snapshot peers from `https://resources.c8ntinuum.io/peers.json`, selected by chain ID. Override individual endpoints when you need private infrastructure: ```bash sudo ./deploy.sh state-sync \ --rpc-1 https:// \ --rpc-2 https:// \ --p2p-host-1 \ --p2p-host-2 ``` Options: | Flag | Default | Purpose | |------|---------|---------| | `--lag N` | `2000` | Blocks behind latest height for the trust point. | | `-y, --yes` | none | Auto-confirm apply, reset, and start prompts. | | `--rpc-1 URL` | first RPC in `peers.json` | Override trusted RPC 1. | | `--rpc-2 URL` | second RPC in `peers.json` | Override trusted RPC 2. | | `--p2p-host-1 HOST` | first peer host in `peers.json` | Override snapshot peer host 1. | | `--p2p-host-2 HOST` | second peer host in `peers.json` | Override snapshot peer host 2. | The trust period is fixed at `168h0m0s`, and snapshot peer addresses use P2P port `26656`. ## What the script does 1. Detects the host OS and node home. 2. Verifies required commands and the initialized node home. 3. Reads the installed chain ID from `client.toml` and resolves trusted RPCs and P2P snapshot peers. 4. Stops the Linux service, or checks for a running local process on macOS. 5. Downloads and verifies `genesis.json`. 6. Repairs chain ID settings in `app.toml`, `client.toml`, and `config.toml`. `[evm] evm-chain-id` in `app.toml` is set from the chain ID: `8` on mainnet, `8888` on testnet. 7. Fetches the latest height from RPC 1 and computes `trust_height = latest_height - lag`. 8. Fetches the `trust_hash` at the trust height. 9. Patches `[statesync]`, `[mempool]`, and `[p2p]`. 10. Resets local data with `unsafe-reset-all --keep-addr-book`. 11. Starts the node and waits until catch-up completes or the node reaches the trusted RPC tip. 12. Sets `[statesync].enable = false`. 13. Restarts the service on Linux or starts the managed background node on macOS. ## Trust verification Before confirming the run, verify the printed `Trust Height` and `Trust Hash` against an independent source. The script uses RPC 1 to compute the trust point, so an explorer or third RPC gives you a better safety check. | Network | Explorer | |---------|----------| | Mainnet | `https://ctmscan.io` | | Testnet | `https://testnet.ctmscan.io` | Look up the block at the printed trust height and compare its block hash to the printed trust hash. The comparison is case-insensitive. If the hashes do not match, do not continue. ## Config written `deploy.sh state-sync` writes these fields in `config.toml`: | Field | Section | Value | |-------|---------|-------| | `enable` | `[statesync]` | `true` during sync, then `false` after catch-up. | | `rpc_servers` | `[statesync]` | `,` | | `trust_height` | `[statesync]` | Latest height minus lag. | | `trust_hash` | `[statesync]` | Block hash at the trust height. | | `trust_period` | `[statesync]` | Fixed script default `168h0m0s`. | | `type` | `[mempool]` | `"app"` | | `seeds` | `[p2p]` | Seeds from `peers.json`, if present. | | `persistent_peers` | `[p2p]` | Snapshot peers discovered from the trusted RPCs. | | `max_packet_msg_payload_size` | `[p2p]` | `10240` | Nodes that should provide snapshots to the network need snapshots enabled in `app.toml`: ```toml [state-sync] snapshot-interval = 5000 snapshot-keep-recent = 2 ``` ## Logs and artifacts | Path | OS | Purpose | |------|----|---------| | `sudo journalctl -u ctmd.service` | Linux | Sync and runtime logs. | | `$HOME/.ctmd/state_sync.log` | macOS | State sync catch-up log. | | `$HOME/.ctmd/node.log` | macOS | Post-sync node log. | Check local sync status: ```bash curl -s http://127.0.0.1:26657/status | jq .result.sync_info ``` ## Troubleshooting | Issue | Fix | |-------|-----| | RPC height or block hash cannot be fetched | Confirm the RPC responds to `/status` and `/block?height=`. | | No peers were discovered | Confirm the RPC `/status` response includes `result.node_info.id`. | | No snapshots found | Confirm the snapshot peers are reachable on P2P port `26656`; if needed, rerun with `--lag 5000`. | | Local node is past the trust height | Skip state sync and let normal block sync continue, or choose an older trust point with a larger lag. | --- # Validator Installation URL: https://docs.c8ntinuum.com/node-ops/validator-installation These values are grounded in the current c8ntinuum genesis and node code. ## Network constants | Field | Value | |-------|-------| | Binary | `ctmd` | | Home directory | `~/.ctmd`; `/var/lib/ctmd` on Linux hosts set up with the [manual mainnet install](#manual-mainnet-install) or `deploy.sh` | | Cosmos chain ID | `c8ntinuum_8-1` | | EVM chain ID | `8` | | Account prefix | `c8` | | Validator prefix | `c8valoper` | | Consensus prefix | `c8valcons` | | Staking denom | `ctm` | | Display symbol | `CTM` | | Denom exponent | `18` | ## Prerequisites - A fully synced `ctmd` node. Install it with the [manual mainnet install](#manual-mainnet-install) below. `deploy.sh`, described in [Full Node Installation](/node-ops/full-node-installation), is the install path for non-validator nodes; validators do not use it. Submit `create-validator` only after `ctmd status` reports `catching_up` as `false`. - The node home. Commands on this page use `~/.ctmd`, the default home of `ctmd`. The manual install below puts the node home at `/var/lib/ctmd`, owned by the `ctmd` user, so run commands that read it, such as `ctmd comet show-validator`, as that user with `--home /var/lib/ctmd`. ## Manual mainnet install This route installs a mainnet `ctmd` node on Linux by hand. Each step does what `deploy.sh install` and `deploy.sh state-sync` do, so the host ends up with the same layout, service unit, and configuration as a `deploy.sh` host, without the script. Release binaries exist for `linux-amd64`, `linux-arm64`, and `darwin-arm64`; the systemd steps apply to Linux only. You need `curl`, `jq`, `tar`, `sha256sum`, and `systemd`. Run the commands in a root shell (`sudo -i`), as `deploy.sh` does, and keep one working directory for the downloaded files. ### Download and verify the release Release binaries are listed in [metadata.json](https://resources.c8ntinuum.io/metadata.json). Resolve the current version and the tarball and checksum URLs for your platform: ```bash PLATFORM=linux-amd64 # or linux-arm64 curl -fsSL https://resources.c8ntinuum.io/metadata.json -o metadata.json VERSION=$(jq -r '.ctmd.latest_version' metadata.json) TARBALL_URL=$(jq -r --arg p "$PLATFORM" '.ctmd.artifacts[$p].url' metadata.json) CHECKSUM_URL=$(jq -r --arg p "$PLATFORM" '.ctmd.artifacts[$p].sha256_url' metadata.json) echo "$VERSION $TARBALL_URL" ``` Download the tarball and its `.sha256` file, then verify the tarball. The check must print `OK`: ```bash curl -fsSLO "$TARBALL_URL" curl -fsSLO "$CHECKSUM_URL" sha256sum -c "ctmd-${VERSION}-${PLATFORM}.tar.gz.sha256" ``` The tarball unpacks to `ctmd--/`, which holds `bin/ctmd` and a `lib/` directory of bundled shared libraries (RocksDB, the SP1 verifier, and their compression libraries). `ctmd` needs both. ```bash tar -xzf "ctmd-${VERSION}-${PLATFORM}.tar.gz" RELEASE_DIR="ctmd-${VERSION}-${PLATFORM}" ``` ### Install the binary and libraries Install into a versioned directory under `/opt`, then point the `/opt/ctmd` and `/usr/local/bin/ctmd` symlinks at it. This is the layout `deploy.sh` uses, so [Rollback](/node-ops/upgrade-instructions#rollback) works the same way on this host. ```bash install -d -m 0755 -o root -g root /opt "/opt/ctmd-${VERSION}" "/opt/ctmd-${VERSION}/bin" "/opt/ctmd-${VERSION}/lib" install -m 0755 -o root -g root "$RELEASE_DIR/bin/ctmd" "/opt/ctmd-${VERSION}/bin/ctmd" cp -aP "$RELEASE_DIR/lib/." "/opt/ctmd-${VERSION}/lib/" chown -R root:root "/opt/ctmd-${VERSION}" ln -sfn "/opt/ctmd-${VERSION}" /opt/ctmd ln -sfn /opt/ctmd/bin/ctmd /usr/local/bin/ctmd ``` ### Create the service user and node home ```bash getent group ctmd >/dev/null || groupadd --system ctmd id -u ctmd >/dev/null 2>&1 || \ useradd --system --gid ctmd --home-dir /var/lib/ctmd --shell /usr/sbin/nologin --no-create-home ctmd install -d -o root -g ctmd -m 0750 /var/lib/ctmd install -d -o ctmd -g ctmd -m 0750 /var/lib/ctmd/config /var/lib/ctmd/data install -d -o ctmd -g ctmd -m 0700 /var/lib/ctmd/tmp ``` Check that the binary runs with its bundled libraries. It prints the release version: ```bash (cd /var/lib/ctmd && sudo -u ctmd env HOME=/var/lib/ctmd LD_LIBRARY_PATH=/opt/ctmd/lib /usr/local/bin/ctmd version) ``` ### Initialize the node Initialize the node home as the `ctmd` user with the mainnet chain ID. Like `deploy.sh`, run it from inside the node home, in a subshell so your working directory does not change: ```bash (cd /var/lib/ctmd && sudo -u ctmd env HOME=/var/lib/ctmd LD_LIBRARY_PATH=/opt/ctmd/lib \ /usr/local/bin/ctmd init --home /var/lib/ctmd --chain-id c8ntinuum_8-1) ``` `ctmd init` also creates the validator consensus key at `/var/lib/ctmd/config/priv_validator_key.json`. Back it up securely, and see [KMS and sentry architecture](#kms-and-sentry-architecture) before you rely on the local file signer. ### Install and verify genesis Replace the generated `genesis.json` with the mainnet genesis, then check it against the SHA-256 that `metadata.json` publishes. The check must print `OK`: ```bash curl -fsSL "$(jq -r '.genesis.mainnet.url' metadata.json)" -o /var/lib/ctmd/config/genesis.json EXPECTED=$(curl -fsSL "$(jq -r '.genesis.mainnet.sha256_url' metadata.json)" | awk '{print $1}') echo "$EXPECTED /var/lib/ctmd/config/genesis.json" | sha256sum -c - ``` The mainnet genesis SHA-256 is `ebe6292aa5885b508b1d455c93564683c92e9cc3f80592ee615b1cffbc74a653`. ### Configure peers and chain IDs Build the seed and persistent peer lists from the mainnet entry in [peers.json](https://resources.c8ntinuum.io/peers.json): ```bash curl -fsSL https://resources.c8ntinuum.io/peers.json -o peers.json jq -r '(.networks.mainnet.seeds // []) | map("\(.id)@\(.host):\(.port // 26656)") | join(",")' peers.json jq -r '(.networks.mainnet.persistent_peers // []) | map("\(.id)@\(.host):\(.port // 26656)") | join(",")' peers.json ``` Set these keys in `/var/lib/ctmd/config/config.toml`, using the two lists printed above: ```toml [p2p] seeds = "" persistent_peers = "" max_packet_msg_payload_size = 10240 [mempool] type = "app" ``` Once your validator runs behind sentries, replace `persistent_peers` with your sentries, as described in [KMS and sentry architecture](#kms-and-sentry-architecture). Set the EVM chain ID in `/var/lib/ctmd/config/app.toml` explicitly: ```toml [evm] evm-chain-id = 8 ``` Set the client chain ID in `/var/lib/ctmd/config/client.toml`: ```toml chain-id = "c8ntinuum_8-1" ``` ### Configure state sync (optional) State sync bootstraps the node from a recent snapshot instead of replaying from genesis. `deploy.sh state-sync` takes the trust height and hash from the first trusted RPC in `peers.json`, 2000 blocks below its latest height, and builds the persistent peers from the node IDs of the two trusted RPCs and the hosts of the first two persistent peers: ```bash RPC_1=$(jq -r '.networks.mainnet.trusted_rpcs[0].url' peers.json) RPC_2=$(jq -r '.networks.mainnet.trusted_rpcs[1].url' peers.json) HOST_1=$(jq -r '.networks.mainnet.persistent_peers[0].host' peers.json) HOST_2=$(jq -r '.networks.mainnet.persistent_peers[1].host' peers.json) LATEST_HEIGHT=$(curl -fsSL "$RPC_1/block" | jq -r '.result.block.header.height') TRUST_HEIGHT=$((LATEST_HEIGHT - 2000)) TRUST_HASH=$(curl -fsSL "$RPC_1/block?height=$TRUST_HEIGHT" | jq -r '.result.block_id.hash') NODE_1=$(curl -fsSL "$RPC_1/status" | jq -r '.result.node_info.id') NODE_2=$(curl -fsSL "$RPC_2/status" | jq -r '.result.node_info.id') echo "rpc_servers = \"$RPC_1,$RPC_2\"" echo "trust_height = $TRUST_HEIGHT" echo "trust_hash = \"$TRUST_HASH\"" echo "persistent_peers = \"$NODE_1@$HOST_1:26656,$NODE_2@$HOST_2:26656\"" ``` Put the printed values in `/var/lib/ctmd/config/config.toml`: ```toml [statesync] enable = true rpc_servers = "," trust_height = trust_hash = "" trust_period = "168h0m0s" [p2p] persistent_peers = "@:26656,@:26656" ``` A node home that has never started holds no chain data. If this node has already run, stop it and clear its chain data first, as `deploy.sh state-sync` does. The reset needs write access to the node home, which the next step closes again: ```bash systemctl stop ctmd.service chmod 0770 /var/lib/ctmd (cd /var/lib/ctmd && sudo -u ctmd env HOME=/var/lib/ctmd LD_LIBRARY_PATH=/opt/ctmd/lib \ /usr/local/bin/ctmd comet unsafe-reset-all --home /var/lib/ctmd --keep-addr-book) ``` Reset only before the node has signed blocks as a validator: `unsafe-reset-all` also resets the validator signing state (`data/priv_validator_state.json`). On a validator that has already signed, that can make it sign the same height twice, and a double sign is slashed 5% and permanently jails (tombstones) the validator. Skip this section to replay from genesis instead. ### Set ownership After editing, restore the ownership and modes that `deploy.sh` sets on the node home: ```bash chown -R ctmd:ctmd /var/lib/ctmd chown root:ctmd /var/lib/ctmd /var/lib/ctmd/config/genesis.json chmod 0750 /var/lib/ctmd /var/lib/ctmd/config /var/lib/ctmd/data chmod 0640 /var/lib/ctmd/config/genesis.json chmod 0700 /var/lib/ctmd/tmp ``` ### Run the node as a service Create `/etc/systemd/system/ctmd.service` with the unit that `deploy.sh` generates. `LD_LIBRARY_PATH` points at the bundled libraries of the active install, and `TMPDIR` points at the node-owned temp directory that state sync writes to. ```ini [Unit] Description=ctmd node After=network-online.target Wants=network-online.target [Service] User=ctmd Group=ctmd WorkingDirectory=/var/lib/ctmd Environment=HOME=/var/lib/ctmd Environment=LD_LIBRARY_PATH=/opt/ctmd/lib Environment=TMPDIR=/var/lib/ctmd/tmp UMask=0077 StateDirectory=ctmd StateDirectoryMode=0750 ExecStart=/usr/local/bin/ctmd start --home /var/lib/ctmd Restart=on-failure RestartSec=10s KillSignal=SIGINT TimeoutStopSec=600 TimeoutStartSec=300 LimitNOFILE=1000000 LimitNPROC=65535 StandardOutput=journal StandardError=journal SyslogIdentifier=ctmd-node KeyringMode=private Nice=-5 CPUWeight=1000 StartupCPUWeight=1000 IOWeight=1000 StartupIOWeight=1000 IOSchedulingClass=best-effort IOSchedulingPriority=0 TasksMax=infinity CPUQuota= MemoryHigh=infinity MemoryMax=infinity CPUAffinity= NoNewPrivileges=yes PrivateDevices=yes PrivateIPC=yes PrivateUsers=no PrivateNetwork=no ProtectSystem=strict ProtectHome=yes ProtectHostname=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes ProtectControlGroups=yes ProtectClock=yes ProtectProc=invisible ProcSubset=all CapabilityBoundingSet= AmbientCapabilities= RestrictSUIDSGID=yes RestrictRealtime=yes RestrictNamespaces=yes LockPersonality=yes MemoryDenyWriteExecute=no SystemCallArchitectures=native RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK [Install] WantedBy=multi-user.target ``` [Linux service unit](/node-ops/full-node-installation#linux-service-unit) explains the fields that matter to an operator. Enable and start the service, then follow the logs: ```bash systemctl daemon-reload systemctl enable ctmd.service systemctl start ctmd.service journalctl -u ctmd.service -f ``` Wait until the node reports `catching_up` as `false`: ```bash curl -s http://127.0.0.1:26657/status | jq '.result.sync_info.catching_up' ``` If you configured state sync, turn it off once the node has caught up, as `deploy.sh` does: stop the service, set `enable = false` under `[statesync]` in `config.toml`, and start the service again. ## Create validator Generate an operator key. `eth_secp256k1` is the default account key algorithm for `ctmd`. ```bash ctmd keys add validator --algo eth_secp256k1 --keyring-backend file ``` Get the node consensus public key from the validator home: ```bash ctmd comet show-validator --home ~/.ctmd ``` On a host set up with the [manual mainnet install](#manual-mainnet-install), run it as the `ctmd` user against `/var/lib/ctmd`: ```bash (cd /var/lib/ctmd && sudo -u ctmd env HOME=/var/lib/ctmd LD_LIBRARY_PATH=/opt/ctmd/lib /usr/local/bin/ctmd comet show-validator --home /var/lib/ctmd) ``` Create `validator.json`. The `pubkey` must be the `/cosmos.crypto.ed25519.PubKey` output from `ctmd comet show-validator`. ```json { "pubkey": { "@type": "/cosmos.crypto.ed25519.PubKey", "key": "" }, "amount": "888888000000000000000000ctm", "moniker": "", "identity": "", "website": "", "security": "", "details": "", "commission-rate": "0.10", "commission-max-rate": "0.20", "commission-max-change-rate": "0.01", "min-self-delegation": "888888000000000000000000" } ``` Submit the transaction: ```bash ctmd tx staking create-validator validator.json \ --from validator \ --keyring-backend file \ --chain-id c8ntinuum_8-1 \ --node \ --fees ctm \ --gas auto \ --gas-adjustment 1.3 ``` `` must be at least the gas limit multiplied by the chain's minimum gas price, currently `222000000000ctm` (222 gwei) per unit of gas, or by the current base fee if it is higher (`ctmd query feemarket base-fee`). To have `ctmd` compute the fee, pass that per-gas price with `--gas-prices`, for example `--gas-prices 222000000000ctm`, instead of `--fees`. See [Gas on c8ntinuum](/developer-essentials/gas) for the fee market parameters. c8ntinuum enforces a validator creation floor of 888,888 CTM (`888888000000000000000000ctm`, `888888 * 10^18`) for both the initial self-delegation amount and `min-self-delegation`. The six genesis validators use this minimum self-delegation and have between `999000000000000000000000ctm` and `999912000000000000000000ctm` self-bonded. For the module-level policy details and EVM staking precompile coverage, see [Minimum Self-Delegation Check](/modules/custom/minimum-self-delegation). ## Commission bounds The six genesis validators use: | Field | Value | |-------|-------| | `commission-rate` | `0.10` | | `commission-max-rate` | `0.20` for three validators, `1.00` for the other three | | `commission-max-change-rate` | `0.01` for the same three validators, `1.00` for the other three | | Chain-wide `min_commission_rate` | `0` | General staking validation still applies: commission values cannot be negative, `rate <= max-rate`, `max-rate <= 1`, `max-change-rate <= max-rate`, and commission can be changed (raised or lowered) at most once per 24 hours, with each increase capped at `max-change-rate`. ## Keys and Ledger - Account/operator keys default to `eth_secp256k1`. - Supported account algorithms are `eth_secp256k1` and `secp256k1`. - Consensus validator keys are CometBFT `ed25519` keys exposed through `ctmd comet show-validator`. - The default coin type for `ctmd` is `60`, with default path `m/44'/60'/0'/0/0`. - Ledger coin type `60` uses the Ethereum app and creates an `eth_secp256k1` public key. - Ledger coin type `118` uses the Cosmos app and creates a Cosmos `secp256k1` public key. - `ctmd keys add --ledger` rejects a custom `--hd-path`; use `--coin-type`, `--account`, and `--index`. ```bash ctmd keys add validator-eth-ledger \ --ledger \ --coin-type 60 \ --algo eth_secp256k1 ctmd keys add validator-cosmos-ledger \ --ledger \ --coin-type 118 \ --algo secp256k1 ``` ## KMS and sentry architecture c8ntinuum does not require a specific KMS or sentry topology; use these as operational recommendations. - Lowest-complexity signing uses the local CometBFT file signer under `~/.ctmd/config/priv_validator_key.json`, but this has the largest key-exposure risk. - Remote signing can be implemented with TMKMS or Horcrux-compatible CometBFT remote signer setups. - For threshold signing or high availability, prefer Horcrux-style threshold signing over running duplicate live validator keys. - Never run the same validator consensus key in two active signers at once. - Keep the validator node private and connect it only to sentry nodes. - Public sentries should expose P2P and any intended public RPC surfaces; the validator should use sentries as persistent peers. - Recommended validator-side settings include `pex = false`, no public RPC, and firewall access limited to sentries and the signer. - Recommended sentry-side settings include `pex = true`, `persistent_peers` including the validator, and `private_peer_ids` including the validator node ID. ## Ports and services | Service | Default | Notes | |---------|---------|-------| | CometBFT P2P | `tcp://0.0.0.0:26656` | Public on sentries | | CometBFT RPC | `tcp://127.0.0.1:26657` | Keep private unless intentionally exposed | | ABCI | `tcp://0.0.0.0:26658` | Internal application transport | | Cosmos REST API | `tcp://localhost:1317` | Disabled by default | | Cosmos gRPC | `localhost:9090` | Enabled by default (`[grpc] enable = true` in `app.toml`), bound to localhost | | gRPC-Web flag default | `0.0.0.0:9900` | Requires gRPC/gRPC-Web enablement | | EVM JSON-RPC HTTP | `127.0.0.1:8545` | Disabled by default | | EVM JSON-RPC WebSocket | `127.0.0.1:8546` | Disabled by default | | EVM JSON-RPC metrics | `127.0.0.1:6065` | Enabled with JSON-RPC and `--metrics` | | Geth metrics address | `127.0.0.1:8100` | Used by telemetry/API metrics plumbing | | CometBFT Prometheus | `:26660` | Disabled by default | gRPC is enabled by default on `localhost:9090`. The REST API, JSON-RPC, and telemetry are disabled by default unless explicitly enabled in config or via start flags. ## Monitoring Monitor at least: - Process health and automatic restart behavior for `ctmd`. - Sync status from `ctmd status`; alert if `catching_up` is true on a validator. - Peer count and sentry connectivity from CometBFT RPC. - Block height progress, block time, disk usage, memory, file descriptors, and database growth. - Validator signing info from `ctmd query slashing signing-info $(ctmd comet show-address)`. - Missed blocks well before the downtime threshold; alert before `4444` missed blocks in the `8888` block window. - JSON-RPC, gRPC, API, and indexer latency only on nodes intentionally serving those surfaces. Validator incentive state is tracked by `x/valrewards`, including epoch reward settings, reward-pool funding, outstanding reward queries, and one-time validator reward claims. See [Validator Rewards](/modules/custom/validator-rewards) for CLI and precompile details. ## Slashing, jailing, and unjail Mainnet (`c8ntinuum_8-1`) slashing params, from `ctmd query slashing params`: | Param | Value | |-------|-------| | `signed_blocks_window` | `8888` | | `min_signed_per_window` | `0.500000000000000000` | | `downtime_jail_duration` | `600s` | | `slash_fraction_downtime` | `0.010000000000000000` | | `slash_fraction_double_sign` | `0.050000000000000000` | A validator is jailed for downtime when it misses more than half of the `8888` block signing window after the slashing warm-up period. Downtime burns `1%` and jails for `600s`. Double-signing burns `5%`, jails, and tombstones the validator; tombstoned validators cannot be unjailed. Unjail only after fixing the signer/node issue, waiting until `jailed_until` has passed, and confirming self-delegation is at least `888888000000000000000000ctm`. ```bash ctmd query slashing signing-info "$(ctmd comet show-address)" ctmd query staking validator ctmd tx slashing unjail \ --from validator \ --keyring-backend file \ --chain-id c8ntinuum_8-1 \ --node \ --fees ctm \ --gas auto \ --gas-adjustment 1.3 ``` ## Upgrade flow and emergency contacts Use [Upgrade Instructions](/node-ops/upgrade-instructions) as the canonical upgrade runbook. Validators should layer signing-key safety, sentry topology, TMKMS, and double-sign protection checks on top of each release-specific upgrade notice. --- # General Operations URL: https://docs.c8ntinuum.com/node-ops/general-operations Use this page as the day-to-day operator checklist once a node is installed and syncing. ## Routine checks | Check | What to verify | |-------|----------------| | Sync status | Node height is near the network tip. | | Peers | The node has enough healthy peers. | | Logs | No repeated consensus, state sync, disk, or RPC errors. | | Disk | Storage has enough free capacity for the selected pruning or archive mode. | | RPC | HTTP and WebSocket services are reachable only where intended. | | Alerts | Monitoring covers block lag, process restarts, disk, memory, and peer count. | ## Useful command shapes c8ntinuum uses `ctmd` as the node binary. Confirm final flags and home paths for your environment. ```bash ctmd status ctmd start --home ~/.ctmd ``` For Linux installs managed by `deploy.sh`, prefer the service and lifecycle helpers: ```bash sudo systemctl status ctmd.service sudo journalctl -u ctmd.service -n 200 ./deploy.sh status sudo ./deploy.sh stop sudo ./deploy.sh start ``` ## Monitoring endpoints | Component | Endpoint | Notes | |-----------|----------|-------| | CometBFT Prometheus | `http://127.0.0.1:26660/metrics` | Controlled by `[instrumentation]` in `config.toml`. | | EVM JSON-RPC metrics | `http://127.0.0.1:6065/debug/metrics/prometheus` | Requires `[json-rpc].metrics-address` and starting `ctmd` with `--metrics`. | Keep metrics endpoints bound to localhost or a private interface and restrict access with a firewall. Run `node_exporter` alongside `ctmd` for host disk, CPU, memory, and network metrics. ## Related runbooks - [Upgrade Instructions](/node-ops/upgrade-instructions) - [Recovering a Node](/node-ops/node-recovery) - [State Sync](/node-ops/state-sync) --- # Upgrade Instructions URL: https://docs.c8ntinuum.com/node-ops/upgrade-instructions `ctmd` release binaries are published through [metadata.json](https://resources.c8ntinuum.io/metadata.json): `.ctmd.latest_version` names the current release, and each platform artifact has a tarball URL and a SHA-256 URL. `deploy.sh install` resolves releases from this file. Upgrade announcements will be published as GitHub releases in the `ctmd` node repository, which is to be announced. Each scheduled network upgrade should publish the upgrade name, target height, binary version, checksums or signatures, and operator deadline before validators replace binaries. ## Current upgrade status | Item | Current documented value | |------|--------------------------| | Upgrade module | Present | | Active genesis upgrade plan | None documented | | Existing app upgrade handler | Registered in the binary; take the handler name from the release notice | | Plan query | `ctmd query upgrade plan` | ## Release-specific runbook | Phase | Operator action | |-------|-----------------| | Announcement | Record the upgrade name, target height, binary version, checksums or signatures, and deadline from the official release notice, published as a GitHub release in the node repository (to be announced). | | Preparation | Back up configs and keys, confirm disk headroom, check service health, and download the release binary. | | Verification | Verify binary checksums or signatures when provided, then confirm `ctmd version` and `ctmd version --long`. | | Execution | Follow the release-specific stop height, binary replacement, config migration, and restart instructions. | | Post-upgrade | Confirm height progression, peer count, logs, RPC health, and application version. | | Rollback | Roll back only when the release instructions say it is allowed for the observed failure mode. | ## Rollback This procedure applies only to the `deploy.sh`-managed `/opt` install described in [Full Node Installation](/node-ops/full-node-installation), and to the same layout set up by the validator [manual mainnet install](/node-ops/validator-installation#manual-mainnet-install). A host where `ctmd` and its libraries were copied straight into `/usr/local` has no versioned directory and no active symlink, so none of this applies — roll that host back using its own layout. Both layouts install a unit at `/etc/systemd/system/ctmd.service`, so confirm the layout before changing anything. ### Preconditions | Precondition | Check | |--------------|-------| | Host uses the managed `/opt` install | `readlink /opt/ctmd` resolves to `/opt/ctmd-`, and `readlink /usr/local/bin/ctmd` resolves to `/opt/ctmd/bin/ctmd`. | | A previous versioned install is still on disk | `ls -d /opt/ctmd-*` lists the directory you intend to return to. | | Node has not passed the announced upgrade height | `./deploy.sh status`, or `curl -s http://127.0.0.1:26657/status` on a host without `deploy.sh`, reports a local height below the upgrade height in the release notice. If the local RPC is down, take the network height from a peer. | ### Linux procedure List the installed versions and confirm which one is active: ```bash ls -d /opt/ctmd-* readlink /opt/ctmd ``` Stop the service, repoint the active symlink at the previous versioned directory, and start again: ```bash sudo systemctl stop ctmd.service sudo ln -sfn /opt/ctmd- /opt/ctmd sudo systemctl start ctmd.service ``` Verify: ```bash readlink /opt/ctmd ./deploy.sh status # On a host without deploy.sh: curl -s http://127.0.0.1:26657/status | jq '.result.sync_info' sudo journalctl -u ctmd.service -f ``` ### Why only `/opt/ctmd` moves `/opt/ctmd` is the path both halves of the install resolve through: `/usr/local/bin/ctmd` is a symlink to `/opt/ctmd/bin/ctmd`, and the unit sets `LD_LIBRARY_PATH=/opt/ctmd/lib`. Repointing `/opt/ctmd` moves the binary and its bundled shared libraries together. Do not repoint `/usr/local/bin/ctmd` instead. That changes the binary without changing the library path, and runs one install's binary against another install's bundled libraries. The unit file carries no version, so it does not change and no `systemctl daemon-reload` is needed. ### macOS The active symlink is `$HOME/.ctmd/current`, and versioned installs live under `$HOME/.ctmd/releases/`. The CLI symlink and the library path both resolve through `current`, so the same single move applies. There is no service: ```bash ls -d "$HOME/.ctmd/releases/ctmd-"* ./deploy.sh stop ln -sfn "$HOME/.ctmd/releases/ctmd-" "$HOME/.ctmd/current" ./deploy.sh start ``` ### Rollback is a pre-upgrade remedy only Moving the symlink works only while the node has not yet passed the announced upgrade height. Once the network passes that height and the migration runs, the node home is migrated and the previous binary cannot serve it. From that point the ways back are restoring a pre-upgrade backup of the node home or re-bootstrapping with [State Sync](/node-ops/state-sync) — not a symlink change. Do not roll back validator infrastructure without confirming signing-key safety and double-signing risk. `deploy.sh` has no rollback command, and `deploy.sh install` always resolves the current release, so moving the active symlink by hand is the supported path back. A later `deploy.sh install` runs normally on a rolled-back host and repoints `/opt/ctmd` to the new versioned directory. ## Operator reminders - Take backups before replacing binaries or changing configs. - Verify binary checksums or signatures when provided. - Watch logs and peer health through the upgrade window. - Coordinate validator-specific steps separately from full-node steps. - Do not use `--unsafe-skip-upgrades` except under explicit emergency coordination. - Security reports go to `security@c8ntinuum.com`; do not open public issues for vulnerabilities. For the official c8ntinuum channels, see [Official Links](/official-links). --- # Recovering a Node URL: https://docs.c8ntinuum.com/node-ops/node-recovery Use recovery procedures when a node is stuck, corrupted, out of disk, badly lagged, or unable to reconnect to peers. ## Recovery paths | Situation | First response | |-----------|----------------| | Node is far behind | Check peers and consider [state sync](/node-ops/state-sync). | | State sync failed | Reset and retry with a fresh trusted height and hash. | | Disk is full | Stop the service, increase disk capacity, and restart after confirming data integrity. | | Config is invalid | Restore the last known-good config and validate network-specific values. | | Validator risk | Stop and follow validator-specific key and slashing safety procedures. | ## Reset and retry For a non-validator node that can safely discard local block data, stop the process and reset the local chain database before retrying state sync or normal block sync. This targeted reset keeps the managed install and node home. Use the node home for the machine you are repairing. ```bash ctmd comet unsafe-reset-all --home ~/.ctmd ``` Linux service installs usually use `/var/lib/ctmd` instead of `~/.ctmd`: ```bash sudo systemctl stop ctmd.service sudo -u ctmd ctmd comet unsafe-reset-all --home /var/lib/ctmd sudo systemctl start ctmd.service ``` Do not run reset commands on validator infrastructure without confirming key safety, double-signing risk, and whether state can be safely discarded. `deploy.sh reset` is more destructive than `unsafe-reset-all`: it removes the managed install, node home, service/cache, and Linux system user/group so a clean install can start over. Use it only when you intentionally want to discard the managed node home and local key material. ## After recovery After any reset or disk repair, confirm the node is advancing and connected: ```bash curl -s http://127.0.0.1:26657/status | jq '.result.sync_info' ./deploy.sh status ``` If the node remains far behind, use [State Sync](/node-ops/state-sync) for non-archive nodes or check [Full Node Block Delivery](/node-ops/full-node-block-delivery) for peer connectivity and block propagation issues. --- # Full Node Block Delivery URL: https://docs.c8ntinuum.com/node-ops/full-node-block-delivery Block delivery depends on healthy peers, correct network configuration, sufficient hardware, and a node that can keep up with consensus and execution. ## Operator checks | Area | What to monitor | |------|-----------------| | Peer connectivity | Peer count, peer churn, failed dials, and seed availability | | Block lag | Local height compared to trusted public RPC or explorer height | | Execution health | Repeated execution, mempool, or application errors in logs | | Resource pressure | CPU, memory, disk I/O, and storage capacity | | Network exposure | P2P, RPC, and WebSocket ports exposed only as intended | ## Peer sources `deploy.sh` reads seeds, persistent peers, state-sync RPCs, and snapshot peer hosts from `https://resources.c8ntinuum.io/peers.json`, selected by chain ID. For deployment scripts and peer-list details, see [Deployment Scripts](/reference/deployment-scripts). For install and state-sync workflows, see [Full Node Installation](/node-ops/full-node-installation) and [State Sync](/node-ops/state-sync). P2P normally uses port `26656`. If the node is behind NAT or a load balancer, set `external_address = ":26656"` in the `[p2p]` section. --- # Events and WebSockets URL: https://docs.c8ntinuum.com/node-ops/events-and-websockets c8ntinuum publishes a public EVM WebSocket endpoint at: ```text wss://public-evm-rpc.c8ntinuum.com/websocket ``` The documented mainnet alternate is `wss://public-evm-rpc.c8ntinuum.io/websocket`, and the testnet endpoint is `wss://public-testnet-evm.c8ntinuum.link/websocket`. Applications can use WebSocket connections for subscription-style workflows, while operators running their own infrastructure should plan for connection limits, reconnection behavior, and monitoring. ## Application guidance | Need | Suggested approach | |------|--------------------| | New block notifications | Use WebSocket subscriptions where supported by your client library. | | Contract events | Subscribe for live events and backfill with bounded `eth_getLogs` queries. | | Long historical scans | Prefer an indexer over an unbounded RPC or WebSocket workload. | ## Operator checklist - Define public versus private WebSocket exposure. - Monitor active connections and reconnect storms. - Apply the documented connection limits: free WebSockets allow `1` connection per IP, while paid WebSockets allow `30` connections per API key. - Keep historical backfills on HTTP RPC or indexer infrastructure instead of long-lived subscription paths. For standard RPC methods, see [JSON-RPC API](/rpc-reference/json-rpc-api). --- # Archive Data Setup URL: https://docs.c8ntinuum.com/node-ops/archive-data Archive-capable nodes must retain historical application state and indexes. A node bootstrapped from a state-sync snapshot does not have pre-snapshot historical state, so a full archive node must replay from genesis if it is expected to serve complete historical queries. ## Archive configuration Archive settings live in two files in the node home: `app.toml` and `config.toml`. Set the pruning strategy in `app.toml`: ```toml pruning = "nothing" ``` `"nothing"` keeps all historic state and is the archive strategy. Do not add `pruning-keep-recent` or `pruning-interval` beside it: they apply only when the strategy is `custom`, so here they are inert and imply a retention limit the node never applies. Block retention is a separate control, also in `app.toml`: ```toml min-retain-blocks = 0 ``` `0` prunes no blocks. It governs CometBFT block pruning only, not application state pruning. Retain historical ABCI responses and transaction indexes in `config.toml`: ```toml [storage] discard_abci_responses = false [tx_index] indexer = "kv" ``` Enable EVM historical indexing on archive RPC profiles in `app.toml`: ```toml [json-rpc] enable-indexer = true ``` Keep CometBFT state sync disabled in `config.toml`: ```toml [statesync] enable = false ``` `[statesync]` in `config.toml` controls whether this node bootstraps *from* a snapshot; an archive node must never do that. `[state-sync]` in `app.toml` controls the snapshots this node *serves* to other nodes, and is safe to enable here. Do not run [`deploy.sh state-sync`](/node-ops/state-sync) on an archive node. It wipes local `data/`, bootstraps from a snapshot, then re-disables `[statesync]` — leaving a config that reads correct on a node with no pre-snapshot history. ## RPC and debug policy The default public EVM namespace allowlist in `app.toml` is: ```toml [json-rpc] api = "eth,net,web3" ``` A private archive or operations node can add `debug`, `txpool`, and `personal`: ```toml [json-rpc] api = "eth,net,web3,debug,txpool,personal" ``` `debug_traceTransaction` needs only `debug` in the `[json-rpc]` `api` list. The trace type comes from the tracer config in each request, so leave `[evm] tracer` at its default empty value. A non-empty value such as `"json"` attaches that tracer to every EVM transaction, `eth_call`, and gas estimate the node executes (`"json"` logs a per-opcode trace to stderr), and is meant only for local debugging. Check the node's `[json-rpc]` `api` list before relying on `txpool` or `debug` methods. ## Archive sizing and backups The current archive data footprint is roughly `1 TB` and grows about `10 GB/month`. Provision more disk than that: `4 TB` minimum and `8 TB` recommended for an archive node, as listed in [Hardware Requirements](/node-ops/hardware-requirements), so there is spare capacity for growth, compaction, upgrades, logs, and backup staging. Run archive backups monthly. ## Application alternatives Most applications do not need an archive node. Use: - [RPC Providers](/tooling-and-infrastructure/rpc-providers) for recent chain data. - [Indexers](/tooling-and-infrastructure/indexers) for token, contract, and application-level history. - [Historical Data](/developer-essentials/historical-data) for choosing the right data source. --- # The Network Score URL: https://docs.c8ntinuum.com/networkscore The Network Score is the c8ntinuum ecosystem contribution program. It covers participation across the interoperability protocol, the c8ntinuum chain, cross-chain CTM liquidity, and the upcoming SuperApp. What you did in the past and everything you do from now on all contribute to one number, measured in p8ints. ## Start here One number, two halves, and what the program rewards. One action, from the moment you take it to the moment it lands on your score. What a qualified account needs, and what qualification determines. What your p8ints are for, and the rules that hold whatever the reward turns out to be. Every category that pays, what it measures, and how it behaves. ## Proof of Social The onboarding checklist: follow, join, connect, verify. Verified ownership in selected NFT communities, three tiers, once per collection. One referral system, two legs: the financial share and the p8ints leg. ## Proof of Legacy OG status, Waitlist status and WC26 entries, credited once ever. ## Proof of Presence One check-in per UTC day, and the thing that carries your ladder through week one. Scored per chain on the balance the day's single reading finds. Scored daily on the CTM side of eligible pool positions. One spin a day, drawn on yesterday's base p8ints, verifiable by design. ## Proof of Passage A fixed-action cross-chain game: every scored operation moves exactly 10 CTM. ## The multiplier and the ledger One ladder, driven by holding, applied at earn time and never to a total. Two stages, per category, per UTC day: the event is priced, then the day is settled. Everything runs on UTC days, and balances are read once a day. Pending, confirmed, reversed, rejected. Only confirmed p8ints count. ## Accounts P8ints belong to an account, not a wallet. Linking, unlinking and merging. How the program defends itself in Season 0, and what it costs an honest user. The Network Score measures participation. It carries no monetary value and is not a token or a promise of one. One person, one identity: duplicate accounts, Sybil behavior and abusive activity are ineligible and may be rejected or reversed. --- # Overview URL: https://docs.c8ntinuum.com/networkscore/overview The Network Score is the c8ntinuum ecosystem contribution program. It covers participation across the interoperability protocol, the c8ntinuum chain, cross-chain CTM liquidity, and the upcoming SuperApp. What you did in the past and everything you do from now on all contribute to one number, measured in p8ints. It opens before the app and continues inside it. Season 0 is the pre-app chapter, and when the app arrives the same score carries in: nothing converts, nothing resets, no second number. Season 0 introduces three active participation pillars. Social covers the checklist and community tasks. Presence covers the daily check-in, the wheel, CTM holding and liquidity. Passage covers cross-chain activity through the interoperability product. Previous participation is carried into the score through Legacy. As the SuperApp launches, the same score expands further into trading, staking, token launches and other product activity. ## One number, two halves Your score has two halves and one deduction. | Part | What it holds | | --- | --- | | Legacy Score | The past only: OG status, Waitlist status, WC26 entries. Credited once, ever. | | Contribution Score | Everything earned from activity: social tasks, daily presence, CTM holding, liquidity, cross-chain operations, Partner NFT community drops, referrals, the daily wheel. | **The Network Score** = Legacy Score + Contribution Score, minus p8ints used for rewards. This is the number you see and the number you draw on. **Total Earned** = Legacy Score + Contribution Score, before any reward use. It is never reduced by rewards, and it is what the leaderboard ranks. Putting p8ints toward a reward never costs you a place. ## What it rewards The program follows a progression: identity, social participation, recurring presence, capital commitment, product usage, ecosystem contribution. On-chain and economic actions carry substantially more weight than social ones. Social tasks are how you enter the system. They are not a route to out-earning people who hold, provide liquidity, or run cross-chain operations. ## It never resets Seasons are chapters, not scoreboards. A season unlocks new ways to earn and frames the campaign around them. It never resets your p8ints and never pauses earning. There is no gap in earning between seasons. Holding pays, cross-chain operations pay, and streak days keep counting whether a season is running or not. ## Where to go next - [How it works](/networkscore/how-it-works) walks one action from the moment you take it to the moment it lands on your score. - [Qualification](/networkscore/qualification) covers the three things a qualified account needs, and what qualification does and does not gate. - [Ways to earn](/networkscore/ways-to-earn) lists every category that pays. The Network Score measures participation. It carries no monetary value and is not a token or a promise of one. One person, one identity: duplicate accounts, Sybil behavior and abusive activity are ineligible and may be rejected or reversed. --- # How it works URL: https://docs.c8ntinuum.com/networkscore/how-it-works Every p8int on your score arrives the same way, through the same five steps, whatever you did to earn it. ## 1. You act You follow an account, check in, hold CTM, add liquidity, or run a cross-chain operation. Some actions are one-time. Some are recurring, measured every day you keep them going. Actions are attributed to the UTC day they happened on, never the day we processed them. ## 2. The action is measured One-time actions have a fixed value. Recurring ones are measured: a balance is read once a day, an operation is confirmed on chain. What gets measured is described on each category's page. ## 3. The category prices it Each category turns what you did into base p8ints through its own scoring logic, then applies its current weight, the tuning value that category carries. Percentage bonuses add on top of the base. Flat one-time bonuses are separate line items and never ride the multiplier. ## 4. Your multiplier applies, at earn time Your [commitment multiplier](/networkscore/commitment-multiplier) applies to recurring earnings only, using the multiplier value active on that day. This matters more than it looks. The multiplier is applied to each event as it is earned, and it is never applied to a running total. Your history is priced at the multiplier you had when you earned it, and it is never repriced afterwards, in either direction. One-time p8ints always credit at x1.0: social tasks, legacy, Partner NFT community drops, destination-chain bonuses, referral fixed bonuses, and every wheel payout. ## 5. Caps, rounding, and the ledger Daily category limits apply after the multiplier. Rounding happens once, at the end, half up. The result is written to the ledger as one event that stores everything it was built from: the source value, the weight, the multiplier, the bonuses, the cap rule if one applied, and the final p8ints. Balances only ever change through events, so any line on your score can be replayed and explained. An event starts as pending and becomes confirmed. Only confirmed events count. --- # Qualification URL: https://docs.c8ntinuum.com/networkscore/qualification Qualification determines what you can do with your p8ints, not whether you can earn them. Your first connection, by email, wallet, or social account, creates your c8ntinuum account. **p8ints accrue to the account, not to an individual wallet.** An account becomes **qualified** when it meets all of the following conditions: - Two linked social accounts, including a verified X follow. - At least one completed capital or cross-chain action. - No unresolved fraud flag. **Email does not count as one of the two required social accounts.** One of the two linked socials must always be the verified X account. Accounts that are not yet qualified can still earn and view p8ints. Qualification is required only for specific utilities, rewards, leaderboard eligibility, and other participation mechanics. ## What it changes **Earning is never gated.** An unqualified account earns normally, sees its score, and has its p8ints credited. Nothing is held back and nothing is lost. Qualification is what lets those p8ints do something: use them for a reward, enter a reward event, appear on the public leaderboard, and receive a distribution. If an account later drops a requirement, those four pause until it is restored. Earning continues throughout, and p8ints already earned stay. ## Why it exists One person, one identity. The requirements are what make a fake account expensive: no account reaches a reward without a real social presence and a real position behind it. --- # Rewards URL: https://docs.c8ntinuum.com/networkscore/rewards P8ints are earned to be used. Rewards are what you use them on. ## Multi-directional by design Rewards are not a single destination. The program is built to hold many reward directions, each one announced and priced separately when it ships, each one with its own rules and its own window. More reward directions follow. The first one is announced before it opens. ## The rules that hold, whatever the reward is These apply to every reward direction, present and future. - **Using p8ints is one way.** P8ints are consumed when you commit them. There is no locking, no pledging, no freezing and no release. - **Qualification is required.** The requirements on the [Qualification](/networkscore/qualification) page determine whether p8ints can be used for rewards. Any additional eligibility checks are defined in the rules of each reward. - **Contribution outweighs onboarding.** Reward mechanics are designed so that social and legacy p8ints alone cannot capture meaningful rewards. On-chain contribution has to carry it. - **If we cannot deliver, you get the p8ints back.** If c8ntinuum cancels a reward or fails to deliver it, the p8ints return through a compensating credit event. Otherwise a commitment is permanent. - **Reversed fraud is taken from the balance first.** If a reversal leaves an account negative, further reward use is blocked until it is cleared. ## Where rewards live The Rewards page ships with the SuperApp at the start of Season 1. It holds every live direction and its price at the time. The Network Score measures participation. It carries no monetary value and is not a token or a promise of one. --- # Ways to earn URL: https://docs.c8ntinuum.com/networkscore/ways-to-earn Nine categories pay into The Network Score. Three of them are one-time. The rest repeat on the schedule in the table below. | Category | What it measures | Repeats | Multiplier applies | | --- | --- | --- | --- | | Social tasks | Following, joining, connecting, verifying | Once ever, per task | No | | Legacy | OG status, Waitlist status, WC26 entries | Once ever | No | | Daily check-in | Showing up, week one only | Daily, days 1 to 7 | No | | CTM holding | CTM balance at the daily reading, scored per chain | Daily | Yes | | Liquidity | The CTM side of eligible pool positions | Daily | Yes | | Tunnel Defense | Confirmed cross-chain operations | Daily | Yes | | Partner NFT drops | Verified ownership in selected collections | Once per collection | No | | Referrals | Referred users who qualify and stay active | Ongoing | No | | The daily wheel | A draw on yesterday's base p8ints | Daily, one spin | No | ## Recurring and one-time The distinction runs through the whole system. **Recurring earning** is what a live position or a repeated action generates: holding, liquidity, Tunnel Defense, and later trading, staking and fees. It is the only earning your [commitment multiplier](/networkscore/commitment-multiplier) touches. **One-time p8ints** credit at x1.0, always. Social tasks, legacy, Partner NFT community drops, destination-chain bonuses, referral fixed bonuses and wheel payouts are never multiplied and never weighted beyond their own rules. --- # Social tasks URL: https://docs.c8ntinuum.com/networkscore/social-tasks The checklist is how you enter the system. Each task pays once, ever, and its value is fixed forever: no weight, no multiplier, no season changes it. ## The checklist | Task | How it is verified | | --- | --- | | Follow @c8ntinuum on X | Social connection | | Follow @c8ntinuum_ on X | Social connection | | Follow @c8ntinuumARENA on X | Social connection | | Join Telegram | Social connection | | Join Discord | Social connection | | Connect your email | Account | | Set up wallets | Wallet confirmation | | Complete Matrica verification | Matrica | ## One X connection covers all three quests Connecting X is a single act, not three. Once it is done, the connect step disappears from the @c8ntinuum, @c8ntinuum_ and @c8ntinuumARENA rows at the same time, and only the follow itself is left outstanding on each one. Each follow is still claimed separately and still pays on its own. ## Set up wallets You are sent to app.c8ntinuum.com/wallets, where you confirm the addresses we already hold for you or connect another wallet. The task completes when **both** addresses are confirmed. ## Email If you signed up with email, it is already connected, so the task cannot be performed a second time. Those p8ints are credited automatically when the account is created. Email does not count toward the two linked socials that [qualification](/networkscore/qualification) requires. One of those two must be the X follow. ## What social tasks do not do - They carry no weight and no multiplier, so they always credit at face value. - They do not keep a streak alive and they do not advance the commitment ladder. - They do not feed the daily wheel. - They do not count toward the five real actions that lift the multiplier cap. --- # Partner NFT community drops URL: https://docs.c8ntinuum.com/networkscore/partner-nft-drops Partner NFT community drops recognize membership in selected external NFT communities. They are a cross-community welcome, not a reward for economic participation, and they are priced as one. Claims open later in the season, with Phase 2, and close with Season 0. ## Three tiers Collections sit in one of three tiers, primarily based on market profile and floor price, with selected manual overrides: | Tier | Profile | | --- | --- | | Tier 1 | Blue chip | | Tier 2 | Established mid-market | | Tier 3 | Entry level and newer chains | The per-collection allowlist is versioned and frozen before claims open. It carries a small number of manual tier overrides, which are decisions rather than outputs of the floor rule. ## Claim rules - You claim once per collection. - A claim needs **24 hours of continuous custody** of the token. - Each token pays **once globally**. A token that has already paid cannot pay again, on any account. --- # Referrals URL: https://docs.c8ntinuum.com/networkscore/referrals ## The financial leg, already live The financial referral leg is already live and continues unchanged, providing a 15% referral share under the live referral program. ## The p8ints leg Referral p8ints open later in the season, with Phase 2. Nothing referral-related pays p8ints before then. Direct referrals only, with no second-level share. Three things pay: your referral becoming qualified, your referral staying active across six distinct days inside a rolling 30, and an ongoing share of the base p8ints they confirm. The ongoing share is the only recurring referral reward, and it is computed on your referral's **base p8ints**, before weight, bonuses, the multiplier and the wheel. It covers what they earn from holding, liquidity and Tunnel Defense, and later from trading, staking and fees. Nothing else generates a share: legacy, social tasks, the check-in, Partner NFT drops, wheel payouts and referral p8ints themselves are all excluded. ## Limits and controls A daily ceiling applies to the ongoing share, so a large network cannot out-earn capital and daily play. You cannot refer yourself, and the same wallet cluster cannot either. Only confirmed p8ints generate a share, and if the originating event is reversed, the referral credit is reversed with it. --- # Legacy URL: https://docs.c8ntinuum.com/networkscore/legacy ## The three sources | Source | What it recognizes | How it is verified | | --- | --- | --- | | OG status | The OG role | Connecting Discord. There is no other way to claim it. | | Waitlist status | Being on the Waitlist | The frozen Waitlist snapshot | | WC26 entries | Valid World Cup 2026 prediction entries | The frozen entries snapshot, per valid entry, up to a ceiling | The three stack, up to a maximum legacy allocation per account. ## How it behaves - **Once ever.** Legacy never repeats and never multiplies. It always credits at x1.0, with no weight. - **It is a head start, not a leaderboard.** Maximum legacy for a single account is smaller than one day of maximum Season 0 earning, so it cannot decide a ranking on its own. - **Qualification is not required to claim.** Legacy credits to your balance whether or not your account is qualified. Qualification is still required before those p8ints can be used for a reward, ranked on the leaderboard, or entered into a reward event. - **A claim over the ceiling is trimmed, not rejected.** If a claim would exceed what is left of your allowance, it credits what fits. The cap limits an allocation. It never voids one. ## The claim window Legacy is claimable from the first day of Season 0 until the season ends, at 23:59:59 UTC on the last day. After that the claim function shuts. Only **unclaimed** entitlements expire. Legacy p8ints already confirmed on your ledger are permanent, and are only ever removed by a fraud reversal. --- # Daily check-in URL: https://docs.c8ntinuum.com/networkscore/daily-check-in The daily check-in runs for the first seven days of Season 0 and then stops. One check-in per UTC day, worth the same fixed amount every time: no weight, no multiplier, no streak bonus on the p8ints themselves. ## Its real job The check-in is not there for its p8ints. It is there to carry your [commitment multiplier](/networkscore/commitment-multiplier) through week one. The ladder runs on capital: any UTC day you hold a position that scores is a qualifying day. CTM holding is live from day 1, so it can carry your ladder from the open. The check-in also stands in during days 1 to 7, so an account that has not bought yet keeps its ladder intact. Miss a day in week one and the ladder resets, exactly as a missed holding day would later. ## The window closes After day 7 the button disappears and does not come back. The design is sequential, not overlapping: presence carries the ladder, then capital does. ## The last check-in feeds the first spin The [daily wheel](/networkscore/daily-wheel) opens later in the season and draws on the previous day's base p8ints. The check-in reaches the wheel once only, as part of the basis for the first spin. Earlier check-in days produce a basis that nothing spins on, because the wheel is not open yet. ## What the check-in does not do - It does not count toward the five real actions that lift the multiplier cap. - It is not multiplied, ever, at any streak length. --- # CTM holding URL: https://docs.c8ntinuum.com/networkscore/holding Holding pays every day you hold. It is the quietest way to earn: once the position exists, it needs nothing from you. ## Scored per chain, not on one balance This is the rule that surprises people. Holding is **not** scored on your combined balance across everything you own. Wallets are summed **within a chain**. The curve then runs on each chain separately, and the results are added. Holding on Ethereum earns its own p8ints, holding on Solana earns its own, and they stack. ## The minimum A chain needs at least **5 CTM** to score. Below that it earns nothing on that chain, and it does not keep your streak alive either. The same floor gates scoring and the [commitment ladder](/networkscore/commitment-multiplier). ## One read a day Holding and liquidity are read once a day. A single job runs every 24 hours and reads every registered wallet, and the balance it finds is the balance your day is scored on. The same reading decides your [streak](/networkscore/commitment-multiplier): a position in place at or above its minimum keeps the day alive. One read, one balance, one accrual per category per day. A position opened after the day's read scores from the next day, not the current one. ## Token quantity, not dollars Season 0 scores holding in CTM token quantity. No price feed is involved. A price move during the season does not change anyone's p8ints, in either direction. ## One token, one category In the day's reading, each CTM token is scored in exactly one category: held in a wallet it scores as holding, pooled it scores as [liquidity](/networkscore/liquidity), staked it scores as staking. This includes LP tokens and liquid staking derivatives. Nothing is counted twice. --- # Liquidity URL: https://docs.c8ntinuum.com/networkscore/liquidity Liquidity opens later in the season, with Phase 2. --- # The daily wheel URL: https://docs.c8ntinuum.com/networkscore/daily-wheel The wheel is the one random layer in the program. It pays a bonus calculated on what you earned the day before. The wheel opens later in the season, with Phase 2. ## How a spin is earned One spin per qualified day. A day qualifies if the **previous** day carried at least one qualifying on-chain action. The first spin runs on the day before it: your holding base p8ints plus that day's check-in. That is the only time a check-in ever reaches the wheel. ## Same day only A spin is offered on the day it is earned and it is gone at the end of it. - The window is **24 hours from reveal**, not from midnight, so a late reveal moves the window forward rather than voiding your spin. - At most one spin is live on an account at any moment. - Nothing banks. There is no backlog, no claim order, and no queue of expired spins to work through. If you do not take the spin inside its window, it lapses. That is the whole rule. ## The basis A spin pays a multiple of your **basis**: the sum of base p8ints from the previous UTC day's confirmed events, in the categories that feed the wheel. That is check-in, holding, liquidity and Tunnel Defense. The basis is taken straight from the ledger, before bonuses and before your multiplier. Excluded in full: legacy, social p8ints, Partner NFT community drops, weight effects, percentage bonuses, the commitment multiplier, boosters, destination-chain bonuses, referral p8ints, every fixed and milestone bonus, manual adjustments, and previous wheel rewards. One consequence worth knowing: the basis reads your **uncapped** base, so a day where your category cap bound still spins on the full amount you generated. ## The outcomes Outcomes range from nothing at all to several times your basis, with a ceiling on what a single spin can pay. - A result that pays nothing means exactly that: **no bonus**. Base p8ints are never removed, ever. The wheel cannot cost you anything. - A payout credits at x1.0. It is excluded from multipliers and boosters, and it never touches your commitment ladder. - The wheel does not count toward the five real actions. ## Verifiable randomness The wheel is designed around a commit-reveal mechanism: a seed commitment is published before play and the corresponding seed is revealed after the relevant spins close. This structure is designed to make the draw independently verifiable by allowing the revealed seed to be checked against the commitment published beforehand. ## After the season The wheel does not stop when a season ends. The program is always on, so activity on the last day of a season earns a spin on the next day. --- # Tunnel Defense URL: https://docs.c8ntinuum.com/networkscore/tunnel-defense Tunnel Defense is the cross-chain mini-app, CTM routes only. It is a fixed-action game, not a volume game: every scored operation is the same size and earns the same base p8ints, whatever the route and whatever you hold. Tunnel Defense opens later in the season, with Phase 2. ## What counts An eligible operation moves **exactly 10 CTM** between **two different supported networks**, and leaves the value on the destination chain for at least **24 hours**. Only the first 12 operations a day score. Same-chain transfers never score. Moving your own funds across chains is eligible. That is exactly what the product is for. Reaching a supported destination chain for the first time pays a one-time bonus, once per destination, per account. ## What one operation unlocks Beyond its p8ints, a single confirmed operation satisfies the capital or cross-chain action that [qualification](/networkscore/qualification) requires, and counts as one of the five **real actions** that lift your [multiplier](/networkscore/commitment-multiplier) cap. --- # Commitment multiplier URL: https://docs.c8ntinuum.com/networkscore/commitment-multiplier The commitment multiplier is the streak mechanic. It rewards staying, not doing. ## One ladder It starts at x1.0 and rises by **+0.1x for every completed block of 6 unbroken qualifying days**, to a ceiling of x2.0. | Day | Multiplier | | --- | --- | | 1 to 5 | x1.0 | | 6 to 11 | x1.1 | | 12 to 17 | x1.2 | | 18 to 23 | x1.3 | | 24 to 29 | x1.4 | | 30 to 35 | x1.5 | | 36 to 41 | x1.6 | | 42 to 47 | x1.7 | | 48 to 53 | x1.8 | | 54 to 59 | x1.9 | | 60 and after | x2.0 | Days count continuously across seasons and across any gap between them. There is no pause and no reset at a season boundary. This is the full ladder. The highest multiplier any account can reach arrives inside Season 1. ## What a qualifying day is Any UTC day your account holds an eligible capital position that actually **scores**: CTM holding from 5 CTM, liquidity, or staking once it is scored. It is passive. Leaving a position alone keeps the ladder alive. You do not need a daily transaction, and there is nothing to click. The streak is worked out when the day is settled. A position below the minimum does not qualify. The 5 CTM floor gates the streak exactly as it gates scoring. CTM holding is live from day 1, so a scoring position can carry the ladder from the open. During days 1 to 7 of Season 0 the daily check-in also stands in for a qualifying day, which keeps the ladder reachable for an account that has not bought yet. Streak qualification uses CTM token **quantity**, fixed when the position starts. A price move alone can never break your streak. ## There is no grace day One missed qualifying UTC day resets the ladder to x1.0, and counting restarts from your next qualifying day. The single exception is our own failure. An action that happened on the correct UTC day is not treated as missed because c8ntinuum settled it late or had an incident. ## What the multiplier applies to **Recurring earning only**: holding, liquidity, Tunnel Defense, and later trading, staking and fees. One-time p8ints always credit at x1.0 and are never multiplied: social tasks, legacy, Partner NFT community drops, destination-chain bonuses, referral fixed bonuses, every "first X" milestone, and every wheel payout. ## Applied at earn time, never to a total Each event is priced with the multiplier active on the day it was earned, and it stays priced that way forever. The multiplier is never applied to a running total. If it were, every movement in your multiplier would reprice your entire history, in both directions. Instead the multiplier lives inside the p8ints as they are credited. --- # How p8ints are calculated URL: https://docs.c8ntinuum.com/networkscore/how-points-are-calculated Scoring runs in two stages. The first prices a single event. The second settles a whole day for one category. ## Stage 1: the event For each event: **measured activity → category scoring logic → category weight → percentage bonuses** - The base comes from the category's scoring logic, computed on what was measured: the balance at the day's reading, a confirmed operation, or a fixed task value. - The **weight** is the category's current tuning value. Weights are bounded, and every change to one is announced and versioned, never silent. A change applies forward and never reprices p8ints already credited. - **Percentage bonuses add on the base.** They never multiply each other. Where two duration bonuses could apply, the highest qualifying one applies and they do not stack. **Flat bonuses are separate.** A destination-chain bonus, a first-trade bonus, a completion bonus: each is its own line item, credited at x1.0, and sitting **outside** its category's daily cap. ## Stage 2: the day Once a UTC day is closed, each category settles: - The multiplier applies to the multiplier-eligible portion of the day, using the multiplier value as of that day's end. - Multiplier-ineligible recurring p8ints are added at face value. - The category's daily cap applies **after** the multiplier, to the final figure. - Rounding happens once, half up, at the end. Internal math carries more precision than the number you see, and it is only resolved once. Flat and one-time bonuses are added separately, at x1.0, outside that cap. ## When a cap binds Later events in the day truncate first, in chronological order. A capped credit records that the cap applied and credits whatever capacity was left. It never reverts. An honest action can never cost you p8ints that were already earned. ## Everything is stored Each event stores the values it was built from: the source value and its unit, the chain, the transaction reference, the base p8ints, the weight, the bonuses, the multiplier, the cap rule if one applied, and the final p8ints. Two consequences: - Any row can be replayed and explained, so a support question has a factual answer rather than an opinion. - Nothing is recalculated later. A reversal uses the exact figure that was originally credited. ## Where the chain and the database each stand Chain-confirmed balances and events are authoritative for The Network Score and Total Earned. The database holds pending calculations, ownership and custody evidence, and the supporting daily readings. When the two disagree, because of a dropped signature, a reverted transaction or a reorg, the chain wins and the database is reconciled to it. --- # Days and readings URL: https://docs.c8ntinuum.com/networkscore/days-and-readings ## Everything is UTC A day runs 00:00:00 to 23:59:59 UTC, and an action belongs to the UTC day it happened on, never the day it was processed. Worth internalizing once if you are in a distant timezone: your streak, your check-in and your wheel window all run on UTC, not on your local midnight. ## One read a day Holding and liquidity are read once a day. A single job runs every 24 hours and reads every registered wallet, and the balance it finds is the balance your day is scored on. The same reading decides your [streak](/networkscore/commitment-multiplier): a position in place at or above its minimum keeps the day alive. One read, one balance, one accrual per category per day. A position opened after the day's read scores from the next day, not the current one. ## One token, one category In any reading, each CTM token is scored in exactly one place: held in a wallet it is holding, pooled it is liquidity, staked it is staking once staking is scored. LP tokens and liquid staking derivatives included. Nothing is counted twice. ## Tokens, not price Season 0 is denominated in CTM token quantity, with no price feed, so a price move during the season changes nobody's p8ints. Season 1 activities that involve other assets, meaning trading, shared fees and staking, are denominated in USD, because they have to be. Each day is settled once it has closed, using your multiplier as it stood at the end of that day. --- # Event states URL: https://docs.c8ntinuum.com/networkscore/event-states Every p8int movement is one immutable event. Balances change only through events, and an event is never edited after it is written. ## The states | State | What it means | | --- | --- | | Pending | The event exists and is visible, but it does not count yet | | Confirmed | The event counts, everywhere | | Reversed | The event was confirmed and has been undone | | Rejected | The event never became confirmed | ## The confirmed-only rule Only **confirmed** events count toward The Network Score, Total Earned, the leaderboard and reward eligibility. Pending p8ints are visible in your account but unusable. That visibility is deliberate: you can see what is being processed rather than wondering whether it registered. On-chain p8ints with a suspicious pattern stay pending until review completes. Most things confirm without anyone looking at them. ## Reversals A reversal is the only thing that reduces Total Earned. Caught fraud loses its p8ints **and** its rank. Three rules govern how a reversal propagates: - It uses the exact figure originally credited. Nothing is recalculated at reversal time. - It breaks the [streak](/networkscore/commitment-multiplier) forward from that day only. Other days are never repriced. - Downstream effects are corrected with compensating events, never by editing history. The ledger is append-only. A reversal takes the current balance first. If the balance does not cover it, the account goes negative and further reward use is blocked until it clears. ## Why it is built this way An append-only ledger with a stored calculation on every row means any figure on your score can be replayed from what it was built from. Support answers become factual: this is the balance the day's read found, this was the weight, this was the multiplier that day, this is why the number is what it is. --- # Accounts and wallets URL: https://docs.c8ntinuum.com/networkscore/accounts-and-wallets ## The account Any first connection creates an account: email, wallet or social. All p8ints accrue **per account**. Everything else in the system, qualification, the streak, caps, the leaderboard, is computed on the account, not on any individual wallet. ## Wallets - One wallet address belongs to **exactly one account**, first come first served. - Linking happens through a real wallet connection that you control. - Value-based formulas compute on the **summed** value across all wallets linked to your account. [Holding](/networkscore/holding) is the one exception, and an important one: it is scored **per chain**. Wallets are still summed, but only within the same chain, and each chain's result is calculated separately and then added. ## Unlinking A wallet can be unlinked. Accrual from it stops going forward, and the history it generated stays exactly as it was. Nothing is retroactively removed. ## Merging duplicate accounts If you accidentally end up with two accounts, they are merged rather than edited: - P8ints from both are combined. - The better streak survives. - Once-ever tasks and lifetime caps are re-checked at merge, so nothing is paid twice. History is never rewritten to do it. The merge is itself an event on the ledger. The same tooling resolves duplicates found by anti-abuse review. See [one person, one identity](/networkscore/one-person-one-identity). --- # One person, one identity URL: https://docs.c8ntinuum.com/networkscore/one-person-one-identity One person, one identity. Sybil, duplicate-account and abusive activity is ineligible and may be rejected or reversed. ## Season 0 anti-abuse approach Every [qualified](/networkscore/qualification) account carries two linked social accounts including a verified X follow, at least one completed capital or cross-chain action, and no unresolved fraud flag. Season 0 combines qualification requirements, wallet analysis, activity-specific controls and manual review of flagged patterns. Additional anti-abuse capabilities may be introduced as the program evolves. Qualification helps ensure that reward participation is backed by both social identity and real ecosystem activity. ## Per-category controls Each category has the specific defense its own shape needs. | Pattern | What defeats it | | --- | --- | | Flash liquidity, in for the snapshot and out | The daily balance reading | | Circular transfers and bounced funds | The 24-hour destination hold | | Same-chain loops dressed as cross-chain | Only transfers between two different networks score | | Wash and self-trading, related-wallet loops | Effective volume rules, Season 1 | | Self-referrals and referral farms | Cluster checks, and the share pays only on confirmed base p8ints | | Claiming an NFT you just borrowed | 24 hours of continuous custody, and each token pays once globally | | Duplicate accounts | Wallet clustering feeding scoring directly, plus merge tooling | ## Review, and what it costs an honest user Suspicious on-chain p8ints stay [pending](/networkscore/event-states) until a review completes. Pending p8ints are visible but unusable, and they confirm when the review clears them. Most activity never goes near a review. Where one happens, the outcome is an event on the ledger like anything else, with a reason recorded. ## If fraud is confirmed P8ints are reversed. A reversal removes them from The Network Score **and** from Total Earned, so a caught account loses the rank as well as the balance, and the streak breaks forward from the reversed day.