For the complete documentation index, see llms.txt. This page is also available as Markdown.

21X Integration Guide

A reference for partners connecting to 21X.

21X Integration Guide

21X Integration Guide

1. Introduction

21X is a multi-chain regulated exchange. Secondary-market trading is executed on a smart-contract orderbook, with each trading pair pinned to a specific blockchain. Two chains are exposed in the proof-of-concept (POC) environment:

  • Polygon Amoy - EVM orderbook contract, ERC-20 token approvals, EVM transactions.

  • Stellar Testnet - Soroban orderbook contract, Soroban token approvals, Soroban contract invocations.

The 21X REST API (Market View) is the single source for trading-pair discovery across both chains. It returns the target chain, the orderbook and token contract addresses, the scaling factors used to encode orders, the trading status, and public market data.

Order placement and cancellation are not REST operations. Both chains require the trading wallet to sign and submit a blockchain transaction directly to the active orderbook contract. REST is used for discovery and market visibility only.

2. Getting started

Before integrating, complete the following:

  1. Confirm which chains and trading pairs your integration will cover. Each pair is bound to either Polygon Amoy or Stellar Testnet; you must support the chain associated with each pair you trade.

  2. Prepare a scripting language of your choice (Python, TypeScript, Java, Go).

  3. Prepare a REST API client such as Postman to test market-data calls and inspect responses.

  4. For Polygon: prepare an EVM wallet on Polygon Amoy with POL test gas, and an EVM library such as ethers.js, web3.js, viem, or web3.py.

  5. For Stellar: install the Stellar CLI or a Soroban-capable SDK, and prepare a funded testnet account.

  6. Coordinate with 21X to obtain whitelist/client authorization on the target orderbooks and beneficiary or trustline authorization on the relevant tokens.

  7. Record the exact versions of every tool and SDK your integration is validated against (Stellar CLI, ethers.js, web3 library, language runtime) and pin them in your build. CLI output formats and SDK APIs change between versions; an integration validated against one version is not automatically valid against another. The JavaScript examples in this guide use the ethers v6 API (ethers.JsonRpcProvider, AbiCoder.defaultAbiCoder()); they do not run unmodified on ethers v5.

Production note. Production REST URLs, contract addresses, custody and signing requirements, and authentication and onboarding details must be obtained from 21X before go-live. Do not hardcode POC contract addresses in production systems.

3. How to connect

21X exposes two integration surfaces relevant to this guide:

  1. REST API. Provides access to structured market data, including instrument metadata, contract addresses, scales, public orderbook snapshots, price levels, reference prices, and trade information.

  2. Smart contract interface. Used for direct on-chain interactions, particularly for executing secondary-market orders and cancellations.

The high-level integration flow is the same for both chains:

3.1 REST environment

3.2 Polygon Amoy environment

3.3 Stellar Testnet environment

4. Market View (REST)

Use the REST API to discover active instruments, target chains, orderbook contracts, token contracts, scales, limits, fees, market status, and public orderbook state. The blockChain field on each pair determines whether the order is executed on Polygon or Stellar.

Units in REST responses. All prices, order values, and reference prices in REST responses are human-readable decimal strings - for example "100.00", "10000000.00" - not scaled integers. Scale factors themselves are returned as strings (for example "1000"). Scaled integers appear only in on-chain order encoding (§5.5) and in on-chain events. Do not apply the internal scales to REST values.

4.1 Discovering pairs

The response is an object, not a top-level array:

Iterate over .items[] in every example below. A snippet that treats the response as a top-level array (jq '.[]') will return nothing.

4.2 Trading pair fields

Field
Purpose

id

REST trading pair identifier.

blockChain

Target execution chain, for example POLYGON_AMOY or STELLAR_TESTNET.

baseTokenData.symbol

Base asset display symbol.

baseTokenData.isin / baseTokenData.dti

Instrument identifiers (ISIN, Digital Token Identifier) of the base asset.

quoteTokenSymbol

Quote asset display symbol.

quoteTokenEquivalentCurrency

Fiat currency the quote token represents, for example USD.

smartContractOrderBook

Blockchain address of the orderbook contract. Orders must be sent to this address.

smartContractBase

Blockchain address of the base (financial instrument) token contract.

smartContractQuote

Blockchain address of the quote (e-money) token contract.

baseTokenInternalScale

Scale used for base quantity in order encoding.

quoteTokenInternalScale

Scale used for quote price in order encoding.

baseTokenNativeScale

Native scale used for base token balances and transfers.

quoteTokenNativeScale

Native scale used for quote token balances and transfers.

minimumOrderValue / maximumOrderValue

Pre-trade order value limits.

makerCommission / takerCommission / marketMakerCommission

Commission parameters. Read per pair - rates differ between pairs and between maker and taker (see §6.5).

tickSizeMode

Tick-size regime applied by pre-trade controls, for example REGULATORY or GRANULAR. See §5.5.

maximumMatches

Maximum number of matches a single incoming order may generate (see MaxMatches rejection, §7.10).

liquidityBand

Liquidity band used by tick-size and volatility controls.

staticReferencePrice

Reference price used by market controls.

staticThreshold / dynamicThreshold

Volatility-management thresholds.

priceCollarFactor

Pre-trade control parameter.

tradingStatus

Current trading status. Submit orders only when the market is open or in continuous trading.

statusChangeReason

Reason for the last trading-status change, for example START_OF_TRADING_DAY.

orderBookVersion

Version of the deployed orderbook contract.

Trading statuses

tradingStatus follows the venue's trading phase lifecycle. The full status set:

Lifecycle status
Meaning

Created

Pair and orderbook deployed but inactive; activated manually by the venue.

Out of Trading

Active pair outside trading hours.

Continuous Trading

Market open; the only status in which orders are accepted.

Automatic Trading Halt

Volatility-triggered halt; resumes automatically (see trading hours below).

Manual Trading Halt

Halt initiated via the venue's Service Portal (regulatory, technical, capacity, …).

Disabled

Pair deactivated.

