# Quickstart

Create a Testnet API key, fund your account, and place, read, and cancel your first limit order with an official SDK or raw HTTP.

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](https://testnet.polyester.com/docs/developer-docs/getting-started/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.

1. Create your account

   Sign up on Testnet as described in [Create your account](https://testnet.polyester.com/docs/user-docs/start/create-account).

2. Create a key

   Open **[API Keys](https://testnet.polyester.com/account/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.

3. 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:

```bash
export POLYESTER_API_KEY_ID="ak_..."
export POLYESTER_API_PRIVATE_KEY="<64 hex characters>"
```

The key screens are described in [API keys](https://testnet.polyester.com/docs/user-docs/account/access-and-security/api-keys), and request signing in [Ed25519 API keys](https://testnet.polyester.com/docs/developer-docs/authentication-security/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](https://testnet.polyester.com/docs/user-docs/help-and-support/faqs).
- **Faucets**: request an Asset from a [Testnet faucet](https://testnet.polyester.com/docs/user-docs/reference/testnet-faucets), then [deposit it](https://testnet.polyester.com/docs/user-docs/funds/deposit-assets). If it arrives in Funding, move it to Trading with an [internal transfer](https://testnet.polyester.com/docs/user-docs/funds/internal-transfers).

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](https://testnet.polyester.com/docs/developer-docs/getting-started/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](https://testnet.polyester.com/docs/sdk/typescript/get-started/quickstart), [Python](https://testnet.polyester.com/docs/sdk/python/get-started/quickstart), [Go](https://testnet.polyester.com/docs/sdk/go/get-started/quickstart), and [Rust](https://testnet.polyester.com/docs/sdk/rust/get-started/quickstart).

## TypeScript

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

```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 });
```

## Python

Install with `pip install polyester-sdk` (Python 3.11 or later), save as `quickstart.py`, and run `python quickstart.py`.

```python
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())
```

## Go

Install with `go get github.com/Fabric-Labs/polyester-sdk-go`; module access and version pinning are on the [Go installation](https://testnet.polyester.com/docs/sdk/go/get-started/installation) page. Save as `main.go` and run `go run .`.

```go
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)
}
```

## Rust

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

```rust
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](https://testnet.polyester.com/docs/developer-docs/error-handling/error-responses).

> **Do not resend a create that timed out**
>
> If a create times out, the order may still exist. In production, set a client order ID on every create and look it up before you send another; see [Client order IDs](https://testnet.polyester.com/docs/developer-docs/shared-concepts/client-order-ids).

## 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`.

```python
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](https://testnet.polyester.com/docs/developer-docs/authentication-security/ed25519-api-keys). Request and response fields are in the API reference for [`POST /v1/orders`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/orders) and its ConnectRPC twin, [`OrdersService.CreateOrder`](https://testnet.polyester.com/docs/api-docs/connect/orders.v1.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](https://testnet.polyester.com/docs/developer-docs/connectrpc/private-websocket).

> **Testnet access for API clients**
>
> Testnet currently refuses WebSocket upgrades to `/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`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/orders/open).

## Next steps

- [Market maker guide](https://testnet.polyester.com/docs/developer-docs/getting-started/market-maker-guide)
- [Build a trading bot](https://testnet.polyester.com/docs/developer-docs/getting-started/build-a-trading-bot)
- [Order types and lifecycle](https://testnet.polyester.com/docs/developer-docs/shared-concepts/order-types-and-lifecycle)
- [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits)
- [Error responses](https://testnet.polyester.com/docs/developer-docs/error-handling/error-responses)
- [Environments and hosts](https://testnet.polyester.com/docs/developer-docs/getting-started/environments-and-hosts)
