API Documentation

HeliEx provides a REST API for trading the CURE/GRC pair (buy and sell CURE, priced in GRC). Public endpoints need no authentication. Private endpoints need an API key and an HMAC-SHA256 signature on every request.

Base URL: https://heliex.net · JSON over HTTPS · last updated 2026-09-29.

Quick Reference (for you and your AI coding assistant)

Paste this block into your assistant along with this page. Every rule below is enforced by the server.

HeliEx REST API - rules
- Base URL https://heliex.net. Pair CURE/GRC: price = GRC per 1 CURE, amount = CURE.
- Send a descriptive User-Agent header on EVERY request, public ones too (e.g.
  "User-Agent: my-bot/1.0"). Default library agents such as Python-urllib may be
  blocked by the site's CDN with HTTP 403 before the request reaches HeliEx.
- Numbers in responses are exact decimal STRINGS with 8 decimals (e.g. "2.00000000").
  Parse with Decimal / BigNumber, never binary floats. Send numbers as strings.
- Timestamps are UTC ISO 8601 with a trailing Z, e.g. "2026-09-26T18:38:11.552Z".
- Private requests need 4 headers: X-API-Key, X-API-Timestamp (unix seconds, within
  60 s of server time), X-API-Nonce (16-128 chars [A-Za-z0-9._~-], NEW for every
  request), X-API-Signature = hex(HMAC_SHA256(secret, timestamp + nonce + METHOD +
  path_with_query + body)). Body = the exact bytes you send ("" for GET).
- Missing nonce -> 401. Reused nonce -> 409. Too fast -> 429 + Retry-After.
- Read the X-RateLimit-Remaining / X-RateLimit-Reset headers to pace requests.
- Always send a unique client_order_id when placing orders. On a timeout or network
  error, resend the SAME order with the SAME client_order_id (and a NEW nonce): you
  get the original order back, never a duplicate.
- Orders are limit orders only. States: open, partial (live) / filled, cancelled (final).
- Use "filled" (not amount - remaining) for executed quantity.
- Price protection: >10% worse than market -> 400 code price_deviation_confirm_required
  (resend with "confirm_price_deviation": true); >50% worse -> always rejected.