Permanently Deleted

Terminal state; the pair is offboarded.

REST renders statuses in upper snake case such as CONTINUOUS_TRADING; expect the same convention for the others (for example OUT_OF_TRADING). Pairs in Created, Disabled, or Permanently Deleted states are hidden from the venue's trading front end; the REST list contains one entry per active pair. Gate order submission on CONTINUOUS_TRADING exactly and treat every other value as not-open.

statusChangeReason reports why the status last changed, from a fixed list: Start of Trading Day, Manual Market Open, End of Trading Day, Manual Market Close, Regulator/Participant/Venue/Capacity Initiated Halt, Volatility Halt, Technical Halt, Trading Pair Activated/Deactivated/Offboarded, Trading Pair Creation, Automatic Resume. (REST renders these in upper snake case as well - START_OF_TRADING_DAY is confirmed live.)

Trading hours and halts

  • Trading days open at 08:00 CET and close at 17:00 CET, observing German daylight-saving time. Pairs do not open on non-working days (weekends, holidays, and venue-defined dates).

  • At opening time, pairs in Out of Trading or Automatic Trading Halt move to Continuous Trading and the daily halt counter resets. At closing time, pairs move to Out of Trading and the static reference price is set to the last execution price.

  • Automatic (volatility) trading halts resume automatically: roughly 5-6 minutes after the first halt of the day and 15-16 minutes after the second, provided the halt occurred more than 30 minutes before close. The tradingHaltCounter field in tradeinfo counts that day's automatic halts.

  • About 5 minutes after closing time, open orders are cleared - see §5.4 for how this surfaces on chain.

4.3 Selecting the right pair

There are two ways to resolve a pair, depending on what you already know.

If the trading pair id is known, call the single-resource endpoint directly. There is no need to list every pair and filter client-side just to find an id you already have:

If you are discovering by attributes, list the pairs and filter by blockChain, baseTokenData.symbol, and quoteTokenSymbol - but do not filter by id (you do not have one yet):

The POC environment exposes several active pairs across both chains, including pairs whose symbols differ by a single prefix (for example AMA1/XUSDT and XAMA1/XUSDT are both Stellar Testnet pairs). When discovering by attributes, match all three of blockChain, baseTokenData.symbol, and quoteTokenSymbol. Do not rely on a partial symbol match alone.

Read this before hardcoding. Symbols are not unique across pairs. Once you have resolved a pair, drive the trading flow by its id, and treat the addresses returned alongside the id as the source of truth for that environment.

4.4 Public market endpoints

Sample responses

orderbook and pricelevels share the same envelope: the pair id plus a buy array and a sell array. An empty book returns empty arrays (this is the normal state of a quiet POC pair, not an error):

The entries inside buy / sell carry price and quantity as human-readable decimal strings, consistent with the units rule at the top of §4. Inspect a live populated response for the exact per-entry field names before writing your parser - place a resting order on your own sandbox pair if every public book is empty.

tradeinfo returns last price, reference price, 24-hour change and volume, and trading status:

All prices here are human decimals ("100.00"), not scaled integers. tradingHaltCounter counts the day's automatic trading halts and resets at market open (see §4.2); statusChangeReason follows the enum in §4.2.

4.5 Pagination and rate limits

The collection response carries a total_count field, which implies the endpoint is designed to page. As of this writing, however, the POC /tradingpairs endpoint ignores limit and offset query parameters and always returns every active pair in a single page (verified against the live POC: ?limit=1 still returns all items). The limit parameter is honoured on the orderbook and pricelevels endpoints, where it bounds the number of levels returned per side.

Code defensively against this changing:

  • After fetching /tradingpairs, compare .items | length against .total_count. If they differ, the endpoint has started paging - implement the paging parameters before trusting the list to be complete.

  • Do not build logic that depends on receiving all pairs in one response.

Rate limits for the POC REST API are not published. Apply standard client-side discipline: cache pair metadata rather than re-fetching it on every order, throttle polling loops, and back off exponentially on HTTP 429 or 5xx responses. Confirm production rate limits with 21X before go-live.

4.6 POC pairs

Chain
Pair
Trading pair id
Orderbook

POLYGON_AMOY

USMO / 21XUSDQ

57472b2c-4215-4cdf-935e-63226ebd004b

0x3e9d9dC85C5De625942483dfa16d20B049254117

STELLAR_TESTNET

XAMA1 / XUSDT

50a1b1ec-b46a-459e-9359-57412351e45f

CCGMSRH63Z7SAQVRH3O4VHOXMLWJZ2MOYWHWMQF7RKUJCBUYWWBQ35RQ

These are examples, not stable identifiers. Trading pair ids, orderbook addresses, and token contract addresses can change at any time. Deploying a new orderbook produces a new trading pair id and new contract addresses; token contract addresses change less often, but can also change. Always discover the active values through the REST API for the target environment immediately before trading, and never hardcode the values above into a production system.

4.7 REST authentication

Public market endpoints - /tradingpairs, /tradingpairs/{id}, /orderbook, /pricelevels, /tradeinfo - do not require authentication. Participant-scoped endpoints for wallet, order, and trade history require a bearer token issued by 21X to your sandbox account; calls without it return HTTP 401. Request credentials when you request your sandbox.

Authenticated history endpoints are out of scope for this guide. Until your integration uses them, treat the on-chain events emitted by the orderbook contract (see §6.8 and §7.10) as the source of truth for order state.

5. Order model

5.1 Buy and sell

  • Buy order. The participant buys the base asset and pays the quote asset. The orderbook may pre-fund or transfer the quote token.

  • Sell order. The participant sells the base asset and receives the quote asset. The orderbook may lock or transfer the base token, depending on configuration.

When the side value appears in events or struct fields, it uses the same enum on both chains:

Value
Side

1

Buy

2

Sell

5.2 Limit vs market

Value
Type
Behaviour

0

Limit

The order has a specified price. Any unmatched remainder may rest on the book, depending on the execution condition.

1

Market

The order executes immediately against available liquidity and does not rest on the book.

