PYX

Lifecycle & Callbacks

How Pyx invokes your script and the callbacks you can define.

Task lifecycle

When a script task starts, the following happens in order:

  1. A sandboxed Luau VM is created with only math, string, and table libraries
  2. The pyx API module is injected as a global table
  3. Your script is compiled and executed (top-level code runs once)
  4. Background services start: balance refresh, order timeout sweeper, event listener
  5. If tick_interval_ms is configured, a timer begins calling on_tick
  6. For each subscribed market, price updates trigger on_price
  7. For each subscribed market, book updates trigger on_book
  8. For each subscribed spot symbol, BBO updates trigger on_spot_price
  9. Execution events (fills, placements, cancellations, split completions, merge completions) trigger their respective callbacks

After startup, a script task can move through these runtime states:

  • running - the VM is active and your strategy can buy or sell
  • paused - pending orders are canceled, callbacks stop firing, task-specific external inputs are unsubscribed, and VM state is preserved for resume
  • draining - new buys are blocked, but sells, order updates, exchange events, and resolution handling continue until the task is flat
  • stopped - the VM is torn down and state is lost
  • error - the task could not continue because of repeated runtime failures or a restart failure

User-triggered actions

  • Pause moves a task from running -> paused
  • Resume moves a task from paused -> running
  • Drain moves a task from running -> draining or paused -> draining
  • Force-stop uses the stop action to market-close open positions, then tear the task down

Use drain for the normal user-facing stop flow. A draining task automatically transitions to stopped once there are no active positions and no active or in-flight orders.

Resolved positions count as done during drain when auto_redeem_on_resolve = false. If auto_redeem_on_resolve = true, the task waits for the normal auto-redeem flow before stopping.

When you force-stop a task, the VM is destroyed and all state is lost after open positions are closed at market.

Automatic error stop

If a task encounters 5 Luau errors within 10 seconds, it is automatically moved to error:

  • Task status is set to error
  • Open orders are cancelled on a best-effort basis

This is a safety mechanism to prevent repeated failing execution loops.

Callbacks

Define any of these as global functions in your script. All are optional - only define what you need.

All callbacks have strict runtime limits. Current timeouts:

  • on_price, on_spot_price, on_book: 25ms
  • on_tick: 150ms
  • execution callbacks (on_fill, on_placed, on_cancelled, on_split_complete, on_merge_complete): 50ms

on_tick(ctx)

Called on a fixed timer interval. Configure tick_interval_ms when creating the task.

function on_tick(ctx)
    pyx.log("tick at " .. ctx.timestamp)
end

Context fields:

FieldTypeDescription
ctx.timestampnumberCurrent time in milliseconds since epoch

Use on_tick for strategies that operate on a schedule rather than reacting to every price update. Good for periodic rebalancing, inventory checks, or slow-moving strategies where you don't need sub-second reaction times.

on_price(ctx)

Called on every price update for your subscribed markets.

function on_price(ctx)
    if ctx.spread < 0.02 then
        pyx.buy(ctx.token_id, ctx.condition_id, ctx.ask, 0.10)
    end
end

Context fields:

FieldTypeDescription
ctx.token_idstringThe token that was updated
ctx.condition_idstringThe market condition ID
ctx.bidnumberBest bid price
ctx.asknumberBest ask price
ctx.lastnumberLast trade price
ctx.midnumberMidpoint (bid + ask) / 2
ctx.spreadnumberSpread ask - bid
ctx.timestampnumberPolymarket price-change timestamp in milliseconds since epoch
ctx.received_atnumberTime Pyx received this update in milliseconds since epoch
ctx.slugstring?Market slug
ctx.questionstring?Market question text
ctx.descriptionstring?Market description
ctx.end_date_msnumber?Market end date timestamp in milliseconds
ctx.start_date_msnumber?Market start date timestamp in milliseconds
ctx.event_start_msnumber?Event start timestamp in milliseconds
ctx.outcomes{string}Array of outcome labels, e.g. "Up", "Down"
ctx.token_ids{string}Array of outcome token IDs (empty if unset)
ctx.outcomestring?Outcome label for this token (e.g. "Yes", "No")

This callback is intentionally strict: on_price has a 25ms timeout per invocation. Keep logic minimal and avoid allocations in hot paths.

Market metadata fields (slug, question, description, end_date_ms, start_date_ms, event_start_ms, outcome) should always be present but are technically optional - check before using.

To detect delayed exchange updates, compare ctx.received_at - ctx.timestamp. Polymarket can occasionally emit updates that are ~1-2 seconds old.

on_book(ctx)

Called on every subscribed market order book update.

function on_book(ctx)
    if ctx.spread <= 0.01 and ctx.total_bid_size > ctx.total_ask_size then
        pyx.log(string.format("book imbalance %s spread=%.4f", ctx.token_id, ctx.spread))
    end
end

Context fields:

FieldTypeDescription
ctx.token_idstringToken that received a book update
ctx.condition_idstringMarket condition ID
ctx.timestampnumberBook event timestamp (ms since epoch)
ctx.best_bidnumberBest bid price
ctx.best_asknumberBest ask price
ctx.spreadnumberSpread best_ask - best_bid
ctx.total_bid_sizenumberSum of all bid sizes in the book payload
ctx.total_ask_sizenumberSum of all ask sizes in the book payload
ctx.bid_levelsnumberNumber of bid levels in ctx.bids
ctx.ask_levelsnumberNumber of ask levels in ctx.asks
ctx.bids{ { price: number, size: number } }Full bid ladder (best first)
ctx.asks{ { price: number, size: number } }Full ask ladder (best first)