- Poll /api/orderbook (compare "sequence") and /api/trades?since_id=N. No WebSocket.
- Read-only market data for trackers/aggregators: /api/v1/* (no key, CORS *, cached
  10 s, 120 requests/min per IP); see "Public Market Data API" below.
- Withdrawals: only with a key that has the Withdraw permission (needs 2FA and an IP
  allowlist), only to addresses on your withdrawal whitelist (added on the website),
  and only after the 24 h activation delays. POST /api/key/withdrawals with a unique
  client_id; on a timeout resend the SAME client_id (new nonce) - never a duplicate.
  Refusals return {"error": CODE, "detail": ...}. They may be switched off site-wide
  (503 API_WITHDRAWALS_DISABLED).
- Bots without an account can skip the swap page's human check by paying a small fee
  on-chain first: POST /api/swaps/fee-request (no key). See "Paid swaps for bots".

Public Endpoints

No authentication. Market data (order book, trades, stats, candles) is sent with Cache-Control: no-store, so every poll gets fresh data.

Order Book

GET /api/orderbook

Top 50 price levels per side, aggregated by price. Bids are sorted highest first, asks lowest first.

{
  "timestamp": "2026-09-26T18:41:12.482Z",
  "last_update": "2026-09-26T18:40:58.103Z",
  "sequence": 18342,
  "bids": [{"price": "2.00000000", "amount": "401.70313869"}, ...],
  "asks": [{"price": "4.50000000", "amount": "505.32222900"}, ...]
}

Recent Trades

GET /api/trades?limit=50 GET /api/trades?since_id=948&limit=500

Without since_id: the most recent trades, newest first. With since_id: trades with a higher id, oldest first - poll with the highest id you have seen to never miss a trade. limit 1-500, default 50. A trade executes at the price of the order that was already resting on the book.

[
  {"id": 949, "price": "4.50000000", "amount": "10.00000000", "created_at": "2026-09-26T18:38:11.552Z"}
]

Market Stats

GET /api/market_stats

Last trade price, 24-hour change, high and low, and 7-day volume in CURE.

{
  "last_price": "4.50000000",
  "daily_change_pct": "125.00",
  "daily_high": "4.50000000",
  "daily_low": "2.00000000",
  "weekly_volume": "233.24354515"
}

Price History (Candles)

GET /api/chart/candles?range=1d&interval=auto

range: 1d, 7d, 30d (default), 90d, 1y, all. interval: auto (default), 1m, 5m, 15m, 1h, 4h, 1d, 1w. At most 2,000 candles per request (a smaller interval over a long range returns HTTP 400). Times are Unix seconds (UTC); values are decimal strings. Refreshed about every 15 seconds.

{
  "range": "1d", "interval_seconds": 900, "from": 1790365500, "to": 1790451900, "as_of": 1790452378,
  "base_asset": "CURE", "quote_asset": "GRC",
  "candles": [
    {"time": 1790403300, "open": "2.00000000", "high": "2.00000000", "low": "2.00000000",
     "close": "2.00000000", "volume": "0.00625000", "quote_volume": "0.01250000", "trade_count": 1}
  ]
}

Service Status

GET /api/health GET /api/wallet_status

/api/wallet_status reports whether each coin's wallet is online: {"online": true, "assets": {"GRC": {"online": true, "error": ""}, "CURE": {...}}}. New orders are refused (HTTP 503) while a wallet is offline.

Public Market Data API (v1)

Read-only market data for price trackers, aggregators (CoinGecko, CoinMarketCap) and dashboards. No authentication and no account data: aggregated order book levels, public trades and 24-hour statistics only. Pair CURE_GRC: base CURE, quote GRC, price = GRC per 1 CURE. Pair names are case-insensitive and may use - instead of _.

Ticker

GET /api/v1/ticker GET /api/v1/ticker?pair=CURE_GRC
{
  "CURE_GRC": {
    "pair": "CURE_GRC", "base": "CURE", "quote": "GRC",
    "last_price": "2.50000000", "high_24h": "3.00000000", "low_24h": "2.00000000",
    "change_24h_pct": "25.00000000",
    "base_volume_24h": "3.50000000", "quote_volume_24h": "8.50000000",
    "best_bid": "2.00000000", "best_ask": "2.60000000",
    "timestamp": "2026-09-29T23:28:32.525Z"
  }
}

Order Book

GET /api/v1/orderbook?pair=CURE_GRC&depth=50

depth: price levels per side, 1-500, default 50. Levels are aggregated by price as [price, amount]; bids highest first, asks lowest first. last_update and sequence work as in /api/orderbook.

{
  "pair": "CURE_GRC", "timestamp": "2026-09-29T23:28:32.525Z",
  "last_update": "2026-09-29T12:00:00.250Z", "sequence": 18342,
  "bids": [["2.00000000", "4.00000000"], ["1.90000000", "1.00000000"]],
  "asks": [["2.60000000", "4.00000000"], ["2.75000000", "0.25000000"]]
}

Trades

GET /api/v1/trades?pair=CURE_GRC&limit=100

Most recent trades, newest first. limit 1-500, default 100. quote_volume = price × base_volume (GRC).

{
  "pair": "CURE_GRC", "timestamp": "2026-09-29T23:28:32.525Z",
  "trades": [
    {"id": 5, "price": "2.50000000", "base_volume": "2.00000000", "quote_volume": "5.00000000",
     "side": "buy", "timestamp": "2026-09-29T22:28:29.341Z"}
  ]
}

Summary

GET /api/v1/summary

Pairs plus the site-wide counts shown in the footer: registered people (system and swap custody accounts excluded), all trades, and active orders.

{
  "exchange": "HeliEx", "timestamp": "2026-09-29T23:28:32.525Z",
  "pairs": [{"pair": "CURE_GRC", "base": "CURE", "quote": "GRC", "last_price": "2.50000000"}],
  "users": 3, "trades": 5, "orders": 9
}

CoinGecko format

GET /api/v1/coingecko/pairs GET /api/v1/coingecko/tickers GET /api/v1/coingecko/orderbook?ticker_id=CURE_GRC&depth=100 GET /api/v1/coingecko/historical_trades?ticker_id=CURE_GRC&type=buy&limit=200&start_time=1790600000&end_time=1790700000

Orderbook depth: 0 = full book (at most 500 levels per side), otherwise depth/2 levels per side (100 = 50 bids and 50 asks); timestamp = last book change in Unix milliseconds. Historical trades: type buy or sell (optional), limit 0-500 (0 = as many as allowed, 500), start_time/end_time in Unix seconds; trade_timestamp in Unix seconds.

[{"ticker_id": "CURE_GRC", "base_currency": "CURE", "target_currency": "GRC",
  "last_price": "2.50000000", "base_volume": "3.50000000", "target_volume": "8.50000000",
  "bid": "2.00000000", "ask": "2.60000000", "high": "3.00000000", "low": "2.00000000"}]

{"buy": [{"trade_id": 5, "price": "2.50000000", "base_volume": "2.00000000",
          "target_volume": "5.00000000", "trade_timestamp": 1790720909, "type": "buy"}],
 "sell": [...]}

CoinMarketCap format

GET /api/v1/cmc/summary GET /api/v1/cmc/assets GET /api/v1/cmc/ticker GET /api/v1/cmc/orderbook/CURE_GRC?depth=0&level=2 GET /api/v1/cmc/trades/CURE_GRC

Orderbook depth as for CoinGecko (default 0 = full book, at most 500 levels per side); level 1 = best bid and ask only, 2 = aggregated by price (default), 3 is answered like 2 (individual orders are not published). Trades: the 500 most recent, timestamp in Unix milliseconds. unified_cryptoasset_id, base_id and quote_id are CoinMarketCap ids (CURE 333, GRC 833).

[{"trading_pairs": "CURE_GRC", "base_currency": "CURE", "quote_currency": "GRC",
  "last_price": "2.50000000", "lowest_ask": "2.60000000", "highest_bid": "2.00000000",
  "base_volume": "3.50000000", "quote_volume": "8.50000000", "price_change_percent_24h": "25.00000000",
  "highest_price_24h": "3.00000000", "lowest_price_24h": "2.00000000"}]

{"CURE_GRC": {"base_id": 333, "quote_id": 833, "last_price": "2.50000000",
              "base_volume": "3.50000000", "quote_volume": "8.50000000", "isFrozen": 0}}

{"timestamp": 1790683200250, "bids": [["2.00000000", "4.00000000"]], "asks": [["2.60000000", "4.00000000"]]}

Science Compute Ticker API

Estimated live compute of distributed science networks, as shown in the ticker on the Markets page. BOINC figures are estimated from credit (200 credits/day = 1 GFLOPS); Folding@home is as reported. No authentication. See the methodology.

Science compute

GET /api/public/science-compute

Every network with its current FLOPS, 24-hour change and 24 hourly sparkline values (oldest first, nulls allowed), plus the totals.

{"updated_at": "2026-10-09T19:00:00Z", "total_estimated_flops": 1.69e16, "gridcoin_whitelist_flops": 1.32e16,
 "total_human": "16.9 PFLOPS", "gridcoin_whitelist_human": "13.2 PFLOPS", "formula": "...",
 "networks": [{"id": "milkyway", "name": "MilkyWay@home", "short": "MW", "type": "boinc",
   "flops": 1.35e14, "flops_human": "135 TFLOPS", "kind": "estimated", "change_24h_pct": 2.4,
   "sparkline": [null, 1.33e14, 1.34e14], "source_url": "https://milkyway.cs.rpi.edu/milkyway/server_status.php?xml=1",
   "sampled_at": "2026-10-09T19:00:00Z", "ok": true, "note": null, "gridcoin_whitelisted": true, "scope": "network"}],
 "methodology_url": "/static/science-compute.html"}

30-day CSV export

GET /api/public/science-compute?format=csv

Every stored sample of the last 30 days: sampled_at, network_id, flops_estimate, credit_per_day, source, ok, source_url.

Authentication

API keys

Create keys in Settings → API Keys. Choose permissions independently: Read (balances, orders, trade history, withdrawals), Trade (place and cancel orders) and Withdraw (send coins to your whitelisted addresses; see below). New keys are read-only by default, and Trade never includes Withdraw. The secret is shown only once. Optionally restrict a key to specific IP addresses. Never share the secret, paste it into a support ticket, or commit it to code; if it leaks, delete the key and create a new one.

Required headers (every private request)

X-API-Key: hx_your_key_id
X-API-Timestamp: 1790452378
X-API-Nonce: 3f9c1d7e8b2a4c6d9e0f1a2b3c4d5e6f
X-API-Signature: 5b1f...hex...
User-Agent: my-bot/1.0                (every request, public ones too)
Content-Type: application/json        (for requests with a body)

Signature

request_target = path + ("?" + raw_query_string if there is a query)
message   = timestamp + nonce + METHOD + request_target + body
signature = hex( HMAC_SHA256(key = secret, msg = message) )

METHOD is uppercase. The query string must be byte-for-byte what you send. The body must be exactly the bytes you send (serialize JSON once, sign that string, send that string); for GET it is empty.

GET  /api/orders?status=open,partial
message = "1790452378" + "3f9c1d7e8b2a4c6d9e0f1a2b3c4d5e6f" + "GET" + "/api/orders?status=open,partial"

POST /api/orders   body = {"side":"buy","price":"2.1","amount":"5","client_order_id":"bot-000123"}
message = "1790452378" + "<nonce>" + "POST" + "/api/orders" + body

Private Endpoints

Balances

GET /api/balances

Permission: Read.

[
  {"asset": "GRC", "available": "100.00000000", "locked": "10.00000000"},
  {"asset": "CURE", "available": "50.00000000", "locked": "0.00000000"}
]

List My Orders

GET /api/orders?limit=200&offset=0 GET /api/orders?status=open,partial GET /api/orders?client_order_id=bot-000123

Permission: Read. Newest first; limit 1-200 (default 200), offset for paging. Optional filters: status (comma-separated: open, partial, filled, cancelled - open,partial lists every live order) and client_order_id.

[
  {
    "id": 42,
    "side": "buy",
    "price": "2.10000000",
    "amount": "10.00000000",
    "remaining": "6.00000000",
    "filled": "4.00000000",
    "status": "partial",
    "created_at": "2026-09-26T18:38:11.552Z",
    "client_order_id": "bot-000123"
  }
]

Get One Order

GET /api/orders/{order_id}

Permission: Read. Same format. HTTP 404 if it does not exist or is not yours.

Order States

There is no "rejected" state: an invalid order returns an HTTP error and is never created.

Place Order

POST /api/orders

Permission: Trade.

{
  "side": "buy",
  "price": "2.1",
  "amount": "5",
  "client_order_id": "bot-000123",
  "confirm_price_deviation": false
}

All orders are limit orders: whatever does not fill immediately rests on the book at your price. Returns HTTP 200 with the order (format above) after any immediate fills. Funds are locked while the order is live: a buy locks price × amount GRC, a sell locks amount CURE.

Price protection: an order priced more than 10% worse than the market - buying above the best ask, or selling below the best bid (the last trade if that side is empty) - returns HTTP 400 with "code": "price_deviation_confirm_required", reference_price and deviation_percent. Resend with "confirm_price_deviation": true only if intended. Orders more than 50% worse are always rejected with "code": "price_deviation_blocked". Resting orders on the passive side (e.g. a low bid) are never affected.

Trading limits: order total (price × amount) at least 0.01 GRC; at most 100 live orders, 5,000 new orders per UTC day and 30 order placements per minute per account. An order that would trade against your own resting order is rejected (HTTP 400) - cancel that order first.

Fees: the trading fee is deducted from the seller's GRC proceeds; buyers pay no trading fee. The exact fee of every fill is in /api/my_trades.

Cancel Order

POST /api/orders/{order_id}/cancel

Permission: Trade. No body. Unfilled funds are unlocked immediately.

Cancel All Orders

POST /api/orders/cancel_all POST /api/orders/cancel_all?side=buy

Permission: Trade. No body. Cancels every live order (open or partial), or only one side. Safe to repeat.

{"detail": "Cancelled 2 orders", "cancelled": 2, "orders": [{...}, {...}]}

My Trades (fills)

GET /api/my_trades?limit=100&offset=0

Permission: Read. Your individual fills, newest first (limit 1-500, default 100). price is the actual execution price; fee is what you paid on that fill (0 on buys).

[
  {
    "id": 9001,
    "side": "sell",
    "order_id": 42,
    "price": "2.00000000",
    "amount": "4.00000000",
    "fee": "0.01600000",
    "fee_asset": "GRC",
    "created_at": "2026-09-26T18:38:11.552Z"
  }
]

API Withdrawals (withdraw permission)

Withdrawals made through the API use the same pipeline as website withdrawals: the same fees, balance debit, chain-safety checks and sending. Extra safeguards apply because a leaked key could otherwise move funds.

Requirements

Create a withdrawal

POST /api/key/withdrawals

Permission: Withdraw (a session login cannot use this endpoint). Body fields: asset (GRC or CURE), amount (decimal string, up to 8 decimals; the fee is added on top), address (a usable whitelisted address), client_id (your unique id, 1-64 characters A-Z a-z 0-9 . _ : -). Limit: 10 requests per minute per key.

POST /api/key/withdrawals
{"asset":"GRC","amount":"25.5","address":"SAbc...xyz","client_id":"payout-2026-10-01-001"}

201 Created
{
  "id": 1532,
  "client_id": "payout-2026-10-01-001",
  "asset": "GRC",
  "amount": "25.50000000",
  "fee": "0.60000000",
  "address": "SAbc...xyz",
  "status": "pending",            ("held" when the key has a hold)
  "txid": null,
  "release_at": null,             (when a held withdrawal will be sent)
  "created_at": "2026-10-01T20:15:03.120Z"
}

Idempotent: sending the same client_id again with the same asset, amount and address returns the original withdrawal (HTTP 200), never a second one. The same client_id with different details returns 409 CLIENT_ID_CONFLICT. After a timeout, resend with the same client_id and a new nonce.

Look up withdrawals

GET /api/key/withdrawals?limit=50&before_id=1532&status=held GET /api/key/withdrawals/1532

Permission: Read. Lists all of your withdrawals (website and API), newest first; limit 1-100, page with before_id = the returned next_before_id. Optional status filter.

{"withdrawals": [{...}, {...}], "next_before_id": 1490}

Withdrawal statuses

Withdrawal error codes

Refusals change nothing (no debit) and return {"error": "CODE", "detail": "message", ...}.

403 Forbidden
{"error": "DAILY_LIMIT_EXCEEDED", "detail": "This key's daily withdrawal limit would be exceeded",
 "limit": "100.00000000", "used": "92.40000000", "remaining": "7.60000000"}

Rate Limits

Every private response includes:

X-RateLimit-Limit: 60        requests allowed per minute for this kind of request
X-RateLimit-Remaining: 57    requests left in the current window
X-RateLimit-Reset: 42        seconds until the oldest counted request expires

When a limit is exceeded: HTTP 429 with a Retry-After header (seconds). Wait that long before retrying. The per-account order limits (30 per minute, 5,000 per UTC day) also send Retry-After.

Errors

Errors return JSON with a detail message; some also include a machine-readable code. The withdrawal endpoint uses an error code instead (see API Withdrawals).

A network error or timeout does not mean an order failed. Before retrying, look it up by client_order_id - or simply resend it with the same client_order_id.

Example Code (Python)

import hashlib, hmac, json, secrets, time
from decimal import Decimal
import requests

BASE_URL = "https://heliex.net"
KEY_ID = "hx_your_key_id"
SECRET = "your_secret"          # load from an environment variable, never commit it
USER_AGENT = "my-bot/1.0"       # descriptive; default library agents may get HTTP 403

def api(method, path, data=None):
    body = json.dumps(data, separators=(",", ":")) if data is not None else ""
    timestamp = str(int(time.time()))
    nonce = secrets.token_hex(16)                     # new for every request
    message = timestamp + nonce + method + path + body
    signature = hmac.new(SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
    headers = {"X-API-Key": KEY_ID, "X-API-Timestamp": timestamp, "X-API-Nonce": nonce,
               "X-API-Signature": signature, "Content-Type": "application/json",
               "User-Agent": USER_AGENT}
    r = requests.request(method, BASE_URL + path, headers=headers, data=body or None, timeout=15)
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", "5")))
        return api(method, path, data)                # re-signed with a new nonce
    r.raise_for_status()
    return r.json()

book = requests.get(BASE_URL + "/api/orderbook", headers={"User-Agent": USER_AGENT}, timeout=15).json()
best_ask = Decimal(book["asks"][0]["price"]) if book["asks"] else None

order = {"side": "buy", "price": "2.1", "amount": "5", "client_order_id": "bot-000123"}
try:
    placed = api("POST", "/api/orders", order)
except (requests.ConnectionError, requests.Timeout):
    placed = api("POST", "/api/orders", order)        # same client_order_id: no duplicate
print(placed["status"], Decimal(placed["filled"]))

live = api("GET", "/api/orders?status=open,partial")
api("POST", "/api/orders/cancel_all")

Notes