Market order execution. Market orders are treated as Immediate or Cancel (IOC) by default: any quantity that cannot be filled immediately is cancelled. If Fill or Kill (FOK) is selected, the market order must execute in full immediately or be rejected.

5.3 Execution conditions

Value
Name
Behaviour
Typical use

0

Standard

Match where possible; any unmatched limit remainder rests on the book.

Normal limit orders.

1

Immediate or Cancel (IOC)

Match what is immediately available; cancel any unfilled remainder.

Take available liquidity only.

2

Book or Cancel (BOC)

Rest on the book only if the order would not immediately match. Reject if it would cross.

Post-only / maker-only orders.

3

Fill or Kill (FOK)

Execute the full quantity immediately or reject the entire order. No partial fill.

All-or-nothing immediate execution.

An IOC or FOK order submitted against an empty or non-crossing book is rejected. The transaction itself succeeds and emits an OrderRejected event with a quantity_not_executed equal to the original quantity.

5.4 Validity constraints

Value
Name
Meaning

0

Good For Day (GFD)

Default validity for limit orders.

1

Good Till Date (GTD)

Valid for the number of days specified in the lifetime field.

Rules for lifetime:

  • Expressed in days. Valid range is 0 to 90.

  • lifetime = 0 is accepted and behaves as the venue default.

  • lifetime > 90 is invalid and should be rejected by client-side validation before submission.

  • The field is only meaningful for limit orders that set validityConstraints = 1 (GTD).

How expiry is surfaced. A GFD or GTD order that reaches its expiry is removed by the venue, not by the participant. Open orders are cleared by an off-chain venue process about 5 minutes after the 17:00 CET close on trading days (§4.2). On chain this appears as a contract-initiated cancellation: a CancelOrder event with cancellationType = 2 (ByContract), returning any pre-funded tokens per the pair configuration. The same ByContract cancellation type is used when a non-pre-funded order fails its balance/allowance check at matching time (§8.1). Reconciliation logic must therefore treat CancelOrder events it did not initiate as a normal terminal state, not as an anomaly - and must not assume every cancellation maps to one of its own cancel*Order transactions.

5.5 Scaling

Orders are encoded with integer values derived from the internal scales returned by REST. Convert human values to scaled integers before encoding:

Use decimal-safe arithmetic. Do not use binary floating point. Examples:

Exactness, rounding, and tick sizes

The scaling formulas above only produce valid encodings when the product is an exact integer. A price of 1.234 against a quote internal scale of 100 yields 123.4, which cannot be encoded. Client-side validation must reject such inputs rather than silently round or truncate them: a silently rounded price is a different order from the one the user asked for, and a truncated quantity changes the order value checked by pre-trade controls.

The internal scale defines the finest representable increment, but the venue may enforce a coarser tick size on top of it: each pair carries a tickSizeMode (REGULATORY or GRANULAR in the POC) and a liquidityBand, which together determine the price increments pre-trade controls accept. An exactly-scaled price can therefore still be rejected for being off-tick. Request the tick-size table for your pairs from 21X, validate against it client-side, and treat a pre-trade price rejection (§9.4) as the venue-side enforcement of the same rule.

Internal vs native scales

A pair carries two scale systems and they are not interchangeable:

  • Internal scales (baseTokenInternalScale, quoteTokenInternalScale) encode quantityRaw and priceRaw inside orderData. They appear in orderbook events.

  • Native scales (baseTokenNativeScale, quoteTokenNativeScale) denominate actual token balances, transfers, approvals, and Transfer events.

On some pairs they coincide (XAMA1/XUSDT: both 10,000,000). On others they do not - on USMO/21XUSDQ the quote internal scale is 100 but the quote native scale is 1,000,000, a factor of 10⁴ apart. A priceRaw of 10000 therefore has no direct relationship to the 100,000,000 raw units that move when the trade settles. Use internal scales only for order encoding, native scales for everything that touches balances - including the approval amounts computed in §6.5.

5.6 Order lifecycle

  1. Pending orders may lock or transfer the relevant token into the orderbook, depending on the pair configuration.

  2. Some pairs do not pre-fund the base token, the quote token, or both. In those configurations, no token is transferred when the order becomes pending.

  3. Matching orders settle base and quote token movements on chain. If a pending order is not pre-funded and the required balance is no longer available at matching time, the pending order is cancelled.

  4. Cancellation removes the order from the book. Any tokens that were locked or transferred are returned according to the pair configuration.

  5. Order ids are emitted by the orderbook in OrderReceived, NewBuyOrder, and NewSellOrder events. Persist them for cancellation and reconciliation.

6. Polygon Amoy execution

Polygon execution uses an EVM orderbook contract. Trading wallets must be whitelisted by 21X for buy, sell, and cancel; restricted tokens additionally require beneficiary status on the receiving wallet.

6.1 USMO / 21XUSDQ pair

Field
Value

Trading pair id

57472b2c-4215-4cdf-935e-63226ebd004b

Blockchain

POLYGON_AMOY

Orderbook

0x3e9d9dC85C5De625942483dfa16d20B049254117

Base token

0x26e1C59fD903aD3Fe6CB4c786E364b79683E0A75

Base token contract symbol

BMAT02 (REST/display symbol: USMO)

Base token decimals

3

Quote token

0xb04969523f8c1C3A81bBF07d73d32F772A0afd6f

Quote token symbol

21XUSDQ

Quote token decimals

6

Base internal / native scale

1000 / 1000

Quote internal / native scale

100 / 1000000

Minimum order value

90

Maximum order value

10000000.00

Static reference price

100.10

Maker / taker commission

10 / 10

Orderbook version

1.0.0

These are examples, not stable identifiers. The trading pair id and the orderbook, base, and quote contract addresses above can change at any time - a redeployed orderbook produces a new trading pair id and new contract addresses. Discover the active values through the REST API (§4.3) immediately before trading; do not hardcode them.

6.2 Prerequisites

Requirement
What it controls
Failure mode

21X whitelist

