Getting started with Trading API
By the end you've minted a token, bought a small amount of SOL, and found the fill in your transactions. It takes about 10 minutes, plus the time your USDC transfer takes.
We don't have a fully working sandbox yet, so every trade here is a real on-chain transaction. Keep the amounts small.
Before you begin
You need the key file from Create your API key. It does two jobs: it signs you in, and it signs your own orders.
The code needs Node.js 18 or later, or Python 3.10 or later. Each step below is a part of one script, quickstart.ts or quickstart.py, that you run at the end.
- TypeScript
- Python
npm install tsx @turnkey/api-key-stamper
export TM_KEY_FILE=path/to/your-api-key.json
pip install requests cryptography turnkey-api-key-stamper
export TM_KEY_FILE=path/to/your-api-key.json
1. Check you can reach the API
This call needs no key. It returns every asset you can trade. The script finds SOL on Solana in this list.
- TypeScript
- Python
- curl
const { data: assets } = await get("/v1/gateway/assets");
const sol = assets.find(
(a: any) => a.symbol === "SOL" && a.chain === "solana",
);
assets = get("/v1/gateway/assets")["data"]
sol = next(
a
for a in assets
if a["symbol"] == "SOL" and a["chain"] == "solana"
)
curl -s https://api.truemarkets.co/v1/gateway/assets
{
"data": [
{
"id": "495e07ac-fa3e-4179-85b7-b1e8cece3dc4",
"symbol": "SOL",
"chain": "solana",
"venue": "defi",
"…": "…"
}
]
}
2. Mint a token
The script signs the string {key_id}.{timestamp} with your key file and exchanges it for a token. The timestamp is Unix seconds and must be within 30 seconds of ours. Send access_token in Authorization: Bearer on every call. It lasts an hour, and refresh_token gets you a new pair without signing again.
The same key file signs your orders later, so the script turns it into the hex keys the Turnkey stamper takes.
- TypeScript
- Python
// One key file does both jobs: it signs the token challenge, and,
// because the app registered it on your wallet, your payloads.
const { key_id, private_key: jwk } = JSON.parse(
readFileSync(KEY_FILE, "utf8"),
);
const y = Buffer.from(jwk.y, "base64url");
const stamper = new ApiKeyStamper({
// The compressed public key and the private key, as hex.
apiPublicKey: Buffer.concat([
Buffer.from([y[31] % 2 === 0 ? 2 : 3]),
Buffer.from(jwk.x, "base64url"),
]).toString("hex"),
apiPrivateKey: Buffer.from(jwk.d, "base64url").toString("hex"),
});
const stamp = async (payload: string) =>
(await stamper.stamp(payload)).stampHeaderValue;
# One key file does both jobs: it signs the token challenge, and,
# because the app registered it on your wallet, your payloads.
with open(KEY_FILE) as f:
key_file = json.load(f)
key_id, jwk = key_file["key_id"], key_file["private_key"]
y = b64url_bytes(jwk["y"])
stamper = ApiKeyStamper(
ApiKeyStamperConfig(
# The compressed public key and the private key, as hex.
api_public_key=(
bytes([2 if y[31] % 2 == 0 else 3])
+ b64url_bytes(jwk["x"])
).hex(),
api_private_key=b64url_bytes(jwk["d"]).hex(),
)
)
def stamp(payload):
return stamper.stamp(payload).stamp_header_value
- TypeScript
- Python
const timestamp = Math.floor(Date.now() / 1000);
const signature = sign(
"sha256",
Buffer.from(`${key_id}.${timestamp}`),
{
key: createPrivateKey({ key: jwk, format: "jwk" }),
// r and s concatenated, which the API expects
dsaEncoding: "ieee-p1363",
},
).toString("base64url");
const minted = await post("/v1/auth/api-key/token", {
key_id,
timestamp,
signature,
});
token = minted.access_token;
// minted.refresh_token gets a new pair later, without signing.
private_key = ec.EllipticCurvePrivateNumbers(
int.from_bytes(b64url_bytes(jwk["d"]), "big"),
ec.EllipticCurvePublicNumbers(
int.from_bytes(b64url_bytes(jwk["x"]), "big"),
int.from_bytes(b64url_bytes(jwk["y"]), "big"),
ec.SECP256R1(),
),
).private_key()
timestamp = int(time.time())
der = private_key.sign(
f"{key_id}.{timestamp}".encode(),
ec.ECDSA(hashes.SHA256()),
)
# The API expects r and s concatenated.
r, s = decode_dss_signature(der)
raw = r.to_bytes(32, "big") + s.to_bytes(32, "big")
signature = (
base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
)
minted = post(
"/v1/auth/api-key/token",
{
"key_id": key_id,
"timestamp": timestamp,
"signature": signature,
},
)
token = minted["access_token"]
# minted["refresh_token"] gets a new pair later, with no
# signing.
{
"access_token": "eyJhbGciOi…",
"refresh_token": "eyJhbGciOi…",
"expires_in": "2026-10-01T18:11:27Z",
"token_type": "Bearer"
}
3. Fund your wallet
The script reads your balances and waits until your Solana wallet holds enough USDC. Send about 3 USDC to it on Solana; Funding shows where to find the address. The order spends 2 of it, and we pay the network fees.
USDC sent on any other network won't reach this wallet.
- TypeScript
- Python
const usdcAvailable = async () => {
const { data } = await get("/v1/gateway/balances");
const usdc = data.find(
(b: any) => b.symbol === "USDC" && b.chain === "solana",
);
return Number(usdc?.available ?? 0);
};
if ((await usdcAvailable()) < Number(ORDER_USDC)) {
console.log("… send about 3 USDC on Solana to your wallet");
while ((await usdcAvailable()) < Number(ORDER_USDC)) {
await sleep(10_000);
}
}
def usdc_available():
balances = get("/v1/gateway/balances")["data"]
usdc = next(
(
b
for b in balances
if b["symbol"] == "USDC"
and b["chain"] == "solana"
),
None,
)
return float(usdc["available"]) if usdc else 0.0
if usdc_available() < float(ORDER_USDC):
print("… send about 3 USDC on Solana to your wallet")
while usdc_available() < float(ORDER_USDC):
time.sleep(10)
4. Place the order and execute
The script places a market buy for 2 USDC of SOL. The response carries the quote and the payloads, the unsigned wallet transactions to sign. The script signs each one with your key and executes right away. auth_type: "api_key" says your key made the signatures, rather than a passkey.
A quote lasts about two minutes, so doing it in one go keeps it from expiring. If execute still returns quote_stale, run the script again.
Execute usually returns complete. A buy that first moves funds between chains returns pending, and the script reads the order until it's final. If order_id comes back empty, no order was created, and quote.issues says why.
- TypeScript
- Python
const order = await post("/v1/gateway/orders", {
asset_id: sol.id,
qty: ORDER_USDC,
qty_unit: "quote",
side: "buy",
type: "market",
});
// An empty order_id means no order was created, and
// quote.issues says why.
if (!order.order_id) {
throw new Error(
`no order: ${JSON.stringify(order.quote?.issues)}`,
);
}
// Sign each payload and execute straight away, before the
// quote expires.
const signatures = await Promise.all(
order.payloads.map((p: any) => stamp(p.payload)),
);
const executed = await post(
`/v1/gateway/orders/${order.order_id}/execute`,
{ signatures, auth_type: "api_key" },
);
order = post(
"/v1/gateway/orders",
{
"asset_id": sol["id"],
"qty": ORDER_USDC,
"qty_unit": "quote",
"side": "buy",
"type": "market",
},
)
# An empty order_id means no order was created, and
# quote.issues says why.
if not order.get("order_id"):
issues = order.get("quote", {}).get("issues")
raise RuntimeError(f"no order: {issues}")
# Sign each payload and execute straight away, before the
# quote expires.
signatures = [stamp(p["payload"]) for p in order["payloads"]]
executed = post(
f"/v1/gateway/orders/{order['order_id']}/execute",
{"signatures": signatures, "auth_type": "api_key"},
)
{ "status": "complete" }
5. See the fill
The script reads the order back, then finds the same fill in your transactions. The row's status reads completed, the word the transactions list uses for anything finished.
- TypeScript
- Python
let filled = executed;
while (
!["complete", "canceled", "failed"].includes(filled.status)
) {
await sleep(2000);
filled = await get(`/v1/gateway/orders/${order.order_id}`);
}
console.log(
`✓ order ${filled.status} ` +
`${filled.executed_qty ?? ""} SOL for ${ORDER_USDC} USDC`,
);
const feed = await get("/v1/gateway/transactions?type=order");
const row = feed.data.find((t: any) => t.id === order.order_id);
filled = executed
while filled["status"] not in (
"complete",
"canceled",
"failed",
):
time.sleep(2)
filled = get(f"/v1/gateway/orders/{order['order_id']}")
print(
f"✓ order {filled['status']} "
f"{filled.get('executed_qty', '')} SOL "
f"for {ORDER_USDC} USDC"
)
feed = get("/v1/gateway/transactions?type=order")
row = next(
(t for t in feed["data"] if t["id"] == order["order_id"]),
None,
)
Run it
Run the script. You can run it again: each run buys another 2 USDC of SOL while your wallet has funds.
- TypeScript
- Python
npx tsx quickstart.ts
python quickstart.py
✓ token expires 2026-10-01T18:11:27Z
… send about 3 USDC on Solana to your wallet
✓ funded
✓ order complete 0.0102 SOL for 2 USDC
✓ in your transactions (completed)
Where to go next
- How requests and signing work: the two signature formats your key makes.
- Place orders: limit orders, listing, canceling and changing orders.
- Your transactions: everything you've traded and sent, in one list.
Every error response includes a request_id. Include it when you email [email protected], so we can find the request.