> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# Conditional orders (TP/SL)

> Arm a take-profit or stop-loss rule on a built execution, and let Predictefy watch the book for you.

A conditional order attaches a **trigger rule** to an execution you have already built, so the
order waits for a price instead of resting on a venue. When the rule is satisfied, Predictefy
either tells you to submit it or — on the one venue where that needs nothing of yours — submits
the body you already signed.

Arming builds nothing, signs nothing, and relays nothing. It takes an execution that is still
`built` and records what should happen to it later.

:::note[Open to Pro and above since 2026-09-11]
The conditional order family is open to every account on Pro or above. An account on Free or
Builder receives `403 PLAN_REQUIRED` naming Pro as the minimum plan.

Hosted firing is a separate switch again, and it is **off**. A hosted order still arms, still
triggers, and still emits `conditional.triggered`; nothing fires automatically until the owner
turns firing on.
:::

## Plan availability

Conditional orders require Pro or above, with these per-account limits (live since
2026-09-11):

| Plan       | Conditional orders armed at once |
| ---------- | -------------------------------- |
| Free       | Not included                     |
| Builder    | Not included                     |
| Pro        | Up to 25                         |
| Enterprise | No cap                           |

Only conditional orders currently in `armed` status count towards the cap. Reaching the cap
returns `409 CONDITIONAL_ARMED_CAP`, with a message naming the cap and your plan. Cancel an
armed order or upgrade before arming another. A plan below Pro receives `403 PLAN_REQUIRED`,
with Pro named as the minimum plan in the message.