Whether the wallet is allowed to buy, sell, or cancel on the orderbook.

OrderBook_NotAllowedToBuy / OrderBook_NotAllowedToSell

Token beneficiary status

Whether the wallet can receive a restricted token.

Receiver must be a beneficiary

ERC-20 allowance

Whether the orderbook can transfer or lock tokens from the wallet.

ERC20InsufficientAllowance

Token balance

Whether the wallet has sufficient base or quote balance for the order.

ERC20InsufficientBalance or simulation revert

Native gas

POL / MATIC test gas to pay transaction fees.

Transaction will not be mined.

For a matched trade, both sides must be authorised to receive the asset they will get on settlement: a buyer must be able to receive the base token, a seller must be able to receive the quote token.

6.3 Reading contract state

Read on-chain configuration and orderbook state with any EVM library. The example below uses ethers.js.

The Polygon orderbook returns the phase as a uint8. Value 1 corresponds to the open / continuous trading state, which is exposed by the REST tradingStatus field as CONTINUOUS_TRADING. Submit orders only when the contract phase is 1 and the REST status is CONTINUOUS_TRADING.

Other phase values. The orderbook contract has four phases, which map onto the venue trading statuses (§4.2): Open for Trading (= Continuous Trading), Closed for Trading (= Out of Trading and the inactive statuses), Manual Halt, and Automatic Halt. 1 (Open for Trading) is the only numeric value documented for the POC; gate on phase === 1 exactly and treat any other value as not-open rather than guessing its numeric meaning.

6.4 Order encoding

The Polygon orderbook expects a two-field orderData payload:

Polygon constraint (v1 Smart Contract). The current v1 Polygon POC orderbook does not accept extended order layouts that include order type, execution condition, validity constraints, or lifetime. Submissions using abi.encode(uint64,uint64,uint8,uint8,uint8), solidityPacked, or single packed words revert with OrderBook_InvalidInputOrderData. This is a limitation of the v1 contract; later contract versions are expected to add further order options. Extended layouts are already supported on Stellar (see §7.6).

The reportingData and crossIdentifier shapes above match the 21X SDK v1.0.1 conventions.

Default scheme. Use zero-valued placeholders unless 21X has explicitly agreed a participant-specific scheme with you. For Polygon, the sandbox-safe defaults are two zero uint32 words for reportingData and one zero uint32 word for crossIdentifier, as shown above.

crossIdentifier is an optional client correlation field, not an exchange-side sequence number. Do not increment it just because orders are submitted sequentially. Use a non-zero value only when your integration has a defined reconciliation need for it and you are certain how that value will be generated, stored, and interpreted across your systems.

reportingData is a 256-bit reporting and attribution field. The 21X backend interprets and attempts to match the two lowest 32-bit values within it; all remaining bits are free for participant-defined use and do not have to be zero. This gives you an optional place to carry your own internal reference - for example a client-side correlation id or a GUID - that flows through the order lifecycle so you can tie on-chain events back to your own records. It is entirely optional: the zero-placeholder shape shown above is a safe default when you have no reporting scheme agreed with 21X.

6.5 Approving tokens

Approve the orderbook as ERC-20 spender for each token the wallet must transfer. The buy side approves the quote token; the sell side approves the base token.

Computing the required amounts

The sell side is simple: the orderbook pulls the scaled base quantity in native units.

The buy side must cover the order notional plus commission, both in native quote units - not in the internal units used by priceRaw:

In the POC configuration the commission values returned by REST (makerCommission, takerCommission) are expressed in basis points (1 bp = 0.01%), so commissionRate = commission / 10000 and the POC value 10 means 0.10%. Confirm the unit and the venue's rounding rules with 21X before go-live - this guide derives the unit from the POC configuration, and a wrong unit makes every approval wrong.

Worked example - buy 1.000 USMO @ 100.00 21XUSDQ (quote native scale 1,000,000, taker commission 10 bp):

