oddsrail

Notes on the venue APIs

A rejected order is a return value, not an exception

The exchange models a rejected order as an ok:false response. Reported carelessly, a rejection reads to an agent as a resting order.

Place an order that the exchange refuses and nothing raises. You get a response object whose ok field is false, alongside a reason. Code written in the usual shape:

try:
    resp = await client.place_limit_order(...)
    return {"placed": True, "response": resp}     # wrong
except Exception as e:
    return {"placed": False, "error": str(e)}

reports every rejection as a successful placement. An agent then believes it has exposure it does not have, and the mistake compounds: it will not retry, it will size the next trade against a position that is not there, and its own risk rules see a phantom.

The related trap: an undocumented minimum

Marketable orders have a one dollar minimum notional that is not in the documentation. Price times size below that is rejected, which is exactly the size an agent picks when it is being careful on its first trade.

The rule that makes this safe

Branch on the response, and treat an unrecognised shape as unknown rather than as success. After a timeout, an order may or may not rest: the only correct move is to read open orders before retrying, never to resubmit blindly, because a doubled position is the expensive failure.

What oddsrail does

place_order returns accepted explicitly, never infers success from the absence of an exception, and on a timeout returns a result that says the order is unconfirmed and names the tool to call next. check_order runs before placement and catches the $1 minimum, the wrong side, the wrong market and a price through the book, deterministically and with no model involved.

Found by driving the venues live, then encoded so an agent never rediscovers it. Reproduce every one of these with no keys: pip install oddsrail && python examples/footguns.py (source).

Try an agent that knows all six All notes