Video 9: Kalshi Trading Bot Version 3
This third iteration brings in an entirely new approach of routing orders through the Kalshi backend API rather than the front end. The story behind Version 3 is that ongoing Kalshi updates kept breaking the HUD for Version 1 and Version 2. To solve this, this strategy implements similar logic by bypassing the front end entirely and hooking directly into the Kalshi backend. This results in a much more lightweight and accurate setup.
YouTube Video
Git Installation Required
This setup requires the installation of Git. Please download and install Git using the link below:
Download GitAPI key Filename
apikey
Private Key Filename
privatekey
Nightshark Code
; =========================
; Strategy Configuration
; =========================
timeDelay := 8 ; activation window in remaining minutes
Entry := 0.85 ; enter when side price >= 85c
Exit := 0.40 ; stop/exit when entered side price < 40c
assets := ["BTC"] ; assets to trade
orderSize := 1
; =============================================================
; Kalshi Order API Config
; DO NOT TOUCH BELOW VALUES UNLESS YOU KNOW WHAT YOU ARE DOING
; =============================================================
enableLiveOrders := true
pollIntervalMs := 700
restDelay := 2000 ; ms to let order rest before checking position
entryDiff := 0.02 ; added to price on BUY orders (aggressive entry)
exitDiff := 0.02 ; subtracted from price on SELL orders (aggressive exit)
kalshiApiBase := "https://api.elections.kalshi.com"
kalshiCryptoExchangeIndex := 2
assetSeriesMap := { "BTC": "KXBTC15M", "ETH": "KXETH15M", "SOL": "KXSOL15M", "XRP": "KXXRP15M", "DOGE": "KXDOGE15M", "HYPE": "KXHYPE15M", "BNB": "KXBNB15M" }
kalshiApiKeyId := ""
kalshiApiKeyFile := "reactions/apikey.json"
kalshiPrivateKeyFile := "reactions/privatekey.json"
kalshiPrivateKeyPath := ""
kalshiApiKeyResolvedPath := ""
kalshiPrivateKeyResolvedPath := ""
kalshiKeyPassphrase := ""
kalshiSignerPath := ""
; Track one open position per asset.
positions := {}
assetPhase := {} ; WAIT_WINDOW | MONITORING | IN_POSITION
assetSessionQuarter := {} ; quarter index currently tracked per asset
assetLastPriceLogTick := {} ; throttled logging control
kalshiShardFundingSessionStatus := {}
kalshiShardPendingTransfers := {} ; retained across market changes until reconciled
kalshiAggregateBalanceSnapshot := ""
kalshiBalanceNextRequestTick := 0
kalshiBalanceRetryCount := 0
LoadKalshiCredentialsFromFiles()
if (kalshiSignerPath = "")
kalshiSignerPath := FindNightSharkSigner()
Log("Application Started | Entry: " Entry " | Exit: " Exit " | Window: last " timeDelay "m")
if (!StartupCredentialCheck())
return
loop {
for _, asset in assets {
snapshot := GetKalshiMarketSnapshot(asset)
if !IsObject(snapshot) {
noDataKey := asset "|NO_DATA_" CurrentQuarterIndex()
if !IsObject(logOnceKeys)
logOnceKeys := {}
if !logOnceKeys.HasKey(noDataKey) {
logOnceKeys[noDataKey] := true
Log(asset " Waiting for market data")
}
continue
}
up := snapshot.up
down := snapshot.down
minsLeft := (snapshot.minutesLeft != "") ? snapshot.minutesLeft : MinutesRemainingInQuarter()
inEntryWindow := (minsLeft > 0 && minsLeft <= timeDelay)
quarter := (snapshot.closeTime != "") ? snapshot.closeTime : CurrentQuarterIndex()
if (!assetSessionQuarter.HasKey(asset) || assetSessionQuarter[asset] != quarter) {
assetSessionQuarter[asset] := quarter
assetPhase[asset] := "WAIT_FUNDING"
assetLastPriceLogTick[asset] := 0
LogOnceReset(asset)
Log(asset " New session (" snapshot.marketTicker ", shard " kalshiCryptoExchangeIndex ")")
continue
}
if IsObject(positions[asset]) {
pos := positions[asset]
current := (pos.side = "UP") ? up : down
assetPhase[asset] := "IN_POSITION"
if (CurrentQuarterIndex() != pos.quarterIndex) {
Log(asset " " pos.side " held to resolution")
positions.Delete(asset)
assetPhase[asset] := "WAIT_FUNDING"
continue
}
if (current < Exit) {
Log(asset " Stop-loss triggered @ " Fmt(current) " < " Fmt(Exit))
sellConfirmed := false
loop, 4 {
if (A_Index > 1) {
freshSnap := GetKalshiMarketSnapshot(asset)
if IsObject(freshSnap)
current := (pos.side = "UP") ? freshSnap.up : freshSnap.down
}
if SellPosition(asset, pos.side, current, "exit_below_" Exit) {
Log(asset " Position closed")
sellConfirmed := true
break
}
if (A_Index < 4) {
Log(asset " Sell retry " A_Index "/3")
Sleep 1000
}
}
if (!sellConfirmed)
Log(asset " SELL FAILED after 4 retries")
positions.Delete(asset)
assetPhase[asset] := "STOPPED_OUT"
LogOnce(asset, "STOPPED_OUT", asset " Stopped out, waiting for next session")
}
continue
}
if (assetPhase[asset] = "STOPPED_OUT") {
LogOnce(asset, "STOPPED_OUT", asset " Stopped out, waiting for next session")
continue
}
if (assetPhase[asset] = "BUY_FAILED") {
LogOnce(asset, "BUY_FAILED", asset " Buy failed earlier, waiting for next session")
continue
}
if (assetPhase[asset] = "HAS_POSITION") {
LogOnce(asset, "HAS_POSITION", asset " Existing position, waiting for next session")
continue
}
if (assetPhase[asset] = "FUNDING_FAILED") {
LogOnce(asset, "FUNDING_FAILED", asset " Funding request rejected; waiting for next session")
continue
}
if (assetPhase[asset] = "WAIT_FUNDING") {
if (minsLeft = "" || minsLeft <= 0) {
LogOnce(asset, "FUNDING_WINDOW_ENDED", asset " Market closed while waiting for funding")
continue
}
fundingFailed := false
if (!EnsureKalshiCryptoShardFunding(asset, fundingFailed)) {
if (fundingFailed)
assetPhase[asset] := "FUNDING_FAILED"
continue
}
positionReadOk := false
existingPosition := HasExistingPosition(snapshot.marketTicker, positionReadOk)
if (!positionReadOk) {
LogOnce(asset, "FUNDING_POSITION_WAIT", asset " Funding ready; waiting for existing-position check")
continue
}
if (existingPosition) {
assetPhase[asset] := "HAS_POSITION"
Log(asset " Existing position found, waiting for next session")
} else {
assetPhase[asset] := "WAIT_WINDOW"
Log(asset " Shard funding ready")
}
; Refresh the market, quote and entry clock after a funding wait.
continue
}
; --- Waiting for entry window ---
if (!inEntryWindow) {
LogOnce(asset, "WAIT_WINDOW", asset " Waiting for last " timeDelay " minutes")
assetPhase[asset] := "WAIT_WINDOW"
continue
}
; --- Entry window open: monitoring prices ---
if (!assetPhase.HasKey(asset) || assetPhase[asset] != "MONITORING") {
assetPhase[asset] := "MONITORING"
Log(asset " Monitoring for entry > " Fmt(Entry) " | UP: " Fmt(up) " DOWN: " Fmt(down))
}
side := ""
entryPrice := ""
if (up >= Entry || down >= Entry) {
if (up >= down) {
side := "UP"
entryPrice := up
} else {
side := "DOWN"
entryPrice := down
}
}
if (side = "")
continue
if (entryPrice > 0.98) {
LogOnce(asset, "PRICE_HIGH", asset " Price too high (" entryPrice "), waiting for drop")
continue
}
if !CanPlaceOrderNow(asset) {
LogOnce(asset, "NO_CREDS", asset " Missing credentials, cannot place orders")
continue
}
if HasExistingPosition(snapshot.marketTicker) {
assetPhase[asset] := "HAS_POSITION"
Log(asset " Existing position found, waiting for next session")
continue
}
Log(asset " " side " triggered @ " Fmt(entryPrice) " — placing order")
buyConfirmed := false
loop, 4 {
if (A_Index > 1 && HasExistingPosition(snapshot.marketTicker)) {
Log(asset " Position detected before retry " A_Index ", skipping")
buyConfirmed := true
break
}
if (A_Index > 1) {
freshSnap := GetKalshiMarketSnapshot(asset)
if IsObject(freshSnap)
entryPrice := (side = "UP") ? freshSnap.up : freshSnap.down
if (A_Index = 4)
entryPrice := 0.97
}
if BuyPosition(asset, snapshot.marketTicker, side, entryPrice) {
Log(asset " Position confirmed")
buyConfirmed := true
break
}
if (A_Index < 4) {
Log(asset " Retry " A_Index "/3")
Sleep 1000
}
}
if (buyConfirmed) {
positions[asset] := { side: side, entry: entryPrice, ticker: snapshot.marketTicker, quarterIndex: CurrentQuarterIndex() }
assetPhase[asset] := "IN_POSITION"
assetLastPriceLogTick[asset] := 0
Log(asset " Monitoring stop-loss @ " Fmt(Exit))
} else {
assetPhase[asset] := "BUY_FAILED"
Log(asset " Order not filled after 4 attempts, skipping session")
}
}
Sleep %pollIntervalMs%
}
LogOnce(asset, reason, msg) {
global logOnceKeys
if !IsObject(logOnceKeys)
logOnceKeys := {}
key := asset "|" reason
if (logOnceKeys.HasKey(key))
return
logOnceKeys[key] := true
Log(msg)
}
LogOnceReset(asset) {
global logOnceKeys
if !IsObject(logOnceKeys)
return
toRemove := []
for k, _ in logOnceKeys {
if (InStr(k, asset "|") = 1)
toRemove.Push(k)
}
for _, k in toRemove
logOnceKeys.Delete(k)
}
Fmt(price) {
return Round(price + 0, 2)
}
BuyPosition(asset, marketTicker, side, price) {
global orderSize
return PlaceKalshiOrder("BUY", marketTicker, side, price, orderSize)
}
SellPosition(asset, side, price, reason, clientOrderId := "") {
global positions, orderSize
if !IsObject(positions[asset]) || !positions[asset].HasKey("ticker")
return true
return PlaceKalshiOrder("SELL", positions[asset].ticker, side, price, orderSize, clientOrderId)
}
HasExistingPosition(marketTicker, ByRef queryOk := false) {
global kalshiCryptoExchangeIndex
queryOk := false
if (marketTicker = "")
return false
res := KalshiSignedRequest("GET", "/trade-api/v2/portfolio/positions?ticker=" marketTicker "&exchange_index=" kalshiCryptoExchangeIndex)
if !IsObject(res) || (res.status != 200)
return false
queryOk := true
qty := JsonField(res.body, "position_fp")
if (qty = "")
qty := JsonField(res.body, "market_exposure_dollars")
return (qty != "" && Abs(qty + 0) > 0)
}
PlaceKalshiOrder(action, marketTicker, side, price, size, clientOrderId := "") {
global enableLiveOrders, kalshiApiKeyId, kalshiPrivateKeyPath, restDelay, entryDiff, exitDiff
if (!enableLiveOrders)
return true
if (kalshiApiKeyId = "" || kalshiPrivateKeyPath = "")
return false
if (marketTicker = "")
return false
isBuy := (ToLower(action) = "buy")
contractPrice := price + 0
if (isBuy)
contractPrice := contractPrice + entryDiff
else
contractPrice := 0.01 ; IoC+reduce_only: sell at any price, exchange fills at best bid
contractCents := KalshiClampCents(Round(contractPrice * 100))
apiAction := ToLower(action)
apiSide := KalshiEventOrderSide(apiAction, side)
apiCents := KalshiEventOrderCents(apiAction, side, contractCents)
priceText := KalshiFixedPriceFromCents(apiCents)
countText := KalshiFixedCount(size)
timeInForce := isBuy ? "good_till_canceled" : "immediate_or_cancel"
reduceOnly := isBuy ? "false" : "true"
if (clientOrderId = "")
clientOrderId := BuildClientOrderId(marketTicker, apiAction, apiSide)
payload := "{"
. """ticker"":""" marketTicker ""","
. """side"":""" apiSide ""","
. """count"":""" countText ""","
. """price"":""" priceText ""","
. """time_in_force"":""" timeInForce ""","
. """self_trade_prevention_type"":""taker_at_cross"","
. """post_only"":false,"
. """cancel_order_on_pause"":false,"
. """reduce_only"":" reduceOnly ","
. """exchange_index"":2,"
. """client_order_id"":""" clientOrderId """"
. "}"
res := KalshiSignedRequest("POST", "/trade-api/v2/portfolio/events/orders", payload)
if !IsObject(res)
return false
orderId := JsonField(res.body, "order_id")
if (res.status = 401 || res.status = 403) {
Log("WARN: HTTP " res.status " on order submit, will retry")
return false
}
if (res.status != 201 && res.status != 200) {
errorCode := JsonField(res.body, "code")
if (InStr(errorCode, "available_balance_too_low") = 1)
Log("ERROR: " marketTicker " Order rejected: insufficient shard 2 balance | API HTTP " res.status " code=" errorCode " msg=" JsonField(res.body, "message"))
return false
}
if (isBuy)
Sleep %restDelay%
else
Sleep 150
hasPos := HasExistingPosition(marketTicker)
if (isBuy && !hasPos && orderId != "") {
CancelKalshiOrder(orderId)
Sleep 500
hasPos := HasExistingPosition(marketTicker)
if (hasPos)
Log(marketTicker " Order filled during cancel race, treating as confirmed")
}
return (isBuy ? hasPos : !hasPos)
}
KalshiEventOrderSide(apiAction, contractSide) {
; V2 quotes everything from the YES book: bid=buy YES, ask=sell YES.
if (apiAction = "buy")
return (contractSide = "UP") ? "bid" : "ask"
return (contractSide = "UP") ? "ask" : "bid"
}
KalshiEventOrderCents(apiAction, contractSide, contractCents) {
contractCents := KalshiClampCents(contractCents)
if (contractSide = "UP")
return contractCents
return KalshiClampCents(100 - contractCents)
}
KalshiClampCents(cents) {
cents := Round(cents + 0)
if (cents < 1)
return 1
if (cents > 99)
return 99
return cents
}
KalshiFixedPriceFromCents(cents) {
cents := KalshiClampCents(cents)
return "0." KalshiPad2(cents) "00"
}
KalshiFixedCount(size) {
countCents := Round((size + 0) * 100)
whole := Floor(countCents / 100)
frac := Mod(countCents, 100)
return whole "." KalshiPad2(frac)
}
KalshiPad2(n) {
n := Round(n + 0)
if (n < 10)
return "0" n
return n
}
NormalizeKalshiExchangeIndex(exchangeIndex) {
exchangeIndex := Trim(exchangeIndex)
if !RegExMatch(exchangeIndex, "^-?\d+$")
return -1
exchangeIndex += 0
return (exchangeIndex >= -1) ? exchangeIndex : -1
}
KalshiBalanceDollarsFromBody(body) {
balanceDollars := JsonField(body, "balance_dollars")
if !IsNumericStr(balanceDollars)
return ""
return balanceDollars + 0
}
GetKalshiBalanceSnapshot(exchangeIndex := "", maxAttempts := 1, forceRefresh := false) {
global kalshiAggregateBalanceSnapshot
snapshot := { ok: false, balance: "", status: "", body: "", userMissing: false, retryable: true, balances: {} }
endpoint := "/trade-api/v2/portfolio/balance"
if (exchangeIndex != "") {
exchangeIndex := NormalizeKalshiExchangeIndex(exchangeIndex)
if (exchangeIndex < 0) {
snapshot.retryable := false
return snapshot
}
endpoint .= "?exchange_index=" exchangeIndex
}
; Startup and funding checks share the same short-lived aggregate read.
if (!forceRefresh && IsObject(kalshiAggregateBalanceSnapshot) && (A_TickCount - kalshiAggregateBalanceSnapshot.tick) < 1100) {
if (exchangeIndex = "")
return kalshiAggregateBalanceSnapshot.Clone()
if kalshiAggregateBalanceSnapshot.balances.HasKey(exchangeIndex) {
snapshot.ok := true
snapshot.status := 200
snapshot.balance := kalshiAggregateBalanceSnapshot.balances[exchangeIndex]
return snapshot
}
}
attempt := 1
while (attempt <= maxAttempts) {
; KalshiSignedRequest paces every balance GET, including scoped reads.
res := KalshiSignedRequest("GET", endpoint)
if IsObject(res) {
snapshot.status := res.status
snapshot.body := res.body
snapshot.retryable := (res.status = 0 || res.status = 200 || res.status = 408 || res.status = 429 || res.status >= 500)
if (res.status = 200) {
balanceDollars := KalshiBalanceDollarsFromBody(res.body)
if (balanceDollars != "") {
snapshot.ok := true
snapshot.balance := balanceDollars + 0
snapshot.tick := A_TickCount
if (exchangeIndex = "") {
snapshot.balances := KalshiBalanceBreakdown(res.body)
kalshiAggregateBalanceSnapshot := snapshot.Clone()
}
return snapshot
}
}
errorCode := JsonField(res.body, "code")
if (exchangeIndex != "" && InStr(errorCode, "user_not_found") = 1) {
snapshot.ok := true
snapshot.balance := 0
snapshot.userMissing := true
return snapshot
}
if (!snapshot.retryable)
break
}
; The shared request scheduler supplies spacing and exponential backoff.
attempt++
}
return snapshot
}
KalshiBalanceBreakdown(body) {
balances := {}
if !RegExMatch(body, """balance_breakdown""\s*:\s*\[([^\]]*)\]", breakdown)
return balances
pos := 1
while (pos := RegExMatch(breakdown1, "\{[^{}]*\}", item, pos)) {
exchangeIndex := JsonField(item, "exchange_index")
balance := JsonField(item, "balance") ; breakdown balances are dollar strings
if (RegExMatch(exchangeIndex, "^\d+$") && IsNumericStr(balance))
balances[exchangeIndex + 0] := balance + 0
pos += StrLen(item)
}
return balances
}
KalshiBalanceForShard(aggregate, exchangeIndex) {
if (IsObject(aggregate) && aggregate.ok && aggregate.balances.HasKey(exchangeIndex))
return { ok: true, balance: aggregate.balances[exchangeIndex], status: 200 }
; Older responses or an unprovisioned shard may have no breakdown entry.
return GetKalshiBalanceSnapshot(exchangeIndex, 1)
}
KalshiFundingReadFailed(asset, exchangeIndex, snapshot, state, ByRef permanentFailure) {
permanentFailure := IsObject(snapshot) && !snapshot.retryable
if (permanentFailure)
state.status := "FAILED"
status := (IsObject(snapshot) && snapshot.status != "") ? snapshot.status : "no response"
LogOnce(asset, "FUNDING_READ_" exchangeIndex "_" status, asset " Balance read " (permanentFailure ? "rejected" : "unavailable; will retry") " | shard " exchangeIndex " | HTTP " status)
return false
}
NewKalshiTransferId() {
VarSetCapacity(guid, 16, 0)
if (DllCall("ole32\CoCreateGuid", "Ptr", &guid) != 0)
return ""
VarSetCapacity(guidText, 78, 0)
if (!DllCall("ole32\StringFromGUID2", "Ptr", &guid, "Ptr", &guidText, "Int", 39))
return ""
return Trim(StrGet(&guidText, "UTF-16"), "{}")
}
SubmitKalshiShardTransfer(asset, exchangeIndex, state, pending, ByRef permanentFailure) {
global kalshiShardPendingTransfers
wasAttempted := pending.attempted
pending.attempted := true
res := KalshiSignedRequest("POST", "/trade-api/v2/portfolio/intra_exchange_instance_transfer", pending.payload)
if (IsObject(res) && res.status = 200) {
pending.transferId := JsonField(res.body, "transfer_id")
Log(asset " Shard " exchangeIndex " transfer requested | client_transfer_id=" pending.clientTransferId " | transfer_id=" pending.transferId)
} else if (!IsObject(res) || res.status = 0 || res.status = 408 || res.status = 409 || res.status >= 500 || wasAttempted) {
; Any retry keeps the exact payload and client ID, including after a 409.
LogOnce(asset, "TRANSFER_AMBIGUOUS_" pending.clientTransferId, asset " Transfer outcome pending; will reconcile before another transfer")
} else {
; A definitive first rejection did not create a transfer.
kalshiShardPendingTransfers.Delete(exchangeIndex)
permanentFailure := (res.status != 429 && !InStr(JsonField(res.body, "code"), "insufficient"))
if (permanentFailure && !pending.baseFundingReady)
state.status := "FAILED"
LogOnce(asset, "TRANSFER_REJECTED_" res.status, asset " Shard transfer rejected | HTTP " res.status " " KalshiFundingApiMessage(res.body) (permanentFailure ? "" : " | will retry"))
}
if (pending.baseFundingReady) {
state.status := "READY"
permanentFailure := false
LogOnce(asset, "OPTIONAL_TRANSFER_PENDING", asset " Existing shard balance ready; additional reserve is best-effort")
return true
}
return false
}
ReconcileKalshiShardTransfer(asset, exchangeIndex, state, ByRef permanentFailure) {
global kalshiShardPendingTransfers
pending := kalshiShardPendingTransfers[exchangeIndex]
state.transferAttempted := true
aggregate := GetKalshiBalanceSnapshot("", 1, true)
if (!aggregate.ok)
return KalshiFundingReadFailed(asset, "all", aggregate, state, permanentFailure)
destination := KalshiBalanceForShard(aggregate, exchangeIndex)
if (!destination.ok)
return KalshiFundingReadFailed(asset, exchangeIndex, destination, state, permanentFailure)
transferComplete := false
historyExhausted := false
if (pending.transferId != "") {
res := KalshiSignedRequest("GET", "/trade-api/v2/portfolio/intra_exchange_instance_transfers/" KalshiQueryEncode(pending.transferId))
if (IsObject(res) && res.status = 200)
transferComplete := (JsonField(res.body, "status") = "complete")
} else {
; Recover a lost POST response by its client ID. Scan one page per retry.
endpoint := "/trade-api/v2/portfolio/intra_exchange_instance_transfers?limit=100"
if (pending.historyCursor != "")
endpoint .= "&cursor=" KalshiQueryEncode(pending.historyCursor)
res := KalshiSignedRequest("GET", endpoint)
if (IsObject(res) && res.status = 200) {
pos := 1
while (pos := RegExMatch(res.body, "\{[^{}]*\}", item, pos)) {
if (JsonField(item, "client_transfer_id") = pending.clientTransferId) {
pending.transferId := JsonField(item, "transfer_id")
transferComplete := (JsonField(item, "status") = "complete")
break
}
pos += StrLen(item)
}
pending.historyCursor := JsonField(res.body, "cursor")
historyExhausted := (pending.transferId = "" && pending.historyCursor = "")
}
}
if (transferComplete)
kalshiShardPendingTransfers.Delete(exchangeIndex)
fundingReady := (destination.balance + 0) >= (pending.requiredDollars - 0.000001)
targetReached := (destination.balance + 0) >= (pending.targetDollars - 0.000001)
if (fundingReady && (transferComplete || targetReached || pending.baseFundingReady)) {
state.status := "READY"
Log(asset " Shard " exchangeIndex " funding confirmed | $" KalshiFundingMoney(destination.balance) " available")
return true
}
if (transferComplete) {
LogOnce(asset, "TRANSFER_SPENT", asset " Transfer completed but shard balance is insufficient; will recheck funding")
return false
}
if (historyExhausted) {
; Kalshi deduplicates this UUID. Reusing the same request cannot top up twice.
return SubmitKalshiShardTransfer(asset, exchangeIndex, state, pending, permanentFailure)
}
LogOnce(asset, "TRANSFER_PENDING_" pending.clientTransferId, asset " Transfer pending confirmation; will retry without creating another transfer")
return false
}
KalshiScheduleNextBalanceRead(status) {
global kalshiBalanceNextRequestTick, kalshiBalanceRetryCount
delayMs := 1100
if (status = 0 || status = 408 || status = 429 || status >= 500) {
kalshiBalanceRetryCount := Min(kalshiBalanceRetryCount + 1, 5)
Random, balanceJitter, 0, 250
delayMs := (1100 * (2 ** (kalshiBalanceRetryCount - 1))) + balanceJitter
if (status = 429)
Log("Balance read rate-limited; retrying after " Round(delayMs / 1000.0, 2) " seconds")
} else {
kalshiBalanceRetryCount := 0
}
kalshiBalanceNextRequestTick := A_TickCount + delayMs
}
KalshiFundingMoney(value) {
return Format("{:.4f}", value + 0)
}
KalshiFundingApiMessage(body) {
code := JsonField(body, "code")
message := JsonField(body, "message")
if (code != "" || message != "")
return code " " message
return SubStr(body, 1, 180)
}
KalshiQueryEncode(value) {
value := StrReplace(value, "%", "%25")
value := StrReplace(value, "+", "%2B")
value := StrReplace(value, "/", "%2F")
value := StrReplace(value, "=", "%3D")
value := StrReplace(value, "&", "%26")
value := StrReplace(value, "?", "%3F")
value := StrReplace(value, "#", "%23")
value := StrReplace(value, " ", "%20")
return value
}
EnsureKalshiCryptoShardFunding(asset, ByRef permanentFailure := false) {
global enableLiveOrders, orderSize, assets, kalshiCryptoExchangeIndex, kalshiShardFundingSessionStatus, kalshiShardPendingTransfers
global kalshiBalanceNextRequestTick, kalshiAggregateBalanceSnapshot
permanentFailure := false
if (!enableLiveOrders)
return true
destinationExchangeIndex := NormalizeKalshiExchangeIndex(kalshiCryptoExchangeIndex)
if (destinationExchangeIndex = 0)
return true
if (destinationExchangeIndex < 0) {
permanentFailure := true
LogOnce(asset, "INVALID_SHARD", "ERROR: " asset " Market did not provide a valid exchange shard")
return false
}
; Include the date so a bot running overnight cannot reuse yesterday's state.
fundingSessionKey := destinationExchangeIndex "|" SubStr(CurrentUtcNowAhk(), 1, 8) "|" CurrentQuarterIndex()
if !kalshiShardFundingSessionStatus.HasKey(fundingSessionKey)
kalshiShardFundingSessionStatus[fundingSessionKey] := { status: "WAIT_FUNDING", retryTick: 0, transferAttempted: false }
state := kalshiShardFundingSessionStatus[fundingSessionKey]
if (state.status = "READY")
return true
if (state.status = "FAILED") {
permanentFailure := true
return false
}
if (A_TickCount < state.retryTick)
return false
; Defer in the strategy loop instead of sleeping through another asset's exit.
cacheFresh := IsObject(kalshiAggregateBalanceSnapshot) && (A_TickCount - kalshiAggregateBalanceSnapshot.tick) < 1100
if (A_TickCount < kalshiBalanceNextRequestTick && (!cacheFresh || kalshiShardPendingTransfers.HasKey(destinationExchangeIndex)))
return false
Random, fundingJitter, 0, 500
state.retryTick := A_TickCount + 5000 + fundingJitter
; A market change never discards a submitted or ambiguous transfer.
if kalshiShardPendingTransfers.HasKey(destinationExchangeIndex)
return ReconcileKalshiShardTransfer(asset, destinationExchangeIndex, state, permanentFailure)
aggregate := GetKalshiBalanceSnapshot("", 1)
if (!aggregate.ok)
return KalshiFundingReadFailed(asset, "all", aggregate, state, permanentFailure)
destination := KalshiBalanceForShard(aggregate, destinationExchangeIndex)
if (!destination.ok)
return KalshiFundingReadFailed(asset, destinationExchangeIndex, destination, state, permanentFailure)
enabledAssetCount := IsObject(assets) ? assets.Length() : 1
requiredDollars := (orderSize + 0) * Max(1, enabledAssetCount)
currentBalance := destination.balance + 0
baseFundingReady := (currentBalance >= (requiredDollars - 0.000001))
if (baseFundingReady && state.transferAttempted) {
state.status := "READY"
return true
}
; Keep the existing best-effort additional reserve for another running agent.
transferDollars := baseFundingReady ? orderSize + 0 : requiredDollars - currentBalance
transferCenticents := Ceil((transferDollars * 10000) - 0.000001)
transferDollars := transferCenticents / 10000.0
confirmationTargetDollars := currentBalance + transferDollars
source := KalshiBalanceForShard(aggregate, 0)
if (!source.ok || (source.balance + 0) < (transferDollars - 0.000001)) {
if (baseFundingReady) {
state.status := "READY"
LogOnce(asset, "OPTIONAL_RESERVE_UNAVAILABLE", asset " Shard already funded; additional agent reserve unavailable")
return true
}
if (!source.ok)
return KalshiFundingReadFailed(asset, 0, source, state, permanentFailure)
LogOnce(asset, "SHARD_INSUFFICIENT", asset " Shard balance insufficient; needs $" KalshiFundingMoney(transferDollars) " from shard 0, available $" KalshiFundingMoney(source.balance) "; will recheck")
return false
}
clientTransferId := NewKalshiTransferId()
if (clientTransferId = "") {
state.status := "FAILED"
permanentFailure := true
Log("ERROR: " asset " Could not create a duplicate-prevention ID for the shard transfer")
return false
}
payload := "{"
. """source"":""event_contract"","
. """destination"":""event_contract"","
. """client_transfer_id"":""" clientTransferId ""","
. """amount"":" transferCenticents ","
. """source_exchange_shard"":0,"
. """destination_exchange_shard"":" destinationExchangeIndex ","
. """source_subaccount"":0,"
. """destination_subaccount"":0"
. "}"
pending := { transferId: "", clientTransferId: clientTransferId, payload: payload, historyCursor: "", attempted: false, requiredDollars: requiredDollars, targetDollars: confirmationTargetDollars, baseFundingReady: baseFundingReady }
; Store before POST: a timeout may occur after Kalshi accepted the transfer.
state.transferAttempted := true
kalshiShardPendingTransfers[destinationExchangeIndex] := pending
return SubmitKalshiShardTransfer(asset, destinationExchangeIndex, state, pending, permanentFailure)
}
CancelKalshiOrder(orderId) {
global kalshiCryptoExchangeIndex
if (orderId = "")
return
KalshiSignedRequest("DELETE", "/trade-api/v2/portfolio/events/orders/" orderId "?exchange_index=" kalshiCryptoExchangeIndex)
}
LoadKalshiCredentialsFromFiles() {
global kalshiApiKeyId, kalshiApiKeyFile, kalshiPrivateKeyPath, kalshiPrivateKeyFile, kalshiApiKeyResolvedPath, kalshiPrivateKeyResolvedPath
kalshiApiKeyId := ""
kalshiApiKeyResolvedPath := ResolveCredentialPath(kalshiApiKeyFile)
if (kalshiApiKeyResolvedPath != "") {
FileRead, apiRaw, %kalshiApiKeyResolvedPath%
apiCode := JsonField(apiRaw, "code")
if (apiCode != "")
kalshiApiKeyId := Trim(JsonUnescape(apiCode))
}
kalshiPrivateKeyPath := ""
kalshiPrivateKeyResolvedPath := ResolveCredentialPath(kalshiPrivateKeyFile)
if (kalshiPrivateKeyResolvedPath != "") {
FileRead, keyRaw, %kalshiPrivateKeyResolvedPath%
keyCode := JsonField(keyRaw, "code")
if (keyCode != "") {
pemText := JsonUnescape(keyCode)
tempBase := A_Temp
if (tempBase = "")
tempBase := A_ScriptDir
tempPem := tempBase "\kalshi_ahk\privatekey_runtime.pem"
FileCreateDir, % tempBase "\kalshi_ahk"
FileDelete, %tempPem%
FileAppend, %pemText%, %tempPem%
kalshiPrivateKeyPath := tempPem
}
}
}
StartupCredentialCheck() {
global kalshiApiKeyId, kalshiPrivateKeyPath, kalshiSignerPath
apiReady := (kalshiApiKeyId != "")
keyReady := (kalshiPrivateKeyPath != "" && FileExist(kalshiPrivateKeyPath))
signerOut := RunAndCapture(QuoteForCmd(kalshiSignerPath) " version")
signerReady := InStr(signerOut, "NightShark Signer")
if (apiReady && keyReady && signerReady) {
snapshot := GetKalshiBalanceSnapshot("", 3, true)
if (snapshot.ok) {
Log("Credentials processed successfully | aggregate balance $" KalshiFundingMoney(snapshot.balance))
return true
}
if (snapshot.status = 401 || snapshot.status = 403) {
Log("ERROR: API key or private key is incorrect (HTTP " snapshot.status ")")
Log("Please fix your API key / private key and restart.")
} else {
Log("ERROR: Startup balance unavailable after retries | HTTP " snapshot.status " | stopping before live trading")
}
stopCode()
return false
}
if (!apiReady)
Log("ERROR: API key not loaded")
if (!keyReady)
Log("ERROR: Private key not loaded")
if (!signerReady)
Log("ERROR: NightShark signer not found")
Log("Please fix API credentials. Stopping script.")
stopCode()
return false
}
ResolveCredentialPath(pathSpec) {
if (pathSpec = "")
return ""
if FileExist(pathSpec)
return pathSpec
normalized := StrReplace(pathSpec, "\", "/")
if FileExist(normalized)
return normalized
relative := normalized
while (SubStr(relative, 1, 1) = "/" || SubStr(relative, 1, 1) = "\")
relative := SubStr(relative, 2)
fromScriptDir := A_ScriptDir "/" relative
if FileExist(fromScriptDir)
return fromScriptDir
fromScriptDirBackslash := StrReplace(fromScriptDir, "/", "\")
if FileExist(fromScriptDirBackslash)
return fromScriptDirBackslash
return ""
}
JsonUnescape(s) {
s := StrReplace(s, "\/", "/")
s := StrReplace(s, "\n", "`n")
s := StrReplace(s, "\r", "`r")
s := StrReplace(s, "\t", A_Tab)
s := StrReplace(s, Chr(92) Chr(34), Chr(34))
s := StrReplace(s, Chr(92) Chr(92), Chr(92))
return s
}
KalshiSignedRequest(method, endpointPath, bodyJson := "") {
global kalshiApiBase, kalshiApiKeyId, kalshiBalanceNextRequestTick, kalshiAggregateBalanceSnapshot
isBalanceRead := (method = "GET" && RegExMatch(endpointPath, "^/trade-api/v2/portfolio/balance(?:\?|$)"))
if (isBalanceRead) {
waitMs := kalshiBalanceNextRequestTick - A_TickCount
if (waitMs > 0)
Sleep %waitMs%
kalshiAggregateBalanceSnapshot := ""
} else if (method != "GET") {
kalshiAggregateBalanceSnapshot := ""
}
; Sign after waiting, so the timestamp stays fresh during a rate-limit backoff.
timestamp := CurrentTimeMillis()
signPath := endpointPath
if InStr(signPath, "?")
signPath := SubStr(signPath, 1, InStr(signPath, "?") - 1)
signature := SignKalshiMessage(timestamp method signPath)
if (signature = "")
return false
url := kalshiApiBase endpointPath
global httpSignedObj
if !IsObject(httpSignedObj)
httpSignedObj := ComObjCreate("WinHttp.WinHttpRequest.5.1")
http := httpSignedObj
http.Option(9) := 2048 | 8192
http.Open(method, url, false)
http.SetTimeouts(1000, 1000, 3000, 5000)
http.SetRequestHeader("User-Agent", "Mozilla/5.0")
http.SetRequestHeader("Cache-Control", "no-cache, no-store")
http.SetRequestHeader("Pragma", "no-cache")
http.SetRequestHeader("KALSHI-ACCESS-KEY", kalshiApiKeyId)
http.SetRequestHeader("KALSHI-ACCESS-TIMESTAMP", timestamp)
http.SetRequestHeader("KALSHI-ACCESS-SIGNATURE", signature)
http.SetRequestHeader("Content-Type", "application/json")
try {
if (bodyJson != "")
http.Send(bodyJson)
else
http.Send()
} catch e {
httpSignedObj := ""
if (isBalanceRead)
KalshiScheduleNextBalanceRead(0)
return false
}
res := { status: http.Status, body: http.ResponseText }
if (isBalanceRead)
KalshiScheduleNextBalanceRead(res.status)
return res
}
SignKalshiMessage(message) {
global kalshiPrivateKeyPath, kalshiKeyPassphrase, kalshiSignerPath
if (kalshiPrivateKeyPath = "" || !FileExist(kalshiPrivateKeyPath))
return ""
passArg := ""
if (kalshiKeyPassphrase != "")
passArg := " --passphrase " QuoteForCmd(kalshiKeyPassphrase)
signCmd := QuoteForCmd(kalshiSignerPath)
. " sign --key "
. QuoteForCmd(kalshiPrivateKeyPath)
. " --message "
. QuoteForCmd(message)
. passArg
out := Trim(RunAndCapture(signCmd))
if (SubStr(out, 1, 6) = "ERROR:")
return ""
return out
}
BuildClientOrderId(ticker, action, side) {
nowUtc := CurrentUtcNowAhk()
if (nowUtc = "")
nowUtc := A_Now
Random, r, 100000, 999999
marketToken := RegExReplace(ticker, "^KX([A-Z0-9]+)15M.*$", "$1")
if (marketToken = ticker || marketToken = "")
marketToken := SubStr(RegExReplace(ticker, "[^A-Za-z0-9]", ""), 1, 12)
clientOrderId := marketToken "-" action "-" side "-" nowUtc "-" A_TickCount "-" r
return SubStr(clientOrderId, 1, 64)
}
CurrentTimeMillis() {
epoch := CurrentUtcNowAhk()
if (epoch = "")
return ""
EnvSub, epoch, 19700101000000, Seconds
return epoch * 1000
}
CurrentUtcNowAhk() {
nowUtc := A_NowUTC
if (nowUtc != "")
return nowUtc
out := Trim(RunAndCapture("powershell -NoProfile -Command ""[DateTime]::UtcNow.ToString('yyyyMMddHHmmss')"""))
if RegExMatch(out, "^\d{14}$")
return out
return ""
}
QuoteForCmd(s) {
s := StrReplace(s, """", "\""")
return """" s """"
}
RunAndCapture(command) {
return RunCMD(command, A_ScriptDir)
}
ToLower(s) {
StringLower, out, s
return out
}
FindNightSharkSigner() {
candidates := [ A_ScriptDir "\nightshark-signer.exe"
, A_ScriptDir "/nightshark-signer.exe"
, A_WorkingDir "\nightshark-signer.exe"
, A_WorkingDir "/nightshark-signer.exe" ]
for _, p in candidates {
if FileExist(p)
return p
}
return A_ScriptDir "\nightshark-signer.exe"
}
CanPlaceOrderNow(asset) {
global enableLiveOrders, kalshiApiKeyId, kalshiPrivateKeyPath
if (!enableLiveOrders)
return true
if (kalshiApiKeyId != "" && kalshiPrivateKeyPath != "" && FileExist(kalshiPrivateKeyPath))
return true
return false
}
ShouldLogPrice(asset, minIntervalMs) {
global assetLastPriceLogTick
nowTick := A_TickCount
if !assetLastPriceLogTick.HasKey(asset) {
assetLastPriceLogTick[asset] := nowTick
return true
}
if ((nowTick - assetLastPriceLogTick[asset]) >= minIntervalMs) {
assetLastPriceLogTick[asset] := nowTick
return true
}
return false
}
MinutesRemainingInQuarter() {
utc := CurrentUtcNowAhk()
if (utc = "") {
; Fallback to local clock if UTC unavailable
currentMinute := A_Min + 0
remaining := 15 - Mod(currentMinute, 15)
remaining := remaining - ((A_Sec + 0) / 60.0)
return remaining
}
mm := SubStr(utc, 11, 2) + 0
ss := SubStr(utc, 13, 2) + 0
remaining := 15 - Mod(mm, 15) - (ss / 60.0)
return remaining
}
IsXMinRemaining(x) {
return (MinutesRemainingInQuarter() <= x)
}
CurrentQuarterIndex() {
utc := CurrentUtcNowAhk()
if (utc = "") {
totalMinutes := (A_Hour + 0) * 60 + (A_Min + 0)
return Floor(totalMinutes / 15)
}
hh := SubStr(utc, 9, 2) + 0
mm := SubStr(utc, 11, 2) + 0
totalMinutes := hh * 60 + mm
return Floor(totalMinutes / 15)
}
GetKalshiMarketSnapshot(asset, maxRetries := 4) {
global assetSeriesMap
if !assetSeriesMap.HasKey(asset)
return false
series := assetSeriesMap[asset]
attempt := 1
while (attempt <= maxRetries) {
url := "https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=" series "&status=open&limit=1&_=" A_TickCount "-" attempt
body := HttpGet(url)
if (body != "") {
yesPrice := GetFirstNonEmptyJsonField(body, ["yes_ask_dollars", "yes_bid_dollars", "last_price_dollars"], true)
noPrice := GetFirstNonEmptyJsonField(body, ["no_ask_dollars", "no_bid_dollars"], true)
if (noPrice = "" && yesPrice != "")
noPrice := 1 - (yesPrice + 0)
if (yesPrice = "" && noPrice != "")
yesPrice := 1 - (noPrice + 0)
closeIso := GetFirstNonEmptyJsonField(body, ["close_time", "expected_expiration_time", "expiration_time", "settlement_time"])
minsLeft := MinutesUntilIsoUtc(closeIso)
; Skip markets that already closed or close in < 30 seconds (stale/expired)
if (minsLeft != "" && minsLeft < 0.5) {
if (attempt < maxRetries) {
Sleep 200
attempt++
continue
}
}
if (yesPrice != "" && noPrice != "") {
obj := {}
obj.up := yesPrice + 0
obj.down := noPrice + 0
obj.minutesLeft := minsLeft
obj.closeTime := closeIso
obj.marketTicker := GetFirstNonEmptyJsonField(body, ["ticker"])
return obj
}
}
if (attempt < maxRetries)
Sleep 200
attempt++
}
return false
}
GetFirstNonEmptyJsonField(json, fields, skipZero := false) {
for _, field in fields {
val := JsonField(json, field)
if (val != "") {
if (skipZero && IsNumericStr(val) && (val + 0) = 0)
continue
return val
}
}
return ""
}
IsNumericStr(s) {
s := Trim(s)
return RegExMatch(s, "^-?\d+(\.\d+)?$")
}
JsonField(json, field) {
pattern := """" field """\s*:\s*(""([^""]*)""|[^,}\s][^,}\r\n]*)"
result := ""
pos := 1
while (pos := RegExMatch(json, pattern, m, pos)) {
val := Trim(m1)
val := Trim(val, """")
if (val != "")
result := val
pos += StrLen(m)
}
return result
}
MinutesUntilIsoUtc(iso) {
ts := IsoUtcToAhk(iso)
if (ts = "")
return ""
nowUtc := CurrentUtcNowAhk()
if (nowUtc = "")
return ""
diff := ts
EnvSub, diff, %nowUtc%, Seconds
return diff / 60.0
}
IsoUtcToAhk(iso) {
if (iso = "")
return ""
if !RegExMatch(iso, "O)^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})", m)
return ""
return m1 m2 m3 m4 m5 m6
}