# Place your first order

Every order is an *intent*: you compose it, the OMS risk-gates it, then it dispatches.
Placing on a paper account runs that entire pipeline with no venue key, so you end up
with a filled order and a position you can read back.

1. ### Compose the order on paper

   `venue`, `symbol`, `side`, `type` and `qty` are the only required fields. `type` is one
   of `market`, `limit`, `stop`, `stop_limit`, `take_profit`. The route branches on the
   *account kind*, so `venue: "paper"` against a live account is rejected with
   `paper_venue_requires_paper_account`.

   ```bash
   curl -s https://api.troncharts.xyz/api/v1/oms/intents \
     -H "authorization: Bearer $TOKEN" \
     -H "x-tenant-slug: $TC_TENANT_SLUG" \
     -H 'content-type: application/json' \
     -H "idempotency-key: $(uuidgen)" \
     -d '{"venue":"paper","symbol":"BTC.HL","side":"buy","type":"market",
          "qty":"0.01","accountId":"'"$ACCOUNT_ID"'"}'
   ```
   ```ts
   const { intentId, state, syncFill } = await sdk.oms.composeIntent(
     { venue: 'paper', symbol: 'BTC.HL', side: 'buy', type: 'market', qty: '0.01', accountId },
     crypto.randomUUID(),   // idempotency key — retries replay instead of re-firing
   )
   ```
   :::note[Paper is not a lower tier]
   Placing needs a `fullTrading` credential on paper exactly as on live, so a challenge can
   never be scored on orders the credential could not have placed for real. Provisioning a
   paper account self-serve is off by default (`SANDBOX_PROVISIONING_ENABLED`) — see
   [Create a paper account](https://docs.troncharts.xyz/docs/recipes/create-a-paper-account/).
   :::

2. ### Read the fill off the response

   The paper engine matches synchronously, so the `201` already carries the outcome.
   `intentId` is the handle for every call after this one.

   ```json
   { "intentId": "3f2a…", "state": "filled",
     "syncFill": { "qty": "0.01", "avgPrice": "64210.5" } }
   ```

3. ### Send the same call to a live venue

   Swap `venue` to `hyperliquid` or `aster`. Type-specific fields are enforced at compose:
   `limit` needs `price`, the trigger types need `triggerPrice`, and `stop_limit` needs
   `stopLimitPrice` on top of that.

   ```bash
   curl -s https://api.troncharts.xyz/api/v1/oms/intents \
     -H "authorization: Bearer $TOKEN" \
     -H "x-tenant-slug: $TC_TENANT_SLUG" \
     -H 'content-type: application/json' \
     -H "idempotency-key: $(uuidgen)" \
     -d '{"venue":"hyperliquid","symbol":"BTC.HL","side":"buy","type":"limit",
          "qty":"0.01","price":"60000","timeInForce":"Gtc"}'
   ```
   ```ts
   const { intentId } = await sdk.oms.composeIntent(
     { venue: 'hyperliquid', symbol: 'BTC.HL', side: 'buy', type: 'limit',
       qty: '0.01', price: '60000', timeInForce: 'Gtc' },
     crypto.randomUUID(),
   )
   ```
4. ### Confirm the live intent reached the venue

   Read the intent back and check `state`. Anything terminal (`filled`, `rejected`,
   `canceled`) is final; `dispatched` means it is resting at the exchange.

   ```bash
   curl -s https://api.troncharts.xyz/api/v1/oms/intents/$INTENT_ID \
     -H "authorization: Bearer $TOKEN" \
     -H "x-tenant-slug: $TC_TENANT_SLUG"
   ```

   :::caution[A 201 is not a dispatch]
   On a live venue the `201` means the intent was composed and risk-gated, not that it
   reached the exchange. If the account owner has no provisioned venue agent it stays at
   `intent_pending` — sign `actionToSign` yourself and POST it to
   `/api/v1/oms/intents/{id}/sign-result`. See [Order lifecycle &
   signing](https://docs.troncharts.xyz/docs/trading/order-lifecycle/).
   :::

**Next:** [Attach a take-profit and stop-loss](https://docs.troncharts.xyz/docs/recipes/attach-a-bracket/) · [Cancel an order or close a position](https://docs.troncharts.xyz/docs/recipes/cancel-and-flatten/) · [Orders & OMS](https://docs.troncharts.xyz/docs/trading/orders/)