This quickstart takes you from a new account to your first limit order on Testnet in about ten minutes. You create an API key, get test funds, then place, read, and cancel one order with an official SDK.
Testnet uses test Assets with no monetary value. Its API host is api.testnet.polyester.com and its
trade host is trade.api.testnet.polyester.com. Mainnet is coming soon. See Environments and hosts.
Create an account and an API key
An API key lets your code act for your account. You keep the private key; Polyester stores only the public key.
Create your account
Sign up on Testnet as described in Create your account.
Create a key
Open API Keys, start a new key for your Main Account, and choose Generate in browser. Copy the private key, 64 hexadecimal characters, into your secret store now: it is shown only once.
Choose its access
Allow Spot Trading with order placement for the Pair you will test, and allow Balances.
Then save the Key ID, which starts with ak_.
Export both values in the terminal where you run the examples:
export POLYESTER_API_KEY_ID="ak_..."
export POLYESTER_API_PRIVATE_KEY="<64 hex characters>"The key screens are described in API keys, and request signing in Ed25519 API keys.
Get test funds
A buy order needs the quote Asset in your Unified Trading Account, such as USDT for BTC-USDT. Two routes:
- Daily claim: after you connect your social accounts, claim paper Assets once a day from the pop-up on your dashboard. They go straight to your Unified Trading Account. See the Testnet questions in the FAQ.
- Faucets: request an Asset from a Testnet faucet, then deposit it. If it arrives in Funding, move it to Trading with an internal transfer.
The claim is a signed-in action in the app, so API keys cannot claim. To claim from code with a session, test deposits from other Networks, or fund several Subaccounts, see Testnet funds.
Place your first order with an SDK
Each program reads the Pair's trading rules, places a post-only limit buy, reads the order back, and cancels it. A post-only order is rejected rather than filled if it would trade at once. Set the price a few percent below the best bid and a multiple of the tick size, and the quantity so that price times quantity meets the minimum notional.
Each SDK's own quickstart goes further: TypeScript, Python, Go, and Rust.
Install with npm install @polyester/sdk, save as quickstart.ts in an ES module project, and run node quickstart.ts (Node.js 22.18 or later) or bun quickstart.ts.
import { PolyesterClient, POLYESTER_TESTNET_ENVIRONMENT, evmHexToBytes } from "@polyester/sdk";
const SYMBOL = "BTC-USDT";
const PRICE = "60000"; // example value: set it below the best bid
const QTY = "0.001";
const client = new PolyesterClient({
environment: POLYESTER_TESTNET_ENVIRONMENT,
auth: {
kind: "api-key-ed25519",
getKeyId: () => process.env.POLYESTER_API_KEY_ID ?? null,
getSecretKey: () => evmHexToBytes(`0x${process.env.POLYESTER_API_PRIVATE_KEY}`),
},
});
// 1. The Pair's trading rules
await client.catalog.ensureReady();
const symbolId = client.catalog.market.requireSymbolIdByPairSymbol(SYMBOL);
const { status, tickSize, stepSize, minQtyBase, minNotionalQuote } =
client.catalog.orders.getSpotOrderConstraints(SYMBOL);
console.log({ status, tickSize, stepSize, minQtyBase, minNotionalQuote });
// 2. Place a post-only limit buy
const created = await client.orders.create({
symbolId,
side: "buy",
qty: QTY,
execution: { type: "limit_gtc", price: PRICE, postOnly: true },
});
console.log("created", created.orderId);
// 3. Read it back, then cancel it
await new Promise((resolve) => setTimeout(resolve, 1000));
const details = await client.orders.getDetails({
orderId: created.orderId,
includeExecutionHistory: false,
});
console.log("status", details?.order.status);
await client.orders.cancel({ orderId: created.orderId, symbolId });Install with pip install polyester-sdk (Python 3.11 or later), save as quickstart.py, and run python quickstart.py.
import asyncio
import os
from polyester import AsyncPolyester, POLYESTER_TESTNET_ENVIRONMENT
from polyester.models import OrderId
SYMBOL = "BTC-USDT"
PRICE = "60000" # example value: set it below the best bid
QTY = "0.001"
async def main() -> None:
async with AsyncPolyester(
environment=POLYESTER_TESTNET_ENVIRONMENT,
api_key_id=os.environ["POLYESTER_API_KEY_ID"],
api_private_key=os.environ["POLYESTER_API_PRIVATE_KEY"],
) as client:
await client.wait_for_catalogs()
# 1. The Pair's trading rules
config = await client.market_data.get_spot_config()
pair = next(p for p in config.raw["pairs"] if p["symbol"] == SYMBOL)
rules = ("status", "tickSize", "stepSize", "minQtyBase", "minNotionalQuote")
print({rule: pair[rule] for rule in rules})
# 2. Place a post-only limit buy
created = await client.orders.create(
symbol=SYMBOL, side="buy", order_type="limit", tif="gtc",
qty=QTY, price=PRICE, post_only=True,
)
print("created", created.order_id)
# 3. Read it back, then cancel it
await asyncio.sleep(1)
key = OrderId(created.order_id)
details = await client.orders.get(key=key, include_execution_history=False)
print("status", details.order.status if details.order else None)
await client.orders.cancel(key=key, symbol=SYMBOL)
asyncio.run(main())Install with go get github.com/Fabric-Labs/polyester-sdk-go; module access and version pinning
are on the Go installation page. Save as main.go and run go run ..
package main
import (
"context"
"fmt"
"log"
"os"
"time"
polyester "github.com/Fabric-Labs/polyester-sdk-go"
"github.com/Fabric-Labs/polyester-sdk-go/models"
)
const (
symbol = "BTC-USDT"
limitPrice = "60000" // example value: set it below the best bid
qty = "0.001"
)
func main() {
check := func(err error) {
if err != nil {
log.Fatal(err)
}
}
ctx := context.Background()
client, err := polyester.New(polyester.Config{
Environment: &polyester.TestnetEnvironment,
APIKeyID: os.Getenv("POLYESTER_API_KEY_ID"),
APIPrivateKey: os.Getenv("POLYESTER_API_PRIVATE_KEY"),
HydrateCatalogs: true,
})
check(err)
defer client.Close()
check(client.WaitForCatalogs(ctx))
// 1. The Pair's trading rules
cfg, err := client.MarketData.GetSpotConfig(ctx)
check(err)
for _, p := range cfg.Pairs {
if p.Symbol == symbol {
fmt.Println(p.Status, p.TickSize, p.StepSize, p.MinQtyBase, p.MinNotionalQuote)
}
}
// 2. Place a post-only limit buy
sym, tif := symbol, "gtc"
price := models.PriceFromDecimal(limitPrice)
created, err := client.Orders.Create(ctx, models.CreateOrderRequest{
Symbol: &sym, Side: "buy", OrderType: "limit", TIF: &tif,
Qty: models.QtyFromDecimal(qty), Price: &price, PostOnly: true,
}, nil)
check(err)
fmt.Println("created", created.OrderID)
// 3. Read it back, then cancel it
time.Sleep(time.Second)
key := models.OrderKeyByID(created.OrderID)
details, err := client.Orders.Get(ctx, nil, key, nil, false, false)
check(err)
if details.Order != nil {
fmt.Println("status", details.Order.Status)
}
_, err = client.Orders.Cancel(ctx, nil, key, &sym, nil, nil)
check(err)
}Install with cargo add polyester-sdk and cargo add tokio --features full. The package is polyester-sdk, and code imports it as polyester. Put this in src/main.rs and run cargo run.
use std::time::Duration;
use polyester::models::{
CreateOrderParams, CreateOrderType, CreateSide, CreateTimeInForce, OrderKey,
};
use polyester::{Client, Config, Error, Price, Quantity};
const SYMBOL: &str = "BTC-USDT";
const PRICE: &str = "60000"; // example value: set it below the best bid
const QTY: &str = "0.001";
#[tokio::main]
async fn main() -> polyester::Result<()> {
let client = Client::new(Config {
environment: Some(polyester::POLYESTER_TESTNET_ENVIRONMENT.clone()),
api_key_id: std::env::var("POLYESTER_API_KEY_ID").ok(),
api_private_key: std::env::var("POLYESTER_API_PRIVATE_KEY").ok(),
..Default::default()
})?;
client.wait_for_catalogs().await?;
// 1. The Pair's trading rules
let config = client.market_data.get_spot_config().await?;
for p in config.raw["pairs"].as_array().into_iter().flatten() {
if p["symbol"] == SYMBOL {
for rule in ["status", "tickSize", "stepSize", "minQtyBase", "minNotionalQuote"] {
println!("{rule}: {}", p[rule]);
}
}
}
// 2. Place a post-only limit buy
let scale = client
.catalogs
.base_quantity_scale_for_symbol(SYMBOL)
.ok_or_else(|| Error::validation("unknown symbol"))?;
let mut order = CreateOrderParams::new(SYMBOL, CreateSide::Buy, CreateOrderType::Limit);
order.quantity = Some(Quantity::from_decimal_str(QTY, scale, None, None)?);
order.price = Some(Price::from_decimal_str(PRICE, None)?);
order.time_in_force = Some(CreateTimeInForce::Gtc);
order.post_only = Some(true);
let created = client.orders.create(order).await?;
println!("created {}", created.order_id);
// 3. Read it back, then cancel it
tokio::time::sleep(Duration::from_secs(1)).await;
let key = OrderKey::OrderId(created.order_id.clone());
let details = client.orders.get(key, None).await?;
println!("status {:?}", details.order.map(|o| o.status));
client.orders.cancel_by_order_id(&created.order_id, None).await?;
Ok(())
}The read shows working once the order rests on the book. A cancel is acknowledged first and the
order then ends canceled. Right after a create, a read can briefly miss the order, so the programs
pause for a second before reading.
A refused order raises an error with a stable code, such as INSUFFICIENT_FUNDS or POST_ONLY_CROSS. See Error responses.
Prefer raw HTTP?
Every private request carries X-API-KEY-ID, X-API-TIMESTAMP (Unix milliseconds), and X-API-SIGNATURE. The signature is the Ed25519 signature, in hex, of five lines: timestamp, method,
path, canonical query, and the SHA-256 of the body. This Python example places the same order over
REST; it needs pip install cryptography.
import hashlib, json, os, time, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = bytes.fromhex(os.environ["POLYESTER_API_PRIVATE_KEY"])
key = Ed25519PrivateKey.from_private_bytes(private_key)
order = {"symbol": "BTC-USDT", "side": "BUY", "baseQty": "0.001",
"limitGtc": {"price": "60000", "postOnly": True}}
body = json.dumps({"order": order}).encode()
# Five lines: timestamp, method, path, canonical query (empty here), body hash
timestamp = str(int(time.time() * 1000))
body_hash = hashlib.sha256(body).hexdigest()
canonical = "\n".join([timestamp, "POST", "/v1/orders", "", body_hash])
request = urllib.request.Request(
"https://api.testnet.polyester.com/v1/orders", data=body, method="POST",
headers={
"Content-Type": "application/json",
"X-API-KEY-ID": os.environ["POLYESTER_API_KEY_ID"],
"X-API-TIMESTAMP": timestamp,
"X-API-SIGNATURE": key.sign(canonical.encode()).hex(),
},
)
print(urllib.request.urlopen(request).read().decode())Query normalization and the full rules are in Ed25519 API keys. Request and
response fields are in the API reference for POST /v1/orders and its ConnectRPC twin, OrdersService.CreateOrder.
Stream your order updates
The private WebSocket, wss://api.testnet.polyester.com/v1/ws/private, pushes a snapshot of your
orders and then every change. It authenticates with short-lived realtime tokens that you fetch with
signed requests; see Private WebSocket.
/v1/ws/private from non-browser
clients with HTTP 403 while access is being enabled. Until then, follow your orders
with GET /v1/orders/open.Next steps
- Market maker guide
- Build a trading bot
- Order types and lifecycle
- Rate limits
- Error responses
- Environments and hosts