> For the complete documentation index, see [llms.txt](https://docs.21x.eu/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.21x.eu/integration-guide/21x-integration-guide.md).

# 21X Integration Guide

## 21X Integration Guide

### 21X Integration Guide <a href="#id-21x-integration-guide" id="id-21x-integration-guide"></a>

#### 1. Introduction <a href="#id-1-introduction" id="id-1-introduction"></a>

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 <a href="#id-2-getting-started" id="id-2-getting-started"></a>

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 <a href="#id-3-how-to-connect" id="id-3-how-to-connect"></a>

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:

```
Discover the pair via REST
  -> read chain, contract addresses, scales, limits, status, and fees
  -> prepare wallet/account and permissions on the target chain
  -> fund native fees and trading-token balances
  -> approve the orderbook to transfer or lock the relevant tokens
  -> encode the order using the target chain's expected format
  -> simulate or dry-run the transaction
  -> submit the blockchain transaction
  -> monitor events, balances, REST market data, and order history
  -> cancel pending orders when needed
```

**3.1 REST environment**

```
POC REST base URL
https://ex.str2.poc.21x.eu/api/v1/

Primary pair discovery
GET https://ex.str2.poc.21x.eu/api/v1/tradingpairs
```

**3.2 Polygon Amoy environment**

```
Network:           Polygon Amoy testnet
Chain ID:          80002
Native gas token:  POL / MATIC test token
RPC URL:           https://rpc-amoy.polygon.technology
POC pair:          USMO / 21XUSDQ
```

**3.3 Stellar Testnet environment**

```
Network:             Stellar Testnet
Network passphrase:  Test SDF Network ; September 2015
RPC URL:             https://soroban-testnet.stellar.org
POC pair:            XAMA1 / XUSDT
```

#### 4. Market View (REST) <a href="#id-4-market-view-rest" id="id-4-market-view-rest"></a>

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**

```
curl -s "https://ex.str2.poc.21x.eu/api/v1/tradingpairs" | jq .
```

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

```
{
  "items": [ /* one entry per active trading pair */ ],
  "total_count": <integer>
}
```

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:

```
curl -s "https://ex.str2.poc.21x.eu/api/v1/tradingpairs/57472b2c-4215-4cdf-935e-63226ebd004b" | jq .
```

**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):

```

# Discover by chain + exact symbols (no id filter)
curl -s "https://ex.str2.poc.21x.eu/api/v1/tradingpairs" | jq '
  .items[]
  | select(.blockChain == "POLYGON_AMOY")
  | select(.baseTokenData.symbol == "USMO")
  | select(.quoteTokenSymbol == "21XUSDQ")
'
```

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**

```
PAIR_ID="<TRADING_PAIR_ID>"

curl -s "https://ex.str2.poc.21x.eu/api/v1/tradingpairs/$PAIR_ID" | jq .
curl -s "https://ex.str2.poc.21x.eu/api/v1/tradingpairs/$PAIR_ID/orderbook?limit=5" | jq .
curl -s "https://ex.str2.poc.21x.eu/api/v1/tradingpairs/$PAIR_ID/pricelevels?limit=5" | jq .
curl -s "https://ex.str2.poc.21x.eu/api/v1/tradingpairs/$PAIR_ID/tradeinfo" | jq .
```

**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):

```
{
  "tradingPairId": "57472b2c-4215-4cdf-935e-63226ebd004b",
  "buy": [],
  "sell": []
}
```

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:

```
{
  "lastPrice": "100.00",
  "referencePrice": "100.00",
  "priceChange24h": "0.000000",
  "tradeVolume24h": "0",
  "liquidityBand": 5,
  "tradingStatus": "CONTINUOUS_TRADING",
  "statusChangeReason": "START_OF_TRADING_DAY",
  "tradingHaltCounter": 0
}
```

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 <a href="#id-5-order-model" id="id-5-order-model"></a>

**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:

```
quantityRaw = humanBaseQuantity * baseTokenInternalScale
priceRaw    = humanQuotePrice   * quoteTokenInternalScale
```

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

```
USMO / 21XUSDQ on Polygon Amoy
  baseTokenInternalScale  = 1000
  quoteTokenInternalScale = 100
  1.000 USMO @ 100.00 21XUSDQ
    quantityRaw = 1.000 * 1000 = 1000
    priceRaw    = 100.00 * 100 = 10000

XAMA1 / XUSDT on Stellar Testnet
  baseTokenInternalScale  = 10,000,000
  quoteTokenInternalScale = 10,000,000
  100 XAMA1 @ 1.05 XUSDT
    quantityRaw = 100 * 10,000,000  = 1,000,000,000
    priceRaw    = 1.05 * 10,000,000 = 10,500,000
```