Two practical rules:

  • Do not derive the approval from priceRaw. priceRaw uses the quote internal scale (100 on this pair); transfers use the native scale (1,000,000). On USMO/21XUSDQ they differ by 10⁴ - see §5.5. Compute the approval from human values and the native scale.

  • Size for the worst-case commission. Which rate applies depends on how the order executes: the portion that matches immediately pays the taker rate (the buyer's wallet is debited rounded(P × Q) × (1 + takerCommission)), while a remainder that rests on the book is pre-funded at the maker rate (limitPrice × Q × (1 + makerCommission)). A single order can do both, and rates can differ on the same pair (a live POC pair charges maker 10 / taker 30) - so use max(makerCommission, takerCommission) when sizing approvals. Wallets designated as market makers pay marketMakerCommission in both roles instead.

Two settlement details worth knowing (they explain "unexpected" transfer amounts during reconciliation): a pre-funded resting buy that executes below its limit is reimbursed the difference (limit − executionPrice) × Q × (1 + makerCommission) from the orderbook, and the venue's rounding always favours participants - each wallet gains at most one smallest unit relative to the exact value, at the expense of the fee wallet.

Approve enough quote balance to cover both the order notional and the commission charged by the orderbook. Approving only the notional results in ERC20InsufficientAllowance when the trade settles.

Do not hardcode the commission. The commission applied at settlement comes from the trading pair / orderbook configuration (makerCommission / takerCommission, exposed via REST and getConfig), not from a fixed constant. Any numeric commission shown in this guide is an example only. Read the live rate for the pair when computing the amount to approve - a stale or wrong rate leaves the approval too low (causing ERC20InsufficientAllowance at settlement) or unnecessarily high.

6.6 Submitting orders

Always dry-run with staticCall before broadcasting:

When a buy rests on the book, quote tokens are transferred or locked. When a matching sell arrives, the trade settles and, on full match, the book returns to empty for that order.

Nonce management for sequential orders. EVM transactions from one wallet execute in strict nonce order. The simple pattern above (await tx.wait() between submissions) is safe but serial. If you submit several orders from the same wallet without waiting, assign nonces explicitly (provider.getTransactionCount(address, "pending") as the starting point) and remember that one stuck underpriced transaction blocks every transaction behind it - replacing it requires re-sending the same nonce with a higher gas price. For higher throughput, prefer separate wallets per strategy over deep nonce pipelines.

6.7 Cancelling orders

Cancellation is signed by the order owner and submitted as an EVM transaction. orderId is read from OrderReceived, NewBuyOrder, or NewSellOrder events emitted when the order was created.

Only the owning wallet can cancel its own order, and only while the order is still resting on the book.

6.8 Events

The Polygon orderbook emits events with client and orderId as indexed parameters. Plain (non-indexed) ABI definitions decode incorrectly: the orderId appears in topics[2], not in the data body, and an ABI without the indexed keyword will silently fail to extract it. Use the definitions below verbatim.

Topic-0 (event signature) hashes for log filtering:

Event
Topic-0

OrderReceived

0x4ac0c3adc766818e295a0a09fe6df33885dcc7b63043a3f99be52297b68ae0eb

NewBuyOrder

0x98b9ce6d33873528f10e868c50b6c2bf164c03e4731d7efee4e1432c5369d279

NewSellOrder

0x17cb6f48b3b2e7c6dd37bfea8d4ee3dd5a44c3b601424ec885b7adf099b64e5f

NewBuyInitiatedTrade

0xf4d687371c4480eafd318881a9010db70ac6a0bb13d5519c41e685d57f4e978f

NewSellInitiatedTrade

0xf85ca0c50549eb62c094afa94bdce1b36e01f30cefd0030d658258ce5177731f

CancelOrder

0xac5175c97a30bd4146ee235883daa9602c4c685ef485916ebb4796d4fc868d9a

Every topic-0 is simply keccak256 of the event's canonical signature, so you can re-derive any of them from the ABI definitions above (in ethers: ethers.id("NewBuyOrder(address,uint64,uint64,uint64)")). The first four hashes have been confirmed against emitted POC logs; the two trade-initiated hashes are derived from the same signature convention - confirm them against an emitted log the first time you observe a match.

Lifecycle:

  • OrderReceived is emitted when the orderbook accepts the submission for processing. The side field follows the enum in §5.1 (1 = Buy, 2 = Sell).

  • NewBuyInitiatedTrade / NewSellInitiatedTrade is emitted when an incoming order matches resting liquidity, for the matched quantity. A fully-matched incoming order emits OrderReceived, the trade-initiated event, and the base/quote token Transfer and commission events, but no NewBuyOrder / NewSellOrder - nothing rests on the book.

  • NewBuyOrder / NewSellOrder is emitted when the (remaining) order rests on the book. A partial match emits the trade-initiated event for the matched quantity and NewBuyOrder / NewSellOrder for the residual that rests.

  • CancelOrder is emitted when an order is removed; token and toAddress identify the asset returned and the receiving wallet.

  • Token Transfer events from the base or quote ERC-20 contract are emitted alongside lifecycle events and reflect the actual balance movements.

Execution price vs limit price. The matching engine uses single-price execution: however many resting orders an incoming order matches, the entire execution happens at one price. That price is the last execution price, clamped into the range the participating orders' limits allow - for an incoming buy, between the highest collected sell limit (lower bound) and the incoming buy's own limit (upper bound); mirrored for an incoming sell. The venue picks the price that moves the market as little as possible, so the fill price is frequently neither side's limit price (an incoming buy at 101 against a resting sell at 99 executes at the last execution price if it lies between 99 and 101). Never assume your limit price is the fill price. Reconcile actual execution economics against the token Transfer amounts in the same transaction: the venue computes the rounded order value rounded(P × Q) plus commission, with rounding always in the participants' favour (by at most one smallest unit, taken out of the fee).

Monitoring events in production

The examples in this guide read events from the transaction receipt of the participant's own submission. That covers self-initiated actions only. Resting orders also change state through other parties' transactions - a fill arrives in the counterparty's transaction, an expiry in a venue transaction - so a production integration must watch the orderbook contract itself:

  • Polling: eth_getLogs over bounded block ranges with address = orderbook and a topic filter. Because client is an indexed topic on every lifecycle event, you can filter server-side for your own wallet: topics: [null, zeroPadValue(walletAddress, 32)]. Persist the last processed block number and resume from it.

  • Streaming: a WebSocket subscription (contract.on(...) over a WebSocketProvider) for low latency, always backed by an eth_getLogs catch-up on reconnect - subscriptions drop silently.

  • Reorg safety: Polygon Amoy can reorganise recent blocks. Treat events as tentative until a confirmation depth your risk tolerance accepts, and key your event store by (transactionHash, logIndex) so replayed logs deduplicate.

6.9 Allowance behaviour

The orderbook pre-funds resting orders by calling transferFrom on the relevant token: a sell-side resting order moves the base token from the wallet into the orderbook, and a buy-side resting order moves the quote token (notional plus commission). ERC-20 allowance is consumed by that transfer.

Cancellation returns the locked token balance to the wallet but does not restore the consumed allowance. After cancellation, re-approve the orderbook before placing another order, or maintain a standing approval that is large enough to absorb expected churn.

This pre-funded behaviour is the only mode available on the current Polygon Smart Contract. It is a property of the orderbook configuration rather than of the chain itself - see §8.1.

7. Stellar Testnet execution

Stellar execution uses a Soroban orderbook contract. Compared to the Polygon flow, Stellar additionally requires trustlines for non-native assets and uses an expiration_ledger on token approvals.

7.1 XAMA1 / XUSDT pair

Field
Value

Trading pair id

50a1b1ec-b46a-459e-9359-57412351e45f

Blockchain

STELLAR_TESTNET

Orderbook

CCGMSRH63Z7SAQVRH3O4VHOXMLWJZ2MOYWHWMQF7RKUJCBUYWWBQ35RQ

Base token

CCAVB4PVYQED74TETVAPWS3MIRSITNKQFVB7Q6WPTSTMJF3WFATTRHO3

Quote token

CBU53REE7UH7DHI2DEZXTRXMTJJS5FODBDMO7M7H6EGVUXEG5ZJSARKK

Base symbol

XAMA1

Quote symbol

XUSDT

Base internal / native scale

10,000,000 / 10,000,000

Quote internal / native scale

10,000,000 / 10,000,000

Minimum order value

20.0000000

Maximum order value

1000000000.0000000

Maker / taker commission

10 / 30

Trading status

CONTINUOUS_TRADING

Orderbook version

1.2.0

These are examples, not stable identifiers. The trading pair id and the orderbook, base, and quote contract addresses above can change at any time - a redeployed orderbook produces a new trading pair id and new contract addresses. Discover the active values through the REST API (§4.3) immediately before trading; do not hardcode them.

7.2 Stellar CLI setup

Install the Stellar CLI and confirm the version:

Pin the CLI version. Record the exact CLI version your integration is validated against and pin it in your tooling. The CLI's human-readable output (ledger headers, event rendering, simulation summaries) is not a stable interface and changes between releases - anything that parses CLI output (§7.4, §7.11) is sensitive to upgrades. Re-validate output parsing whenever you bump the CLI.

Configure the testnet network:

Import or generate a local signing identity:

The local alias (for example my-test-account) is used as the --source-account on every contract invocation.

--source-account vs --user

The orderbook methods take an explicit user: Address parameter. The CLI also takes a --source-account flag for the local signing identity. The two must be reconciled:

  • --source-account is the local alias that signs and broadcasts the transaction. The corresponding account pays the network fee.

  • --user is the on-chain Stellar address whose action this is. The orderbook resolves the participant against its client registry using this value, and the contract calls require_auth(user).

For a self-trading flow, set --source-account and --user to the same identity; the source signature satisfies require_auth(user) and the orderbook reads the participant's client info for the same address. For a custodial flow where the trading account is signed for by a different submitter, the user must additionally provide a Soroban authorization entry for the call. The participant whitelist applies to --user, not to --source-account.

7.3 Trustlines and authorization

Trustlines and allowances are independent prerequisites:

Requirement
What it does
Why it matters

Trustline

Allows the Stellar account to hold a non-native asset.

Required before the account can receive or hold the asset.

21X authorization

Issuer / whitelist permission for the participant.

Required before the orderbook accepts orders from the account.

Allowance

Permits the orderbook to transfer the participant's token balance up to an approved amount.

Required before the orderbook can lock or pre-fund tokens.

The orderbook participant interface uses a ClientInformation structure for permissioning:

If the participant is not registered or lacks a permission, order placement fails with ClientNotFound, NotAllowedToBuy, NotAllowedToSell, or NotAllowedToCancel.

Create trustlines and check authorization and balances:

7.4 Allowances

Soroban approvals carry an expiration_ledger. Read the latest testnet ledger and pick a future ledger high enough for the intended trading window:

The awk '/Sequence:/' extraction above parses the CLI's human-readable output, which is not a stable interface (see §7.2). If your CLI version offers a JSON output mode for ledger queries, prefer it; in SDK-based integrations, read the latest ledger from the Soroban RPC (getLatestLedger) instead of shelling out to the CLI.

Approve both the base token (used by sell-side flows) and the quote token (used by buy-side flows). Refresh allowances before they expire or run out.

The CLI returns the allowance as a single numeric amount (for example "10000000000"). The associated expiration_ledger is internal state and is not echoed back; track it locally when you set the allowance.

Pre-fund configuration. Whether an order is pre-funded at placement or only at matching is determined by the orderbook's pre-funding configuration (no_prefund_config in get_config), not by the chain. Where the active configuration permits it, a sell order can be accepted without an immediate base-token transfer. Do not rely on any single behaviour globally - always read get_config and treat the simulation result as authoritative for the current orderbook deployment. See §8.1 for the full set of pre-funding options.

7.5 Inspecting the orderbook

Participant-facing orderbook methods:

View / configuration methods:

The CLI renders get_order_book_phase as a quoted string (for example "OpenForTrading"), not as the integer used by the Polygon orderbook. Compare against the string symbol when gating order placement. The contract phase model is the same four-state machine as on Polygon (§6.3): Open for Trading, Closed for Trading, Manual Halt, Automatic Halt. "OpenForTrading" is the only string rendering confirmed for the POC; treat any other value as not-open.

Inspect the deployed interface, phase, and configuration with the CLI:

7.6 Order encoding

Although order_data is a Soroban Bytes value, the 21X orderbook expects EVM-style 32-byte padded words. Do not encode as compact u64 || u64 bytes.

Field layout:

Position
Field
Type
Description

1

quantity

uint64 as 32-byte word

Scaled base quantity.

2

price

uint64 as 32-byte word

Scaled quote price.

3

orderType

uint8 as 32-byte word

0 = Limit, 1 = Market.

4

executionCondition

uint8 as 32-byte word

0 = Standard, 1 = IOC, 2 = BOC, 3 = FOK.

5

validityConstraints

uint8 as 32-byte word

0 = GFD, 1 = GTD.

6

lifetime

uint8 as 32-byte word

GTD lifetime in days. Valid range 0-90.

Common encodings

Defaults when fields are omitted: orderType = 0 (Limit), executionCondition = 0 (Standard), validityConstraints = 0 (GFD), lifetime = 0.

The contract accepts payloads containing two through six complete 32-byte words; omitted trailing fields take the defaults above. If a sixth lifetime word is present, validityConstraints must be 1 (GTD). Do not explicitly encode a sixth lifetime word for GFD, even when the value is zero.

Python helper

(scale_decimal rejects values that do not scale to an exact integer - see §5.5.)

Validating order_data

Validate the encoded payload before invoking the orderbook. Valid payloads contain two through six complete 32-byte words: 128, 192, 256, 320, or 384 hex characters. Manually editing hex strings is a frequent source of malformed payloads - generate the value from decimal inputs instead.

A one-character truncation is enough to fail Soroban Bytes parsing before simulation. Fail fast on the client.

7.7 cross_identifier and reporting_data

cross_identifier is raw bytes that must decode to a u32. Use exactly 4 bytes of hex; an 8-byte value fails with InvalidInputOrderData (#2004).

Default scheme. Use 00000000. cross_identifier is an optional field used in self-trade scenarios, request additional information from 21X in need.

reporting_data is a raw bytes field for reporting and attribution metadata. As on Polygon (§6.4), the 21X backend interprets and attempts to match the two lowest 32-bit values within it, while all remaining bits are free for participant-defined use and need not be zero - an optional place to carry your own internal reference (for example a correlation id or GUID). Default scheme: use the placeholder value 00 for sandbox integrations; this is accepted by the POC orderbook. The sandbox identity already includes the 00 placeholder, so a working integration can be built before any participant-specific reporting scheme is finalised.

The same defaulting approach applies on Polygon to reportingData and crossIdentifier. Use the zero-placeholder shapes shown in §6.4 unless 21X provides a production reporting or correlation scheme.

7.8 Submitting orders

Always simulate with --send=no before submitting with --send=yes:

Sell example - 100 XAMA1 @ 1.20 XUSDT, Standard limit:

Buy example - 100 XAMA1 @ 0.80 XUSDT, Standard limit:

Sequence numbers and retries. Each Stellar transaction consumes the source account's sequence number, so transactions from one source account are strictly serialised - submit one at a time and wait for the result before the next, or use distinct source accounts (channel accounts) for parallel submission. If the RPC responds with TRY_AGAIN_LATER or tryAgainLater, the transaction was not accepted: rebuild it against the current sequence number and resubmit with backoff. A transaction also carries an expiry (time bounds / max ledger); once that passes without inclusion, it can never be applied, which makes "expired" a safe signal that the order was not placed (see §9.6).

7.9 Cancelling orders

Cancellation is a Soroban call signed by the order owner. Persist order_id from the OrderReceived event when the order is placed.

Use cancel_buy_order for buy-side cancellation. Cancelling a non-existent or already-cancelled order id fails with contract error #8; keep client-side order state idempotent.

7.10 Events and rejections

Soroban events are emitted as a topic vector plus a data vec. The Rust struct in the contract source defines the conceptual fields, but the on-chain emission places the most important participant-facing fields in the topic vector for cheap filtering, and the rest in the data body. Treat the topic vector as the stable source of truth: the order id, in particular, is a topic, not a data field, and parsers that look for a named order_id JSON key in the body will not find it.

Topic vector layout

Event
topic[0]
topic[1]
topic[2]

OrderReceived

symbol "OrderReceived"

address client

u64 order_id

NewBuyOrder

symbol "NewBuyOrder"

address client

-

NewSellOrder

symbol "NewSellOrder"

address client

-

CancelOrder

symbol "CancelOrder"

address client

u64 order_id

OrderRejected

symbol "OrderRejected"

address client

u64 order_id

For NewBuyOrder and NewSellOrder the order_id is the first u64 in the data body (alongside quantity, price, and event_id). Always read it from there for these two events, and from topic[2] for the others.

Sample payloads

The data-body ordering is contract-specific and may evolve across orderbook versions. Use the topic vector for the order id; use the data body for the trade-level numbers (quantity, price) and only as far as your integration genuinely needs them.

Reference enums

Matching events

When an incoming order crosses existing liquidity, the orderbook emits a trade event in addition to (or in place of) the resting-order event. An incoming buy that matches a resting sell emits NewBuyInitiatedTrade. For a full match, the transaction emits OrderReceived, base/quote token transfer events, NewBuyInitiatedTrade, and commission transfer events; no NewBuyOrder is emitted because no remainder rests on the book. For a partial match where the incoming buy is larger than the available sell liquidity, the transaction emits NewBuyInitiatedTrade for the matched quantity and NewBuyOrder for the residual buy.

Execution price vs limit price. The same single-price execution model as on Polygon applies (§6.8): the whole match executes at one price - the last execution price clamped into the range allowed by the participating orders' limits - so the fill price is frequently neither side's limit. Reconcile fills against the token transfer events in the same transaction rather than assuming the incoming limit price.

Monitoring events in production

Parsing the CLI's printed event output (as the demo script in §7.11 does) is suitable for sandbox experiments only. A production integration should consume events from the Soroban RPC getEvents endpoint instead: filter by the orderbook contract id and, because client is topic[1] on every lifecycle event, by your own address as a topic filter. getEvents is paginated and covers a bounded retention window of recent ledgers - poll it on a schedule well inside that window, persist the last processed ledger/cursor, and resume from it. As on Polygon, fills and venue-initiated cancellations of your resting orders arrive in transactions you did not submit, so receipt-watching alone is not sufficient.

OrderRejected

The transaction succeeded but matching rules, controls, or execution-condition behaviour rejected the order or its remainder. Read order_id from the topic vector and the rejection details (reason, quantity_not_executed, side, price) from the data body.

8. Chain differences

Topic
Polygon Amoy
Stellar Testnet

Signing identity

EVM transaction sender is the trading wallet.

The Soroban call carries an explicit user address; the transaction is signed by the source account.

Order entry

newBuyOrder(bytes,bytes,bytes) / newSellOrder(bytes,bytes,bytes).

new_buy_order(user, order_data, cross_identifier, reporting_data) / new_sell_order(...).

Order data encoding

abi.encode(uint64 quantity, uint64 price).

32-byte padded words inside Soroban Bytes.

Order options

Two-field encoding only on the POC orderbook. Extended layouts revert.

Standard, IOC, BOC, FOK, GFD, GTD via the explicit field layout.

Token authorization

Restricted token receivers must be 21X beneficiaries.

Trustline + token authorization; participant authorization on the orderbook.

Allowance

ERC-20 allowance, no expiration.

Soroban allowance with expiration_ledger.

Phase return type

uint8 (open = 1).

String enum (open = "OpenForTrading").

Order id surface

Indexed event topic (topics[2]) on every lifecycle event.

Topic vector (topic[2]) for OrderReceived, CancelOrder, OrderRejected; data body for NewBuyOrder / NewSellOrder.

Transaction ordering

Per-wallet nonces; parallel submission requires explicit nonce management (§6.6).

Per-source-account sequence numbers; parallel submission requires channel accounts (§7.8).

Event monitoring

eth_getLogs / WebSocket subscription, filter by indexed client topic (§6.8).

Soroban RPC getEvents, filter by contract id and client topic (§7.10).

Native fees

POL / MATIC.

XLM.

Simulation

eth_call / ethers staticCall.

Stellar CLI / SDK with --send=no.

8.1 Pre-funding and allowance behaviour

Allowance behaviour is configuration-dependent, not chain-dependent. Whether the orderbook locks/transfers tokens when an order is placed ("pre-funded") or only when it matches ("no-prefund") is a property of the specific orderbook deployment. Stellar can behave exactly like Polygon - and vice versa - depending on how the orderbook was compiled and initialised. Always read getConfig / get_config for the pair you are trading and treat the simulation result as authoritative, rather than assuming a per-chain rule.

Behaviour by mode:

  • Pre-funded. The relevant token is locked or transferred into the orderbook when the order is posted - via transferFrom on Polygon, or via the Soroban allowance on Stellar. A sell-side resting order moves the base token; a buy-side resting order moves the quote token (notional plus commission). The allowance is consumed at placement, and cancellation returns the token balance but does not restore the consumed allowance (re-approve before placing again - see §6.9).

  • No-prefund. No token moves when the order rests; settlement transfers occur directly between the trading wallets at matching time. During matching, the orderbook checks each collected non-pre-funded order's wallet for sufficient balance and allowance - cumulatively, counting all orders collected from the same wallet in that match. An order that fails the check is cancelled on chain with cancellation type ByContract and matching continues with the next eligible order (see §5.6). On Stellar the allowance must remain valid for the order's lifetime - refresh it before its expiration_ledger.

Current availability. The old Polygon Smart Contract supports only the fully pre-funded mode; the additional configurations above are available on Stellar today - the active POC XAMA1/XUSDT pair is a no-prefund deployment - with more options to come on Polygon later. Treat allowance behaviour as a property of the specific orderbook deployment you are integrating against, and confirm the configuration of your target pair via get_config rather than assuming a per-chain rule.

9. Troubleshooting

9.1 Permissions and authorization

Error
Chain
Cause
Action

OrderBook_NotAllowedToBuy / OrderBook_NotAllowedToSell

Both

Wallet is not whitelisted for the side being placed.

Complete 21X whitelist / client setup.

Receiver must be a beneficiary

Polygon

Wallet cannot receive a restricted token.

Enable beneficiary status for the wallet and token.

ClientNotFound (#6)

Stellar

Account not registered with the orderbook's client registry.

Confirm onboarding with 21X; confirm the correct user address and orderbook id.

NotAllowedToBuy (#4) / NotAllowedToSell (#5) / NotAllowedToCancel (#7)

Stellar

Participant exists but lacks the relevant permission.

Confirm client permissions with 21X.

Unauthorized (#4001)

Stellar

Method requires an elevated role (for example, detailed order inspection).

Use emitted events and authenticated REST history endpoints.

9.2 Encoding and input validation

Error
Chain
Cause
Action

OrderBook_InvalidInputOrderData

Polygon

Wrong EVM orderData layout.

Use abi.encode(["uint64","uint64"], [quantityRaw, priceRaw]).

InvalidInputOrderData (#2004)

Stellar

Compact encoding, an invalid word count or field combination, or a cross_identifier that is not 4 bytes.

Use two through six 32-byte words; encode the sixth word only for GTD; use exactly 4 bytes for cross_identifier.

CLI rejects order_data as not parseable to Bytes

Stellar

Odd number of hex characters or incomplete 32-byte words.

Generate the value from decimal inputs; validate length is 128, 192, 256, 320, or 384 hex characters.

order_id reads as 0 or undefined after submit

Polygon

ABI declared without indexed on client / orderId; the value lives in topics[2], not the data body.

Use the indexed ABI definitions in §6.8 verbatim.

Order id parser returns nothing

Stellar

Parser searched the event data body for an order_id JSON field; the value is in topic[2].

Read the third element of the topic vector - see §7.10.

9.3 Balances and allowances

Error
Chain
Cause
Action

ERC20InsufficientAllowance

Polygon

Missing or consumed ERC-20 approval.

Approve the orderbook again or maintain a larger standing approval.

Allowance unexpectedly insufficient after cancel

Polygon

Allowance is consumed on order placement; cancellation does not restore it.

Re-approve before placing another order.

Buy order locks more quote than notional

Polygon

Commission / pre-fund margin requirement.

Approve enough quote balance for notional plus commission (§6.5).

Error(Contract, #10) - resulting balance not in allowed range

Stellar

Insufficient quote balance for value plus commission.

Top up the quote balance; an allowance does not replace a balance.

Error(Contract, #13) - trustline entry is missing

Stellar

Token trustline missing.

Create the trustline and retry.

9.4 Pre-trade controls and matching rejections

Stellar pre-trade and volatility-management codes:

Use REST pair data and get_config to inspect thresholds, limits, static reference price, liquidity band, and the price collar factor before submitting. An OrderRejected event indicates that the transaction itself succeeded but the order was rejected by matching rules, controls, or execution-condition behaviour; check reason, quantity_not_executed, side, price, and pre-submission book state.

Boundary behaviour. Orders at the exact minimumOrderValue may be rejected by pre-trade controls (for example, PreTradeControlOrderValueBelowMinRange). Use values comfortably above the minimum unless intentionally testing boundary rejection.

9.5 REST endpoint authentication

Authenticated wallet, order, and trade history endpoints require a bearer token. Calls without authentication return HTTP 401. Use the public market-data endpoints for unauthenticated discovery, or obtain API credentials from 21X for participant-scoped history.

Last updated