Arbitrage Risk Settings and Fees
This page covers the settings that decide how much an arbitrage Bot (platform: arbitrage) spends and when it trades: the risk block, the deploy dialog's per-order limits and loop.interval. It also explains how a lock is placed, when the Bot halts, and the fee estimates behind every edge check, including the fees overrides that replace them. For every key at a glance, see All arbitrage parameters.
Risk settings
The risk block sets what one lock may spend and when the Bot may trade. The deploy dialog adds a second layer; see Deployment risk limits.
risk.max_position_usdc
The most one lock may spend across both legs, in dollars. It's required and must be above 0.
It's a per-lock budget, not a cap on your total position. The Bot can lock again on the next loop, and each new lock can spend up to this amount again while the edge and your balances last, until Max open contracts or Max daily notional traded leaves no room; see Deployment risk limits.
The number of contracts in a lock is the smallest of these caps, rounded down:
| Cap | Formula | Illustrative value |
|---|---|---|
| Strategy budget | max_position_usdc ÷ (ask A + ask B) | $25 ÷ 0.95 = 26.3 |
| Kalshi balance | cash ÷ ask A | $40 ÷ 0.42 = 95.2 |
| Leg-B balance | balance ÷ ask B | $30 ÷ 0.53 = 56.6 |
| Max dollars per order | limit ÷ (ask A + ask B) | $1 ÷ 0.95 = 1.05 |
| Max contracts per order | limit | 1 |
| Max open contracts | limit − contracts already held in that leg's market, on each leg | 25 − 0 = 25 |
| Max daily notional traded | (limit − today's fills) ÷ (limit price A + limit price B) | $25 ÷ 0.952 = 26.2 |
| Visible depth | contracts at the best ask, on each leg | 120 and 35 |
With these numbers the lock is 1 contract, because the deploy limits Studio pre-fills for Kalshi + Polymarket US (1 contract and $1 per order) are the smallest caps. For Kalshi + Polymarket, Studio pre-fills 5 contracts and $5 per order ($5 ÷ 0.95 = 5.26), so the same lock would be 5 contracts. With deploy limits of 20 contracts and $25 per order, it would be 20 contracts.
The lock must also reach the larger of the two venue minimums: 1 contract on Kalshi and Polymarket US, and the market minimum (usually 5 shares) on Polymarket. Below that, the Bot skips.
For directional it caps the whole position instead of each buy; see directional.
risk.min_net_edge_bps
The minimum edge after estimated fees before the Bot trades, in basis points of the $1 payout: 100 bps is 1 cent per contract. The default is 0, so when you leave it out, any lock whose estimated fees don't exceed its gross edge can trade.
Net edge in bps = ((1 − ask A − ask B) × contracts − fee A − fee B) ÷ contracts × 10,000.
A lock is checked three times: at 1 contract on fresh quotes, at the lock size after a re-quote, and once more after the legs are ordered and quoted again. All three checks use the quoted asks, not the limit prices the orders are sent at.
Net edge after fees, against min_net_edge_bps
- Net edge
- Estimated fees
Dashed line: min_net_edge_bps 150
- Quoted asks: Kalshi YES 0.42 + Polymarket US NO 0.53 = 0.95Gross 500 bps. Fees $0.02 + $0.01 = 300 bps. Net 200 bps.Trades: 200 ≥ 150
- Quoted asks: Kalshi YES 0.47 + Polymarket US NO 0.50 = 0.97Gross 300 bps. Fees $0.02 + $0.01 = 300 bps. Net 0 bps.Skips: 0 < 150
- The first lock, filled at its 150 bps slippage limits: 0.43 + 0.54 = 0.97Gross 300 bps. Fees $0.02 + $0.01 = 300 bps. Net 0 bps.Already traded: realized 0 bps
Basis points of the $1 payout
min_net_edge_bps: 150 and max_leg_slippage_bps: 150. Each bar is the gross edge, $1 minus the two asks. The estimated fees come off the right end, and what’s left is the net edge the Bot compares with the dashed line. The check uses quoted asks, so the first lock passes, but if both legs fill at their slippage limits its realized edge is 0.Two things can take a filled lock below your minimum:
- Slippage. Each order may fill up to
max_leg_slippage_bpsabove its ask, and the edge check doesn't include that allowance. - Moves during the hedge. The second leg is sent at a fresh ask without a new edge check.
At 1 contract, Kalshi's fee estimate rounds up to a whole cent: $0.02 (200 bps) for prices between about 18¢ and 82¢, and $0.01 outside that range (standard 0.07 rate; INX and NASDAQ100 tickers are $0.01 at every price, and a fees.a override replaces this estimate). Small edges rarely pass the first check.
For directional the check is different; see directional. For custom, execute_lock always applies this minimum, whatever the rule's own thresholds.
risk.max_leg_slippage_bps
How far above the quoted ask each order's limit may be, as a share of the ask. The default is 25 (0.25%). It applies to both legs of a lock and to directional buys.
Limit = ask × (1 + max_leg_slippage_bps ÷ 10,000), capped just below $1.
Unlike min_net_edge_bps, these basis points are of the price, not of the $1 payout. The limit is then rounded to the venue's price grid, so small values can round away:
| Venue | How the limit is rounded | Ask 0.42 with 25 bps | 100 bps | 150 bps |
|---|---|---|---|---|
| Kalshi | Nearest cent | 0.42105 → 42¢ | 0.4242 → 42¢ | 0.4263 → 43¢ |
| Polymarket US | Nearest cent | 42¢ | 42¢ | 43¢ |
| Polymarket | Down to the market's price step | 42¢ | 42¢ | 42¢ with a 1¢ step |
At a 42¢ ask on Kalshi, anything below about 120 bps still sends 42¢. An explicit 0 sends the order at the ask.
On Polymarket US, a NO order's limit is sent as a YES price and rounded there. A NO ask of 0.53 with 150 bps gives 0.53795, sent as YES 0.46, which is a NO limit of 0.54.
risk.max_unhedged_seconds
The longest the second leg may take, in seconds. The default is 10.
The clock starts once the first leg has fully filled. It covers re-quoting the second leg, sending it and confirming the fill. If that took longer than this limit, the Bot cancels its open orders and halts, even if the hedge filled. The check happens after the hedge returns: it never cancels the hedge early, and it never sells the first leg.
0halts the Bot after every lock, because some time always passes. Don't use 0.- A partly filled or unconfirmed first leg halts the Bot whatever this is set to.
directionaldoesn't use it.
risk.accept_resolution_source_mismatch
Your explicit acceptance that the two markets may settle from different sources or rules. The default is false.
It changes nothing about how the Bot trades. It's a record. When Studio's AI finds a settlement difference between the two markets, it lists it and asks whether you accept it. Studio won't save a strategy whose YAML, comments included, contains a settlement caveat, such as the phrases "resolution-source mismatch", "resolution caveat", "mismatch risk", "candidate only" or "trimmed mean vs spot", until this is true. With true, Studio describes the Bot as carrying accepted basis risk, never as risk-free.
Setting it doesn't make a lock safer. If the markets settle differently, both legs can lose. Studio's own crypto templates, for example, warn that Kalshi and Polymarket settle their short-window crypto markets on different price sources, so the legs can resolve differently near a price boundary.
How a lock is placed
Placement is the riskiest part of a lock: between the first fill and the second, you hold one side alone. This applies to complementary and to execute_lock in custom.
How one lock is placed, and where it can stop
- Normal path
- Skip: no position
- Safety halt: stops trading
- 1 · ScreenPrice both lock pairs at 1 contractEstimated fees included. Quotes must be at most 5 seconds old.If: Best net edge below
min_net_edge_bpsSkip this loopNo orders are placed. - 2 · SizeRe-quote and size the lockThe smallest cap, rounded down. The edge is checked again at that size.If: Below the venue minimum, or the edge is now too lowSkip this loopNo orders are placed.
- 3 · Order the legsChoose which leg goes firstKalshi + Polymarket: Polymarket first. Kalshi + Polymarket US: the leg with more visible depth first; Kalshi on a tie or when a depth is unknown. Re-quote, then check the edge and both legs’ depth.If: The edge or the depth is no longer enoughSkip this loopNo orders are placed.
- 4 · First legSend the first order at ask + slippageKalshi: immediate-or-cancel. Polymarket US: fill-or-kill. Polymarket: limit, then the rest is cancelled.If: Nothing filledSkip this loopNo position was opened.If: Only part of the order filledSafety haltOpen orders are cancelled. The partial fill is not hedged.If: The send failed after it may have reached the venue, or the fill status is unknownSafety haltNothing is cancelled and no hedge is sent. Check the venue for an order or position.
Unhedged window, timed by
max_unhedged_seconds: from the first leg’s full fill until the hedge order returns- 5 · HedgeSend the second leg for the filled quantityAt a fresh ask + slippage. There is no new edge check.If: The order fails, or a different quantity fillsSafety haltOpen orders are cancelled. The first leg stays open.
- 6 · ClockCheck how long the hedge tookCompared with
max_unhedged_seconds(default 10 seconds) after the hedge returns. It never cancels the hedge early.If: It took longer thanmax_unhedged_secondsSafety haltOpen orders are cancelled, even if the hedge filled. - 7 · HoldHold both legs until the markets settleNo take-profit or early exit. A later loop can lock again.
Which leg goes first:
- Kalshi + Polymarket: Polymarket, always. Its order is a limit order; whatever doesn't fill at once is cancelled, and the Bot waits up to about 4 seconds to confirm the fill before it sends the Kalshi leg.
- Kalshi + Polymarket US: when both books show depth at the best ask, the leg with more depth goes first. Otherwise, or on a tie, Kalshi goes first.
Kalshi orders are immediate-or-cancel limit orders, and Polymarket US orders are fill-or-kill, so no order is left resting.
The second leg is sent for the quantity the first leg filled, at a fresh ask plus slippage, without a new edge check. If prices moved, the hedge can cost more than planned.
Quotes must be no more than 5 seconds old at each check.
Safety halts
When something goes wrong once the Bot starts placing a lock, it writes a safety halt. For a partial first leg, or a hedge that failed, filled a different quantity or took too long, it first cancels its open orders. When the first order failed or its fill status is unknown, it halts without cancelling anything. It halts when:
- sending the first order failed after it may have reached the venue, or its fill status is unknown,
- on Kalshi + Polymarket, the Bot refused the Polymarket order just before sending it, for example because your Polymarket collateral doesn't cover the order at its slippage limit, or the Bot's Polymarket trading approvals failed when it started (no order is placed, but the Bot still halts),
- the first leg filled only partly (the partial fill is not hedged),
- the hedge order failed,
- the hedge filled a different quantity than the first leg,
- the hedge took longer than
max_unhedged_seconds, even if it filled.
Halt log lines start with SAFETY_HALT and name the reason:
| Reason in the log | What happened |
|---|---|
first_leg_unknown_or_failed | Sending the first order failed after it may have reached the venue. On Kalshi + Polymarket, this also covers a Polymarket order refused just before sending. |
first_leg_fill_unknown_after_order | The first order was sent, but its fill couldn't be confirmed. |
first_leg_filled_second_leg_failed | The first leg filled, then the hedge failed, filled a different quantity or was late, or the first leg filled only partly. The log also names the cause, such as second_leg_exceeded_max_unhedged_seconds (the hedge was late), second_leg_fill_mismatch (the hedge filled a different quantity) or first_leg_partial_fill_before_second_leg (the first leg filled only partly and was not hedged). |
A halt doesn't unwind anything. There's no automatic recovery or flattening: a one-sided position stays open until the market settles or you close it yourself. While halted, the Bot logs SAFETY_HALT and places no orders.
How a halt clears:
- Rolling strategies (leg A uses
series_ticker, or leg B rolls) clear a halt on their own when the first leg was on Kalshi and that market has closed or settled, when about 30 minutes have passed since the halted markets closed, or when Kalshi confirms that an unconfirmed first order was never placed or never filled. Trading then resumes on the current market. Clearing the halt doesn't close anything from the halted window. - Fixed-market strategies never clear a halt on their own. The Bot stays halted.
When a Bot halts, check both venues for a one-sided position and decide what to do with it yourself. For a fixed-market Bot, stop it once you've reconciled both accounts. See Monitoring.
Some stops don't write a halt:
- API errors. When the deploy dialog's API error limit is reached inside its error window (3 errors in 5 minutes by default), the Bot pauses new locks for one window, logs "API error breaker tripped", then resumes on its own. Errors older than the window don't count.
- Fixed Kalshi market closed. The Bot stops.
- Missing data. Stale quotes, a one-sided book, a Polymarket fee rate, or a position that can't be read make the Bot skip that loop rather than guess.
Deployment risk limits
Four of the deploy dialog's limits size every buy: both legs of a lock, and each directional buy. Sizing checks them before the first order of a lock; the hedge is never refused, because that would leave the first leg unhedged.
| Limit | What it caps | Studio pre-fills |
|---|---|---|
| Max contracts per order | Contracts in one order | 1 |
| Max dollars per order | One order's cost: both legs together for a lock | $1 |
| Max open contracts | Contracts held in each leg's current market, both sides, as that venue reports them, plus the new order | max_position_usdc, rounded up |
| Max daily notional traded | This Bot's fills since 00:00 UTC on both legs, valued at their limit prices, plus the new order | max_position_usdc rounded up, or $10 if that's higher |
For Kalshi + Polymarket, the dialog raises Max contracts per order, Max dollars per order and Max open contracts to at least 5, so an order can reach Polymarket's usual 5-share minimum. When the running Bot's limits can be verified, the redeploy dialog starts from those limits. If readback is missing or unverified, review the suggested limits before confirming an update; see deployment risk settings. See the sizing table in risk.max_position_usdc for how they combine.
Max open contracts counts everything the account holds in that market, including contracts you or another Bot bought there. If the Bot can't read a position, it skips the loop. The daily total is kept in a file next to the Bot's safety-halt marker: restarting the Bot doesn't reset it, but a runner upgrade that rebuilds the runner does.
Arbitrage Bots don't read Max daily loss. The API error limit and window set the stop described in Safety halts.
Each leg trades from your account on that venue, so its balance is shared with every other Bot (on any runner) and any manual trading that uses the same account. See Run.
loop.interval
Seconds the Bot waits after each loop before starting the next. It's required, and must be an unquoted number of 10 or more. Lower values are rejected with "must be >= 10 seconds (prevents API rate limiting)", and decimals are dropped.
The interval doesn't relax quote freshness: quotes must be at most 5 seconds old whenever the Bot decides, whatever the interval. With bucket.select: all, the list of matched buckets is refreshed about every half interval.
Fees
The edge checks subtract an estimate of each leg's taker fee. The estimates never change what a venue actually charges.
Built-in fee estimates
| Leg | Estimate per order | Rounding | Example, 1 contract |
|---|---|---|---|
| Kalshi | 0.07 × contracts × p × (1 − p). The rate is 0.035 when the ticker starts with INX or NASDAQ100 | Up to the cent | At 0.42: $0.0171 → $0.02 |
| Polymarket | The market's live fee rate × min(p, 1 − p) × shares | None | Depends on the market's rate |
| Polymarket US | 0.05 × contracts × p × (1 − p) | Nearest cent (ties to even) | At 0.53: $0.01246 → $0.01 |
Here p is the price paid. The Kalshi half rate depends only on the ticker's text before the first hyphen: a ticker such as KXINXU-… starts with KX, so it uses 0.07.
Polymarket's rate is looked up for each token and reused for up to 5 minutes. If the lookup fails, the Bot doesn't trade that loop.
Fee overrides
fees replaces the estimate for one or both legs. a is the Kalshi leg and b is the leg-B venue.
fees:
a:
flat_bps: 700 # Kalshi estimate: 7% of notional, not rounded
b:
fee_rate_bps: 200 # Polymarket: this rate instead of the live lookupflat_bps(either leg): fee = flat_bps ÷ 10,000 × price × contracts. It isn't rounded.fee_rate_bps(leg B): fee = fee_rate_bps ÷ 10,000 × min(p, 1 − p) × shares. On Polymarket it replaces the live lookup. On Polymarket US it replaces the published formula with this different formula, not just a different rate.fees.a.fee_rate_bpsis rejected. It's an input to the Polymarket fee formula, and leg A is always Kalshi. Usefees.a.flat_bps, or leavefees.aout for the built-in Kalshi estimate.- Set at most one key per leg: both gives "set flat_bps or fee_rate_bps, not both". Negative values are rejected. Write unquoted whole numbers: a quoted
"5"is rejected, and so is a decimal such as2.5. feesand each leg must be blocks of keys:fees: 5is rejected.0counts:flat_bps: 0assumes that leg is free, and on Polymarket it skips the live lookup. An empty or null leg (a: {}ora: ~) means no override.- Keys other than
aandb, and unknown keys inside a leg, are ignored. - Studio's strategy preview shows the fee source for leg B only. A
fees.aoverride is still used even though the preview doesn't list it.
# Rejected: fee_rate_bps is a Polymarket input and can't be used on the Kalshi leg
# error: fees.a.fee_rate_bps: is a Polymarket fee input and cannot be used on the Kalshi leg A
version: 1
platform: arbitrage
strategy: complementary
venues:
a:
platform: kalshi
market:
ticker: KXFEDDECISION-26OCT-C25
b:
platform: polymarket_us
market:
slug: fed-cuts-rates-25bps-october-2026
mapping:
a_yes_equals: b_yes
risk:
max_position_usdc: 25
loop:
interval: 30
fees:
a:
fee_rate_bps: 700Overrides feed the choice between the two lock pairs, every min_net_edge_bps check, the directional check, and the custom fields fee_a, fee_b and net_edge_bps.
Warning: An estimate below the real fee makes the edge check optimistic, so a filled lock can earn less than
min_net_edge_bps, or lose. Flat overrides aren't rounded, so even a high rate can come in under the rounded real fee at low prices. Kalshi charges $0.01 for 1 contract at 0.10, whileflat_bps: 700estimates $0.007.