**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.

```
from decimal import Decimal

def scale_exact(value: str, scale: str) -> int:
    scaled = Decimal(value) * Decimal(scale)
    if scaled != scaled.to_integral_value():
        raise ValueError(f"{value} is not representable at scale {scale}")
    return int(scaled)
```

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 <a href="#id-6-polygon-amoy-execution" id="id-6-polygon-amoy-execution"></a>

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.

```
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://rpc-amoy.polygon.technology");
const orderbook = "0x3e9d9dC85C5De625942483dfa16d20B049254117";

const orderbookAbi = [
  "function getConfig() view returns (tuple(tuple(tuple(uint128 nonNative,uint128 native) base,tuple(uint128 nonNative,uint128 native) quote) scales,uint64 makerCommission,uint64 takerCommission,uint64 marketMakerCommission,address quoteToken,address baseToken,address whitelist,address commissionWallet,address baseTokenFallbackWallet,address quoteTokenFallbackWallet,uint8 liquidityBand,bytes11 version))",
  "function getOrderBookPhase() view returns (uint8)",
  "function countBuyOrders() view returns (uint256)",
  "function countSellOrders() view returns (uint256)",
  "function bestFiftyOffers() view returns (uint256[],uint256[])",
];

const ob = new ethers.Contract(orderbook, orderbookAbi, provider);
console.log(await ob.getOrderBookPhase()); // 1 = open for trading
console.log(await ob.countBuyOrders());
console.log(await ob.countSellOrders());
```

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:

```
abi.encode(uint64 quantity, uint64 price)
```

```
const orderData = ethers.AbiCoder.defaultAbiCoder().encode(
  ["uint64", "uint64"],
  [quantityRaw, priceRaw]
);

const reportingData = ethers.AbiCoder.defaultAbiCoder().encode(
  ["uint32", "uint32"],
  [0, 0]
);

const crossIdentifier = ethers.AbiCoder.defaultAbiCoder().encode(
  ["uint32"],
  [0]
);
```

> **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.

```
requiredBaseNative = humanBaseQuantity * baseTokenNativeScale
```

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

```
notionalQuoteNative  = humanBaseQuantity * humanQuotePrice * quoteTokenNativeScale
commissionQuoteNative = notionalQuoteNative * commissionRate        (round UP)
requiredQuoteNative  = notionalQuoteNative + commissionQuoteNative
```

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):

```
notionalQuoteNative   = 1.000 * 100.00 * 1,000,000          = 100,000,000
commissionQuoteNative = 100,000,000 * 10 / 10000 (round up) =     100,000
requiredQuoteNative   = 100,000,000 + 100,000               = 100,100,000
```

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.

```
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://rpc-amoy.polygon.technology");
const orderbook = "0x3e9d9dC85C5De625942483dfa16d20B049254117";
const baseAddress  = "0x26e1C59fD903aD3Fe6CB4c786E364b79683E0A75";
const quoteAddress = "0xb04969523f8c1C3A81bBF07d73d32F772A0afd6f";

const erc20Abi = [
  "function approve(address spender,uint256 amount) returns (bool)",
  "function allowance(address owner,address spender) view returns (uint256)",
  "function balanceOf(address owner) view returns (uint256)",
];

// Signers must be configured through local keys or the partner custody stack.
const buyer  = new ethers.Wallet("<BUYER_PRIVATE_KEY>",  provider);
const seller = new ethers.Wallet("<SELLER_PRIVATE_KEY>", provider);

const quote = new ethers.Contract(quoteAddress, erc20Abi, buyer);
const base  = new ethers.Contract(baseAddress,  erc20Abi, seller);

// Buy 1.000 USMO @ 100.00 21XUSDQ: notional + commission, computed as above.
// The commission rate comes from the pair's makerCommission / takerCommission
// via REST - never a hardcoded constant.
await quote.approve(orderbook, 100_100_000n);

// Sell 1.000 USMO requires 1.000 base = 1000 raw units.
await base.approve(orderbook, 1000n);
```

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:

