# reTrade developer docs > We make AI Agentic Trading mistakes insurable. reTrade is an independent pre-trade check for AI trading: each order an AI agent sends through the SDK is checked against rules the business sets and gets one of three signed answers, ALLOW, CUT (a smaller size) or BLOCK. There are no holds and no approval step, and every decision is chained into a tamper-evident log. Pre-release: live trading isn't switched on yet. Every page of the docs follows, in reading order. Each is also at its own address with .md added, listed in https://retrade.app/llms.txt. Instructions for an AI coding agent adding reTrade to a bot: https://retrade.app/SKILL.md. --- # Pre-trade checks for AI trading agents > reTrade is an independent pre-trade check for AI trading. Before your agent’s order reaches the broker, the SDK asks reTrade, and the order is placed as sent, cut to what fits, or blocked. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs ## What reTrade does **We make AI Agentic Trading mistakes insurable.** Your AI agent still decides what to trade. reTrade checks each of its orders against rules your business sets, such as how much of the account one stock may take, and answers in the same request. No AI can change those rules, your own agent included. Each order gets one of three answers. Nothing waits for a person: there are no holds and no approval step. reTrade’s three answers, and what happens to the order: | Answer | What it means | What the SDK does | What your code does | |---|---|---|---| | ALLOW | The order fits every rule. | Places it as sent, once. | Keep the decision id with your order record. | | CUT | It fits only at a smaller quantity. | Places the smaller quantity reTrade approved. | Treat `quantity` as what was placed. Don’t send the rest another way. | | BLOCK | It breaks a rule, or no room is left. | Places nothing. | Don’t send it another way. Resize to `fits` when it’s set, re-plan or ask your user. | Every answer comes with a signed receipt, chained into your log so any later change shows: the log is tamper-evident. The SDK keeps the signed records on your side, as evidence an insurer can review. reTrade is not an insurer, guarantees nothing and pays no losses. ## How one order flows One order, from your agent to your broker: 1. **Your agent**: Decides on an order and hands it to the SDK with `place`. 2. **The SDK**: Reads the account and a fresh quote from your broker through your adapter, signs the request with your bot’s key and sends it to reTrade. 3. **reTrade**: Checks the order against your rules, answers ALLOW, CUT or BLOCK with the rule that decided, signs the answer and chains it into your log. 4. **The SDK**: Checks the signature and that the answer is about this exact request, then places what was approved through your adapter: the order, the smaller size, or nothing. 5. **Your records**: The SDK reports what the broker did, gets a signed receipt back and keeps all of it in the claim kit. Your agent gets the result. reTrade never connects to your broker, never holds your broker login and never moves money. Only your own code, the adapter, talks to the broker. ## An example Your agent asks to buy 40 shares at a 187.25 limit, an order worth 7,490. The account’s per-order line is 5,000, so reTrade answers BLOCK, reason `approval_line` `per_order`, with `fits` of 26: 26 shares at 187.25 is 4,868.50. Nothing is placed. Your agent can send 26, re-plan, or ask its user. ## What you need - Python 3.12 or later, or Node 22.18 or later. - A tenant at reTrade with at least one account in it. Access comes through a pilot: [ask for one](https://retrade.app/#pilot). - From your admin: your bot’s key id and the account’s ref. From reTrade: its root public key. - Your broker code: four small functions the SDK calls to read the account and to place, find and cancel orders. ## Where to go next - [Quickstart](https://retrade.app/docs/quickstart.md): install, set your keys and run a first check. - [ALLOW, CUT and BLOCK](https://retrade.app/docs/concepts/decisions.md): what each answer means for your code. - [Python SDK](https://retrade.app/docs/sdk/python.md) and [TypeScript SDK](https://retrade.app/docs/sdk/typescript.md): the full guides. - [SKILL.md](https://retrade.app/docs/skill.md): a file your AI coding agent can follow to add reTrade to a bot you already have. > reTrade is pre-release, and live trading isn’t switched on yet: test with a paper or practice account. Ids, prices and orders in these docs are illustrations. --- # Quickstart > Install the SDK, set your keys, connect a practice broker and send a first order through reTrade. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/quickstart ## Install Python: ```shell pip install retrade==0.1.2 ``` TypeScript: ```shell npm install retrade@0.1.2 ``` The package is `retrade` on [PyPI](https://pypi.org/project/retrade/) and on [npm](https://www.npmjs.com/package/retrade). Python needs 3.12 or later; Node needs 22.18 or later. Both install a `retrade` command. The SDK is pre-release, so pin the version you test with. ## Get your keys You need four things. Your admin and reTrade give you three; you make the fourth. What the SDK needs, and where each comes from: | What | Looks like | Where it comes from | |---|---|---| | Executor key file | `executor.pem` | You make it, below, and keep it. reTrade only ever sees its public half. | | Bot key id | `exk_test_` and 26 more characters | Your admin, after registering your key. | | Root public key | 43 characters | reTrade, when your tenant is set up. | | Account ref | the name your admin gave the account | Your admin. | Python: ```shell retrade keygen executor.pem ``` TypeScript: ```shell npx retrade keygen executor.pem ``` This writes a new private key, readable only by you, and prints its public half. Send only the public half to your admin. They register it for one mode and send back your bot key id. Start with a `test` key: its orders are checked like real ones, but its decisions aren’t evidence for an insurer. Keep `executor.pem` secret: anyone holding it can ask for decisions as your agent. What your admin runs: ```shell # Your admin runs this with their own admin key. # The admin command line is in development. retrade-admin keys add-executor --mode test ``` ## Set your environment ```shell export RETRADE_EXECUTOR_KEY_ID="" export RETRADE_EXECUTOR_KEY_PATH="$PWD/executor.pem" export RETRADE_ROOT_KEY="" export RETRADE_ACCOUNT="" export RETRADE_AGENT="" ``` These names are a convention for your own code: the samples read them and pass the values to the SDK. The SDK itself reads one variable, `RETRADE_BASE_URL`, and only when you pass no address. Without either, it uses reTrade’s hosted service, `https://api.retrade.app`. ## Connect a practice broker The SDK reads the account and places orders only through an adapter: four functions you write against your broker’s API. For a first run, this one keeps a pretend account in memory and records orders instead of sending them anywhere. Python: ```python from datetime import UTC, datetime from decimal import Decimal import retrade placed: list[str] = [] # the tags of the orders this pretend broker took def read_account( instrument: str | None, adjustments_since: datetime | None ) -> tuple[retrade.Snapshot, retrade.Quote | None]: now = datetime.now(UTC) snapshot = retrade.Snapshot( as_of=now, cash={"USD": Decimal("50000")}, settled_cash={"USD": Decimal("50000")}, positions=(), open_orders=(), ) if instrument is None: return snapshot, None quote = retrade.Quote(instrument, Decimal("187.10"), Decimal("187.08"), Decimal("187.12"), now) return snapshot, quote def submit(order: retrade.ApprovedOrder, tag: str) -> str: placed.append(tag) return f"paper-{len(placed)}" def find(tag: str, order: retrade.ApprovedOrder, since: datetime) -> str | None: return f"paper-{placed.index(tag) + 1}" if tag in placed else None def cancel(broker_order_id: str) -> None: pass # nothing rests at this pretend broker broker = retrade.GenericAdapter( snapshot=read_account, submit=submit, find=find, cancel=cancel, g_seconds=60, converts_cash=False, ) ``` TypeScript: ```typescript import { GenericAdapter } from "retrade"; const placed: string[] = []; // the tags of the orders this pretend broker took const broker = new GenericAdapter({ snapshot(instrument) { const asOf = new Date(); const snapshot = { asOf, cash: { USD: "50000" }, settledCash: { USD: "50000" }, positions: [], openOrders: [], }; const quote = instrument === null ? null : { instrument, last: "187.10", bid: "187.08", ask: "187.12", asOf }; return { snapshot, quote }; }, submit(order, tag) { placed.push(tag); return `paper-${placed.length}`; }, find(tag) { const index = placed.indexOf(tag); return index === -1 ? null : `paper-${index + 1}`; }, cancel() { // nothing rests at this pretend broker }, gSeconds: 60, convertsCash: false, }); ``` The adapter’s `g_seconds` and `converts_cash` (`gSeconds` and `convertsCash` in TypeScript) must equal the account’s settings at reTrade; 60 and false are the SDK’s defaults. Ask your admin if the account uses others. When you’re ready, swap in your real broker: see [Connect your broker](https://retrade.app/docs/sdk/python.md#connect-your-broker). ## Run your first check Python: ```python import os from decimal import Decimal from retrade import Client, Order, OrderType, Side, Tif with Client( os.environ["RETRADE_EXECUTOR_KEY_ID"], os.environ["RETRADE_EXECUTOR_KEY_PATH"], root_key=os.environ["RETRADE_ROOT_KEY"], ) as rt: order = Order("XNAS:AAPL", Side.BUY, 40, OrderType.LIMIT, Tif.DAY, limit_price=Decimal("187.25")) result = rt.place(order, account=os.environ["RETRADE_ACCOUNT"], agent=os.environ["RETRADE_AGENT"], via=broker) why = result.deciding or result.reason print(result.outcome, result.placement, result.quantity, why) ``` TypeScript: ```typescript import { Client } from "retrade"; function env(name: string): string { const value = process.env[name]; if (!value) throw new Error(`${name} is not set`); return value; } const rt = await Client.open({ keyId: env("RETRADE_EXECUTOR_KEY_ID"), keyPath: env("RETRADE_EXECUTOR_KEY_PATH"), rootKey: env("RETRADE_ROOT_KEY"), }); try { const order = { instrument: "XNAS:AAPL", side: "buy", quantity: 40, type: "limit", tif: "day", limitPrice: "187.25", } as const; const result = await rt.place(order, { account: env("RETRADE_ACCOUNT"), agent: env("RETRADE_AGENT"), via: broker, }); const why = result.deciding ?? result.reason; console.log(result.outcome, result.placement, result.quantity, why); } finally { await rt.close(); } ``` Save the practice broker and this check in one file. In Python, as `first_check.py`, run with `python first_check.py`. In TypeScript, as `first-check.mts`, run with `node first-check.mts`: Node 22.18 and later run TypeScript files directly. ## Read the answer `place` returns a result. A BLOCK, or reTrade not answering, is a result too, never an exception. The fields you’ll use first: The first fields of a result: | Field | What it tells you | |---|---| | `outcome` | ALLOW, CUT or BLOCK. | | `placement` | `placed`, `not_placed`, or `unknown` when the broker call’s outcome isn’t known. Never send an `unknown` order again. | | `quantity` | What was approved: smaller than you asked on a CUT. | | `deciding` | On a CUT or BLOCK, the rule that decided, such as `{"code": "order_size_cap"}`. | | `fits` | On a BLOCK over a dollar line or in an agent conflict, the largest quantity that would pass. | | `reason` | The SDK’s own reason when it decided without reTrade, such as `unreachable`. | | `decision_id` (`decisionId`) | The receipt’s id. Store it with your own order record. | If reTrade doesn’t answer within 2.5 seconds, you get BLOCK with reason `unreachable`, and nothing is placed. That’s [fail-closed](https://retrade.app/docs/concepts/fail-closed.md). ## Next steps - Connect your real broker: [Python](https://retrade.app/docs/sdk/python.md#connect-your-broker) or [TypeScript](https://retrade.app/docs/sdk/typescript.md#connect-your-broker). - Handle every answer: [ALLOW, CUT and BLOCK](https://retrade.app/docs/concepts/decisions.md). - Adding reTrade to a bot you already have? Give your coding agent [SKILL.md](https://retrade.app/docs/skill.md). --- # Rules and limits > Your admins set the rules each account’s orders are checked against. Agents can’t change them. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/concepts/rules ## Who sets the rules Your business’s people do, with their admin keys, through reTrade’s admin API and its command line, `retrade-admin` (both in development). A tenant has at least two admin keys. An agent’s key can ask for decisions and engage a stop, never change limits, modes or keys. So no AI can change the rules it’s checked against, including your own agent. A tighter limit applies at once. A looser one waits 24 hours. > First setup is the time before any account enforces. During it, changes apply at once. Whether to keep this setup window is still being decided. ## What a rule can limit Each rule has a code, and a CUT or BLOCK names the one that decided it. Limits that leave room for fewer shares give a CUT, or a BLOCK when there’s no room at all: Limits that can cut an order: | Code | What it limits | |---|---| | `single_name_cap` | How much of the account one stock may take, such as no more than 10% of the account in any one stock. | | `group_cap` | How much a group of holdings may take. | | `order_size_cap` | How big one order may be. | | `cash_funding` | Buying with cash the account doesn’t have. | | `no_short` | Selling shares the account doesn’t hold. | Rules that forbid an order outright give a BLOCK: Rules that block an order: | Code | What it checks | |---|---| | `restricted_list` | The stocks the account won’t trade. | | `agent_permission` | Which agents may trade the account. | | `order_form` | The order’s form. | | `new_position_pace` | The pace of new positions. | | `written_exit` | The order’s exit level: it reads the order’s stop price. | | `runaway_throttle` | Too many orders a minute from one agent, or a duplicate. | | `agent_conflict` | Another agent trading the same stock the other way. `fits` is 0. | [Reason codes](https://retrade.app/docs/reference/reason-codes.md) lists every code, with what the agent can do about each. ## Dollar lines Dollar lines cap the value of orders: per order (`per_order`), per agent per day (`per_agent_day`), per day (`per_day`), and in two other ways (`openings_pct`, `hard_cap_day`). An order over a line isn’t held for anyone. It comes straight back as a BLOCK, code `approval_line` with the line as its reason, and `fits`: the largest quantity that would pass. The lines are your own, not limits of reTrade’s. A new tenant starts at 5,000 per order and 10,000 per day. Your admins set their own lines when the tenant is made, or later with `retrade-admin policy set`. For example, a buy of 40 at a 187.25 limit is worth 7,490. Under the 5,000 per-order line it comes back as BLOCK, `approval_line` `per_order`, with `fits` of 26: 26 shares at 187.25 is 4,868.50. ## Missing or old facts mean BLOCK reTrade decides only on facts it can check. An account it doesn’t know, a price older than 30 seconds, or a stock it can’t match exactly is a BLOCK, with a code that says which: `ACCOUNT-UNKNOWN`, `QUOTE-STALE`, `SYMBOL-UNKNOWN`. So is account data that’s missing, old or doesn’t add up. The account data always comes from your broker, through your adapter, never from the agent. ## Stops A stop halts trading for the whole tenant, one account or one agent. Any executor key can engage one, your agent’s included, with `stop`. Only an admin key releases it (`retrade-admin release`). While a stop is engaged, the orders it covers come back as BLOCK `STOPPED`. When reTrade’s runaway check switches an agent off, its orders come back as `AGENT-STOPPED` until an admin turns it back on. --- # ALLOW, CUT and BLOCK > Every order gets one of three answers, in the same request. What each one means, and what your code does with it. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/concepts/decisions ## The three answers reTrade’s three answers, and what happens to the order: | Answer | What it means | What the SDK does | What your code does | |---|---|---|---| | ALLOW | The order fits every rule. | Places it as sent, once. | Keep the decision id with your order record. | | CUT | It fits only at a smaller quantity. | Places the smaller quantity reTrade approved. | Treat `quantity` as what was placed. Don’t send the rest another way. | | BLOCK | It breaks a rule, or no room is left. | Places nothing. | Don’t send it another way. Resize to `fits` when it’s set, re-plan or ask your user. | Nothing waits for a person. There are no holds and no approval step. An order that once would have waited for someone comes straight back to your agent as a BLOCK, with a reason code, and the agent decides what to do next. ## The rule that decided A CUT or BLOCK names the rule that decided it in `deciding`, exactly as reTrade sent it: `{"code": "order_size_cap"}`, for example, or `{"code": "approval_line", "reason": "per_order"}` for a dollar line. An ALLOW has none. [Reason codes](https://retrade.app/docs/reference/reason-codes.md) lists every code and what the agent can do. ## Resizing to fits Over a dollar line or in an agent conflict, a BLOCK also carries `fits`: the largest quantity that would pass, or 0 when none would. Your agent can send the order again at `fits`. That’s a new decision, checked in full, and it counts toward the agent’s orders-per-minute limit (`runaway_throttle`), as the BLOCK before it did. Resend once; don’t loop. Python: ```python from dataclasses import replace from retrade import Outcome, Placement if (result.outcome is Outcome.BLOCK and result.fits and result.placement is Placement.NOT_PLACED): result = rt.place(replace(order, quantity=result.fits), account="A1", agent="agt_momentum", via=broker) ``` TypeScript: ```typescript const resizable = result.outcome === "BLOCK" && result.placement === "not_placed" && result.fits !== null && result.fits !== "0"; if (resizable) { result = await rt.place({ ...order, quantity: Number(result.fits) }, { account: "A1", agent: "agt_momentum", via: broker, }); } ``` Check `placement` first, as here: in shadow mode the original order was already placed. ## When the SDK decides itself Sometimes the SDK has no answer it can trust: reTrade didn’t answer within 2.5 seconds (`unreachable`), or the answer failed a check (`unverified`). Then the SDK decides on its own: `local` is true, `reason` says why, and `deciding` is empty. In enforce and test mode that’s a BLOCK, and nothing is placed. A local result is never evidence for an insurer. The SDK also says why in `reason` when it doesn’t place an approved order, such as `expired` or `broker-rejected`. [Reason codes](https://retrade.app/docs/reference/reason-codes.md#the-sdks-own-reasons) lists them all. ## How long an answer lasts An ALLOW or CUT lives 30 seconds by default (an admin can set 5 to 60), counted from when the SDK sent the request. The SDK places it only while time is left and no known stop covers the account and agent, and it places each decision at most once. Otherwise the result’s reason is `expired` or `stopped`, and nothing is placed. A BLOCK has no lifetime. ## Modes An account runs in one mode, and your key’s mode must match it: | Mode | What happens | Evidence for an insurer | |---|---|---| | `enforce` | reTrade’s answer decides what reaches the broker. | Yes, for placed orders. | | `shadow` | Your agent’s orders are placed unchanged, whatever the answer. reTrade only records what it would have done. | No. | | `test` | Like enforce, for testing. | No. | The mode is part of your bot key id, such as `exk_test_…`. A result’s `claimable` is true only for an enforce decision whose order was placed. It marks evidence and promises no payment. --- # Receipts and verification > Every answer is signed and chained into your log. The SDK checks each one before it acts, and keeps them as evidence. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/concepts/receipts ## What gets signed - **The decision.** reTrade’s answer is a compact JWS, signed with Ed25519 by that day’s decision key. It records the answer, the order as approved, the SHA-256 of your exact request, the rules in force, reTrade’s clock and the decision’s place in your log. - **The receipt.** After the broker call, the SDK reports what happened (placed, rejected or withdrawn), and reTrade answers with a signed receipt. - **The log head.** reTrade signs the latest position of your log, and the SDK keeps each head it receives. ## Chained, so any change shows Each record in your tenant’s log carries its number and the hash of the record before it (`seq` and `prev_hash`). Change, remove or reorder a record and the chain no longer matches: the log is tamper-evident. reTrade signs with a new decision key each day (UTC). Every key stays on the root-signed key list, `GET /v1/keys`, so an old answer can always be checked again. ## What the SDK checks Before anything reaches your broker, the SDK checks every answer: 1. It’s a well-formed JWS of type `retrade-decision`, signed with EdDSA. 2. Its key is on the key list your pinned root key signed, for your mode’s environment, and was valid when the decision was made. 3. The Ed25519 signature verifies. 4. It’s for your mode, account and agent, and its `request_sha256` is the SHA-256 of the bytes the SDK sent. 5. It was made within 60 seconds of your clock. 6. Its order fields and requested quantity are your order’s, and an ALLOW or CUT lives 5 to 60 seconds. An answer that fails any check is never placed: you get a local BLOCK with reason `unverified`, and nothing is reported. The status reTrade signs, with the stops in force and each account’s settings, is checked too (its signature, mode, log position and age) before the SDK uses it. The root key is pinned: the SDK never fetches it. If the key list wasn’t signed by your root key, the client refuses to start, with `VerificationFailed: bad signature on a retrade-keys document`. ## The claim kit The SDK writes everything to a local SQLite file as it goes: `claim-kit.sqlite3`, in its state directory (`retrade-state` by default). It holds your exact request bytes, reTrade’s signed decisions, the rules each was decided under, your submission reports with reTrade’s receipts, and the signed log heads. Each record is saved before the SDK goes on, and a decision is marked used before the broker call. Both SDKs write the same layout. It’s the evidence that makes a mistake insurable: what the agent asked, what reTrade answered and what reached the broker, which an insurer can review to underwrite the risk and assess a claim. Keep it for at least 90 days. Only enforce-mode decisions are that evidence. reTrade is not an insurer and pays no losses: whether a loss is covered is up to the insurer. Python: ```python if result.decision_id is not None: # None only when the SDK decided on its own evidence = rt.claim_kit(result.decision_id) # {"format": "retrade-claim-kit/1", "decision_id": ..., "request": ..., # "decision": ..., "policy": ..., "report": ..., "receipt": ..., "heads": [...]} ``` TypeScript: ```typescript if (result.decisionId !== null) { // null only when the SDK decided on its own const evidence = rt.claimKit(result.decisionId); // { format: "retrade-claim-kit/1", decision_id, request, decision, // policy, report, receipt, heads } } ``` Python: ```shell retrade claim-kit --state-dir retrade-state ``` TypeScript: ```shell npx retrade claim-kit --state-dir retrade-state ``` In the export, `request` and `policy` are base64url bytes, so they survive exactly; `decision` and `receipt` are compact JWS documents. Anyone with the root public key can check them again against `GET /v1/keys`. ## Reading your log `log` reads your tenant’s log for your key’s mode, each record checked against the key list and chained to the one before it. Python: ```python records = rt.log() # the whole log, from record 1 first20 = rt.log(from_seq=1, to_seq=20) # records 1 to 20 for record in first20: print(record.seq, record.kind, record.hash) ``` TypeScript: ```typescript const records = await rt.log(); // the whole log, from record 1 const first20 = await rt.log({ from: 1, to: 20 }); // records 1 to 20 for (const record of first20) console.log(record.seq, record.kind, record.hash); ``` reTrade sends the log a page at a time, up to 1,000 records and 512 KiB a page, and `log` keeps asking until it reaches the end or the last record you asked for. The records are held in memory, so read a long log in ranges. --- # Keys and roles > Your bot holds one key, and it can only ask. Your people hold the keys that change things. reTrade signs its answers with its own. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/concepts/keys ## The four kinds of key Who holds each key, and what it can do: | Key | Id | Who holds it | What it can do | |---|---|---|---| | Executor key, your bot’s | `exk__` | Your agent’s machine. reTrade only ever sees its public half. | Ask for decisions, report what happened, read the status and the log, and engage a stop. Never release a stop or change limits, modes or keys. | | Admin keys, two or more | `adm_` | Your people. | Change limits, modes and keys, and engage and release stops, through the admin API and command line (in development). | | Daily decision keys | `dk__` | reTrade, one a day (UTC). | Sign every answer, receipt and log head. | | Root key | `root` | reTrade, kept offline. | Signs the list of decision keys. Your SDK pins its public half. | ## Your bot’s key Python: ```shell retrade keygen executor.pem ``` TypeScript: ```shell npx retrade keygen executor.pem ``` This writes an Ed25519 private key to `executor.pem`, in unencrypted PKCS8 PEM, readable only by you (mode 0600), and prints its public half. It never overwrites a key file, and a key made by either SDK works with the other. An admin registers the public half for one mode and sends back its id; it works at once. The SDK refuses a key file other users can read. Anyone holding the file can ask for decisions as your agent, so keep it out of your repository, your logs and your images. On a server or in a container, mount it as a file readable only by the bot’s user: the SDK reads the key from a path, not from an environment variable. The mode in the id (`enforce`, `shadow` or `test`) is the mode of every order the key asks about. A decision key’s `env` is `live` for enforce and `nonlive` for shadow and test. ## Admin keys A tenant has at least two admin keys. A new admin key works 24 hours after it’s added, or at once during first setup. Admin keys sign every change to limits, modes and keys, and release stops. If your people lose them, only reTrade’s operator can help: after checking who is asking, outside reTrade, it adds one new admin key with `retrade-server admin-key-recover`. That key works only 24 hours later, and the change is written to your log at once, where any admin who still holds a key sees it and can revoke the new key before it works. `retrade-admin keys list` warns when you have exactly two admin keys. ## reTrade’s keys Each daily decision key is valid from 00:00 UTC on its day to 00:05 the next day, and stays on the list forever, so an old answer can always be checked. The SDK fetches the list from `GET /v1/keys` and checks it against the pinned root key. The root public key is 43 characters of base64url. reTrade gives it to you when your tenant is set up; it isn’t published on this site yet. Pass it as `root_key` (`rootKey`); the SDK never fetches it. ## Signed requests The SDK signs every request with your executor key, so reTrade knows which bot is asking. You don’t sign anything yourself. The headers and signed lines are in the [API reference](https://retrade.app/docs/reference.md#request-signing). --- # Fail-closed behaviour > If reTrade can’t answer, or its answer can’t be trusted, the order isn’t placed. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/concepts/fail-closed ## The 2.5-second rule The SDK gives each decision 2.5 seconds in total. A timeout, a dropped connection, a 429 or any 5xx status, or an unreadable answer counts as no answer. Within that time it retries once, at once, with the same bytes and the same idempotency key, signed again, so reTrade can never decide one order twice. If there’s still no answer, the SDK blocks the order itself: Python: ```python result.outcome # Outcome.BLOCK result.placement # Placement.NOT_PLACED result.reason # "unreachable" result.deciding # None: no answer from reTrade decided this result.local # True result.claimable # False ``` TypeScript: ```typescript result.outcome // "BLOCK" result.placement // "not_placed" result.reason // "unreachable" result.deciding // null: no answer from reTrade decided this result.local // true result.claimable // false ``` In enforce and test mode nothing is placed. In shadow mode the original order is placed, with reason `shadow-unreachable`. A local BLOCK is never evidence for an insurer. ## Answers that fail a check An answer with a bad signature, an unknown key, other request bytes, or order fields or a quantity that differ from your request is treated the same way, with reason `unverified`: nothing is placed and nothing is reported. See [what the SDK checks](https://retrade.app/docs/concepts/receipts.md#what-the-sdk-checks). ## At start-up The client fetches the key list and the signed status when it starts, which also opens the connection early. If reTrade can’t be reached then, the client still starts and logs a warning, and every order fails closed until reTrade answers. Any other start-up failure is an error: a root key that didn’t sign the key list raises `VerificationFailed`, and a request reTrade refuses raises `RequestRefused`. ## Errors are for your code `place` raises only for your code to fix (`InvalidInput`, `ConfigError`, `RequestRefused`) or when your adapter fails to read the account. Each of these comes before your adapter’s `submit` is called, so nothing was placed. A `RequestRefused` is a 4xx other than 429: a request reTrade couldn’t accept, to fix rather than retry. ## Exits-only The one exception to fail-closed is exits-only, an account setting an admin turns on. It’s a loosening, so it takes effect 24 hours later, or at once during first setup, and the SDK reads it only from the signed status. While reTrade can’t be reached, it lets the SDK place a limit sell of shares the broker holds now, less sells already working and recent sells the broker may not show yet; no more than 10% below the last price; of no company on the won’t-sell list; within the per-order line of the last rules the SDK decided under (5,000 when those rules have none); with no stop known for the account or the agent. Everything else is still blocked. Such a sell is a local ALLOW with reason `exit`; a refused one gives a reason starting `exits-only:`, such as `exits-only:over-held`. Exits-only never applies to an `unverified` answer, and a sell it places isn’t evidence for an insurer either. ## Fail closed in your own code - Route every order through `place`. Keep no second path to the broker that skips it, such as a retry, a fallback or an error handler that calls the broker directly. - If the client can’t start, don’t start trading. Don’t catch the error and carry on without reTrade. - Never send an order again when its `placement` is `unknown`: the broker may have it. The decision keeps counting at reTrade until the account data shows the order or its absence. - Keep your clock in time (NTP). An answer more than 60 seconds from your clock is refused as `unverified`. - Test it: point the SDK at an address where nothing listens and check that orders come back BLOCK `unreachable` with nothing placed. [SKILL.md](https://retrade.app/docs/skill.md) has the test, in both languages. A quick check with the quickstart: ```shell # Nothing listens on port 9, so every order must come back BLOCK "unreachable". RETRADE_BASE_URL=http://127.0.0.1:9 python first_check.py ``` --- # The reTrade SDKs > Two SDKs with one behaviour. Both talk to the same hosted service, place orders the same way and keep the same evidence. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/sdk ## Pick your language The two SDKs: | SDK | Package | Needs | Guide | |---|---|---|---| | Python | [`retrade` on PyPI](https://pypi.org/project/retrade/) | Python 3.12 or later. One dependency, `cryptography`. | [Python SDK](https://retrade.app/docs/sdk/python.md) | | TypeScript | [`retrade` on npm](https://www.npmjs.com/package/retrade) | Node 22.18 or later. No runtime dependencies. | [TypeScript SDK](https://retrade.app/docs/sdk/typescript.md) | Python: ```shell pip install retrade==0.1.2 ``` TypeScript: ```shell npm install retrade@0.1.2 ``` ## What both share - Version 0.1.2, built to the wire spec `retrade-arith/2.0-draft`, which may still change. A 0.x version may change the API between minor releases: pin the version you test with. - The default address: reTrade’s hosted service, `https://api.retrade.app`. Pass `base_url` (`baseUrl`) or set `RETRADE_BASE_URL` to use another. Plain `http` is allowed only for a loopback address. - The same executor key file: a key made with either SDK’s `retrade keygen` works with the other. - The same claim kit file and export format, `retrade-claim-kit/1`, so one checker reads both. - The same rules for placing: in enforce and test mode, nothing reaches your broker that reTrade didn’t approve, and never more than the approved quantity. A decision is placed at most once. ## What isn’t built yet - Snapshots sent with no order (`send_snapshot` and `keep_fresh`; `sendSnapshot` and `keepFresh`). The hosted service answers 404 for now: `send_snapshot` raises `RequestRefused`, and `keep_fresh` only logs it. - Webhooks (`stop.engaged`, `stop.released`, `policy.changed`). The Python SDK’s `verify_webhook` checks their planned format. - Live trading isn’t switched on yet. Test with a paper or practice account. Start with the [Quickstart](https://retrade.app/docs/quickstart.md), or go straight to the [API reference](https://retrade.app/docs/reference.md). --- # Python SDK > Install it, connect your broker, check each order and act on the signed answer, in Python. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/sdk/python ## Install ```shell pip install retrade==0.1.2 ``` The package is `retrade` on [PyPI](https://pypi.org/project/retrade/), and you import it as `retrade`. It needs Python 3.12 or later, has one dependency, `cryptography`, ships its type hints, and installs the `retrade` command. ## Configure the client ```python import os import retrade rt = retrade.Client( os.environ["RETRADE_EXECUTOR_KEY_ID"], # exk__, from your admin os.environ["RETRADE_EXECUTOR_KEY_PATH"], # the file retrade keygen wrote root_key=os.environ["RETRADE_ROOT_KEY"], # from reTrade: pinned, never fetched state_dir="retrade-state", # where the claim kit lives ) ``` The client’s arguments: | Argument | What it is | |---|---| | `key_id` | Your bot key id, `exk__`, from your admin. Its mode is the client’s. | | `key_path` | The executor key file `retrade keygen` wrote. It must be readable only by you. | | `root_key` | reTrade’s root public key, 43 characters of base64url. Pinned: the SDK never fetches it. | | `base_url` | reTrade’s address. Without it, `RETRADE_BASE_URL`, else `https://api.retrade.app`. (`server` is its older name.) | | `state_dir` | Where the claim kit lives, `retrade-state` by default. One directory per executor. | On start-up the client fetches the key list and the signed status, which holds the stops in force and each account’s settings, and checks both before using them; `rt.refresh()` fetches them again. If reTrade can’t be reached then, it still starts, and orders fail closed until reTrade answers. Call `rt.close()` when you’re done, or use the client in a `with` block. ## Connect your broker The SDK reaches your broker only through an adapter. `retrade.GenericAdapter` takes four plain functions. Here `my_broker` stands for your own broker client. ```python from datetime import UTC, datetime from decimal import Decimal import retrade def read_account( instrument: str | None, adjustments_since: datetime | None ) -> tuple[retrade.Snapshot, retrade.Quote | None]: # Read the account now, from the broker, never from the agent. # Amounts and prices are Decimal (or int), never float: # Decimal(str(x)) converts one. account = my_broker.account() snapshot = retrade.Snapshot( as_of=datetime.now(UTC), cash={"USD": Decimal(str(account.cash))}, settled_cash={"USD": Decimal(str(account.settled_cash))}, positions=tuple( retrade.Position(p.symbol, p.quantity, Decimal(str(p.mark))) for p in account.positions ), open_orders=tuple( retrade.OpenOrder( o.id, o.client_order_id or None, o.symbol, o.side, o.remaining, o.type, o.tif, None if o.limit_price is None else Decimal(str(o.limit_price)), ) for o in account.open_orders ), ) if instrument is None: return snapshot, None q = my_broker.quote(instrument) quote = retrade.Quote(instrument, Decimal(str(q.last)), Decimal(str(q.bid)), Decimal(str(q.ask)), q.time) return snapshot, quote def submit(order: retrade.ApprovedOrder, tag: str) -> str: # Place exactly these fields, with the tag as the client order id. try: return my_broker.place(order, client_order_id=tag) except my_broker.Refused as error: # the broker definitely said no raise retrade.BrokerRejected(str(error)) from error # Any other error means the outcome is unknown: let it through. def find(tag: str, order: retrade.ApprovedOrder, since: datetime) -> str | None: found = my_broker.order_by_client_id(tag) return found.id if found else None def cancel(broker_order_id: str) -> None: my_broker.cancel(broker_order_id) broker = retrade.GenericAdapter( snapshot=read_account, submit=submit, find=find, cancel=cancel, g_seconds=60, # must equal the account's setting at reTrade converts_cash=False, # likewise ) ``` What each function must do: | Function | Rules | |---|---| | `snapshot` | Read the account now and return a `Snapshot` with settled cash, and a `Quote` when `instrument` is given. Include the USDCAD rate (`fx`) when the broker has one, and the deposits, withdrawals, dividends, fees, transfers and splits since `adjustments_since`. | | `submit` | Place the `ApprovedOrder` exactly, with `tag` as the client order id, and return the broker order id. Raise `BrokerRejected` only when the broker definitely refused it; anything else you raise means the outcome is unknown. The SDK never retries a submit. | | `find` | Look the order up by `tag`, or, where the broker doesn’t keep tags, match the order’s fields among orders placed since `since`. `None` never means “not placed”: the broker may not show it yet. | | `cancel` | Return once the broker has accepted the cancel; raise if it refused or the outcome is unknown. | `g_seconds` (60 by default) is how long the broker takes to show a new order: a whole number of seconds from 1 to 3,600, or `GenericAdapter` raises `InvalidInput`. `converts_cash` (False by default) says whether the broker converts currency on its own. Both must match the account’s settings at reTrade, or `place` raises `ConfigError`. > For a market buy of a stock the account doesn’t hold, include a position of 0 shares with its current mark. Without one, a buy whose rules count that order is BLOCK `STATE-INCOMPLETE`. ## Place an order ```python from decimal import Decimal from retrade import Order, OrderType, Side, Tif order = Order("XNAS:AAPL", Side.BUY, 40, OrderType.LIMIT, Tif.DAY, limit_price=Decimal("187.25")) result = rt.place(order, account="A1", agent="agt_momentum", via=broker) ``` `Order(instrument, side, quantity, type, tif, limit_price=None, stop_price=None)`. The instrument is an exact listing id, `:`, such as `XNAS:AAPL`. Quantity is a whole number. Prices are `Decimal`, never float. A limit order needs a limit price; a market order can’t have one. A stop price, the exit level the `written_exit` rule reads, must be above 0, and isn’t sent to the broker. Plain strings such as `"buy"` work too; the enums are what a type checker accepts. One call does the whole round: it reads the account, asks reTrade, checks the answer, places what was approved, reports it and keeps the evidence. The SDK works out from the account whether the order opens, adds to or reduces a position, so an agent can’t mislabel one. ## Handle the result `place` returns a `Result`. An answer is never an exception: a BLOCK, and reTrade not answering, both come back as a result. Check `placement` first, because in shadow mode the agent’s order is placed whatever the answer. ```python from retrade import Outcome, Placement, Result def report_to_agent(result: Result) -> dict[str, object]: if result.placement is Placement.UNKNOWN: # The broker may have the order: never place it again. return {"status": "unknown", "why": result.reason} if result.placement is Placement.PLACED: # On a CUT, quantity is smaller than the agent asked for. return {"status": "placed", "quantity": result.quantity, "outcome": result.outcome} if result.outcome is Outcome.BLOCK: # Nothing was placed. Over a dollar line, fits is the largest quantity # that would pass: the agent can resize, re-plan or ask its user. why = result.deciding or result.reason return {"status": "blocked", "why": why, "fits": result.fits} # An ALLOW or CUT that wasn't placed: expired, stopped or rejected. return {"status": "not placed", "why": result.reason} ``` To resize, send the order again at `fits`, once. It’s a new decision, so it counts toward the agent’s orders-per-minute limit, as the BLOCK did: ```python from dataclasses import replace from retrade import Outcome, Placement if (result.outcome is Outcome.BLOCK and result.fits and result.placement is Placement.NOT_PLACED): result = rt.place(replace(order, quantity=result.fits), account="A1", agent="agt_momentum", via=broker) ``` The fields of a Result: | Field | What it holds | |---|---| | `outcome` | `Outcome.ALLOW`, `CUT` or `BLOCK`. | | `placement` | `Placement.PLACED`, `NOT_PLACED`, or `UNKNOWN` when the broker call’s outcome is unknown. | | `quantity` | The approved quantity on ALLOW or CUT, or what the SDK placed on its own; `None` for a BLOCK. | | `requested_quantity` | The order’s own quantity. | | `deciding` | reTrade’s deciding rule, verbatim, such as `{"code": "approval_line", "reason": "per_order"}`; `None` for an ALLOW and for every local result. | | `fits` | On a BLOCK over a dollar line or in an agent conflict, the largest quantity that would pass (0 when none would); `None` otherwise. | | `flags` | Empty: over a dollar line the answer is BLOCK, not a flag. | | `alerts` | The decision’s alerts, passed through. | | `reason` | The SDK’s own reason when it acted without or against an answer, such as `unreachable`, `expired` or `broker-rejected`. | | `decision_id` | The decision’s id, also in the broker tag. `None` only when the SDK decided on its own. | | `broker_order_id` | The broker’s id for the order, when one was placed. | | `local` | True when the SDK decided without a verified answer from reTrade. | | `claimable` | True only for a placed enforce-mode decision: evidence an insurer can review. It promises no payment. | ## When reTrade doesn’t answer reTrade has 2.5 seconds in total, including one retry with the same bytes and key. After that, in enforce and test mode, the SDK blocks the order itself (in shadow mode it places the original order, with reason `shadow-unreachable`): ```python result.outcome # Outcome.BLOCK result.placement # Placement.NOT_PLACED result.reason # "unreachable" result.deciding # None: no answer from reTrade decided this result.local # True result.claimable # False ``` An answer that fails a check gets reason `unverified` instead, and nothing is placed or reported. See [Fail-closed behaviour](https://retrade.app/docs/concepts/fail-closed.md), including exits-only. ## After the order ```python # There's no modify: replace cancels, then asks for a new decision. result = rt.replace(broker_order_id, new_order, account="A1", agent="agt_momentum", via=broker) # Anyone may stop: an executor key can engage the kill switch. # Only an admin key releases it (retrade-admin release). stop_id = rt.stop("tenant", reason="agent misbehaving") cancelled = rt.cancel_open_orders(broker) # the ones with a reTrade tag # Reports that didn't reach reTrade are kept; send them again later. sent = rt.send_pending_reports() ``` The SDK sends the submission report itself. One that doesn’t reach reTrade is kept, and `rt.send_pending_reports()` sends it again and returns how many went. `rt.stop` takes `"tenant"`, `"account"` (with `account=`) or `"agent"` (with `agent=`), and a reason of up to 500 characters, and returns the stop’s id; the client refuses its own unused answers in the stop’s scope at once. `rt.cancel_open_orders(broker, all_orders=True)` cancels every working order, for a broker that drops tags. A `placement` of `UNKNOWN` means the submit failed in a way that doesn’t say whether the broker took the order, and `find` couldn’t see it. Don’t place it again: the decision keeps counting until the account data shows the order or its absence. ## Read your log ```python records = rt.log() # the whole log, from record 1 first20 = rt.log(from_seq=1, to_seq=20) # records 1 to 20 for record in first20: print(record.seq, record.kind, record.hash) ``` Each record is a `retrade.LogRecord` (`seq`, `kind`, `hash`, `token`, `payload`), checked against the key list and chained to the one before it. ## The claim kit ```python if result.decision_id is not None: # None only when the SDK decided on its own evidence = rt.claim_kit(result.decision_id) # {"format": "retrade-claim-kit/1", "decision_id": ..., "request": ..., # "decision": ..., "policy": ..., "report": ..., "receipt": ..., "heads": [...]} ``` ```shell retrade claim-kit --state-dir retrade-state ``` Both give one decision’s evidence, in the format `retrade-claim-kit/1`. The file is `retrade-state/claim-kit.sqlite3`, the same layout the TypeScript SDK writes. Keep it for at least 90 days. See [Receipts and verification](https://retrade.app/docs/concepts/receipts.md). ## Errors Every error the SDK raises is a `retrade.ReTradeError`. They’re for your code to fix, never an answer about an order. The Python SDK’s errors: | Error | When | |---|---| | `InvalidInput` | A value the SDK can’t send, such as a float price or a malformed order. Also a `ValueError`. | | `ConfigError` | A malformed key id, root key or server address, or an adapter whose settings differ from the account’s. | | `RequestRefused` | reTrade refused a request as malformed or unauthorised: a 4xx other than 429. It has `status` and `detail`. | | `KeyFileError` | The key file can’t be created or used. | | `VerificationFailed` | Something claiming to be from reTrade, such as the key list or the status, failed a check, outside `place`. | | `Unreachable` | No usable answer in time, from a call other than `place`, which blocks the order instead. | | `BrokerRejected` | Raised by your adapter’s `submit` when the broker definitely refused the order. | Fixes for the common ones are in [Errors and troubleshooting](https://retrade.app/docs/errors.md). --- # TypeScript SDK > Install it, connect your broker, check each order and act on the signed answer, in TypeScript or JavaScript on Node. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/sdk/typescript ## Install ```shell npm install retrade@0.1.2 ``` The package is `retrade` on [npm](https://www.npmjs.com/package/retrade). It needs Node 22.18 or later, is plain JavaScript (ES modules) with TypeScript declarations, and has no runtime dependencies: it uses Node’s own `crypto`, `http`, `https` and `sqlite`. It installs the `retrade` command. On Node 22, Node prints an `ExperimentalWarning` for `node:sqlite`, which the claim kit uses; the warning changes nothing. ## Configure the client ```typescript import { Client } from "retrade"; const rt = await Client.open({ // exk__, from your admin keyId: process.env.RETRADE_EXECUTOR_KEY_ID!, // the file retrade keygen wrote keyPath: process.env.RETRADE_EXECUTOR_KEY_PATH!, // from reTrade: pinned, never fetched rootKey: process.env.RETRADE_ROOT_KEY!, // where the claim kit lives stateDir: "retrade-state", }); ``` The options of Client.open: | Option | What it is | |---|---| | `keyId` | Your bot key id, `exk__`, from your admin. Its mode is the client’s. | | `keyPath` | The executor key file `retrade keygen` wrote. It must be readable only by you. | | `rootKey` | reTrade’s root public key, 43 characters of base64url. Pinned: the SDK never fetches it. | | `baseUrl` | reTrade’s address. Without it, `RETRADE_BASE_URL`, else `https://api.retrade.app`. (`server` is its older name.) | | `stateDir` | Where the claim kit lives, `retrade-state` by default. One directory per executor. | | `logger` | Where the SDK’s warnings and errors go: an object with `warn` and `error`. | `Client.open` fetches the key list and the signed status, which holds the stops in force and each account’s settings, and checks both before using them; `rt.refresh()` fetches them again. If reTrade can’t be reached then, the client still opens, and orders fail closed until reTrade answers. Any other failure, such as a key list the pinned root didn’t sign, is thrown. Call `await rt.close()` when you’re done. ## Connect your broker The SDK reaches your broker only through an adapter. `GenericAdapter` takes four functions, sync or async. Here `myBroker` stands for your own broker client. ```typescript import { BrokerRejected, GenericAdapter } from "retrade"; const broker = new GenericAdapter({ // Read the account now, from the broker, never from the agent. // Amounts and prices are decimal strings (or bigint), never number: // String(x) converts one. async snapshot(instrument, adjustmentsSince) { const account = await myBroker.account(); const snapshot = { asOf: new Date(), cash: { USD: String(account.cash) }, settledCash: { USD: String(account.settledCash) }, positions: account.positions.map((p) => ({ instrument: p.symbol, quantity: p.quantity, mark: String(p.mark), })), openOrders: account.openOrders.map((o) => ({ brokerOrderId: o.id, tag: o.clientOrderId || null, instrument: o.symbol, side: o.side, remainingQty: o.remaining, type: o.type, tif: o.tif, limitPrice: o.limitPrice === null ? null : String(o.limitPrice), })), }; if (instrument === null) return { snapshot, quote: null }; const q = await myBroker.quote(instrument); const quote = { instrument, asOf: q.time, last: String(q.last), bid: String(q.bid), ask: String(q.ask), }; return { snapshot, quote }; }, // Place exactly these fields, with the tag as the client order id. async submit(order, tag) { try { return await myBroker.place(order, { clientOrderId: tag }); } catch (error) { if (error instanceof myBroker.Refused) throw new BrokerRejected(String(error)); throw error; // anything else: the outcome is unknown } }, async find(tag) { const found = await myBroker.orderByClientId(tag); return found ? found.id : null; }, async cancel(brokerOrderId) { await myBroker.cancel(brokerOrderId); }, gSeconds: 60, // must equal the account's setting at reTrade convertsCash: false, // likewise }); ``` What each function must do: | Function | Rules | |---|---| | `snapshot` | Read the account now and return `{ snapshot, quote }`: the account with settled cash, and a quote when `instrument` isn’t null. Include the USDCAD rate (`fx`) when the broker has one, and the deposits, withdrawals, dividends, fees, transfers and splits since `adjustmentsSince`. | | `submit` | Place the `ApprovedOrder` exactly, with `tag` as the client order id, and return the broker order id. Throw `BrokerRejected` only when the broker definitely refused it; anything else you throw means the outcome is unknown. The SDK never retries a submit. | | `find` | Look the order up by `tag`, or, where the broker doesn’t keep tags, match the order’s fields among orders placed since `since`. `null` never means “not placed”: the broker may not show it yet. | | `cancel` | Return once the broker has accepted the cancel; throw if it refused or the outcome is unknown. | `gSeconds` (60 by default) is how long the broker takes to show a new order: a whole number of seconds from 1 to 3,600 (`MAX_G_SECONDS`). `convertsCash` (false by default) says whether the broker converts currency on its own. Both must match the account’s settings at reTrade, or `place` throws `ConfigError`. > For a market buy of a stock the account doesn’t hold, include a position of 0 shares with its current mark. Without one, a buy whose rules count that order is BLOCK `STATE-INCOMPLETE`. ## Place an order ```typescript const order = { instrument: "XNAS:AAPL", side: "buy", quantity: 40, type: "limit", tif: "day", limitPrice: "187.25", } as const; const result = await rt.place(order, { account: "A1", agent: "agt_momentum", via: broker, }); ``` The instrument is an exact listing id, `:`, such as `XNAS:AAPL`. Quantity is a whole number. Prices are decimal strings (or bigint), never a JavaScript number. Times are `Date`s or RFC 3339 strings. A limit order needs `limitPrice`; a market order can’t have one. An optional `stopPrice`, the exit level the `written_exit` rule reads, must be above 0, and isn’t sent to the broker. One call does the whole round: it reads the account and a quote through your adapter, asks reTrade, checks the signed answer, places what was approved, reports it and keeps the evidence. ## Handle the result `place` resolves to a `Result`. An answer is never an exception: a BLOCK, and reTrade not answering, both come back as a result. Quantities are strings, as on the wire. Check `placement` first, because in shadow mode the agent’s order is placed whatever the answer. ```typescript import type { Result } from "retrade"; function reportToAgent(result: Result) { if (result.placement === "unknown") { // The broker may have the order: never place it again. return { status: "unknown", why: result.reason }; } if (result.placement === "placed") { // On a CUT, quantity is smaller than the agent asked for. return { status: "placed", quantity: result.quantity, outcome: result.outcome }; } if (result.outcome === "BLOCK") { // Nothing was placed. Over a dollar line, fits is the largest quantity // that would pass: the agent can resize, re-plan or ask its user. const why = result.deciding ?? result.reason; return { status: "blocked", why, fits: result.fits }; } // An ALLOW or CUT that wasn't placed: expired, stopped or rejected. return { status: "not placed", why: result.reason }; } ``` To resize, send the order again at `fits`, once. It’s a new decision, so it counts toward the agent’s orders-per-minute limit, as the BLOCK did: ```typescript const resizable = result.outcome === "BLOCK" && result.placement === "not_placed" && result.fits !== null && result.fits !== "0"; if (resizable) { result = await rt.place({ ...order, quantity: Number(result.fits) }, { account: "A1", agent: "agt_momentum", via: broker, }); } ``` The Result type, as the package declares it: ```typescript type Result = { outcome: "ALLOW" | "CUT" | "BLOCK"; placement: "placed" | "not_placed" | "unknown"; quantity: string | null; // the approved quantity; null for a BLOCK requestedQuantity: string; deciding: Ref | null; // null for ALLOW and local results fits: string | null; // on a BLOCK over a line or in an agent conflict flags: Ref[]; // empty: over a line the answer is BLOCK alerts: unknown[]; reason: string | null; // the SDK's own reason decisionId: string | null; // null only when the SDK decided on its own brokerOrderId: string | null; local: boolean; // decided without a verified answer claimable: boolean; // a placed enforce decision; promises no payment }; type Ref = { code: string } | { code: "approval_line"; reason: LineReason }; type LineReason = "hard_cap_day" | "per_order" | "per_agent_day" | "per_day" | "openings_pct"; ``` ## When reTrade doesn’t answer reTrade has 2.5 seconds in total, including one retry with the same bytes and key. After that, in enforce and test mode, the SDK blocks the order itself (in shadow mode it places the original order, with reason `shadow-unreachable`): ```typescript result.outcome // "BLOCK" result.placement // "not_placed" result.reason // "unreachable" result.deciding // null: no answer from reTrade decided this result.local // true result.claimable // false ``` An answer that fails a check gets reason `unverified` instead, and nothing is placed or reported. See [Fail-closed behaviour](https://retrade.app/docs/concepts/fail-closed.md), including exits-only. ## After the order ```typescript // There's no modify: replace cancels, then asks for a new decision. const replaced = await rt.replace(brokerOrderId, newOrder, { account: "A1", agent: "agt_momentum", via: broker, }); // Anyone may stop: an executor key can engage the kill switch. // Only an admin key releases it (retrade-admin release). const stopId = await rt.stop("tenant", { reason: "agent misbehaving" }); // Cancel the working orders that carry a reTrade tag. const cancelled = await rt.cancelOpenOrders(broker); // Reports that didn't reach reTrade are kept; send them again later. const sent = await rt.sendPendingReports(); ``` The SDK sends the submission report itself. One that doesn’t reach reTrade is kept, and `rt.sendPendingReports()` sends it again and resolves to how many went. `rt.stop` takes `"tenant"`, `"account"` (with `account`) or `"agent"` (with `agent`), and a reason of up to 500 characters, and resolves to the stop’s id. `rt.cancelOpenOrders(broker, { allOrders: true })` cancels every working order, for a broker that drops tags; failures come back together as an `AggregateError`. A `placement` of `"unknown"` means the submit failed in a way that doesn’t say whether the broker took the order, and `find` couldn’t see it. Don’t place it again: the decision keeps counting until the account data shows the order or its absence. ## Read your log ```typescript const records = await rt.log(); // the whole log, from record 1 const first20 = await rt.log({ from: 1, to: 20 }); // records 1 to 20 for (const record of first20) console.log(record.seq, record.kind, record.hash); ``` Each record is a `LogRecord` (`seq`, `kind`, `hash`, `token`, `payload`), checked against the key list and chained to the one before it. ## The claim kit ```typescript if (result.decisionId !== null) { // null only when the SDK decided on its own const evidence = rt.claimKit(result.decisionId); // { format: "retrade-claim-kit/1", decision_id, request, decision, // policy, report, receipt, heads } } ``` ```shell npx retrade claim-kit --state-dir retrade-state ``` Both give one decision’s evidence, in the format `retrade-claim-kit/1`, from `retrade-state/claim-kit.sqlite3`: the same file and layout the Python SDK writes, so one checker reads both. Keep it for at least 90 days. See [Receipts and verification](https://retrade.app/docs/concepts/receipts.md). ## Errors Every error the SDK throws is a `ReTradeError`. They’re for your code to fix, never an answer about an order. The TypeScript SDK’s errors: | Error | When | |---|---| | `InvalidInput` | A value the SDK can’t send, such as a number price or a malformed order. | | `ConfigError` | A malformed key id, root key or server address, or an adapter whose settings differ from the account’s. | | `RequestRefused` | reTrade refused a request as malformed or unauthorised: a 4xx other than 429. It has `status` and `detail`. | | `KeyFileError` | The key file can’t be created or used. | | `VerificationFailed` | Something claiming to be from reTrade, such as the key list or the status, failed a check, outside `place`. | | `Unreachable` | No usable answer in time, from a call other than `place`, which blocks the order instead. | | `BrokerRejected` | Thrown by your adapter’s `submit` when the broker definitely refused the order. | Fixes for the common ones are in [Errors and troubleshooting](https://retrade.app/docs/errors.md). --- # API reference > Every SDK call, the fields it takes and returns, and the HTTP endpoints it uses on your behalf. Status: In development. Not released yet; names and formats may change. Web page: https://retrade.app/docs/reference ## Client methods Python names first, then TypeScript. Every TypeScript method that talks to reTrade returns a promise. The client’s methods: | Python | TypeScript | What it does | |---|---|---| | `retrade.Client(key_id, key_path, *, root_key, base_url=None, state_dir="retrade-state")` | `Client.open({ keyId, keyPath, rootKey, baseUrl, stateDir, logger })` | Load the key, open the claim kit, and fetch and check the key list and the status. | | `rt.place(order, *, account, agent, via)` | `rt.place(order, { account, agent, via })` | Check one order and act on the answer. Returns a Result. | | `rt.replace(broker_order_id, order, *, account, agent, via)` | `rt.replace(brokerOrderId, order, { account, agent, via })` | Cancel a working order, then decide on the new one. If the cancel fails, nothing new is placed. | | `rt.stop(scope, *, account=None, agent=None, reason)` | `rt.stop(scope, { account, agent, reason })` | Engage a stop for the `tenant`, an `account` or an `agent`. Returns the stop id. | | `rt.cancel_open_orders(via, *, all_orders=False)` | `rt.cancelOpenOrders(via, { allOrders })` | Cancel the working orders that carry a reTrade tag, or all of them. Returns the ids cancelled. | | `rt.send_pending_reports()` | `rt.sendPendingReports()` | Resend submission reports that didn’t reach reTrade. Returns how many went. | | `rt.refresh()` | `rt.refresh()` | Fetch and check the key list and the status again. | | `rt.log(*, from_seq=None, to_seq=None)` | `rt.log({ from, to })` | Read your log, each record checked and chained. Returns a list of LogRecords. | | `rt.claim_kit(decision_id)` | `rt.claimKit(decisionId)` | One decision’s evidence, or `None` (`null`). | | `rt.send_snapshot(account, via)` | `rt.sendSnapshot(account, via)` | Send an account’s snapshot with no order. Not built on the hosted service yet. | | `rt.keep_fresh(adapters, *, every_s=300)` | `rt.keepFresh(adapters, { everyMs })` | Every 5 minutes until close, refresh the status and send each account’s snapshot. Snapshots aren’t built on the hosted service yet. | | `rt.close()`, or a `with` block | `rt.close()` | Stop background work and close the connection and the claim kit. | | `retrade.generate_key(path)` | `generateKey(path)` | Write a new executor key and return its public half. | | `retrade.load_key(path)` | `loadKey(path)` | Load an executor key file. | | `retrade.verify_webhook(secret, timestamp, body, signature)` | None | Check a webhook in its planned format. reTrade doesn’t send webhooks yet. | `rt.mode` is the client’s mode, and `rt.base_url` (`rt.baseUrl`) the address it talks to. `DEFAULT_BASE_URL` is `https://api.retrade.app`. ## The command line The retrade command (npx retrade, for the TypeScript package): | Command | What it does | |---|---| | `retrade keygen PATH` | Write a new executor key, never over an existing file, and print its public half. | | `retrade claim-kit DECISION_ID --state-dir DIR` | Print one decision’s evidence as JSON. `DIR` is `retrade-state` by default. | Your admins have their own command line, `retrade-admin`, for limits, modes, keys and stops. It’s in development. ## Orders An order’s fields: | Field | Python | TypeScript | Rules | |---|---|---|---| | Instrument | `instrument` | `instrument` | An exact listing id, `:`, such as `XNAS:AAPL`; at most 128 characters. | | Side | `Side.BUY`, `Side.SELL` | `"buy"`, `"sell"` | | | Quantity | `quantity` | `quantity` | A whole number, at least 1, at most 15 digits. | | Type | `OrderType.LIMIT`, `OrderType.MARKET` | `"limit"`, `"market"` | | | Time in force | `Tif.DAY`, `Tif.IOC`, `Tif.GTC` | `"day"`, `"ioc"`, `"gtc"` | | | Limit price | `limit_price` | `limitPrice` | Required on a limit order, not allowed on a market order. Above 0. | | Stop price | `stop_price` | `stopPrice` | Optional: the exit level `written_exit` reads. Above 0. Not sent to the broker. | Amounts are exact: `Decimal` or `int` in Python, decimal strings or bigints in TypeScript, never floats or JavaScript numbers. At most 15 digits before the point and 10 after it. In Python the enums’ plain strings (`"buy"`) work too. ## Account data What your adapter’s `snapshot` function returns, and what `submit` is given. The account’s types and their fields: | Type | Python fields | TypeScript fields | |---|---|---| | `Snapshot` | `as_of`, `cash`, `settled_cash`, `positions`, `open_orders`, `fx=None`, `adjustments=()` | `asOf`, `cash`, `settledCash`, `positions`, `openOrders`, `fx?`, `adjustments?` | | `Quote` | `instrument`, `last`, `bid`, `ask`, `as_of` | `instrument`, `last`, `bid`, `ask`, `asOf` | | `Position` | `instrument`, `quantity`, `mark` | `instrument`, `quantity`, `mark` | | `OpenOrder` | `broker_order_id`, `tag`, `instrument`, `side`, `remaining_qty`, `type`, `tif`, `limit_price` | `brokerOrderId`, `tag`, `instrument`, `side`, `remainingQty`, `type`, `tif`, `limitPrice` | | `Fx` | `rate` (USDCAD: CAD per 1 USD), `as_of` | `rate`, `asOf` | | `Adjustment` | `kind`, `amount`, `currency`, `instrument`, `quantity` | `kind`, `amount`, `currency`, `instrument`, `quantity` | | `ApprovedOrder` | `instrument`, `side`, `quantity`, `type`, `tif`, `limit_price` | `instrument`, `side`, `quantity`, `type`, `tif`, `limitPrice` | - `cash` and `settled_cash` map a currency to an amount; a missing currency counts as 0. - Times are timezone-aware `datetime`s in Python, and `Date`s or RFC 3339 strings with an offset in TypeScript. - An open order’s `tag` is the broker’s client order id, or `None` (`null`) when it has none; its `tif` may be `None` when the broker doesn’t report it. - An adjustment’s `kind` is `deposit`, `dividend`, `withdrawal`, `fee`, `transfer_in`, `transfer_out` or `split`. Cash kinds carry an amount and a currency; transfers and splits carry an instrument and a quantity. ## Result What place returns: | Python | TypeScript | What it holds | |---|---|---| | `outcome` | `outcome` | ALLOW, CUT or BLOCK: reTrade’s, or the SDK’s own when `local` is true. | | `placement` | `placement` | `placed`, `not_placed` or `unknown`. | | `quantity` | `quantity` | The approved quantity, or what the SDK placed on its own. None for a BLOCK. | | `requested_quantity` | `requestedQuantity` | The order’s own quantity. | | `deciding` | `deciding` | reTrade’s deciding rule, verbatim. None for an ALLOW and for every local result. | | `fits` | `fits` | On a BLOCK over a dollar line or in an agent conflict, the largest quantity that would pass. | | `flags` | `flags` | Empty: over a dollar line the answer is BLOCK. | | `alerts` | `alerts` | The decision’s alerts, passed through. | | `reason` | `reason` | The SDK’s own reason. See [Reason codes](https://retrade.app/docs/reference/reason-codes.md#the-sdks-own-reasons). | | `decision_id` | `decisionId` | The decision’s id: the receipt id to store. | | `broker_order_id` | `brokerOrderId` | The broker’s id for the order, when one was placed. | | `local` | `local` | The SDK decided without a verified answer from reTrade. | | `claimable` | `claimable` | A placed enforce-mode decision: evidence an insurer can review. It promises no payment. | Python quantities are `int`; TypeScript quantities are strings, as on the wire. ## HTTP endpoints The SDK calls these for you, at `https://api.retrade.app` by default. It signs every request; `GET /v1/keys` and `GET /healthz` also answer unsigned ones. reTrade’s endpoints: | Endpoint | What it does | |---|---| | `GET /healthz` | Answers `{"ok":true}` when the service is up. The SDK doesn’t call it. | | `GET /v1/keys` | The list of decision keys, signed by the root key: `{"keys": ""}`. | | `GET /v1/status` | The latest signed log head and the signed status: the stops in force and each account’s settings. | | `POST /v1/decisions` | Ask about one order. Answers `{"decision": ""}`. | | `POST /v1/decisions/{decision_id}/submission` | Report what happened at the broker. Answers with a signed receipt. | | `GET /v1/policies/{policy_hash}` | The rules document a decision was made under. | | `POST /v1/stops` | Engage a stop. Answers with the stop’s id and a signed receipt. | | `GET /v1/log?from=N&to=M` | Your log’s records, a page at a time. | | `POST /v1/snapshots` | An account’s snapshot with no order. Not built yet: it answers 404. | ## Request signing Every signed request carries `Retrade-Key-Id`, `Retrade-Timestamp`, an `Idempotency-Key` on a POST, and `Retrade-Signature`: base64url, without padding, of an Ed25519 signature over five lines. reTrade accepts a timestamp within 60 seconds of its own clock. The five signed lines of a GET request: ``` GET /v1/log?from=1&to=2 2026-10-22T14:03:09Z e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ``` In order: the method, the full path with its query, the timestamp, the idempotency key (empty for a GET) and the lowercase hex SHA-256 of the body (here of an empty body). The same idempotency key with the same body gets the stored answer back, byte for byte; with a different body it’s refused. ## The decision request `POST /v1/decisions` carries the order, a quote and the account data your adapter read, in one JSON body. Numbers are strings, so nothing is rounded on the way. The SDK keeps these exact bytes, because reTrade’s answer is bound to their SHA-256. A request body, laid out for reading: ```json {"account_ref": "A1", "agent_id": "agt_m", "order": {"instrument": "XNAS:AAPL", "side": "buy", "quantity": "40", "type": "limit", "limit_price": "187.25", "tif": "day", "intent": "increase"}, "quote": {"instrument": "XNAS:AAPL", "last": "187.10", "bid": "187.08", "ask": "187.12", "as_of": "2026-10-22T14:03:06.415Z"}, "snapshot": {"as_of": "2026-10-22T14:03:05.415Z", "cash": {"USD": "38380.90"}, "settled_cash": {"USD": "38380.90"}, "positions": [ {"instrument": "XNAS:AAPL", "quantity": "81", "mark": "187.10"}, {"instrument": "ARCX:VTI", "quantity": "160", "mark": "290.40"}], "open_orders": []}} ``` The SDK works out `intent` itself: a buy is `open` when the account holds none of the stock and `increase` otherwise, and a sell is `reduce`. A stop price goes as `"exit": {"stop_price": …}`. There’s no approval field: nothing is ever approved by a person. ## The signed decision reTrade answers `{"decision": ""}`, signed with Ed25519 by the day’s decision key. Its header is `alg` `EdDSA`, a `kid` and `typ` `retrade-decision`. The payload: A decision’s payload, with its context left out: ```json {"decision_id": "01JB8Z6Q4M3T9V2K7X5R1C0N8D", "tenant_id": "T1", "mode": "enforce", "account_ref": "A1", "agent_id": "agt_m", "outcome": "BLOCK", "quantity": null, "deciding": {"code": "approval_line", "reason": "per_order"}, "fired": [{"code": "approval_line", "reason": "per_order"}, {"code": "approval_line", "reason": "per_day"}], "rooms": {"cash_funding": "204"}, "fits": "26", "opening": false, "p": "187.25", "line_value": "7490", "expires": null, "accepts": {"snapshot": true, "mark": true, "rate": false}, "switch_off_agent": false, "links": [], "alerts": [], "value": "100000", "requested_quantity": "40", "order": {"instrument": "XNAS:AAPL", "side": "buy", "type": "limit", "tif": "day", "limit_price": "187.25"}, "request_sha256": "a7d194e7a02eebf02e40d2eaaeb6c5d4b9619cab31913abbb2b765ecf2303fbc", "policy_hash": "5639cfbad8cb84092c15c01832cd535606538cde4f9c798ca1da43df4f4adf07", "policy_epoch": "1", "master_sha256": "a58c17ac155efdc751b4f028db283d14e889a67425ed21ebd2b15c7e25078f32", "engine_version": "retrade-engine/0.2.0", "spec_version": "retrade-arith/2.0-draft", "evaluated_at": "2026-10-22T14:03:07.415Z", "seq": "1", "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000", "claimable": true} ``` An ALLOW or CUT carries `quantity` and `expires`, the end of its life: 30 seconds by default, and an admin can set it from 5 to 60. A BLOCK has neither. The full payload also carries `context`, the data reTrade decided on. `claimable` is true for an enforce decision: it says the decision is evidence an insurer can review, and promises no payment. The SDK ignores members it doesn’t know. ## The submission report After the broker call, the SDK sends `POST /v1/decisions/{decision_id}/submission` with one of two bodies, and the decision id as its idempotency key. reTrade answers with a signed receipt. The two kinds of report: ```json {"kind": "placed", "broker_order_id": "...", "submitted_at": "