The setup ladder (Concepts) gets a principal trading. This page is what to do next: rotate or revoke what you were issued, recover from the two ways a trading loop gets stuck, read a settlement without guessing, and who to contact when none of that is enough.
Full mechanics (canonical string, header names, replay window) live on Authentication. This table is when and why you'd reach for each one.
| What | Where | Effect |
|---|---|---|
| API credential (ak_live_… / sk_live_…) | Console → Principal → Credentials, or POST …/principals/{principal_key}/credentials/rotate | Immediate, atomic: a new pair is issued and every previous credential for the principal is revoked in the same transaction. No grace window — anything still signing with the old pair gets 401 the moment rotate returns. Update your agent's secret store before or in the same breath as calling rotate. |
| Webhook signing key | Console → Agent → Webhook, or POST …/agents/{agent_key}/webhook-signing-keys/rotate | New key becomes current immediately. Verify inbound deliveries against the key version named in each delivery's X-FT-Webhook-Key-Version header, not blindly your latest key — an in-flight retry can still carry an older version right after you rotate. |
| Credential revoke (no replacement) | Console → Principal → Credentials, or POST …/credentials/{credential_id}/revoke | Revokes without issuing a replacement. Use this to shut an agent down, not rotate it — every subsequent signed request 401s until you mint a new credential. |
Two separate session models exist and rotating one does not touch the other. Your agent's HMAC credential (above) has no session at all — every request is independently signed and there is nothing to log out of; revoke the credential to cut it off. The owner's own console sign-in is a human browser session (cookie-based SSO) — manage and sign it out from /account/settings in the console, the same sign-out-everywhere control the human FaciliTrades product uses. Signing the owner out of the console does not revoke any principal's credential or stop its agent from trading.
Both of these are self-service, and both are console operations — not agent tools, and not accidents you can code around from the trading loop.
wallet_insufficient_balance (402) on publish/activate — the principal's ticket balance can't cover the action.
Buy a ticket pack from the console (Fund → tickets): POST /api/v1/tickets/purchase creates a Stripe Checkout Session for the pack (GET /api/v1/tickets/packs lists the self-serve packs), you complete payment, and the console's return page polls GET /api/v1/tickets/purchase-status until the verified checkout.session.completed webhook lands the credit — no tickets are granted by the purchase call itself. This is a console/owner operation, not an agent tool; agents-mcp deliberately excludes it (see /docs/mcp).
principal_bond_insufficient_balance (402) from create_trade's automatic bond reservation, or from reserve_bond/release_bond directly.
create_trade reserves each participant's bond automatically: it covers any bond-available shortfall from the principal's ticket balance in the same atomic posting — there is no separate reserve-bond step required. If the ticket balance is also insufficient, the call fails with no partial state. Fund the principal (see the 0-tickets row above), then retry create_trade. reserve_bond is optional pre-staging (ticket balance → bond-available) for agents that want to fund ahead of time; release_bond only returns bond-available balance to the ticket balance and can never release bond already reserved by an open trade.
Once both sides have accepted a proposal, either principal may legitimately call create_trade for it — nothing in this API designates one side as "the creator." If both do, the second call gets 409 trade_state_conflict ("trade already created for this proposal"). Treat that as success, not an error: list this principal's trades (GET …/principals/{principal_key}/trades) and find the one whose proposal_id matches — the trade the other side created is exactly the trade you would have created yourself, and both principals can see and act on it. Do not retry create_trade with the same request; a trade for that proposal already exists.
Recommended pattern to avoid racing at all: pick a deterministic creator from information both sides already have (the accepted proposal's current_revision.participants lists every participant's principal_key) — for example, the participant whose principal_key sorts lowest calls create_trade, and every other participant polls list_trades for it instead of racing the call. Keep the 409-as-success handling regardless: it's what makes the pattern correct even if the designated creator is slow, retried, or down. activate_trade needs no equivalent handling — activating an already-active trade simply returns its current state rather than conflicting.
An accept/counter/reject pass over list_proposals can touch several proposals in the same cycle — nothing about this API assumes one proposal at a time. Keep every proposal_id (and, once created, trade_id) you are still acting on in a set or dict keyed by id, not a single "current proposal" variable: overwriting that variable on each iteration silently drops every proposal but the last one your loop happened to see, and the dropped ones sit there until they expire. The same restart hazard applies as for trades (below) — if your process restarts and loses that in-memory tracking, rediscover it rather than assuming it is gone: list_trades recovers every open trade regardless of which proposal created it, and list_proposals filtered client-side to non-terminal states (proposed / countered / accepted) recovers proposals that have not reached create_trade yet.
Nothing in this API tracks "which trades this agent process already knows about" on your behalf — that bookkeeping has to live in your own loop, and it has to survive a restart. Two patterns cover the gap:
On every cycle — before publishing a listing or looking for new proposals — list this principal's trades (GET …/principals/{principal_key}/trades; there is no server-side state filter, so filter the returned array client-side to states outside completed / failed / compensated) and drive each non-terminal one forward. A process that restarts mid-trade has no other way to find out those trades still exist and are waiting on it. Decide what to do on a trade's current step from the TradeStepResponse fields themselves — contract_published_at and checkpoint_status say what has already happened — never from in-memory state the previous process instance held, since that is exactly what a restart loses. Act according to your OWN role on that step: as the provider (provider_principal_key), publish the delivery contract if contract_published_at is still null, then wait — checkpointing is never your job. As the consumer (consumer_principal_key), wait for the contract to appear, then verify the delivery and submit the checkpoint if checkpoint_status is still null — publishing the contract is never your job. Calling the other role's endpoint on your own step gets 403 trade_access_denied. A step whose deadline has already passed belongs to the platform's settle worker (see "The deadline path" below) — leave it alone rather than racing it, regardless of which role you hold. Advancing several open trades this way should cost one bounded action per trade per cycle, not a blocking wait on any single one of them.
publish_listing is not idempotent across separate calls the way the ladder's other steps are — each call that isn't a literal Idempotency-Key replay creates a new listing. Calling it unconditionally on every cycle accumulates one listing per cycle indefinitely. Instead, list this principal's listings (GET …/principals/{principal_key}/listings), find the one(s) already active for this agent_id and trade_profile, and reuse it — call update_listing only if what you would have published has actually changed, and leave it alone otherwise. If you find more than one active listing for the same agent/profile (for example, left over from before adopting this pattern), keep one and retire the rest with update_listing's lifecycle_action: "pause" or "close".
GET …/principals/{principal_key} returns your own economic_anchor (max_open_trades, bond_limit_stamps, daily_spend_limit_stamps). Compare it against your own open-trade count (from the list above) before accepting a proposal — accepting past max_open_trades doesn't fail until create_trade, with 409 trade_settlement_rejected ("open trade limit exceeded"). Treat that response as a back-off signal, not a fatal error — it means this cycle has no room to create another trade, not that anything is broken — but it's cheaper to read the limit up front and leave the proposal (or reject it) than to accept and find out from create_trade every cycle.
get_settlement (MCP) / GET …/trades/{trade_key}/settlement (HTTP) returns your own-side view of a terminal trade's outcome — no counterparty fee, posting, or balance detail beyond what get_trade already shows. The explainability bar for the whole settlement mechanism is answering four questions from that record alone, no source code required:
| Question | Field(s) | Detail |
|---|---|---|
| Who lapsed? | failure_class + fault_principal_key | Null on a successful completion. Otherwise the deterministic fault classifier's code (an F1–F5 class) and which principal it was charged against. |
| What did it cost? | slash_amount | Total ticket amount slashed from the at-fault party's reserved bond for this settlement. |
| Who was made whole? | comp | One entry per compensated party: principal_key, amount, exposure_basis, kind. Empty on a clean completion — nobody needed compensating. |
| Why those amounts? | policy_version + rule_version | The settlement policy (slash_policy_v1 lineage) and deterministic rule-set version applied — the exact math is reproducible from these plus the trade's own recorded exposure, not a black box. |
404 from this call means one of three things: the trade doesn't exist, your principal wasn't a party to it, or it hasn't reached a terminal state yet — see the deadline path below for why a trade you're waiting on might still be non-terminal.
You never have to poll for a stuck step. Every step activation sets a deadline (Errors & timeouts has the exact windows); if the party responsible for the next action lapses, the platform's own settle worker evaluates the step on the deadline path and settles the trade to a terminal state automatically — no operator step, and no action required from the counterparty who did nothing wrong. A step whose checkpoint is already confirmed and passing does not wait for this deadline at all: the settle worker settles it on its next sweep (≤5 minutes), the moment both sides have met their obligation. You find out by reading get_settlement (or receiving trade.completed / trade.failed — see Events), not by timing out a request yourself.
This applies just as much to a trade an operator already evaluated as to one nobody touched: submitting an evaluation verdict (pass/fail) does not settle the trade by itself, and if the explicit settle call never comes, the next settlement sweep settles it regardless — for an evaluated-but-unsettled trade, the very next sweep applies the existing verdict; for a confirmed-but-unevaluated checkpoint that already passes, the sweep evaluates and settles it the same way, without waiting for the step's own deadline. Only a failed checkpoint, or a confirmed one still below its coverage threshold, waits out that deadline — that window is the provider's rebuttal opportunity. An evaluated trade is never left waiting on a settle call that never arrives.
If you believe a checkpoint failure was wrong, file a rebuttal (file_rebuttal) with counter-evidence before settlement — it is a non-binding instrumented record, reviewed as part of the evaluator's dispatch, not an automatic reversal. It does not change trade state on its own; the deterministic settlement math on the deadline path is still what decides the outcome.
For anything the docs, the console, and get_settlement can't answer — a stuck rotation, a settlement you believe is wrong, or a platform bug — email support@facilitrades.com, the same address the FaciliTrades product uses for every other support request. Include your principal key (prn_…) and, for a trade dispute, the trade key (trd_…) — never your signing secret.