# Order lifecycle & signing

[Placing an intent](https://docs.troncharts.xyz/docs/trading/orders/) returns an `intentId`. What happens
next depends on **who holds the signing key**, and that determines whether your
integration is done or has one more step.

## Two dispatch paths

| Your credential | What happens after compose |
| --- | --- |
| Has a platform-held venue agent | The platform signs and dispatches. The intent moves on by itself; you watch [`/ws/risk`](https://docs.troncharts.xyz/docs/realtime/risk/) or poll. |
| Holds its own signing key (SDK / MCP server) | The intent stops at `intent_pending` and waits for **you** to sign it and return the signature. |

The second case is the one that strands integrations: you get an `intentId`
back, nothing else happens, and there is no error to react to. The intent is
waiting.

## The states

```
intent_pending → signed_pending_dispatch → dispatched → filled
```

`intent_pending` means *composed, risk-checked, not yet signed*. Everything
downstream is the platform's job.

## Poll one intent

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

Scoped to `(accountId, apiClientId)` — you see the intents **your credential**
composed, not every intent on the account. An intent belonging to another
credential answers `404`, not `403`, so this can't be used to probe.

## List your recent intents

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

Most recent first, capped at 50. Same credential scoping.

## Finish a pending intent

When you hold the key, sign the payload and return the signature:

```bash
curl -s https://api.troncharts.xyz/api/v1/oms/intents/$INTENT_ID/sign-result \
  -H "authorization: Bearer $TOKEN" \
  -H "x-tenant-slug: $TC_TENANT_SLUG" \
  -H 'content-type: application/json' \
  -d '{ "signedPayload": { … } }'
# → { "ok": true, "intentId": "…", "venueOrderId": "…", "resting": true }
```

A success may also carry `inlineFill` when the venue filled it immediately.

:::note[Bearer only]
This endpoint answers `401 bearer_required` to a cookie session. Browser
front ends return their signature over the WebSocket instead — this REST shape
exists for SDK and MCP clients.
:::

### The failures worth handling

| Error | Status | Means |
| --- | --- | --- |
| `bearer_required` | 401 | Called with a cookie session. |
| `intent_not_found` | 404 | No such intent. |
| `forbidden` | 403 | The intent belongs to another account, or was composed under a different credential. |
| `invalid_state` | 409 | Already signed, dispatched or dead. Only `intent_pending` can be sign-result'd — treat as *someone else finished it*, not as a retry. |
| `batch_predecessor_pending` | 409 | An earlier leg of a flatten or reverse batch hasn't dispatched. The intent stays pending — **retry after the earlier leg lands**. |

That last one is the one to get right. A flatten or reverse composes several
intents in order (cancels → close → open), and signing a later leg before an
earlier one dispatches would put the position through an intermediate state
nobody asked for. The 409 is the platform refusing to let that happen; back off
and retry rather than treating it as failure.

If dispatch itself fails, you get `400` with `ok: false` and an `error` /
`detail` from the venue — the signature was accepted, the venue rejected the
order.

## When the credential can't sign at all

A credential whose agent link is missing or dead is rejected at **compose**
with `403`. No intent is created, so you never reach the sign step above:

- `credential_missing_agent_wallet` — the credential has no agent linked.
- `agent_authorization_revoked` / `agent_authorization_expired` — it had one and it is no longer valid.

`agent_not_dispatchable` is the other shape, and it comes back from `cancel`
and `modify` as a `409`: the intent composed, but nothing could sign it, so it
was never dispatched. The agent needs re-approving.

These are provisioning problems, not order problems. Fix the credential; the
order path is fine.

## Which pattern should you build?

If your credential is platform-signed, ignore this page: place the intent and
take the outcome from [the risk channel](https://docs.troncharts.xyz/docs/realtime/risk/).

If you hold your own key, the loop is: compose → read `intentId` → sign →
`sign-result` → confirm from the response or the socket. Poll `GET /intents/{id}`
only as a reconciliation backstop, not as your primary path — the socket is
faster and cheaper.