```
const orderbookAbi = [
  "function newBuyOrder(bytes orderData, bytes reportingData, bytes crossIdentifier)",
  "function newSellOrder(bytes orderData, bytes reportingData, bytes crossIdentifier)",
  "function cancelBuyOrder(uint64 orderId)",
  "function cancelSellOrder(uint64 orderId)",
];

const obAsBuyer  = new ethers.Contract(orderbook, orderbookAbi, buyer);
const obAsSeller = new ethers.Contract(orderbook, orderbookAbi, seller);

await obAsBuyer.newBuyOrder.staticCall(orderData, reportingData, crossIdentifier);
await obAsSeller.newSellOrder.staticCall(orderData, reportingData, crossIdentifier);

const buyTx = await obAsBuyer.newBuyOrder(orderData, reportingData, crossIdentifier);
await buyTx.wait();

const sellTx = await obAsSeller.newSellOrder(orderData, reportingData, crossIdentifier);
await sellTx.wait();
```

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.

```
const ob = new ethers.Contract(orderbook, orderbookAbi, wallet);

await ob.cancelBuyOrder(59n);
await ob.cancelSellOrder(62n);
```

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.

```
event OrderReceived(
    address indexed client,
    uint64  indexed orderId,
    uint64  quantity,
    uint64  price,
    uint8   side,
    bytes   reportingData,
    uint64  crossIdentifier,
    uint8   orderType,
    uint8   executionCondition
);

event NewBuyOrder (address indexed client, uint64 quantity, uint64 price, uint64 indexed orderId);
event NewSellOrder(address indexed client, uint64 quantity, uint64 price, uint64 indexed orderId);

event NewBuyInitiatedTrade (address indexed client, uint64 quantity, uint64 price, uint64 indexed orderId);
event NewSellInitiatedTrade(address indexed client, uint64 quantity, uint64 price, uint64 indexed orderId);

event CancelOrder(
    address indexed client,
    uint64  indexed orderId,
    uint64  quantity,
    uint64  price,
    uint8   cancellationType, // 0 ByParticipant, 1 ByAdmin, 2 ByContract
    address token,
    address toAddress
);
```

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 <a href="#id-7-stellar-testnet-execution" id="id-7-stellar-testnet-execution"></a>

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:

```
stellar --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:

```
stellar network add testnet \
  --rpc-url "https://soroban-testnet.stellar.org" \
  --network-passphrase "Test SDF Network ; September 2015"
```

Import or generate a local signing identity:

```

# Existing secret key
printf '%s' '<STELLAR_SECRET_KEY>' | stellar keys add my-test-account --secret-key --overwrite


# Or generate and fund a new test account
stellar keys generate my-test-account --network testnet --fund
```

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:

```
pub struct ClientInformation {
    pub can_buy: bool,
    pub can_cancel: bool,
    pub can_sell: bool,
    pub can_transfer: bool,
    pub client: Address,
    pub id: u64,
    pub is_market_maker: bool,
}
```

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:

```
USER="<USER_G_ADDRESS>"
ORDERBOOK="<smartContractOrderBook>"
BASE="<smartContractBase>"
QUOTE="<smartContractQuote>"
ALIAS="<LOCAL_SIGNING_ALIAS>"


# Trustlines are safe to repeat.
stellar contract invoke --network testnet --id "$BASE" \
  --source-account "$ALIAS" --send=yes -- trust --addr "$USER"
stellar contract invoke --network testnet --id "$QUOTE" \
  --source-account "$ALIAS" --send=yes -- trust --addr "$USER"


# Confirm token authorization and balances.
stellar contract invoke --network testnet --id "$BASE" \
  --source-account "$ALIAS" --send=no -- authorized --id "$USER"
stellar contract invoke --network testnet --id "$QUOTE" \
  --source-account "$ALIAS" --send=no -- authorized --id "$USER"

stellar contract invoke --network testnet --id "$BASE" \
  --source-account "$ALIAS" --send=no -- balance --id "$USER"
stellar contract invoke --network testnet --id "$QUOTE" \
  --source-account "$ALIAS" --send=no -- balance --id "$USER"
```

**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:

```
CURRENT_LEDGER="$(stellar ledger latest --network testnet | awk '/Sequence:/ {print $2}')"
EXPIRATION_LEDGER="$((CURRENT_LEDGER + 100000))"
ALLOWANCE="1000000000000"

stellar contract invoke --network testnet --id "$BASE" \
  --source-account "$ALIAS" --send=yes -- approve \
  --from "$USER" --spender "$ORDERBOOK" \
  --amount "$ALLOWANCE" \
  --expiration_ledger "$EXPIRATION_LEDGER"

stellar contract invoke --network testnet --id "$QUOTE" \
  --source-account "$ALIAS" --send=yes -- approve \
  --from "$USER" --spender "$ORDERBOOK" \
  --amount "$ALLOWANCE" \
  --expiration_ledger "$EXPIRATION_LEDGER"