This callback is intentionally strict: on_book has a 25ms timeout per invocation. Keep logic minimal and avoid allocations in hot paths.

on_spot_price(ctx)

Called on each Binance BBO update for subscribed spot symbols. Throttled to ~10 updates/sec per symbol.

pyx.subscribe_spot({"BTC", "SOL"})

function on_spot_price(ctx)
    if ctx.symbol == "BTC" and ctx.mid > 100000 then
        pyx.log("BTC above 100k: " .. ctx.mid)
    end
end

Context fields:

FieldTypeDescription
ctx.symbolstring"BTC", "ETH", "SOL", or "XRP"
ctx.bidnumberBest bid price
ctx.asknumberBest ask price
ctx.bid_qtynumberBest bid size
ctx.ask_qtynumberBest ask size
ctx.midnumberMidpoint (bid + ask) / 2
ctx.timestampnumberReceipt time (ms since epoch)

You must call pyx.subscribe_spot(symbols) in your script body (top-level code) before this callback will fire. This callback fires frequently - keep logic fast.

on_fill(fill)

Called when one of your orders is filled (partially or fully).

function on_fill(fill)
    pyx.log(string.format(
        "FILLED: %s %.4f @ %.4f (%s)",
        fill.side, fill.qty, fill.price, fill.liquidity
    ))
end

Fill fields:

FieldTypeDescription
fill.order_idstringThe exchange order ID
fill.token_idstringToken that was filled
fill.condition_idstringMarket/ condition ID
fill.sidestring"buy" or "sell"
fill.pricenumberFill price
fill.qtynumberQuantity filled
fill.cumulative_qtynumberThe total quantity filled for this order
fill.original_sizenumberThe initial/ original size of the order
fill.fully_filledbooleanIf the order is fully filled
fill.position_qtynumberTotal share qty of token position
fill.position_avg_costnumberAvg. cost of token position
fill.realized_pnlnumberRealized P&L from position
fill.liquiditystring"maker" or "taker"

Use this to react to fills - hedge positions, update internal state, log P&L, or trigger follow-up orders.

on_placed(order)

Called when your order is confirmed on the exchange order book.

function on_placed(order)
    pyx.log("Order live: " .. order.order_id)
end

Order fields:

FieldTypeDescription
order.order_idstringThe exchange order ID
order.token_idstringToken
order.condition_idstringMarket/ condition ID
order.sidestring"buy" or "sell"
order.pricenumberLimit price
order.sizenumberOrder size

on_cancelled(order)

Called when an order is cancelled (by you, by timeout, or by the exchange).

function on_cancelled(order)
    pyx.log("Cancelled: " .. order.order_id)
end

Order fields:

FieldTypeDescription
order.order_idstringThe cancelled order ID
order.token_idstringToken
order.sidestring"buy" or "sell"
order.filled_qtynumberQuantity that was filled
order.unfilled_qtynumberQuantity unfilled

on_split_complete(ctx)

Called when a split requested via pyx.split() completes.

function on_split_complete(ctx)
    if not ctx.success then
        pyx.error("Split failed")
        return
    end
    pyx.log(string.format(
        "Split complete: %s amount=%.4f",
        ctx.condition_id,
        ctx.amount
    ))
end

Context fields:

FieldTypeDescription
ctx.condition_idstringMarket condition ID
ctx.amountnumberCompleted split amount
ctx.successbooleanWhether the split was successful

on_merge_complete(ctx)

Called when a merge requested via pyx.merge() completes.

function on_merge_complete(ctx)
    if not ctx.success then
        pyx.error("Merge failed")
        return
    end
    pyx.log(string.format(
        "Merge complete: %s amount=%.4f",
        ctx.condition_id,
        ctx.amount
    ))
end

Context fields:

FieldTypeDescription
ctx.condition_idstringMarket condition ID
ctx.amountnumberCompleted merge amount
ctx.successbooleanWhether the merge was successful

Execution order

Within a single tick, price, book, or spot price event cycle:

  1. Your callback runs (on_tick, on_price, on_book, or on_spot_price)
  2. If pyx.cancel_all() was called, all orders are cancelled; pyx.cancel(order_id) cancels only the specified order
  3. All orders enqueued via pyx.buy() / pyx.sell() will be executed sequentially, immediately after submitting
  4. Any pending execution events (fills, placements, cancellations, split completions, merge completions) are processed, triggering their callbacks

Top-level code

Code outside of callback functions runs once when the VM is created. Use it for initialization:

-- This runs once at task start
local prices: {number} = {}
local tick_count = 0
local MAX_POSITION = 5.0

pyx.log("Strategy initialized")

-- This runs on every price update
function on_price(ctx)
    tick_count += 1
    table.insert(prices, ctx.mid)
    -- ...
end

Avoid heavy computation in top-level code - if compilation + initialization exceeds 5 seconds, the task will fail to start.

On this page