Orders and Signing
Placing, modifying and cancelling orders on the venue: headers, the Ed25519 signature over a borsh payload, field orders, enumerations and a working request.
Every write on the venue is Ed25519 signed and the signature is checked against the account it names. The API key identifies you; the signature proves the order came from your signer. TRUE never holds either.
The apikey_ prefix (see Keys). The base URL is https://dex-prod.truefinance.ai, a different host from the app. The key goes in Authorization, not X-Api-Key. And you sign the hex text of the digest, not the digest bytes.
Headers on every signed request
Authorization: Bearer apikey_<64 hex>
X-Signature: <hex of the ed25519 signature>
X-Signature-Timestamp: <ms, equals the timestamp inside the signed payload>
X-Signer-Pubkey: <the key that signs, base58>
X-Account-Pubkey: <the account owner, base58>
Content-Type: application/json
On API key auth a read must NOT carry X-Signer-Pubkey or X-Account-Pubkey: the key already pins both identities, and sending them is rejected as ApiKeyIdentityConflict unless they byte match. Signed writes still carry them.
The timestamp is the last field of the borsh payload you sign and must equal X-Signature-Timestamp. That equality is the replay guard; a mismatch is rejected even when the signature is correct. It does not go in the JSON body.
The signature
message = sha256( "true-trading" + ":place_order" + accountOwnerPubkey[32 raw bytes] + borsh(payload) )
signature = ed25519_sign( utf8( lowercase_hex(message) ) )
Commands: :place_order :cancel_order :modify_order :place_batch_orders :cancel_batch_orders.
Enumerations
| Field | Values |
|---|---|
| side | BUY 0 SELL 1 |
| order_type | MARKET 0 LIMIT 1 TAKE_PROFIT_MARKET 5 STOP_LOSS_MARKET 7 |
| flags (bitfield) | POST_ONLY 1 REDUCE_ONLY 2 |
| tif | GTC 0 IOC 1 FOK 2 |
| trigger_source | LastPrice 0 MarkPrice 1 |
place_order, borsh field order
Borsh carries no field names: a field in the wrong position is a different message.
| # | Field | Type | Note |
|---|---|---|---|
| 1 | market | string | BTCUSDC |
| 2 | side | u8 | BUY 0, SELL 1 |
| 3 | size | string | decimal as text |
| 4 | price | Option<string> | null for market orders |
| 5 | order_type | u8 | see above |
| 6 | flags | u8 | bitfield |
| 7 | tif | u8 | GTC, IOC, FOK |
| 8 | trigger_price | Option<string> | stops and take profits |
| 9 | trigger_source | Option<u8> | 0x01 0x01 for MarkPrice, 0x00 for none |
| 10 | timestamp | u64 LE | milliseconds, equals the header |
Sizes and prices are strings. Whatever decimal formatting you sign has to be the byte identical string you send, so format the value once and reuse it. The JSON body uses time_in_force, not tif, and carries no timestamp.
modify_order, borsh field order
order_id u64
market str
side u8
order_type u8
size str
price Option<str> 0x00, or 0x01 then the string
timestamp u64 == X-Signature-Timestamp
This is not the place_order layout with an id in front. Mirroring place_order produces “Invalid signature”.
A working request
import crypto from "node:crypto";
// borsh writer, only what place_order needs
const buf = [];
const u8 = n => buf.push(n & 0xff);
const u32 = n => { for (let i = 0; i < 4; i++) buf.push((n >> (8 * i)) & 0xff); };
const u64 = n => { let v = BigInt(n); for (let i = 0; i < 8; i++) { buf.push(Number(v & 0xffn)); v >>= 8n; } };
const str = s => { const b = new TextEncoder().encode(s); u32(b.length); b.forEach(x => buf.push(x)); };
const opt = s => { if (s == null) u8(0); else { u8(1); str(s); } };
const ts = Date.now();
str("BTCUSDC"); u8(0); str("0.01"); opt("60000");
u8(1); u8(0); u8(0); opt(null); u8(0); u64(ts);
const owner = base58decode(ACCOUNT_PUBKEY); // 32 bytes
const prefix = new TextEncoder().encode("true-trading:place_order");
const message = Buffer.concat([Buffer.from(prefix), Buffer.from(owner), Buffer.from(Uint8Array.from(buf))]);
const hex = crypto.createHash("sha256").update(message).digest("hex");
// sign the HEX TEXT, not the digest bytes
const pkcs8 = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from(SIGNER_SEED_32)]);
const key = crypto.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" });
const sig = crypto.sign(null, Buffer.from(hex), key);
const DEX = "https://dex-prod.truefinance.ai";
await fetch(DEX + "/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer apikey_" + API_KEY, // prefix REQUIRED
"X-Signature": sig.toString("hex"),
"X-Signature-Timestamp": String(ts),
"X-Signer-Pubkey": SIGNER_PUBKEY,
"X-Account-Pubkey": ACCOUNT_PUBKEY,
"Content-Type": "application/json",
},
// time_in_force, NOT tif. No timestamp field: it is signed, not sent.
body: JSON.stringify({ market: "BTCUSDC", side: "BUY", size: "0.01", price: "60000", order_type: "LIMIT", time_in_force: "GTC" }),
});
// A read, to check auth before signing anything. NO pubkey headers here.
await fetch(DEX + "/v1/accounts", { headers: { "Authorization": "Bearer apikey_" + API_KEY } });
Account reads
GET /v1/accounts GET /v1/positions GET /v1/orders GET /v1/orders/{id} GET /v1/transfers/search?account=<pubkey> All with the API key only. The transfers ledger carries deposits and withdrawals with their on chain transaction hashes.
Treat any write without a 2xx within 5 s as unknown. Read GET /v1/orders before retrying, so a retry never doubles an order.