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:
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.
Prepare a scripting language of your choice (Python, TypeScript, Java, Go).
Prepare a REST API client such as Postman to test market-data calls and inspect responses.
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.
For Stellar: install the Stellar CLI or a Soroban-capable SDK, and prepare a funded testnet account.
Coordinate with 21X to obtain whitelist/client authorization on the target orderbooks and beneficiary or trustline authorization on the relevant tokens.
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:
REST API. Provides access to structured market data, including instrument metadata, contract addresses, scales, public orderbook snapshots, price levels, reference prices, and trade information.
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
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:
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 TradingorAutomatic Trading Haltmove toContinuous Tradingand the daily halt counter resets. At closing time, pairs move toOut of Tradingand 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
tradingHaltCounterfield intradeinfocounts 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 | lengthagainst.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
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:
1
Buy
2
Sell
5.2 Limit vs market
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
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
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 = 0is accepted and behaves as the venue default.lifetime > 90is 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) encodequantityRawandpriceRawinsideorderData. They appear in orderbook events.Native scales (
baseTokenNativeScale,quoteTokenNativeScale) denominate actual token balances, transfers, approvals, andTransferevents.
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
Pending orders may lock or transfer the relevant token into the orderbook, depending on the pair configuration.
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.
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.
Cancellation removes the order from the book. Any tokens that were locked or transferred are returned according to the pair configuration.
Order ids are emitted by the orderbook in
OrderReceived,NewBuyOrder, andNewSellOrderevents. 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
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
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 onphase === 1exactly 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 withOrderBook_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.priceRawuses the quote internal scale (100on 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 maker10/ taker30) - so usemax(makerCommission, takerCommission)when sizing approvals. Wallets designated as market makers paymarketMakerCommissionin 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 andgetConfig), 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 (causingERC20InsufficientAllowanceat 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:
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:
OrderReceivedis emitted when the orderbook accepts the submission for processing. Thesidefield follows the enum in §5.1 (1 = Buy,2 = Sell).NewBuyInitiatedTrade/NewSellInitiatedTradeis emitted when an incoming order matches resting liquidity, for the matched quantity. A fully-matched incoming order emitsOrderReceived, the trade-initiated event, and the base/quote tokenTransferand commission events, but noNewBuyOrder/NewSellOrder- nothing rests on the book.NewBuyOrder/NewSellOrderis emitted when the (remaining) order rests on the book. A partial match emits the trade-initiated event for the matched quantity andNewBuyOrder/NewSellOrderfor the residual that rests.CancelOrderis emitted when an order is removed;tokenandtoAddressidentify the asset returned and the receiving wallet.Token
Transferevents 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
Transferamounts in the same transaction: the venue computes the rounded order valuerounded(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_getLogsover bounded block ranges withaddress = orderbookand a topic filter. Becauseclientis 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 aWebSocketProvider) for low latency, always backed by aneth_getLogscatch-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
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-accountis the local alias that signs and broadcasts the transaction. The corresponding account pays the network fee.--useris the on-chain Stellar address whose action this is. The orderbook resolves the participant against its client registry using this value, and the contract callsrequire_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:
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_configinget_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 readget_configand 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:
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
reportingDataandcrossIdentifier. 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_LATERortryAgainLater, 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
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
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
transferFromon 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
ByContractand 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 itsexpiration_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/XUSDTpair 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 viaget_configrather than assuming a per-chain rule.
9. Troubleshooting
9.1 Permissions and authorization
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
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
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
minimumOrderValuemay 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

