Skip to content

Paper accounts

POST /api/v1/sandbox/paper-account

Every field is optional — an empty body gets sensible defaults.

Terminal window
curl -s https://api.troncharts.xyz/api/v1/sandbox/paper-account \
-H "authorization: Bearer $TOKEN" \
-H "x-tenant-slug: $TC_TENANT_SLUG" \
-H 'content-type: application/json' \
-d '{
"baselineUsd": "50000",
"symbol": "BTC.HL",
"seedDemoPosition": true,
"displayName": "integration rehearsal"
}'
Field Default Notes
baselineUsd "50000" Starting balance, as a positive decimal string.
symbol "BTC.HL" The symbol for the seeded demo position.
seedDemoPosition true Opens one small position so your first read isn’t empty.
displayName — A label for your own bookkeeping.

201, with a fixed shape:

{
"accountId": "…",
"accountNumber": "000123",
"firmId": "…",
"propTenantId": "…",
"templateId": "…",
"mode": "paper",
"demoPositionSeeded": true,
"ws": { "hint": "Subscribe on the risk WS …" }
}

accountId is the canonical identity — use it everywhere. accountNumber is bare zero-padded digits (a legacy prefixed form is still accepted on input, but not emitted). firmId and propTenantId carry the same value: the firm this sandbox account was provisioned under. propTenantId is the older spelling and reads as a tenant id without being one — prefer firmId.

Three gates, checked in this order:

  1. Deployment — off means 404, before anything else is looked at.
  2. Credential type — an API-client bearer token. A browser cookie session gets 403 api_client_required.
  3. Tier — fullTrading. readonly and liquidation get 403 tier_insufficient.

No prop:manage scope is needed. Other failures: 400 invalid_body with the offending fields, 402 where a card on file is required, and 422 when provisioning can’t complete.

The account is in your credential’s ownership scope from the moment it exists:

Terminal window
curl -s https://api.troncharts.xyz/api/v1/accounts \
-H "authorization: Bearer $TOKEN" \
-H "x-tenant-slug: $TC_TENANT_SLUG"
curl -s https://api.troncharts.xyz/api/v1/accounts/$ACCOUNT_ID/state \
-H "authorization: Bearer $TOKEN" \
-H "x-tenant-slug: $TC_TENANT_SLUG"

A /ws/risk socket carries one active account at a time, so after provisioning you re-subscribe with the new id explicitly:

{ "type": "Subscribe", "id": "…", "topics": ["…"], "accountId": "<accountId>" }

The account must be one your credential owns; anything else is rejected with account_forbidden. You’ll then receive Account-State-Update, Balance-Update, and Margin-Update for it like any other account — see Risk channel.

The supporting chain behind a sandbox account is reused across calls, but the account itself is not deduplicated — every call creates a new one. That’s deliberate (a clean account per rehearsal), but it means a retry loop around this endpoint quietly accumulates accounts. Provision once per rehearsal and keep the accountId.

The sandbox runs a paper-only template. The template carries two rules:

Rule Threshold At the 50000 default
max_drawdown_usd 10% of the baseline 5000
daily_loss_cap_usd 5% of the baseline 2500

Orders are not checked against these rules when they are placed. The risk engine connected to your instance monitors them, exactly as it does for a real prop account, and blocks the account with POST /api/v1/accounts/{id}/block when one is broken. A blocked account refuses new opening orders with 403 account_suspended. Reduce-only orders still go through, so you can close what you hold.

To rehearse again from a clean state, provision a fresh account with another POST /api/v1/sandbox/paper-account.

This is the same flow described in Where rules are enforced, so the error handling you build here carries over to a real template unchanged.