Hosted firing stays off for every plan. See the
[plan table](/guides/credits/#feature-access).

## Three modes, and which venue gets which

There is no single "stop-loss" feature here. Which mode a venue can serve is **derived** from
what that venue's own submit requires, and `GET /v1/exec/venues` reports it per venue as
`conditional: { native, hosted, client }`.

| Mode         | Who watches the price | Who submits when it fires  | Where it works                |
| ------------ | --------------------- | -------------------------- | ----------------------------- |
| **`native`** | The venue's own engine | The venue                  | Hyperliquid only              |
| **`hosted`** | Predictefy            | Predictefy, replaying your signed body | Limitless only    |
| **`client`** | Predictefy            | You, with your credentials | Every venue                   |

- **`native`** is not a conditional order at all — it is a venue-held trigger order you sign once.
  On Hyperliquid, pass `trigger.triggerPx` and `trigger.tpsl` in the ordinary build body and
  Hyperliquid's engine watches and fires. See
  [the Hyperliquid build schema](/guides/trading/hyperliquid/). Nothing on this page is involved.
- **`hosted`** is limited by custody, not by ambition. A hosted fire replays a stored submit body
  with no caller present, so it is offered only where **every** credential the venue requires is a
  client-side wallet signature, **and** the lane declares that its submit carries no time-bound
  permit and no caller secret. Both gates must hold. Today that is **Limitless alone**: Rain never
  posts (your client broadcasts its own transaction), Pascal's stored permit carries a staleness
  window a delayed fire would trip, and every remaining venue needs an API key, a session bearer,
  or an OAuth pair at submit time — none of which we will ever hold.
- **`client`** is always available, because watching a book and telling you costs us nothing of
  yours. You submit with your own credentials when the trigger fires.

Read the modes off the API rather than off this table. Execution lives on its own origin, separate
from the reads API — `https://exec.predictefy.com`, which the examples below call
`$PREDICTEFY_EXEC_URL`:

```sh
curl -s "$PREDICTEFY_EXEC_URL/v1/exec/venues" \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
| jq '.data[] | {venue, conditional}'
```

## The lifecycle

```
armed ──▶ triggered ──▶ fired          (client: you submit, then report it fired)
   │           ├──────▶ fire_failed   (client: you report the venue never took it)
   │           ├──────▶ canceled      (client: you withdraw, until a reservation fences it)
   │           └──────▶ firing ──▶ fired | fire_failed   (hosted, once firing is on)
   ├──▶ canceled     (you cancelled it, or an OCO sibling triggered first)
   ├──▶ expired      (expiresAt passed while still armed)
   └──▶ superseded   (the execution was already advanced by something else)
```

Every transition is appended to the order's own `events` trail, with the evidence that caused it —
the record that answers "why did this fire". Read it with the single-order route.

## Routes

Every route needs an API key with the `trade` scope; new keys default to `read`, so opt into trading per key in the console. All
three POSTs require an `Idempotency-Key`.

| Route                                          | What it does                                        |
| ---------------------------------------------- | --------------------------------------------------- |
| `POST /v1/exec/{venue}/orders/{executionId}/arm` | Attach a trigger rule to a built execution (201)   |
| `GET /v1/exec/conditional`                     | Your conditional orders, newest first, cursor-paged  |
| `GET /v1/exec/conditional/{id}`                | One order plus its event trail                       |
| `POST /v1/exec/conditional/{id}/cancel`        | Cancel a still-cancellable order, erasing any artifact |
| `POST /v1/exec/conditional/{id}/report`        | Client mode: report the outcome — `fired`, or `not_submitted` |

## Arming

The path `executionId` must name one of your own executions that is still `built`; a submitted one
has already spent its artifact and is refused. It must also be an **order** execution (`intent:
order`) — a cancel, modify or redeem execution is refused with `400`, because only an order can be
a conditional's leg. `expiresAt` is required. When the built artifact names only a venue ticker
(Kalshi does), also pass the catalog `marketId` and `outcomeId` so the trigger can be bound to a
book — without them the arm is refused with `400`.

```sh
curl -s -X POST "$PREDICTEFY_EXEC_URL/v1/exec/kalshi/orders/$EXECUTION_ID/arm" \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
  -H "Idempotency-Key: 9d2f0f1c-arm-1" \
  -H "content-type: application/json" \
  -d '{
    "fireMode": "client",
    "marketId": "KXFEDDECISION-26SEP-H0",
    "outcomeId": "KXFEDDECISION-26SEP-H0",
    "trigger": { "tpsl": "sl", "price": 0.40 },
    "expiresAt": "2026-11-01T00:00:00.000Z"
  }'
```

```ts
import { Predictefy, PREDICTEFY_EXEC_BASE_URL } from '@predictefy/sdk';

const client = new Predictefy({
  apiKey: process.env.PREDICTEFY_API_KEY,
  execBaseUrl: PREDICTEFY_EXEC_BASE_URL,
});

const armed = await client.exec.armConditionalOrder({
  venue: 'kalshi',
  executionId,
  fireMode: 'client',
  // Kalshi's stored artifact names only the venue ticker, so the catalog ids come from the
  // caller — without them the arm is refused with 400.
  marketId: 'KXFEDDECISION-26SEP-H0',
  outcomeId: 'KXFEDDECISION-26SEP-H0',
  trigger: { tpsl: 'sl', price: 0.4 },
  expiresAt: '2026-11-01T00:00:00.000Z',
  idempotencyKey: '9d2f0f1c-arm-1', // reuse this exact key on a retry
});
```

```python
from predictefy import Predictefy, PREDICTEFY_EXEC_BASE_URL

client = Predictefy(
    api_key="pk_live_YOUR_KEY",
    exec_base_url=PREDICTEFY_EXEC_BASE_URL,
)

armed = client.exec.arm_conditional_order(
    "kalshi",
    execution_id,
    {
        "fireMode": "client",
        # Kalshi's stored artifact names only the venue ticker, so the catalog ids come from
        # the caller — without them the arm is refused with 400.
        "marketId": "KXFEDDECISION-26SEP-H0",
        "outcomeId": "KXFEDDECISION-26SEP-H0",
        "trigger": {"tpsl": "sl", "price": 0.40},
        "expiresAt": "2026-11-01T00:00:00.000Z",
    },
    idempotency_key="9d2f0f1c-arm-1",  # reuse this exact key on a retry
)
```

`Idempotency-Key` is required and neither SDK mints one for you here: a fresh key per retry would
arm a **second** conditional order on the same execution. Reuse the same key and a repeat returns
`200` with the order you already armed, plus `Idempotency-Replay: true`.

### The trigger rule

`when` and `price` are the rule; everything else has a default that is stored **explicitly** on the
armed row, so an order always says exactly what it meant.

| Field           | Default  | What it means                                                             |
| --------------- | -------- | ------------------------------------------------------------------------- |
| `when`          | required | `at_or_above` or `at_or_below` — the direction the reference must cross    |
| `price`         | required | A probability in `(0, 1)`, on the `1e-6` grid (max six decimals)            |
| `reference`     | `mark`   | `mark`, `bid`, or `ask`                                                    |
| `confirmations` | `2`      | Consecutive qualifying samples before it triggers                          |
| `dwellMs`       | `2000`   | How long the condition must hold, measured on book timestamps              |
| `maxSpread`     | `0.10`   | No trigger while the spread is wider than this; same `1e-6` grid           |
| `minSize`       | `1`      | The qualifying quote must show at least this size                          |
| `tpsl`          | —        | Sugar: `tp` or `sl`, rewritten into `when` from your order's side          |

The **mark** is the median of the best bid, the best ask, and the last trade — but only while that
trade is fresh. A stale last trade is excluded and the mark falls back to the mid of bid and ask, so
one old print cannot drag a trigger. A book with only one side never triggers at all: without both
quotes there is no mark and no provable spread.

`confirmations` has one documented shortcut: a single qualifying sample whose displayed size at the
touch **grew** since the previous sample counts as confirmed on its own, because real interest
arriving at the touch is worth as much as a second sample. `dwellMs` still has to be satisfied
either way.

`tpsl` is sugar for people who think in stops rather than in directions. A stop protects a position,
so on a sell it becomes `at_or_below` and on a buy `at_or_above`; take-profit mirrors it. The
rewrite reads the side out of your stored order, so if that order does not name a side you must give
`when` yourself.

`price` and `maxSpread` must land on the engine's `1e-6` tick grid, which is to say at most six
decimals. The engine compares in whole ticks, so an off-grid rule could never mean what it says:
`0.4100004` would trigger at `0.41`, and `0.4050005` would never match at all.

Every bound above is a **rejection, never a clamp**. An out-of-range or off-grid `price`,
`maxSpread`, or `confirmations` is a `400 VALIDATION_ERROR`, not a silently corrected value — a
rounded trigger is a different trigger from the one you asked for, and you would never be told.

### Expiry

`expiresAt` must be in the future, within a year, and — when our catalog knows the market's close
date — at or before it. An order that outlives its market can never fire, so we refuse to arm one.
When it passes while the order is still armed, the order moves to `expired` and emits
`conditional.expired`.

### One live order per execution

An execution can own at most one live conditional order — one that is `armed`, `triggered`, or
`firing`. Arming a second answers `409 CONDITIONAL_ARMED`: two rules on one intent are two answers
to the same question, and whichever fired second would find the artifact already spent. Retrying
your original arm with the same `Idempotency-Key` still replays, because the key is read first.

An execution you closed by reporting `not_submitted` accepts no new order either, in either fire
mode: that report says nothing reached the venue, so arming again answers
`409 CONDITIONAL_CLOSED_NOT_SUBMITTED`. Build a new execution instead.

### OCO groups

Pass the same `ocoGroupId` (a uuid you choose) on two or more armed orders and they become
one-cancels-other: when one triggers, its **still-armed** siblings move to `canceled` in the same
transaction, each with an `oco` event naming the order that triggered. A sibling that has already
settled is left alone.

A group accepts new members only while every member is still `armed`. Once one has triggered,
fired, been canceled, or expired, the group has resolved and joining it answers
`409 OCO_GROUP_CLOSED` — start a new group instead. Groups are scoped to your account, so the uuid
you pick is never visible to, and never closeable by, anyone else.

### Hosted arming

Hosted mode additionally takes `artifact`: the already-signed submit body you would have posted
yourself. It is encrypted at rest and never returned by any read.

It carries **only** the fields that venue's submit actually reads, each in the shape that submit
expects — and it must carry the ones that submit cannot run without. On Limitless `owner` and
`signature` are both required: `owner` an address string, `signature` a 65-byte `0x` hex ECDSA
signature. A 32-byte hex value is a wallet **private key**, not a signature, and we refuse it as
one rather than encrypting it and keeping it. `executionId` is optional and must match the order
you are arming.

Everything else is refused, and that deliberately includes the order: we relay the order we stored
when you built it, which is what stops a signature being reused for a different trade. An object or
an array where a string belongs is refused too, because a wrapper is where data hides.

Two more refusals back that up — a credential field at any depth, matched however you spell it
(`apiSecret`, `api_secret` and `API-SECRET` are one field), because the hosted lane exists precisely
because its venue needs none; and anything nested more than six levels deep, because storing what we
cannot inspect is the same risk as storing a credential.

A venue that is not hosted-eligible answers `400 HOSTED_FIRE_NOT_ELIGIBLE`. A deployment holding no
artifact key answers `503 HOSTED_FIRE_UNAVAILABLE`, and **client arming keeps working** — that is a
capability gap, not a caller error.

## The client-fire loop

In client mode Predictefy watches the book and tells you; you submit. Both SDKs ship the loop, and
both hand each order to your callback exactly once for as long as the loop runs.

```ts
const controller = new AbortController();

await client.exec.serveConditional({
  pollMs: 2000,
  signal: controller.signal,
  onTriggered: async (order) => {
    // Submit with your own credentials, exactly as you would any built execution.
    const submitted = await client.exec.submitOrder({
      venue: order.venue,
      executionId: order.executionId,
      signature,
      owner,
    });
    // Then close the loop so the order's trail is complete.
    await client.exec.reportConditionalOrder(order.id, {
      executionId: submitted.executionId,
      venueRef: submitted.venueRef ?? undefined,
    });
  },
});
```

```python
import threading

stop = threading.Event()


def on_triggered(order):
    submitted = client.exec.submit_order(
        order["venue"],
        {"executionId": order["executionId"], "signature": signature, "owner": owner},
    )
    client.exec.report_conditional_order(
        order["id"],
        execution_id=submitted["executionId"],
        venue_ref=submitted["venueRef"],
    )


client.exec.serve_conditional(on_triggered, poll_s=2.0, stop=stop)
```

Both loops poll for your triggered client-mode orders and stop the moment you ask them to —
aborting the signal, or setting the stop event, ends the wait immediately instead of sitting out the
rest of the poll gap. Dedupe is in memory and lives as long as the call, so a restarted loop sees
still-triggered orders again.

Polling the route yourself is the same two filters: `status=triggered` for the orders that are
waiting on you, and `fireMode=client` to keep hosted orders — which you can do nothing about — out
of the page. `fireMode` accepts `all` (the default), `client`, or `hosted`, and rejects anything
else rather than silently ignoring it.

Report is what closes a triggered order, and only a triggered client-mode order accepts one. It
takes an `outcome`:

- `"fired"` — the default. You submitted; name the execution it became. The order moves to `fired`.
- `"not_submitted"` — the venue never accepted it. The order moves to `fire_failed`, and its
  `executionId` is left as armed. Send neither `executionId` nor `venueRef` with it, because there
  is no submission to point at.

`not_submitted` is judged on the evidence the venue left on your execution, never on its status
word. It answers `409 CONDITIONAL_NOT_ARMED` when that execution carries a reference the venue
itself issued — whatever its status reads now — when a relay is in flight or acknowledged
(`submitted`, `acked`, `filled`), or when a reservation for it was consumed. Report `fired` in that
case; our own record contradicts the claim that nothing reached the venue.

It is accepted everywhere else: an execution still `built`, and a `signed`, `failed`, `canceled` or
`expired` execution that never earned a venue reference. A venue business rejection
has exactly that shape — we stamped the submission time and the venue said no. A refusal never
earns a reference on any lane: not the digest we derived from your order before sending it, and not
an identifier the venue's own error body happens to echo back. Only an order the venue took
records one.

That holds however the venue says no. Some venues refuse with an error status; others answer `200`
and carry the refusal inside the body — Polymarket's `success: false`, XO's `success: false`,
Opinion's non-zero `errno`, PRED's `failed` state, Myriad's `rejected` state, PredictStreet's
`REJECTED` state, Kalshi's `rejected` and `failed` states, Novig's `REJECTED` state. Both are the
same event, so both record no reference. Kalshi is the one defensive entry in that list: its
published contract lists no `rejected` or `failed` order state, so the lane maps both in case one
ever arrives, and the rule applies if it does. That holds whoever minted the identifier: a digest we
derived before sending, and an id the venue itself issued for an order it then refused, are equally
worthless, because neither names an order resting on the book. Refusing your cancel counts too: a
cancel the venue turned down leaves no reference either, in either of those encodings.

Be plain about what that costs you: a refused order records **no venue identifier as a reference**.
Not the digest we derived before sending, and not one the venue's own error body echoes back —
`venueRef` is `null`. The venue refused it, so nothing exists to refresh or cancel and no identifier
of ours would name anything real. What the execution's `error` carries is the venue's own rejection
reason, sanitized the way every venue reason is; where the venue's own sentence happens to name its
identifier, that sentence is kept as the venue wrote it. A venue message that embeds our own
post-acceptance wording is not kept — it is reduced to a bounded sentence of ours (`rejected by venue`, or `venue request timed out` when the venue's text also names a timeout, which is judged first). The report fence reads
`venueRef`, never `error`, so an identifier sitting inside a reason changes nothing it decides.

The reference is also what tells the two events apart. A `failed` execution that **does** carry one
failed **after** the venue accepted your order, and its `error` says exactly that rather than
calling it a rejection — PredictStreet's `SETTLEMENT_FAILED` is the case you are most likely to
meet. A refusal carries no reference, and its `error` is the venue's own sanitized reason.

Two answers follow from that, and they are worth knowing before you meet them. A rejected execution
has no venue order id, so `POST /v1/exec/{venue}/orders/{executionId}/refresh` answers `400` —
`cannot refresh: the execution has no venue order id yet`. And you cannot name a **terminal**
execution — `failed`, `canceled` or `expired` — as the target of a cancel build: that answers
`409 EXECUTION_TERMINAL`, before the venue's lane builds anything, because nothing of that order
rests at the venue to call back. The status is what decides it, not whether a particular venue
happens to keep some other handle on the order.

Neither is a loss: the venue refused the order, so there is nothing resting to read or to call back.
What you do instead is report `not_submitted` and build a fresh order.

Rain is the relay-less lane, and it is the exception worth knowing. We never call a venue for it:
submit verifies the transaction **you** signed and records the hash derived from your own bytes, so
the `venueRef` on a `signed` Rain execution is your artifact rather than anyone's answer, and it
never refuses your report.

Be plain about what that means today: **nothing on our side watches the chain for you.** A Rain
execution stops at `signed` and no path we run moves it on — the background reconciler and the
status refresh both advance `submitted` and `acked` rows only, so a receipt we read is never
written back to the row. `not_submitted` on Rain is therefore accepted on your word alone, and the
trail records it as yours. The refusal below it is written for the day that changes: an execution
we had actually observed `acked` or `filled` would contradict you, and today no writer puts a
relay-less execution into either state.

Predict.fun's on-chain cancel path records a self-derived hash the same way, but its order and
cancel relays do reach the venue and record the venue's own order id, so Predict.fun is treated as
the relay venue it is.

An accepted `not_submitted` closes the execution off as well as the order. Every live, unconsumed
reservation on it is handed back — a token you still hold then answers `409 RESERVATION_RELEASED`
rather than telling you to fetch another — and a later `/reserve`, `/submit`, `/ack` or `/arm` of
that execution answers `409 CONDITIONAL_CLOSED_NOT_SUBMITTED`. Build a new execution if you decide
to trade after all. A conditional closed any other way — canceled, expired, or `fired` — leaves its
execution usable.

```sh
curl -s -X POST "$PREDICTEFY_EXEC_URL/v1/exec/conditional/$CONDITIONAL_ID/report" \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
  -H "Idempotency-Key: 9d2f0f1c-report-1" \
  -H "content-type: application/json" \
  -d '{ "outcome": "not_submitted" }'
```

Reporting is safe to retry: repeating the same report returns `200` with `Idempotency-Replay: true`
and records nothing twice, while reporting a *different* execution, a different `venueRef`, or a
different outcome is a different claim about what happened and answers `409 CONDITIONAL_NOT_ARMED`.

If you would rather be pushed than poll, register a webhook instead — the events below carry the
same information.

## While an order is armed

A direct submit, ack, or reservation of the execution is refused with `409 CONDITIONAL_ARMED` while
the order is still `armed`, in either mode: you told us to wait for a price, so acting on it by
hand now would be two different instructions for one intent. Cancel the conditional first if you
want to submit it yourself instead.

Once a **client** order has triggered the refusal lifts, because submitting — and reserving in
order to submit — is exactly what you were just told to do. A **hosted** order that has triggered or is firing keeps refusing: the fire
loop is about to replay your signed body, and firing it by hand as well would spend the same
signature twice.

Cancelling follows the same line, with two boundaries. An armed order cancels in either mode, and a
triggered `client` order still cancels — nothing but you fires it — until either boundary is
crossed.

The first is the execution itself. Cancel needs it to still be `built`; once anything has advanced
it, including a relay the venue then rejected as `failed`, cancel answers
`409 CONDITIONAL_NOT_ARMED`.

The second is a reservation. Because your venue submit never passes through this API, a reservation
you could still be acting on is the last point at which we can be sure nothing is live: one that is
unexpired and unconsumed, one that was consumed however long ago, or any taken since the order was
armed. A reservation that predates the arm and has already lapsed does not fence, so an abandoned
reservation never makes a later order uncancellable.

Past either boundary, cancel points you at report and the order stays `triggered`. Report is what
closes it — with `fired` if you got it in, or `not_submitted` if you did not, including when the
venue rejected your submit. A triggered or firing `hosted` order belongs to the loop and answers
`409 CONDITIONAL_NOT_ARMED` too, as does one that has already fired or expired.

Cancelling is idempotent: an already-canceled order returns success unchanged, and two cancels that
race each other both succeed rather than one taking a spurious conflict.

## Webhook events

All four are tenant-scoped: they reach only endpoints owned by the order's account. Register them
the same way as any other event — see [Webhooks](/guides/webhooks/).

| Event                      | Fires when                                                    |
| -------------------------- | ------------------------------------------------------------- |
| `conditional.triggered`    | The rule was satisfied — in client mode, your cue to submit    |
| `conditional.fired`        | The order became a venue submission                            |
| `conditional.fire_failed`  | A hosted fire ended without a venue submission, or you reported `not_submitted` |
| `conditional.expired`      | `expiresAt` passed while the order was still armed             |

```json
{
  "conditionalId": "0f2c9a1e-2d44-4e2b-9d31-2c9a1e0f2c9a",
  "executionId": "8a4fdf19-49e1-4702-9cb8-31fd9c46f7e1",
  "venue": "kalshi",
  "marketId": "PRES-2028-DEM",
  "outcomeId": "kalshi:PRES-2028-DEM:yes",
  "fireMode": "client",
  "status": "triggered",
  "triggerEvidence": { "mark": 0.39, "samples": 2, "spread": 0.02 },
  "expiresAt": "2026-11-01T00:00:00.000Z",
  "occurredAt": "2026-09-05T00:05:00.000Z",
  "action": "submit execution 8a4fdf19-49e1-4702-9cb8-31fd9c46f7e1 with your credentials"
}
```

`action` is present only on a client-mode `conditional.triggered`, and says exactly what to do next.

`conditional.fire_failed` has two causes, and the payload's `fireMode` tells them apart: `hosted`
means our fire loop could not turn your signed body into a venue submission: a terminal refusal
(the venue rejected the order, or a credits, intent or `not_submitted` check refused it) settles
it on the first attempt, and an ambiguous failure settles it once the attempts run out. `client`
means you reported `not_submitted` yourself. Only the first is something going wrong.

## Errors

| Code                        | Status | Meaning                                                            |
| --------------------------- | -----: | ------------------------------------------------------------------ |
| `ROUTE_NOT_FOUND`           |    404 | Only while the family is switched off platform-wide (not since 2026-09-11) |
| `VALIDATION_ERROR`          |    400 | A trigger, expiry, or artifact bound was not met                    |
| `EXECUTION_NOT_FOUND`       |    404 | The execution is missing or not yours                               |
| `CONDITIONAL_NOT_FOUND`     |    404 | The conditional order is missing or not yours                       |
| `CONDITIONAL_NOT_ARMED`     |    409 | It already left the state this change requires                      |
| `CONDITIONAL_ARMED`         |    409 | A live conditional still owns this execution — cancel it first      |
| `CONDITIONAL_CLOSED_NOT_SUBMITTED` | 409 | You closed a conditional on this execution as `not_submitted`   |
| `OCO_GROUP_CLOSED`          |    409 | That OCO group already has a member that is no longer armed          |
| `HOSTED_FIRE_NOT_ELIGIBLE`  |    400 | This venue's submit needs a credential we will not hold             |
| `HOSTED_FIRE_UNAVAILABLE`   |    503 | No artifact key on this deployment; client arming still works       |
| `SPEND_CAP_EXCEEDED`        |    402 | The stored notional does not fit your remaining rolling budget      |

## Credits

Arming, listing, reading, cancelling, and reporting are metered at **0** credits during the beta.
A hosted fire bills the standard 5-credit trade operation, refunded the same way a failed submit is.
See [Credits & billing](/guides/credits/).

## Honest limitations

- Hosted firing is off. Nothing fires itself today, on any venue.
- Hosted eligibility is **Limitless alone**, and it is computed, not configured. Adding a venue
  means changing what its submit requires, not flipping a flag.
- Spend caps are checked at arm time against the stored notional, and again at fire. An order that
  fits your budget today can still be refused later.
- The country recorded when you arm is re-applied to the venue's geofence at fire. A hosted order
  armed from a region that venue restricts will not fire from it later either.
