> ## Documentation Index
> Fetch the complete documentation index at: https://docs.simmer.markets/llms.txt
> Use this file to discover all available pages before exploring further.

# Balance Pre-flight

> Catch underfunded wallets before placing orders.

Use `client.ensure_can_trade()` alongside [position sizing](/sdk/position-sizing): sizing decides *how much*, this pre-flight decides *whether to trade at all* given current wallet balance.

## `client.ensure_can_trade()`

A one-call pre-flight that catches underfunded wallets *before* you try to place an order. Every rejected trade round-trips to the backend, logs a failure, and gets retried on the next cron tick — replacing the loop with a single status fetch per run is a pure win for skill reliability and observability.

<Note>
  Available in `simmer-sdk >= 0.11.1`. Collateral-agnostic — reads pUSD on V2 (post-2026-04-28 cutover), USDC.e on V1, so it keeps working across the migration without any code change on your side.
</Note>

### Quick start

```python theme={null}
from simmer_sdk import SimmerClient

client = SimmerClient()
preflight = client.ensure_can_trade(min_usd=1.0)

if not preflight["ok"]:
    # Skip cleanly — harness + automaton reporting distinguish this from "skill broken"
    print(f"Skip: {preflight['reason']} (balance ${preflight['balance']:.2f} {preflight['collateral']})")
    return

# Cap your per-run size to leave room for fees + slippage
order_size = min(MY_MAX_BET, preflight["max_safe_size"])
```

### Arguments

| Arg             | Default        | Meaning                                                                                           |
| --------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| `min_usd`       | `1.0`          | Minimum viable trade size in active collateral. Below this, `ok=False`.                           |
| `venue`         | client's venue | Only `"polymarket"` runs the check. Other venues short-circuit to `ok=True`.                      |
| `safety_buffer` | `0.02`         | Fraction of balance kept as fee/slippage buffer. `max_safe_size = balance × (1 − safety_buffer)`. |

### Returns

A dict with the following keys:

| Field              | Meaning                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `ok`               | `True` if balance ≥ `min_usd` (or non-polymarket venue).                                                 |
| `balance`          | Active collateral balance in USD-equivalent units.                                                       |
| `collateral`       | `"pUSD"` (V2), `"USDC.e"` (V1), or `""` (non-polymarket).                                                |
| `exchange_version` | `"v1"` or `"v2"` — matches server-side flag.                                                             |
| `reason`           | `"ok"`, `"insufficient_balance"`, `"no_wallet"`, `"balance_unavailable"`, or `"skipped_non_polymarket"`. |
| `max_safe_size`    | `balance × (1 − safety_buffer)`, or `0.0` when `ok=False`.                                               |

### When to call it

Call `ensure_can_trade()` **once per skill run**, before any market discovery or signal generation. Running it at the top of your loop costs one REST call and eliminates every downstream rejected order when the wallet is under-funded.

* **Underfunded** → emit a skip report (`skip_reason="insufficient_balance"`) and `return`. The automaton reporter will surface this cleanly, distinct from "skill broken."
* **Sized correctly** → clamp your per-run `MAX_BET_USD` (or equivalent) to `max_safe_size` so you leave headroom for fees + price slippage.

### Why not just trust `client.get_portfolio()`?

You can — but `ensure_can_trade()` bundles three things you'd otherwise re-implement in every skill:

1. **Collateral-agnostic balance selection**: reads the correct token for the active `exchange_version` (pUSD on V2, USDC.e on V1).
2. **Failure-mode distinction**: returns a stable `reason` string across RPC outages, missing wallets, and genuine zero-balance cases.
3. **Safety buffer math**: the `max_safe_size` return is already clamped for fees and slippage — no off-by-one between skills.

## Sell pre-flight pattern

`ensure_can_trade()` covers buys (do I have collateral?). For sells, the equivalent pre-flight is "re-fetch positions immediately before each attempt." This catches the most common sell-side bug: a stop-loss / take-profit loop that fires every N seconds with a cached shares value, then submits stale orders after a previous sell already filled. Polymarket rejects the second attempt with [`Insufficient shares to sell`](/api/errors#insufficient-shares-to-sell).

### Pattern

```python theme={null}
def safe_sell(client, market_id, side, max_shares=None):
    positions = client.get_positions(venue="polymarket")
    pos = next((p for p in positions if p.market_id == market_id), None)
    if not pos:
        return None  # position cleared (sold / redeemed / resolved)
    fresh_shares = pos.shares_yes if side == "yes" else pos.shares_no
    if fresh_shares < 5.0:  # Polymarket's 5-share minimum
        return None
    sell_size = min(fresh_shares, max_shares) if max_shares else fresh_shares
    return client.trade(market_id=market_id, side=side, action="sell", shares=sell_size)
```

The reference `polymarket-weather-trader` skill uses this pattern in its exit logic.

### Server-side backstop

If a stale sell slips through, Simmer's server pre-checks the on-chain position before submitting and fails fast with a diagnostic message instead of round-tripping to Polymarket. This is a backstop, not a substitute: refreshing positions in your loop avoids the network round-trip entirely and lets your skill skip cleanly without writing a failure row.

### When to call it

For passive monitors / GTC strategies — at the top of every monitor cycle. For active traders — once per cycle is plenty; positions don't change between sell decisions within the same run unless your skill is multi-threaded.
