Skip to content
NoetherDocs

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:

  1. Prepare. POST /v1/orders/prepare builds an unsigned, simulated Soroban transaction for one trading operation and returns it as base64 XDR.
  2. Sign. You sign that XDR client-side with your own Stellar key (wallet or keypair). The gateway is never involved.
  3. Submit. POST /v1/tx/submit relays the signed XDR to the network and polls until it reaches a final status or times out.
Prepare, sign, submit A trade through the gateway: the client asks the gateway to prepare a simulated, unsigned transaction, signs it locally with the wallet so the key never reaches the gateway, submits the signed XDR, and the gateway sends it to the Stellar network and reports the final status. PREPARE SIMULATE UNSIGNED XDR SIGN SIGNED XDR SUBMIT SEND TX STATUS SUCCESS Wallet keeps the key Client SDK or bot Gateway holds no keys Stellar testnet RPC LEGEND Signs locally HTTP request Call Response Final status
The gateway prepares and relays; only your wallet signs. A PENDING result means resubmit the same signed XDR, never a new one. Open full size (opens in a new tab)

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:

opRequired fields (besides op)Optional fields and constraints
open_positionasset, collateral, leverage, directionasset 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_crossasset, collateral, leverage, directionSame constraints as open_position; opens under cross margin
close_positionpositionIdInteger or numeric string
close_position_crosspositionIdInteger or numeric string
place_limit_orderasset, direction, collateral, leverage, triggerPrice, triggerCondition, slippageToleranceBpstriggerPrice 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_orderSame as place_limit_order plus limitPricelimitPrice digit string, 7-decimal fixed point; same optional timeInForce and reduceOnly
place_trailing_stoppositionId, trailingPercentBps, slippageToleranceBpstrailingPercentBps integer 1 to 10000
set_stop_losspositionId, triggerPrice, slippageToleranceBpstriggerPrice digit string, 7-decimal fixed point
set_take_profitpositionId, triggerPrice, slippageToleranceBpsOptional limitPrice (digit string) turns it into a take-limit; omit or send "0" for a plain take-profit
cancel_orderorderIdInteger or numeric string

Notes on field conventions:

  • All money and price fields are strings of 7-decimal fixed-point integers (10000000 is 1.0). Ten USDC of collateral is "100000000".
  • direction in request bodies is the string "Long" or "Short". Read endpoints and event payloads encode direction as an integer, 0 long and 1 short.
  • 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_VALIDATION even 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"
}
  • xdr is 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.
  • minResourceFee is 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_failed instead of an XDR, so you find out before signing anything.

Prepare errors

HTTPBodyMeaning
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 bodiesMissing 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

FieldTypeRequiredNotes
signedXdrstringyesThe signed transaction, base64 XDR
pollTimeoutMsintegerno1000 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

HTTPWhenBody
200Confirmed on chain{"hash":"...","status":"SUCCESS","ledger":4731900}
200Not final within pollTimeoutMs{"hash":"...","status":"PENDING"}
200Failed on chain{"hash":"...","status":"FAILED","contractError":{"code":22,"name":"InsufficientCollateral"},"hostError":null,"txResultCode":"txFailed","resultXdr":"..."}; contractError and hostError may be null
400Rejected at submission{"error":"submission_rejected","message":"Submission rejected by RPC","contractError":{...},"hostError":null,"txResultCode":"..."}; contractError and hostError may be null
503RPC 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
502Transport 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:

  1. Get {"status":"PENDING","hash":"..."}.
  2. Resubmit the exact same signedXdr (optionally with a fresh pollTimeoutMs).
  3. Repeat until you get SUCCESS or FAILED.

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

ParamInTypeRequiredNotes
traderquerystringnoStellar address, 56 characters
statusquerystringnoopen (default) or all to include executed and cancelled orders (order history)
limitqueryintegerno1 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

On this page