Uller is now live on mainnet. For more info click here.
Docs

Uller Documentation

Uller is a Solana perpetuals trading terminal with live market data and simulated execution. This documentation covers the trading API, order types, leverage, fees, and the data sources behind the chart and order book.

On this page
01

Introduction

Uller pairs a JTX-trade-inspired terminal with a simulated matching engine. Live order book, candles, and trade tape come from Coinbase for SOL and GeckoTerminal-backed Solana pools for token markets. Every fill, position, and funding payment is calculated locally against the selected market's live mark price.

Connect a wallet through Privy or stay in guest mode — both flows seed a fresh account with 25,000 USDC of simulated balance. Place market or limit orders with market-specific leverage caps; the engine bills fees, accrues funding, and marks open exposure against live market data.

02

Quickstart

Place your first simulated order in four steps.

  1. 1
    Connect or stay guest.

    Tap Connect Wallet in the header to authenticate through Privy, or keep using the auto-issued guest id. Guest accounts persist in memory; wallet accounts persist in Postgres when DATABASE_URL is set.

  2. 2
    Pick a side.

    Toggle long or short in the order ticket. The selected button highlights mint for long and soft red for short.

  3. 3
    Enter size and leverage.

    Size is denominated in the selected base asset or USDC. The slider uses the selected market's leverage cap; margin required updates live as size and leverage change.

  4. 4
    Submit.

    Market orders fill at the current mark price. Limit orders rest until the mark crosses them. Open positions appear instantly in the history panel.

03

Order types

The API accepts two base order types with optional execution flags. All numeric fields use plain JSON numbers — sizes in SOL, prices in USD.

TypeBehaviorFlags
marketFills immediately at the current mark price.reduceOnly
limitRests at the supplied price; fills when the mark crosses the level.postOnly, reduceOnly

Examples of each payload (all sent to POST /api/orders):

// market long, 5 SOL at 20x
{
  "side": "buy",
  "type": "market",
  "size": 5,
  "leverage": 20
}
// limit short with post-only
{
  "side": "sell",
  "type": "limit",
  "price": 92.50,
  "size": 10,
  "leverage": 25,
  "postOnly": true
}
// reduce-only close
{
  "side": "sell",
  "type": "market",
  "size": 5,
  "leverage": 10,
  "reduceOnly": true
}
04

Leverage and margin

Leverage is selected per order from 1xup to the selected market's cap. Margin required by the engine is computed as:

margin = size * price / leverage

For a 10 SOL long at $90 with 20x leverage, the engine reserves 10 * 90 / 20 = 45 USDC of equity. The remainder stays available for further orders.

The simplified liquidation distance is symmetric around entry:

buffer = 0.95 / leverage
liq_long  = entryPrice * (1 - buffer)
liq_short = entryPrice * (1 + buffer)

See estimateLiquidation in lib/market.ts. At 20x, a long is liquidated when the mark drops roughly 4.75% below entry; at 100x, only 0.95%.

05

Fees

Every fill is charged a flat 0.035% taker fee, encoded as FEE_RATE = 0.00035. Maker fees are identical in the simulator — there is no rebate, and post-only orders pay the same rate when they eventually fill.

NotionalFee charged
$1,000$0.35
$10,000$3.50
$100,000$35.00
06

Funding

Funding is settled every 1h against open positions. The funding rate floats with a sine-driven offset so the UI always shows movement:

fundingRate = 0.0012 + Math.abs(Math.sin(now / 900000)) * 0.0005

When the rate is positive, longs pay shorts; when negative, shorts pay longs. The settlement amount per position is:

sign   = side === "long" ? -1 : 1
amount = sign * size * markPrice * (fundingRate / 100)

Accruals are bookkept in lib/memory-account.ts by accrueFunding on every account snapshot. Each payment lands in the funding history visible from the trade panel.

07

Market data

Market data is sourced from the public Coinbase Exchange REST API for SOL and GeckoTerminal for token markets. The server caches each upstream call with a short TTL and de-duplicates concurrent in-flight requests so every viewer sees the same snapshot.

EndpointSource pathTTL
Ticker/products/SOL-USD/ticker600 ms
Order book/products/SOL-USD/book?level=2700 ms
Stats (24h)/products/SOL-USD/stats30 s
Trades/products/SOL-USD/trades1.5 s
Candles/products/SOL-USD/candles5 s

A composed snapshot is itself memoized for 600 ms keyed by tick:granularity. The active candle is patched against the live ticker before the response is returned, so the chart can advance even between upstream candle updates.

08

Rate limits

Limits are bucketed per user. The key is the x-sim-user header when present, falling back to x-forwarded-for / x-real-ip.

EndpointBucketWindow
GET /api/market12060 s
GET /api/account12060 s
POST /api/orders2010 s
POST /api/orders/cancel3010 s
POST /api/positions/close3010 s