stellar contract invoke --network testnet --id "$BASE" \
  --source-account "$ALIAS" --send=no -- allowance \
  --from "$USER" --spender "$ORDERBOOK"
stellar contract invoke --network testnet --id "$QUOTE" \
  --source-account "$ALIAS" --send=no -- allowance \
  --from "$USER" --spender "$ORDERBOOK"
```

> 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:

```
fn new_buy_order  (user: Address, order_data: Bytes, cross_identifier: Bytes, reporting_data: Bytes);
fn new_sell_order (user: Address, order_data: Bytes, cross_identifier: Bytes, reporting_data: Bytes);
fn cancel_buy_order  (user: Address, order_id: u64);
fn cancel_sell_order (user: Address, order_id: u64);
```

View / configuration methods:

```
fn get_config()           -> OrderBookConfigData;
fn get_order_book_phase() -> OrderBookPhase;     // string enum, e.g. "OpenForTrading"
fn count_buy_orders()     -> u64;
fn count_sell_orders()    -> u64;
fn best_bid_offer()       -> U256;
fn best_fifty_offers()    -> (Vec<U256>, Vec<U256>);
```

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:

```
stellar contract invoke --network testnet \
  --id "$ORDERBOOK" --source-account "$ALIAS" --send=no -- --help

stellar contract invoke --network testnet \
  --id "$ORDERBOOK" --source-account "$ALIAS" --send=no -- get_order_book_phase

stellar contract invoke --network testnet \
  --id "$ORDERBOOK" --source-account "$ALIAS" --send=no -- get_config
```

**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**

```
Minimal standard limit order
  word32(quantity) || word32(price)

Explicit standard limit order
  word32(quantity) || word32(price) || word32(0) || word32(0)

Explicit BOC limit order
  word32(quantity) || word32(price) || word32(0) || word32(2)

Explicit IOC limit order
  word32(quantity) || word32(price) || word32(0) || word32(1)

Explicit FOK limit order
  word32(quantity) || word32(price) || word32(0) || word32(3)

Explicit GTD limit order
  word32(quantity) || word32(price) || word32(0) || word32(executionCondition)
   || word32(1) || word32(lifetime_days)
```

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**

```
from decimal import Decimal

def word32(value: int) -> str:
    return int(value).to_bytes(32, "big").hex()

def scale_decimal(value: str, internal_scale: str) -> int:
    scaled = Decimal(value) * Decimal(internal_scale)
    if scaled != scaled.to_integral_value():
        raise ValueError(f"{value} is not representable at scale {internal_scale}")
    return int(scaled)

def encode_order_data(
    quantity: str,
    price: str,
    base_scale: str,
    quote_scale: str,
    order_type: int = 0,
    execution_condition: int = 0,
    validity_constraints: int = 0,
    lifetime: int = 0,
) -> str:
    if order_type not in (0, 1):
        raise ValueError("order_type must be 0 (Limit) or 1 (Market)")
    if execution_condition not in (0, 1, 2, 3):
        raise ValueError("execution_condition must be 0, 1, 2, or 3")
    if validity_constraints not in (0, 1):
        raise ValueError("validity_constraints must be 0 (GFD) or 1 (GTD)")
    if validity_constraints == 0 and lifetime != 0:
        raise ValueError("lifetime is only valid for GTD orders")
    if validity_constraints == 1 and not 0 <= lifetime <= 90:
        raise ValueError("GTD lifetime must be between 0 and 90 days")

    quantity_i = scale_decimal(quantity, base_scale)
    price_i    = scale_decimal(price,    quote_scale)
    words = [
        word32(quantity_i),
        word32(price_i),
        word32(order_type),
        word32(execution_condition),
    ]
    if validity_constraints == 1:
        words.extend([
            word32(validity_constraints),
            word32(lifetime),
        ])
    return "".join(words)


# Example: 100 XAMA1 @ 1.05 XUSDT, Standard limit
order_data = encode_order_data(
    quantity="100",
    price="1.05",
    base_scale="10000000",
    quote_scale="10000000",
)
print(order_data)
```

(`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.

```
ORDER_DATA="<ORDER_DATA_HEX>"

case "${#ORDER_DATA}" in
  128|192|256|320|384) ;;
  *) echo "Invalid order_data hex length: ${#ORDER_DATA}" >&2; exit 1 ;;
esac

case "$ORDER_DATA" in
  (*[!0-9a-fA-F]*) echo "Invalid order_data: expected hex only" >&2; exit 1 ;;
esac
```

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)`.

```
00000000  -> 0
```

**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`:

