These steps require a human operator because they use real funds and on-chain approvals. No agent or
test in this repository places a real order. The default mode is DRY_RUN. Complete every operator
step below before you enable real-money execution.
A real Polymarket order needs every condition below. Each condition defaults to off.
- Mode.
execution_mode == LIVE_GUARDED(default isdry_run). - Route arm gate (
_live_arm_gateinveridex/competition/service.py). An operatorLiveExecutionDepsbundle is supplied and itslive_ready is Trueand the adapter is a genuine real-venue adapter. If one value is missing, the run uses a dry-run simulation and records the reason indegraded_because_not_armed. - Adapter lock (
_require_armedinveridex/venues/polymarket.py). Independently, thePolymarketAdapterrefuses to submit unlessPOLYMARKET_WRITE_ENABLEDis set,dry_run=False, and a write client is injected.
These are two independent locks (route + adapter) plus the mode. On top of them, at submit time the
circuit breaker, the max_stake_live_guarded cap, and the resolver (which raises MarketUnavailable
rather than guess a token) all apply. Start with 1 share.
The store-backed AgentInstance (deploy persistence) is fully implemented for both InMemoryStore and
PostgresStore, but the automated tests exercise the InMemory path. Before relying on a Postgres-backed
deploy, run one AgentInstance round-trip smoke against a real DATABASE_URL:
- Point
DATABASE_URLat the target Postgres and run the app's DB init (theagent_instancestable + statusCHECKconstraint are created idempotently alongside the other tables). - Deploy a replay/paper agent with Studio or
POST /agents/deploy. This checks the default path, no real money. - Confirm the round-trip: after the deploy returns a
run_id, load the instance back (store.get_agent_instance(instance_id)) and verify the pinned fields survive (config_hash,policy_hash,run_id,status,preflight_checks), and that after the run seals the storedstatusadvanced tosealed. This is the one path the InMemory tests can't prove for the SQL backend. - Confirm the deployed run verifies through the same
/runs/{id}/verifypath as an arena run.
If the round-trip works, Postgres deploy persistence is trustworthy for production.
This smoke test is a small check of the full real-money path. Run it once with one share. FAK means fill-and-kill. The venue fills the available amount now and cancels the rest.
- Fund the trading wallet with USDC.
- Grant USDC and CTF (ERC-1155) approvals to both the regular exchange and the neg-risk exchange. (The neg-risk exchange is a separate contract; missing its approval is a common failure mode.)
- Run
scripts/polymarket_smoke.py, gated by the env vars it requires:POLYMARKET_SMOKE=yesandPOLYMARKET_WRITE_ENABLED(the script refuses to place a real order unless both are set. This is deliberate friction). - It places a single 1-share Fill-Or-Kill order to prove the write path end-to-end on mainnet.
- Acceptance criterion (important): the smoke passes only if you get a real
LIVE_GUARDEDsubmit/receipt. A run that comes backdry_runmeans the path was not armed. Treat that as a failed smoke, not a pass. (A degraded dry run is safe, but it is not proof the live path works.)
- Independently confirm the neg-risk exchange's ERC-1155 approval is set on-chain. This is not offline-verifiable by the preflight. It must be operator-confirmed.
run_preflight(...) in veridex/venues/polymarket_preflight.py takes two operator-verify tri-state
params that default to None (unverified) and are never auto-run:
neg_risk_approved=True. Your confirmation of B2.fak_smoke_passed=True. Your confirmation of B1.
live_ready becomes True only when PreflightReport.ok is True and both of these are
explicitly True. ok=True alone does not arm live. The two operator checks are mandatory.
- Resolve the market:
resolve_market(...)→ aResolvedMarket(handles 1X2 per-team / draw-as-YES / O/U multi-slug; raisesMarketUnavailableon any ambiguity rather than guessing a token). - Construct the write-enabled adapter:
PolymarketAdapter(resolved=..., side=..., write_client=..., dry_run=False, ...)withPOLYMARKET_WRITE_ENABLED=true. This adapter carries the real-venue marker (soreal_venue_quoteis earned) and the_require_armedlock.
On the PolicyEnvelope (veridex/policy/envelope.py):
max_stake_live_guarded. The tighter per-order cap that applies only on the live path. Set this small (e.g. 1 share) for the smoke.circuit_breaker_threshold. Consecutive executed failures that trip the breaker (blocks the rest of the run, fail-closed).cooldown_s. Minimum seconds between consecutive orders.
LiveExecutionDeps(adapter=<armed>, live_ready=report.live_ready)→start_competition(..., live_deps=<that bundle>).- Note: the HTTP
POST /competitions/{id}/startendpoint passes nolive_depsby design, so a real order cannot be armed over the API. Real money is reachable only by an operator directly supplying the armedLiveExecutionDeps. This is intentional (real money is operator-direct-only, never over HTTP or by an automated agent).
- Two independent locks. The route arm gate and the adapter
_require_armed; both must pass. - Mode check. Anything other than
LIVE_GUARDEDuses dry-run mode. - Circuit breaker. An OPEN breaker denies the submit with zero venue I/O; only executed failures trip it (policy denials and dry fills do not).
- Live cap.
max_stake_live_guardeddenies an over-cap order pre-quote. - Quote-size coupling. The quoted edge is priced for the exact size that submits.
- Resolver never guesses. An ambiguous market/side raises
MarketUnavailable, never a wrong token (the1X2|awaybet maps to the away-wins token; draw maps to YES on the draw-binary market). - Recorded fallback. If the run is configured live but not armed, it uses dry-run mode and records
degraded_because_not_armedwith the reason (live_ready_false|missing_live_deps|non_real_adapter). So you can always tell why a run went dry.
- To stop arming, unset
POLYMARKET_WRITE_ENABLED/ passdry_run=True/ omitlive_deps. Any one of these returns the run to a dry-run simulation with no real orders. - The circuit breaker and stake cap limit the possible loss if a run is armed.