Skip to content

Quickstart for agents

The whole point of HardBasis's API is that an agent can go from nothing to a live, hedged, self-limiting trading loop without a human step. This page is that loop, end to end, as copy-pasteable curl. Every response is a decimal- string JSON body (see Wire encoding); pipe through jq.

Verified live 2026-08-11 against gateway 94f4623 — all 8 steps

This whole sequence was run end to end against the deployed testnet gateway on 2026-08-11 (gateway SHA 94f4623), withdrawal included via the Spark path (settled to state:"paid"). A Lightning payout is not exercisable on testnet's sim rail — see step 8 — so the withdrawal example uses Spark.

Set your base URL once:

bash
export HB=https://testnet.hardbasis.com

1. Sign up (testnet self-serve)

Testnet lets you mint an account with no operator action. Signup returns a full-scope master key — shown exactly once, so save it.

bash
curl -fsS -X POST "$HB/v1/signup" \
  -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" -d '{}' | jq .
# → { "accountId": "...", "apiKey": "hb_...", ... }
export MASTER=hb_...   # from the response; shown once, never recoverable

2. Fund it from the faucet

The testnet faucet credits a fixed amount through the normal deposit path.

bash
curl -fsS -X POST "$HB/v1/faucet" \
  -H "x-api-key: $MASTER" -H "idempotency-key: $(uuidgen)" | jq .

3. See the market

Read the live market id straight from the list and keep it — every later call uses it, so nothing can drift from what the venue actually serves:

bash
curl -fsS "$HB/v1/markets" | jq '.[0] | {marketId, status, tickSizeQ8, priceDp}'
export MARKET=$(curl -fsS "$HB/v1/markets" | jq -r '.[0].marketId')   # e.g. btc-usd

priceDp tells you how many decimal places prices carry; tickSizeQ8 is the minimum price increment (a q8 fixed-point integer — Wire encoding).

Prices, per-market stats, and your account events come over one WebSocket. See WebSocket for the full protocol.

bash
# with websocat: subscribe to the oracle feed, then the auth'd account channel
websocat "wss://testnet.hardbasis.com/v1/ws" <<'WS'
{"op":"auth","apiKey":"REPLACE_MASTER"}
{"op":"subscribe","channel":"prices","feedId":"btc-usd"}
WS

5. Place an order

Size is an integer count of $1-notional contracts (a decimal string). The idempotency-key is the order id, so a retry is safe.

bash
curl -fsS -X POST "$HB/v1/orders" \
  -H "x-api-key: $MASTER" -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d "{\"marketId\":\"$MARKET\",\"side\":\"buy\",\"type\":\"market\",\"contracts\":\"10\"}" | jq .

Check the fill and your position:

bash
curl -fsS "$HB/v1/account"   -H "x-api-key: $MASTER" | jq .
curl -fsS "$HB/v1/positions" -H "x-api-key: $MASTER" | jq .

6. Delegate a trade-only key for the bot loop

Put the master key cold and run the loop on a trade+read key. A stolen delegate can lose you a position, never your balance — it cannot withdraw. See Authentication.

bash
curl -fsS -X POST "$HB/v1/sessions" \
  -H "x-api-key: $MASTER" -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{"label":"bot","scopes":["read","trade"]}' | jq .
export BOT=hb_...   # the delegate key, shown once

7. Arm the dead-man's switch

Before the loop runs unattended, arm cancel-all-after: if your bot stops calling in, the engine cancels its resting orders on its own. Re-arm on a timer; disarm by sending a zero deadline.

bash
curl -fsS -X POST "$HB/v1/cancel-all-after" \
  -H "x-api-key: $BOT" -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" -d '{"timeoutMs":"60000"}' | jq .

8. Withdraw

Withdrawals need a withdraw-scoped key and always work — they never gate on trading state or halts. Always quote first. The quote (a trade-scoped call) returns the fee before you commit and, for a Spark destination, whether the address is first-seen — a wrong Spark address is irreversible, so a first-seen destination at or above confirmThresholdMsat sets confirmRequired and returns a confirmToken you must echo into the withdrawal.

bash
# Spark (the native rail): quote by address + amount, then withdraw.
curl -fsS -X POST "$HB/v1/withdrawals/quote" \
  -H "x-api-key: $MASTER" -H "content-type: application/json" \
  -d '{"toAddress":"sprt1q…","amountMsat":"50000000"}' | jq .
# → { "path":"spark","feeMsat":"…","firstSeen":true,"confirmRequired":false,
#     "confirmThresholdMsat":"…" }        # first-seen ≥ threshold also returns confirmToken

curl -fsS -X POST "$HB/v1/withdrawals" \
  -H "x-api-key: $MASTER" -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{"toAddress":"sprt1q…","amountMsat":"50000000"}' | jq .
# → { "railRef":"…","queued":false }

Confirm it settled:

bash
curl -fsS "$HB/v1/withdrawals?limit=1" -H "x-api-key: $MASTER" \
  | jq '.withdrawals[0] | {state, path, railRef, paidTsMs}'
# → { "state":"paid","path":"spark", … }

Lightning payouts

The same endpoint takes a bolt11 instead of an address — {"invoice":"lnbc…"} on both the quote and the withdrawal. A real deployment on the Spark rail settles either path. On testnet's sim rail a Lightning payout is not exercisable (the sim rail exposes no payable-invoice source over the API, so every bolt11 comes back unknown payout invoice), which is why the runnable example above uses the native Spark path.

That is the whole loop: signup → faucet → market → subscribe → order → delegate a trade-only key → arm cancel-all-after → withdraw, no human in it. Endpoint-by-endpoint details — every field, every response — are in the generated REST reference.

Program policy v1 · numbers current as of 2026-08-28

Testnet — test sats have no value. Nothing on this page is an offer, a solicitation, or advice.

Program policy v1 · numbers current as of 2026-08-28
Testnet — test sats have no value. Nothing here is an offer, a solicitation, or advice.