```
stellar contract invoke \
  --network testnet \
  --id "$ORDERBOOK" \
  --source-account "$ALIAS" \
  --send=no \
  -- new_sell_order \
  --user "$USER" \
  --order_data "$ORDER_DATA" \
  --cross_identifier "$CROSS_IDENTIFIER" \
  --reporting_data "$REPORTING_DATA"
```

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

```
ORDERBOOK="CCGMSRH63Z7SAQVRH3O4VHOXMLWJZ2MOYWHWMQF7RKUJCBUYWWBQ35RQ"
USER="<USER_G_ADDRESS>"
ORDER_DATA="000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000b71b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
CROSS_IDENTIFIER="00000000"
REPORTING_DATA="00"

stellar contract invoke --network testnet --id "$ORDERBOOK" \
  --source-account "$ALIAS" --send=yes \
  -- new_sell_order \
  --user "$USER" \
  --order_data "$ORDER_DATA" \
  --cross_identifier "$CROSS_IDENTIFIER" \
  --reporting_data "$REPORTING_DATA"
```

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

```
ORDER_DATA="000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000007a120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
CROSS_IDENTIFIER="00000000"

stellar contract invoke --network testnet --id "$ORDERBOOK" \
  --source-account "$ALIAS" --send=yes \
  -- new_buy_order \
  --user "$USER" \
  --order_data "$ORDER_DATA" \
  --cross_identifier "$CROSS_IDENTIFIER" \
  --reporting_data "$REPORTING_DATA"
```

> **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.

```

# Simulate
stellar contract invoke --network testnet --id "$ORDERBOOK" \
  --source-account "$ALIAS" --send=no \
  -- cancel_sell_order --user "$USER" --order_id "$ORDER_ID"


# Submit
stellar contract invoke --network testnet --id "$ORDERBOOK" \
  --source-account "$ALIAS" --send=yes \
  -- cancel_sell_order --user "$USER" --order_id "$ORDER_ID"
```

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**

```
// new_sell_order - resting on the book
topics: [{"symbol":"OrderReceived"}, {"address":"<client>"}, {"u64":"<order_id>"}]
data  : vec[ u64 quantity, u64 price, ..contract-specific.. , u128 event_id ]

topics: [{"symbol":"NewSellOrder"}, {"address":"<client>"}]
data  : vec[ u64 order_id, u64 quantity, u64 price, u128 event_id ]

// cancel_sell_order
topics: [{"symbol":"CancelOrder"}, {"address":"<client>"}, {"u64":"<order_id>"}]
data  : vec[ u64 quantity, u64 price, u32 cancellation_type, address token, address to_address, u128 event_id ]
```

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**

```
pub enum OrderKind     { Buy = 1, Sell = 2 }
pub enum OrderType     { Limit = 0, Market = 1 }
pub enum CancellationType { ByParticipant = 0, ByAdmin = 1, ByContract = 2 }

pub enum RejectionReasons {
    None = 0,
    SelfTrade = 1,
    MaxMatches = 2,
    CircuitBreakerPriceOutOfMaximumStaticRange  = 3,
    CircuitBreakerPriceOutOfMinimumStaticRange  = 4,
    CircuitBreakerPriceOutOfMaximumDynamicRange = 5,
    CircuitBreakerPriceOutOfMinimumDynamicRange = 6,
    PartialRejected = 7,
    FullyRejected   = 8,
}
```

**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 <a href="#id-8-chain-differences" id="id-8-chain-differences"></a>

| 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 <a href="#id-9-troubleshooting" id="id-9-troubleshooting"></a>

**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:

```
PreTradeControlPriceAboveMaxRange         = 1018
PreTradeControlPriceBelowMinRange         = 1019
PreTradeControlOrderValueAboveMaxRange    = 1020
PreTradeControlOrderVolumeAboveMaxRange   = 1021
PreTradeControlOrderValueBelowMinRange    = 1022
PreTradeControlOrderVolumeBelowMinRange   = 1023
VolatilityManagementPriceOutOfMaximumStaticRange  = 1025
VolatilityManagementPriceOutOfMinimumStaticRange  = 1026
VolatilityManagementPriceOutOfMaximumDynamicRange = 1027
VolatilityManagementPriceOutOfMinimumDynamicRange = 1028
```

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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.21x.eu/integration-guide/21x-integration-guide.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
