Who credits the user
RESOLUTION question is also open for
Customer Statements and reconciliation — see Reporting pack.What lands where
At resolution, winning contracts settle at 0.00. The credit goes to the participant’s clearing account — the same account the position was held in. Three things stay in that account and become buying power there:- settlement credits,
- realized profit,
- collateral released when a position closes or settles.
OdfSweep — which used to exist for this shape of problem — was removed on 2026-08-06 along with CreateFundedOrder and PreviewFundedOrder. If your design has a sweep step in it, delete the step.
What you must not do
Do not credit your user from your own balance when a market resolves. The user’s money is already in their participant account. A second credit on your side is a real double payout, not a display bug, and you will discover it in reconciliation after the user has withdrawn. Two partners shipped their settlement crediting code switched off in production because this was never written down. Switching it off was the right call with the information available. The safe design is one ledger of record per dollar:Mirroring settlement into your own wallet
If your product needs the money to appear in a wallet you operate, move it deliberately withCashMovementService rather than crediting it twice.
Detect the resolution
CreatePositionChangeSubscription tells you something happened on a market your user holds; the
market data stream or GET /v1/orderbook/{symbol} carries the values. Gate it properly —
settlement_preliminary: false does not mean resolved. See
Detect that a market resolved.Read the account, do not compute it
afterBalance.
GetAccountBalance returns InvalidArgument: invalid account for a participant clearing
account, so a ledger read is the working path — see Balance ledger.Move it with a WITHDRAWAL transfer
CashMovementService reasons are DEPOSIT, WITHDRAWAL and VENDOR_FEES, and direction is
fixed by the reason. You name only the participant account; we resolve your funding account
from your configured relationship. Transfers to your firm as a destination you choose, between
participants, or to an external destination are structurally impossible.Send no participant header
CashMovementService calls are firm-scoped. Do not send x-participant-id — that is the
opposite of the account-scoped reads.Round to 2 decimal places and keep the residue
172.395 and
1377.57275 have both been observed. There is no sanctioned handling for the sub-cent residue,
so your balance sheet cannot reach zero on those accounts. Track the residue rather than forcing
it.Never reuse a terminally-rejected idempotency key
idempotency_key replays its stored rejection. After a terminal reject,
use a new key; an identical retry will not make a fresh attempt.Fees do not come out of settlement
There is nosettlement_fee. It was cited to a partner in error and retracted. The only fees are the trading fees in the published Fee Schedule, and the platform never knows your own fee basis — you track your accruals yourself and reconcile against the daily Vendor Fees report. That report has no delivery mechanism today (“Delivery method TBD”), so agree the channel with your integration lead. See Settlement fields.
Detect that a market resolved
With the crediting question settled, the next problem is knowing that a market resolved at all. Resolution detection is two calls, not one: a stream tells you something happened, and a read tells you what the value is.The pipeline
CreatePositionChangeSubscription tells you a market you hold moved
The market data stream or GET /v1/orderbook/{symbol} carries the settlement values
settlement_px, settlement_preliminary, settlement_price_calculation_method,
settlement_price_calculation_text and settlement_set_time live here. /v1/orderbook/* needs
no x-participant-id.You gate, then you act
CreateInstrumentStateChangeSubscription does not carry the settlement result fields. It returns the updated Instrument and nothing more, so it is not a settlement source and not a gap-free replay source for settlement values. We told a partner the opposite and then corrected it. See Instrument state changes.
settlementPreliminary: false is not a resolved flag
Two more facts that break the obvious gates:
- Do not gate on
settlement_price_calculation_text. It is not guaranteed present on a settled instrument. A partner who gated on it had a market stuck suspended. settlementSetTimecan precedeexpirationDateby ~13 hours on a mark, so a populatedsettlementSetTimeis not evidence of resolution on its own.
The safest gate available today
This is a workaround, not a supported resolved flag. It is built from the facts we can stand behind: winning contracts settle at 0.00, de-scaled withpriceScale.
Require all of these before you treat an instrument as resolved:
settlementPreliminaryisfalse,settlementPx ÷ priceScaleis exactly1.00or0.00,settlementSetTimeis at or afterexpirationDate,- the instrument has reached a terminal lifecycle state.
settlementSetTime before expiry — holds for manual review. Do not auto-pay and do not auto-void.
settlementPriceScale is reserved and reads 0 — de-scale with priceScale. int64 fields serialize as strings in JSON. proto3 does not put a scalar at its default value on the wire, so an absent field and a zero are indistinguishable: treat a missing settlementPx as “no value”, never as 0.00.
Detection code
settlementPx: "100" at priceScale: 100 is **0.10, which is a mark, not a win. That is why the scale is read per instrument and never hard-coded.
camelCase while requests are snake_case and webhooks are snake_case, and the instrument-state
field on this response is not documented.automaticResolution controls nothing
[VERIFY] — the list of auto-resolving series and prefixes has to come from us, not from a guess. What is evidenced is that the atp and wta series resolve automatically, and that aec-atp is a real symbol prefix. No other prefix is evidenced: a plausible-looking sibling such as aec-wta is constructed, and a constructed prefix either never matches or matches the wrong markets. The snippets above therefore carry aec-atp only.
Keep the list in configuration, not in code, so you can extend it the day your integration lead confirms the authoritative set.
Preprod will not show you a resolution
Markets do not resolve in preprod. Instruments reachINSTRUMENT_STATE_EXPIRED in batches — 363, 455, 544 and 549 in a single session — with none resolving. One partner asked six times over 40 days to test a settlement flow and never could. Test your gate against captured or synthetic messages and treat the first production resolution as the real test, with the review queue switched on.
Settlement fields
Five fields carry settlement, and three of them mean less than their names suggest. Winning contracts settle at 0.00.The five fields
priceScale: settlement_px ÷ priceScale. Do not use
settlementPriceScale — it is reserved and reads 0, so dividing by it is a divide-by-zero or a
silently wrong number depending on your language.A non-binary value on a non-resolved instrument is a mark, not an outcome.false does not mean “resolved to an outcome” — marks carry false too,
and there is no positive resolved indicator anywhere on the API. Gating a payout on this field
alone pays out on marks. Use the full gate on
Detect that a market resolved.CALCULATION_METHOD_VALUE appears on the wire with no documented
meaning.expirationDate by ~13 hours on a mark, so a
populated settlement_set_time is not evidence of resolution. In the gate it is a necessary
condition (settlement_set_time at or after expirationDate), never a sufficient one.GET /v1/orderbook/{symbol}. CreateInstrumentStateChangeSubscription returns the updated Instrument and does not include them.
De-scaling, worked
priceScale: 1000 instrument is $0.10 — a mark, not a win. Two of 121 open ATP instruments publish priceScale: 1000, so a hard-coded divisor is right everywhere else and wrong by 10× there.
proto3 does not put a scalar at its default value on the wire, so an absent field and a zero are indistinguishable. Treat a missing settlement_px as “no value”, never as $0.00 — a losing contract and an unpopulated field look identical otherwise.
Fields that appear on the wire with no documented meaning
settlementPriceScale is in the same category with one difference: we know what it does, which is nothing. It is reserved and reads 0.
Fees
There is nosettlement_fee on the partner API. It was cited to a partner in error and retracted. The only fees are the trading fees in the published Fee Schedule.
Fee = Θ × C × p × (1 − p), rounded to the cent with banker’s rounding, at the taker coefficient — an ISV is structurally always the taker on CreateVendorOrder and can never earn the maker rebate. The coefficient changed on 2026-09-14 and there is no fee-schedule endpoint, so partners hand-edit it. The formula, the current coefficient and the scale handling are on Money on the wire.
SETTLEMENT_FEE exists as a suppressed internal LedgerEntryType. Requesting it returns Aborted / 409. Its existence in the enum is not a fee you pay — see Balance ledger.
Gross or net: unanswered
Cancelled and voided markets
Cancellation and void semantics are not settled, and we will not give you a rule that might be wrong. Hold these positions for review instead of paying them.What has been observed
Both items are unresolved. Until they are resolved, treat any settlement that is not exactly 0.00 as a case for a human.What to do in the meantime
Do not auto-pay on a non-binary settlement
Hold the position for review
settlementPx, the instrument’s priceScale,
settlementPreliminary and settlementSetTime attached. Those five values are what anyone will
ask you for.Reconcile manually against the ledger
RESOLUTION ledger entry is what actually happened to the account. Reconcile against it
rather than against your own expectation of the outcome — and note that whether that entry is
gross or net of commission is itself unresolved, see Gross or net.Do not originate a credit or a reversal
Escalate the first one you see