Orders and transactions
The prepare, sign and submit flow behind every API trade, all ten operations, submit statuses and retries, and the public open-orders feed.
This page covers the two endpoints that place and execute trades through the gateway, POST /v1/orders/prepare and POST /v1/tx/submit, plus the public GET /v1/orders/open feed. The two trading endpoints require an API key; see Authentication.
Base URL: https://noether-api.proudmeadow-533cf0d8.germanywestcentral.azurecontainerapps.io.
The non-custodial model
The gateway never holds private keys. Every trade is a three-step flow:
- Prepare.
POST /v1/orders/preparebuilds an unsigned, simulated Soroban transaction for one trading operation and returns it as base64 XDR. - Sign. You sign that XDR client-side with your own Stellar key (wallet or keypair). The gateway is never involved.
- Submit.
POST /v1/tx/submitrelays the signed XDR to the network and polls until it reaches a final status or times out.
Two properties of the prepared transaction matter for your integration:
- The trader is always the API key's owner. The gateway sets the transaction's source account to the Stellar address that issued your key. You cannot prepare a transaction for another address.
- It consumes your account's sequence number and expires after 300 seconds. Sign and submit it before anything else uses that sequence, and within the five-minute window; otherwise it becomes invalid and you must prepare again.
Both endpoints sit on the geo-guarded surface: depending on gateway configuration they can return HTTP 451 with body {"error":"region_restricted","message":"Trading is not available in your jurisdiction. See the Terms of Service (restricted persons)."}. Read endpoints, authentication and health are never geo-blocked.
POST /v1/orders/prepare
Builds an unsigned, simulated, prepared Soroban transaction for one trading operation on the market contract. Auth: API key required (Authorization plus X-Timestamp).
The request body is a union discriminated by the op field. All ten operations:
op | Required fields (besides op) | Optional fields and constraints |
|---|---|---|
open_position | asset, collateral, leverage, direction | asset must be a supported symbol; collateral is a digit string in 7-decimal USDC ("100000000" is 10 USDC); leverage integer 1 to 10; direction is "Long" or "Short" |
open_position_cross | asset, collateral, leverage, direction | Same constraints as open_position; opens under cross margin |
close_position | positionId | Integer or numeric string |
close_position_cross | positionId | Integer or numeric string |
place_limit_order | asset, direction, collateral, leverage, triggerPrice, triggerCondition, slippageToleranceBps | triggerPrice digit string, 7-decimal fixed point; triggerCondition is "Above" or "Below"; slippageToleranceBps integer 0 to 10000; optional timeInForce (0 GTC, the default, 1 IOC, 2 post-only) and reduceOnly (boolean) |
place_stop_limit_order | Same as place_limit_order plus limitPrice | limitPrice digit string, 7-decimal fixed point; same optional timeInForce and reduceOnly |
place_trailing_stop | positionId, trailingPercentBps, slippageToleranceBps | trailingPercentBps integer 1 to 10000 |
set_stop_loss | positionId, triggerPrice, slippageToleranceBps | triggerPrice digit string, 7-decimal fixed point |
set_take_profit | positionId, triggerPrice, slippageToleranceBps | Optional limitPrice (digit string) turns it into a take-limit; omit or send "0" for a plain take-profit |
cancel_order | orderId | Integer or numeric string |
Notes on field conventions:
- All money and price fields are strings of 7-decimal fixed-point integers (
10000000is 1.0). Ten USDC of collateral is"100000000". directionin request bodies is the string"Long"or"Short". Read endpoints and event payloads encode direction as an integer,0long and1short.- Basis-point fields (
slippageToleranceBps,trailingPercentBps) are plain integers out of 10,000. - The supported asset list is served live by
GET /v1/markets. - Body validation runs before authentication: a body that matches none of the ten schemas gets
400 FST_ERR_VALIDATIONeven without a key.
Request
curl -s -X POST https://noether-api.proudmeadow-533cf0d8.germanywestcentral.azurecontainerapps.io/v1/orders/prepare \
-H "Authorization: Bearer nk_<keyId>:<secret>" \
-H "X-Timestamp: $(date +%s)" \
-H "content-type: application/json" \
-d '{"op":"open_position","asset":"BTC","collateral":"100000000","leverage":2,"direction":"Long"}'Response 200
{
"op": "open_position",
"trader": "G<your address>",
"xdr": "AAAAAgAAA...base64 unsigned transaction XDR...",
"minResourceFee": "187123"
}xdris the unsigned transaction, base64-encoded. The source account is your address; it contains one invoke-host-function operation against the market contract, with the fee set from the simulated Soroban resource fee.minResourceFeeis the simulated resource fee in stroops, as a string. It may be absent.- The transaction has already been simulated against the current chain state. If the simulation fails (insufficient collateral, contract-level rejection, stale price), prepare returns
400 simulation_failedinstead of an XDR, so you find out before signing anything.
Prepare errors
| HTTP | Body | Meaning |
|---|---|---|
400 | {"error":"FST_ERR_VALIDATION","message":"...","validation":[...]} | The body does not match any operation schema; check required fields and constraints in the table above |
400 | {"error":"unsupported_asset","asset":"XYZ"} | The asset is not in the supported list |
400 | {"error":"simulation_failed","message":"..."} | The Soroban simulation rejected the operation (insufficient collateral, contract error, and so on) |
401 | {"error":"missing_bearer"} and the other 401 bodies | Missing or invalid API key or timestamp; see Authentication |
451 | {"error":"region_restricted", ...} | Trading blocked for your region (configuration-dependent) |
502 | {"error":"rpc_error","message":"Upstream RPC request failed."} | The gateway could not reach the Soroban RPC |
Signing
Sign the returned XDR with the key of the trader address, against the testnet network passphrase Test SDF Network ; September 2015. With the JavaScript Stellar SDK the shape is: parse the XDR into a transaction, sign with your keypair, serialize back to XDR. Ready-made signing code lives in the SDK pages:
Browser wallets that support signTransaction can sign the same XDR.
POST /v1/tx/submit
Submits a signed transaction and polls the network until it reaches a final status or the poll window closes. Auth: API key required.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
signedXdr | string | yes | The signed transaction, base64 XDR |
pollTimeoutMs | integer | no | 1000 to 60000, default 30000. How long the gateway polls before returning PENDING. The poll interval is one second |
curl -s -X POST https://noether-api.proudmeadow-533cf0d8.germanywestcentral.azurecontainerapps.io/v1/tx/submit \
-H "Authorization: Bearer nk_<keyId>:<secret>" \
-H "X-Timestamp: $(date +%s)" \
-H "content-type: application/json" \
-d '{"signedXdr":"AAAAAgAAA...signed...","pollTimeoutMs":30000}'Responses
| HTTP | When | Body |
|---|---|---|
200 | Confirmed on chain | {"hash":"...","status":"SUCCESS","ledger":4731900} |
200 | Not final within pollTimeoutMs | {"hash":"...","status":"PENDING"} |
200 | Failed on chain | {"hash":"...","status":"FAILED","contractError":{"code":22,"name":"InsufficientCollateral"},"hostError":null,"txResultCode":"txFailed","resultXdr":"..."}; contractError and hostError may be null |
400 | Rejected at submission | {"error":"submission_rejected","message":"Submission rejected by RPC","contractError":{...},"hostError":null,"txResultCode":"..."}; contractError and hostError may be null |
503 | RPC transaction queue full | {"error":"try_again_later","retryable":true,"hash":"...","message":"RPC transaction queue is full — resubmit the same signed XDR shortly"} plus header Retry-After: 2 |
502 | Transport error | {"error":"rpc_error","retryable":true,"message":"Upstream RPC request failed."} |
PENDING and idempotent resubmission
Submission is idempotent. If you resubmit the same signed XDR, the network reports it as a duplicate and the gateway polls the existing transaction by hash and returns its current result. So the polling loop for a PENDING response is:
- Get
{"status":"PENDING","hash":"..."}. - Resubmit the exact same
signedXdr(optionally with a freshpollTimeoutMs). - Repeat until you get
SUCCESSorFAILED.
The same applies to a 503 try_again_later: wait for the Retry-After interval (2 seconds) and resubmit the same XDR. Do not prepare a fresh transaction to retry a PENDING one: the original may still land, and you would risk executing the trade twice.
Contract and host errors
When a transaction fails inside the market contract, the gateway decodes the Soroban diagnostic error into contractError: {"code": <number>, "name": "<ErrorName>"}; for example code 22 is InsufficientCollateral and code 30 is PriceStale. Unknown codes decode to "UnknownContractError" with the raw number preserved. When the failure happened outside contract code, contractError is null and hostError names the host subsystem and code instead: {"type":"budget","code":"exceeded_limit"} for a resource overrun, or a storage trap when the ledger state moved between simulation and apply. A storage or budget trap, unlike a contract revert, is worth exactly one retry with a fresh prepare and signature; txResultCode carries the outer transaction result name (for example txFailed) for anything else.
The full code-to-name table is on the Errors page.
GET /v1/orders/open
Public feed of orders placed on the current market, folded from order_placed, order_executed and order_cancelled events, newest first. Rows carry only what the order_placed event carries (orderId, trader, triggerPrice); hydrate asset, direction and size on chain via the market's get_order for the ids returned. Use it to avoid iterating every on-chain order id.
Auth: none (public).
| Param | In | Type | Required | Notes |
|---|---|---|---|---|
trader | query | string | no | Stellar address, 56 characters |
status | query | string | no | open (default) or all to include executed and cancelled orders (order history) |
limit | query | integer | no | 1 to 200, default 50 |
curl -s "https://noether-api.proudmeadow-533cf0d8.germanywestcentral.azurecontainerapps.io/v1/orders/open?limit=2"{
"orders": [
{
"orderId": 38,
"trader": "GCAAZWLEY65OHIDYXS6LVHVW7VHGFNFJ3UYBXSRLIRQMCD5NLCZTL3HZ",
"triggerPrice": "848297700000",
"status": "open",
"ledger": 4263856,
"ts": 1787341873,
"txHash": "2c003a6bd3ae9705eef3e87e8e3d970f01321761a556a8ae4efacd680144adc8"
},
{
"orderId": 17,
"trader": "GAT47PJIXN5EGLBLLZJHB6K42IXVDDNQ5OYWX6UL4MG5W4JLZWYXNLXA",
"triggerPrice": "590000000000",
"status": "open",
"ledger": 4025882,
"ts": 1786149997,
"txHash": "dc491b28ea33cf67d73526444cccdbe1e5ef690ddfcdebb9dbf9b704a60a90a8"
}
]
}status is open, executed or cancelled; triggerPrice is a 7-decimal string; ts is unix seconds. This feed is indexer-backed and can lag the chain.
End-to-end walkthrough
Open a 2x long on BTC with 10 USDC collateral, using curl for the HTTP calls and the JavaScript Stellar SDK for the signature. The wallet needs test USDC first: Friendbot only funds XLM, and the app's faucet provides collateral.
Prepare the transaction
curl -s -X POST https://noether-api.proudmeadow-533cf0d8.germanywestcentral.azurecontainerapps.io/v1/orders/prepare \
-H "Authorization: Bearer nk_<keyId>:<secret>" \
-H "X-Timestamp: $(date +%s)" \
-H "content-type: application/json" \
-d '{"op":"open_position","asset":"BTC","collateral":"100000000","leverage":2,"direction":"Long"}'Response:
{"op":"open_position","trader":"G<your address>","xdr":"AAAAAgAAA...","minResourceFee":"187123"}Sign the XDR client-side
import { TransactionBuilder, Networks, Keypair } from '@stellar/stellar-sdk'
const tx = TransactionBuilder.fromXDR(xdr, Networks.TESTNET)
tx.sign(Keypair.fromSecret(process.env.STELLAR_SECRET!))
const signedXdr = tx.toXDR()The transaction expires 300 seconds after prepare; sign and submit promptly. For full client setups (including wallet-based signing), see the TypeScript SDK and Python SDK pages.
Submit the signed XDR
curl -s -X POST https://noether-api.proudmeadow-533cf0d8.germanywestcentral.azurecontainerapps.io/v1/tx/submit \
-H "Authorization: Bearer nk_<keyId>:<secret>" \
-H "X-Timestamp: $(date +%s)" \
-H "content-type: application/json" \
-d '{"signedXdr":"'"$SIGNED_XDR"'","pollTimeoutMs":30000}'On success:
{"hash":"85895449a4...","status":"SUCCESS","ledger":4731900}If you get {"status":"PENDING", ...}, resubmit the same signed XDR to keep polling. If you get {"status":"FAILED","contractError":{...}, ...}, look the code up on the Errors page.
Confirm the result
Read your position back with GET /v1/account/me/positions or the public GET /v1/positions/open?trader=, or stream fills live over the WebSocket API.
Next steps
- Authentication: get an API key (closed beta; issuance is allowlist-gated)
- Errors: the full contract error code table
- Positions and Account: read back your open positions and order events
- WebSocket API: live fills and prices without polling