When a bucket is exceeded the server replies with 429, sets a Retry-After header in seconds, and emits X-RateLimit-Limit / X-RateLimit-Remaining on market responses.

09

API reference

All endpoints return JSON. Always send x-sim-user with a stable id for the simulated account (any non-empty string). Without it the server uses a shared anonymous guest account.

GET/api/market

Snapshot of stats, order book, trades, and candles for a supported market.

Query parameters
  • tick (number)Order-book bucket size, default 0.01, max 1.
  • granularity (number)Candle seconds: 60, 300, 900, 3600, 21600, 86400.
Response
{
  "stats": {
    "symbol": "SOL",
    "lastPrice": 89.42,
    "markPrice": 89.42,
    "indexPrice": 89.41,
    "change24h": -1.12,
    "change24hPercent": -1.24,
    "volume24h": 168983420,
    "openInterest": 1452563,
    "fundingRate": 0.0016,
    "fundingCountdown": "00:42:18"
  },
  "asks": [{ "price": 89.44, "size": 12000, "total": 12000 }],
  "bids": [{ "price": 89.40, "size": 15000, "total": 15000 }],
  "trades": [{ "id": "1", "side": "buy", "price": 89.42, "size": 1200, "time": "..." }],
  "candles": [{ "time": 1716120000, "open": 89.0, "high": 89.6, "low": 88.8, "close": 89.42, "volume": 12000 }],
  "spreadPercent": 0.044
}
Example
curl -s "https://uller.trade/api/market?market=SOL-PERP&tick=0.01&granularity=3600"
GET/api/account

Account balance, positions, open orders, trade history, and funding events.

Headers
  • x-sim-userStable id for the simulated account (e.g. wallet address or guest token).
Response
{
  "account": { "id": "guest-abc", "balance": 25000, "equity": 25000, "createdAt": "..." },
  "positions": [
    { "id": 1, "market": "SOL-PERP", "side": "long", "size": 5, "entryPrice": 89.42,
      "leverage": 20, "margin": 22.36, "unrealizedPnl": 0, "createdAt": "..." }
  ],
  "openOrders": [],
  "orderHistory": [],
  "tradeHistory": [],
  "fundingHistory": []
}
Example
curl -s "https://uller.trade/api/account" \
  -H "x-sim-user: 0xabc123..."
POST/api/orders

Place a market or limit order with optional execution flags.

Headers
  • x-sim-userStable simulated account id.
Body
{
  "side": "buy" | "sell",
  "type": "market" | "limit",
  "size": number,            // SOL
  "leverage": number,        // capped per market
  "price": number | null,    // required for type=limit
  "reduceOnly": boolean,
  "postOnly": boolean
}
Response
// Returns the full AccountSnapshot after the order is applied.
{ "account": { ... }, "positions": [ ... ], "openOrders": [ ... ], ... }
Example
curl -s -X POST "https://uller.trade/api/orders" \
  -H "content-type: application/json" \
  -H "x-sim-user: 0xabc123..." \
  -d '{"side":"buy","type":"market","size":5,"leverage":20}'
POST/api/orders/cancel

Cancel a resting limit order by id.

Headers
  • x-sim-userMust match the order owner.
Body
{ "id": "order-id-string" }
Response
// AccountSnapshot with the order removed from openOrders.
Example
curl -s -X POST "https://uller.trade/api/orders/cancel" \
  -H "content-type: application/json" \
  -H "x-sim-user: 0xabc123..." \
  -d '{"id":"o_abc123"}'
POST/api/positions/close

Close an open position at the current mark.

Headers
  • x-sim-userMust match the position owner.
Body
{ "id": 1 }
Response
// AccountSnapshot with the position removed and a fill recorded in tradeHistory.
Example
curl -s -X POST "https://uller.trade/api/positions/close" \
  -H "content-type: application/json" \
  -H "x-sim-user: 0xabc123..." \
  -d '{"id":1}'
10

Simulation notes

  • No real funds. Nothing on Uller touches a wallet balance, signs a transaction, or routes to a third-party venue.
  • No on-chain settlement. Fills are bookkeeping entries against the in-process or Postgres account state — there is no Solana program, no escrow, no clearing.
  • Account seed. Every new account is credited with 25,000 USDC. There is no faucet beyond that.
  • Persistence. When DATABASE_URL is configured the engine writes through Postgres; otherwise state lives in a per-process Map and resets on redeploy.
  • Latency. Short-lived upstream caches cap refresh rate, so the order book and trade tape update at whole-millisecond granularity rather than streaming tick-by-tick.
11

Disclaimer

Uller is for interface evaluation only. No real perpetuals are routed, matched, or settled. Market data is sourced from public market-data APIs under their own terms; the simulator and its account state carry no monetary value and no settlement guarantee.