Skip to main content
This page is the collected wisdom for running a programmatic taker in production. First, complete the Quickstart, and then implement the best practices here.

Slippage and worst_price

worst_price is your hard price limit. The contract rejects any individual quote worse than it. You must set it correctly. For longs (you’re buying): worst_price is the maximum price you’ll accept. Set it above your expected fill. For shorts (you’re selling): worst_price is the minimum price you’ll accept. Set it below your expected fill. A sensible formula for a non-arb taker:
worst_price = mark_price * (1 ± max_slippage_bps / 10000)
Where + is for longs and is for shorts. Start with max_slippage_bps = 50 (0.5%) for liquid markets and widen for illiquid ones. For arbitrage takers where the edge is known: set worst_price at your breakeven against the hedge venue. Any quote worse than that loses money – you’d rather not fill. Too tight and you’ll get rejected often in fast markets. Too wide and you’re defenseless against adverse selection. Measure your fill rate and P&L distribution and tune.

Expiry races

Quotes carry an expiry that’s guaranteed valid for a short window from signing. You must submit AcceptQuote and have it confirm before that timestamp passes. The onchain check is block_time_ms > expiry → reject. What eats the budget:
  • Maker response (a few hundred milliseconds)
  • Your collection window (whatever’s left after MM response)
  • Quote selection and message building (~100ms)
  • Network RTT to the chain gRPC endpoint (~50–200ms)
  • Injective block time (~600ms average)
  • Any retries you do on broadcast failure
The quote lifetime is short. By the time you’ve waited for makers to respond and a block to confirm, there is very little headroom. If you’re running across regions, mempool contention is high, or you have backoff retries on the broadcast path, you can burn it. Mitigations:
  • Submit immediately after the collection window closes. Don’t batch or wait.
  • Use a short collection window. TrueCurrent currently uses 500 ms. The value can vary by frontend and protocol configuration, and API takers can tune the collection timeout. Waiting near the full live quote expiry window leaves little time for block inclusion.
  • Co-locate near testnet-grpc.injective.dev / sentry.tm.injective.network to minimize RTT.
  • Don’t retry failed broadcasts on an expiry error – the quote is dead, get a new one.
  • Use cid to echo your own trade ID in the settlement event – handy when debugging which quote won the race.

Subaccount balance

Takers trade from their Injective exchange subaccount, not the main bank module. Before any trade, you need USDC sitting in subaccount index 0 (or whichever subaccount_nonce you passed). Deposit USDC to the subaccount:
from pyinjective.composer_v2 import Composer

composer = Composer(network="testnet")
deposit_msg = composer.msg_deposit(
    sender=taker_inj_address,
    subaccount_id=f"{taker_eth_address}000000000000000000000000",
    amount=10_000_000_000,  # 10,000 USDC in 6-decimal units
    denom="erc20:0x0C382e685bbeeFE5d3d9C29e29E341fEE8E84C5d",  # testnet USDC
)
await broadcaster.broadcast([deposit_msg])
Check your subaccount balance before submitting requests. If it is below your requested margin, every AcceptQuote will fail with insufficient balance. Top up preemptively – a taker that bounces on insufficient funds burns quotes and contributes to its own adverse selection. Run a monitor that tops up when balance falls below a threshold.

Idempotency and rfq_id reuse

The contract treats rfq_id as a per-taker nonce. Reusing the same (taker, rfq_id) pair for two different AcceptQuote calls causes a nonce replay error on the second. Across different takers, duplicate rfq_id values can exist, so makers should key state by (taker, rfq_id). Use the indexer-assigned ID:
ack = await taker_ws.send_request(request_data, wait_for_response=True, response_timeout=5.0)
rfq_id = int(ack["rfq_id"])
The outgoing TakerStream request carries a client_id; the indexer assigns the rfq_id in its ACK. Use that ACK-returned rfq_id for quote collection and settlement. Do not retry a failed AcceptQuote with the same (taker, rfq_id) pair. If a submission fails mid-flight (broadcast error, timeout, chain rejection), request fresh quotes with a new rfq_id for that taker. The original quotes are probably also expired or consumed.

Handling partial and rejected quotes

When you submit a multi-quote AcceptQuote, the contract processes quotes in order and skips quotes that fail validation rather than aborting the whole transaction (see Accepting quotes). This means you can get:
  • Partial fills – some quotes consumed, others skipped, residual quantity simply not traded
  • Zero fills – every quote rejected, whole transaction fails
Parse the settlement event on success to know what actually happened:
tx = await chain.get_tx(tx_hash)
for event in tx.events:
    if event.type == "wasm-accept_quote":
        for attr in event.attributes:
            if attr.key == "quote_results":
                results = json.loads(attr.value)
                for r in results:
                    if "e" in r:
                        print(f"Quote from {r['maker']} rejected: {r['e']}")
                    else:
                        print(f"Quote from {r['maker']} filled: qty={r['q']}")
Track per-maker rejection reasons over time – a maker that consistently rejects with “insufficient balance” or “nonce replay” is broken, and you can down-rank them in your quote selection.

Selection strategies

For longs, lowest price wins. For shorts, highest price wins. Beyond that, you have choices: Single-quote, cheapest: Pick exactly one quote – the cheapest that covers your full requested quantity. Simplest, lowest variance, sometimes leaves quantity unfilled if no single maker is big enough. Multi-quote, sorted: Sort by price ascending (longs) / descending (shorts), greedily consume until your quantity is covered, submit all. Best average price, larger message, slightly higher rejection risk (one bad quote poisons nothing but the transaction takes longer). Multi-quote, price-capped: Same as above, but drop any quote worse than your worst_price before submitting. Reduces onchain work and improves settlement latency. Multi-quote, maker-scored: Weight makers by historical fill reliability. Prefer makers that reliably settle even if their price is slightly worse. Worth it for high-frequency takers where the fill rate dominates the price delta. First-fit: Accept the first quote that’s inside worst_price and covers enough quantity. Lowest latency, worst execution – use only when latency is the primary metric.

Reconnection and liveness

The TakerStream will drop you eventually – maintenance, network blips, indexer restarts. Your taker needs to survive it. Minimum reconnect loop:
async def run_with_retries():
    while True:
        try:
            await taker_ws.connect()
            await taker_ws.run()
        except (ConnectionError, asyncio.TimeoutError) as e:
            logger.warning(f"Stream dropped: {e}")
            await asyncio.sleep(1.0)
Better: two connections, fail over on the first dropped frame. Cold reconnection on a single WS means you’re blind for 1–3 seconds, which in a fast market is too long. Monitor staleness. If you’ve sent a request and received zero quotes in your window, that’s either a dead stream or a dead maker set. Alert on repeated empty windows. Heartbeat the chain side too. Your taker should fail-fast if the gRPC endpoint to Injective itself is unreachable – there’s no point in collecting quotes you can’t settle.
Last modified on June 2, 2026