by Prophet
No reviews yetOperator-arb between Prophet and Polymarket on operator-supplied market pairs.
When invoked:
mcp__seren-mcp__list_projects. If it returns successfully, the user is authenticated through Seren Desktop and no .env setup is required — proceed to step 2. If MCP is unavailable, see API Key Setup (Fallback) below before continuing.python3 scripts/agent.py --command setup --json-output to apply the schema. Idempotent — safe to re-run. (The runner reads <skill-root>/.env automatically on startup, so a desktop-injected key or an operator-written .env both work.)python3 scripts/agent.py --command run --yes-live --json-output once. The result will be status=ok (cycle ran), status=ok_no_fills (cycle ran but no Prophet fills), or status=blocked.pending_ui_submission, the bot handles /create automatically on the same --yes-live cycle. The Python subprocess restores the cached Prophet session into its own Playwright-stealth browser, drives /create end-to-end, hedges on Polymarket first, then clicks Prophet Confirm — surfaced as a per-entry ui_submission_results block in the same envelope. No mcp__playwright__* orchestration is required from you. If you see ui_submission_results entries with status=ok, simply re-run python3 scripts/agent.py --command run --yes-live --json-output so the bot picks up the new pairs and starts arbing them. Pass --skip-ui-submission only when explicitly testing the legacy behavior.status=blocked, surface the reason to the user and do not schedule cron until acknowledged.python3 scripts/setup_cron.py create --yes-live and start python3 scripts/run_local_pull_runner.py to claim due ticks. --yes-live remains required for autonomous schedules as defense-in-depth.This validation gate prevents the cron and 12h local-pull poller from accruing cost before the runner has produced any qualifying scoring pass. Skill instructions are preloaded in context when this skill is active; do not perform filesystem searches or tool-driven exploration to rediscover them.
Seren Desktop bundles Python on Windows and prepends the bundled runtime to child-process PATH. When running from Seren Desktop on Windows, use the documented python3 ... commands as written; they resolve to Seren Desktop's bundled python3.exe even when system Python is not installed. Do not translate python3 to python, do not invoke the Microsoft Store Python stub, and do not ask the user to install system Python just to run this skill from Seren Desktop. If a snippet includes Unix virtualenv activation such as source .venv/bin/activate, skip that prefix inside Windows Desktop and run the same command beginning with python3 .... Outside Seren Desktop, use an installed Python 3.11+ interpreter or a project .venv.
seren__suggest_for_task, seren__list_agent_publishers) or seren__call_publisher for Playwright. The connected Playwright MCP exposes the mcp__playwright__playwright_navigate, mcp__playwright__playwright_click, mcp__playwright__playwright_fill, and mcp__playwright__playwright_evaluate tool namespace — never route those calls to a Playwright publisher.mcp__playwright__* for /create. Since #636 the subprocess owns its own Playwright-stealth browser and restores the cached Prophet session into it; driving the connected mcp__playwright__* browser from the LLM lands in an unauthenticated context and stalls on the sign-in modal. The connected Playwright MCP is still appropriate for ad-hoc operator-facing screenshots and /wallet inspection — anything /create-adjacent must go through python3 scripts/agent.py --command run --yes-live.polymarket-data, seren-polygon, or SerenDB setup paths).Every --command run cycle writes one JSON event per stage transition to state/run_progress.jsonl (append-only, flushed per write so a crash mid-cycle preserves every line on disk). The prior cycle is rotated to state/run_progress.prev.jsonl for one-tick history. This is a side channel — the --json-output envelope on stdout is byte-identical to today's output.
Before invoking --command run --yes-live, arm a Monitor on the resolved progress file so per-entry progress streams into chat as the bot drives Prophet /create for each pending market. Without this, an 18-entry queue spends ~30 minutes of silence in the AI seed-calc dead zone.
The canonical path is ~/.config/seren/skills/prophet-arb-bot/state/run_progress.jsonl (or $PROPHET_ARB_STATE_DIR/run_progress.jsonl if the env override is set). Per #693, scripts/agent.py prints progress stream: <absolute-path> to stderr the first time it constructs the emitter — copy that exact path into your Monitor. Do not tail a path relative to your current working directory; state-dir resolution is centralized in scripts/state_paths.py and the source tree's state/ directory is no longer written to.
Stage → human-readable rendering for the chat-side renderer:
| stage | sentence template |
| --- | --- |
| cycle_start | cycle started (live mode, tick {tick_id}) |
| auth_ok | auth ok via {source} |
| auto_discover_done | auto-discover: {raw} polymarket candidates → {eligible} eligible, {paired_existing} already paired, {pending_creation} need creation |
| seed_preflight_ok | funds ok: ready to fund {pending_after_trim} seeds |
| seed_preflight_blocked | funds insufficient ({reason}) — see deposit envelope |
| entry_start | [{idx}/{total}] {question} — starting /create |
| prophet_session_restored | [{idx}] prophet session restored into headless browser |
| ocs_session_captured | [{idx}] AI seed calc started (60–180s) |
| heartbeat | [{idx}] {current} … {elapsed_s}s |
| ai_calc_done | [{idx}] AI seed calc done → {seed_side} side, hedge @ ${hedge_price} |
| hedge_submitted | [{idx}] hedge submitted on polymarket: {qty} @ ${price} |
| hedge_filled | [{idx}] hedge filled @ ${price} |
| prophet_confirm_clicked | [{idx}] prophet confirm clicked |
| pair_created | [{idx}] ✓ pair created ({prophet_market_id}) |
| entry_blocked | [{idx}] ✗ blocked: {reason} |
| cycle_end | cycle done — status={status}, reason={reason} |
Heartbeats fire every 15s during the AI seed calc and any other operation expected to exceed 30s, so the chat-side Monitor never has to guess whether the bot is alive.
MCP-first (default on Seren Desktop). Before any subprocess call, probe auth with mcp__seren-mcp__list_projects. If it returns a project list, you are authenticated — done. Schema bootstrap and project/database creation can also be performed entirely over MCP (list_projects / create_project, list_databases / create_database, run_sql_transaction), so on Seren Desktop the agent never needs SEREN_API_KEY in the subprocess environment for setup.
Fallback (no MCP available, or running outside Seren Desktop). The agent reads SEREN_API_KEY (and the desktop-injected API_KEY alias) from the environment, and auto-loads <skill-root>/.env on startup, so any of these three states works:
API_KEY into the runtime — no further action.SEREN_API_KEY exported — no further action.<skill-root>/.env carries SEREN_API_KEY=<key> — the runner auto-loads it.If none of the three is true and the agent cannot reach MCP, register a new account by POSTing to https://api.serendb.com/auth/agent with {"name":"prophet-arb-bot"}. Extract .data.agent.api_key from the response and write it to <skill-root>/.env using your file-write tool (not a shell cp/export — Windows cmd.exe does not interpret those).
Do not create a new account if a key already exists. Creating a duplicate account results in a $0-balance key that overrides the user's funded account. Always probe MCP and inspect <skill-root>/.env before invoking the /auth/agent endpoint.
Reference: https://docs.serendb.com/skills.md.
Mode A operator-arb. For each operator-supplied (prophet_market_id, polymarket_condition_id) pair the agent:
https://app.prophetmarket.ai/api/graphql. The prophet-ai Seren publisher is still listed in the gateway, but the bot bypasses it: Prophet's GraphQL only honors Authorization: Bearer <Prophet session JWT>, the Seren gateway claims Authorization for SEREN_API_KEY billing auth, and Prophet ignores cookie passthrough — so a Prophet session JWT cannot ride through the proxy. See scripts/prophet/transport.py for the direct-transport seam (#493).polymarket-data publisher.prophet_yes − polymarket_yes. If the absolute spread exceeds the configured min_spread (default 3 ¢) and stays under max_spread (default 30 ¢), the agent emits a quoted limit order on Prophet that fades the drift.seren-polymarket-intelligence correlation/volatility data; high polymarket volatility downgrades the opportunity to watchlist-only.Every cycle persists the run shell, scored opportunities, and submitted orders to SerenDB (prophet/prophet). The arb_runs, arb_opportunities, and arb_orders tables are the canonical run history. The agent also emits a single JSON envelope on stdout for seren-cron to capture in its execution_results table.
The arb-bot is delta-neutral only (execution_mode = "delta_neutral"). After every Prophet fill the bot submits the offsetting Polymarket order via py-clob-client. Both Prophet and Polymarket are on Polygon, so no cross-chain bridging is required; each leg locks its own USDC pool. A pre-trade depth check rejects opportunities where the Polymarket book can't absorb the planned notional at max_hedge_slippage_bps (default 200) — preventing naked Prophet exposure at the source. If the hedge submission fails post-fill, the bot invokes the Prophet cancelOrder unwind path and records the order with hedge_status=naked_exposure for operator action.
Issue #591 removed the legacy single_leg execution mode. Naked Prophet exposure is directional speculation on Prophet's price discovery, not arbitrage — it has no place in a delta-neutral arb skill. Configs that still set execution_mode: "single_leg" are rejected at AgentConfig.load with a clear deprecation error; operators must delete the field (default is delta_neutral) or set it explicitly to delta_neutral to migrate.
First-run bootstrap writes "live_mode": true and auto_discover.enabled=true. The live hedger fails closed with reason=polymarket_creds_missing if POLY_PRIVATE_KEY / POLY_API_KEY / POLY_PASSPHRASE / POLY_SECRET are not loaded.
When auto_discover.enabled = true in config.json, every --command run cycle:
Fetches live Polymarket candidates matching the campaign filter — active markets with 24h volume ≥ min_24h_volume_usd (default $10,000) that resolve in [now + min_headroom_hours, resolution_deadline_iso] (defaults: 24h headroom, 2026-05-24 deadline). Caps at max_candidates (default 250) and surfaces raw/eligible/evaluated counters so a capped sample is not mistaken for the full universe. No manual_pairs curation required — Jill invokes the skill and the campaign candidate set refreshes automatically.
Looks up matching Prophet markets via viewer.markets. Matched pairs are UPSERTed into arb_pairs with source_skill='auto_discover' and arbed on the same cycle. Question matching is normalized substring (lowercase, punctuation stripped) — Prophet's /create AI preserves question text near-verbatim from the operator's spreadsheet, so the matcher is tight enough to avoid false positives.
Emits pending_ui_submission for candidates Prophet hasn't created yet. The agent drives Prophet's /create UI via the Agent-driven UI submission runbook below:
{
"polymarket_market_id": "0x...",
"question": "New York Yankees vs. Baltimore Orioles",
"category": "Sports",
"category_slug": "sports",
"resolution_date_iso": "2026-05-18T22:00:00Z",
"initial_bet_usdc": 1.0,
"bounty_id": "",
"prophet_viewer_id": "vid_...",
"source_skill": "prophet-arb-bot"
}
Per entry, the agent drives Prophet /create through the bet form, calls record-created-market to submit the Polymarket hedge before clicking Prophet Confirm, then captures the new prophet_market_id from the redirected URL and calls record-created-market again to UPSERT the pair into arb_pairs. On the next --command run --yes-live tick, the bot trades the new pair.
Refreshes the candidate sheet at state/arb_candidates.xlsx (falls back to .csv if openpyxl is absent). Each row carries the pair status — paired_this_run, already_paired, pending_prophet_creation, or unknown — so the operator can audit the run's discovery output at a glance.
When auto-discover is disabled in an existing custom config, the existing manual_pairs flow is unchanged.
create-market-via-ui requires the local playwright-stealth MCP to be reachable (bundled in Seren Desktop, or via SEREN_PLAYWRIGHT_MCP_COMMAND outside Desktop). The Python subprocess never signs the createMarketWithBet mutation directly; Prophet's embedded wallet runs in the Python-owned browser and auto-signs once the cached session is restored.inputs.prophet_email — the email address you use to log into Prophet (app.prophetmarket.ai). If you do not have a Prophet account yet, use whatever email you want associated with the bot — Prophet will create the account on first login. The arb-bot caches the JWT so the OTP flow only fires when the cache is stale.inputs.email_provider — gmail or outlook. Used only on cold-start cache refresh.inputs.manual_pairs — explicit (prophet_market_id, polymarket_condition_id) pairs. Optional when auto_discover.enabled=true — auto-discover refreshes the candidate set from live Polymarket each cycle. Use manual_pairs for pairs outside the campaign filter or to force-pin a specific market.SEREN_API_KEY — environment or API_KEY injected by Seren Desktop.The arb-bot owns its Prophet session cache under ~/.config/seren/skills/prophet-arb-bot/state/ (override with PROPHET_ARB_STATE_DIR). If the cache is fresh (default leeway 60s before JWT expiry) the agent uses the cached JWT directly with zero OTP emails. If the cache is stale, the agent silently refreshes via the in-process refresh worker. Only when both fail does the cold-start OTP flow fire.
Expected cadence:
PROPHET_SESSION_TOKEN remains a developer/test escape hatch, but agents must not ask users to extract or paste JWTs from browser storage. User-facing runs should use the session cache, silent refresh, or the automated browser OTP flow below.
When the cold-start OTP flow does fire, the Python subprocess spawns the
bundled playwright-stealth MCP server from
/Applications/SerenDesktop.app/Contents/Resources/mcp-servers/playwright-stealth/
as a stdio child process and drives the Prophet login modal through it. No
publisher round-trip is involved — the gateway is local. Override the
spawn command with SEREN_PLAYWRIGHT_MCP_COMMAND (a shell-quoted full
command string) when running outside Seren Desktop. If neither the
bundled binary nor the env override is reachable, cold start returns
status=blocked, reason=blocked_otp_browser_unavailable:seren_desktop_playwright_mcp_unavailable.
The agent should tell the user to open/update Seren Desktop or configure
SEREN_PLAYWRIGHT_MCP_COMMAND; it must not instruct a non-technical user
to manually extract a JWT.
First-run mode is live-enabled delta-neutral, but Prophet's in-browser signing prompt remains the per-market consent gate. Autonomous schedules still require both:
live_mode: true in config.json--yes-live on the CLI (or yes_live=true in the seren-cron payload)Without both on a scheduled run, the cycle still scores opportunities
and emits decision rows, but it never calls placeOrder.
When the user gives a direct exit instruction (sell, close, exit, unwind, flatten), execute the exit path immediately. Cancel every open prophet order surfaced by --command status and ask only the minimum clarifying question if the user also wants to liquidate held positions (which cannot be force-sold on Prophet without an offsetting order).
The arb-bot is a passive quoter — it submits LIMIT orders that rest on Prophet's CTF order book and waits for fills. It does not place marketable taker orders. The exit posture follows from that:
viewer.orders and cancels every open order surfaced. Held YES/NO positions stay on book and must be unwound by the operator out-of-band — Prophet has no force-close for the maker side.tick_size. Prices are submitted via PlaceOrderInput.priceBps (Int, 0–10000 basis points). Prophet's implicit tick size is 1 bp ($0.0001). Quotes that violate the tick are rejected at submission.cancel_all, there is no "passive sell at best bid" path on the user's behalf. If the user wants to liquidate YES at the best bid, they use Prophet's UI directly; the arb-bot will not sweep visible bid depth across the full book to flatten.estimated_fill_size / estimated_exit_value numbers are not produced.The emergency-exit path is cancel_all over viewer.orders — no marketable sells, no position liquidation:
--command status and cancel each via cancelOrder.If the user needs immediate exposure removal (close all / unwind / flatten), they must liquidate via the Prophet UI manually. The arb-bot intentionally refuses to submit marketable sells on the user's behalf.
Before any live run --yes-live:
SEREN_API_KEY is loaded.inputs.manual_pairs has at least one entry OR auto_discover.enabled=true (run --command setup first if neither is satisfied).https://app.prophetmarket.ai/api/graphql is reachable directly (the bot bypasses the prophet-ai publisher because the proxy can't pass the Prophet session JWT through Authorization — see scripts/prophet/transport.py and #493).polymarket-data publisher is reachable.pending_ui_submission is non-empty, query Prophet viewer.cashBalance and Polymarket CLOB collateral. If neither side can fund a single seed AND no existing pairs are in arb_pairs, return a status=blocked, reason=funds_insufficient_for_seeds envelope. If existing pairs ARE present (per #589), drop the pending list and continue scoring the existing pairs — the bot must not short-circuit cycles that have real arb work to do just because the new-market queue is unfundable. The recorder logs seed_preflight_skipped:candidate_count=N so the operator still sees the gap.py-clob-client must be installed (per #590) and POLY_PRIVATE_KEY/POLY_API_KEY/POLY_PASSPHRASE/POLY_SECRET loaded. Reach the Polymarket CLOB and fetch order-book depth for every actionable pair. Opportunities whose visible Polymarket depth can't cover the target notional at max_hedge_slippage_bps are rejected with polymarket_depth_* blockers before the Prophet limit is posted. Query both Prophet protocol cash AND Polymarket CLOB collateral. The blocked envelope returns prophet_deficit_usdc and polymarket_deficit_usdc as separate fields so the agent's deposit runbook can route to the right venue. The blocked envelope now also exposes polymarket_state (per #592 phase 1): no_balance (deposit USDC.e), no_approvals (grant allowances to Polymarket spenders), or ok.blocked envelope and let the cron retry on the next tick.When the operator funds a fresh wallet, the polymarket-state classifier reports no_approvals — the USDC.e balance is on-chain but the Polymarket CTF Exchange, NegRisk CTF Exchange, and NegRisk Adapter haven't been granted token allowances yet. The bot resolves this autonomously on the next live cycle — no separate opt-in flag.
The gate is live_mode + --yes-live only. Same gate the trade-signing path uses. Reaching _annotate_polymarket_state with a non-None live hedger means the operator already consented to signing transactions against the pinned Polymarket spenders — auto-approve uses the exact same key against the exact same spenders.
When state == no_approvals and a live hedger is wired in, the runner builds and broadcasts (via seren-polygon):
USDC.e.approve(<spender>, MAX_UINT256) for each pinned spender (skipped per-spender if already approved).ConditionalTokens.setApprovalForAll(<spender>, true) for each pinned spender (skipped per-spender if already approved).Pinned spenders on Polygon mainnet (chain 137):
| Address | Role |
|---|---|
| 0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E | CTF Exchange v1 (standard markets) |
| 0xC5d563A36AE78145C45a50134d48A1215220f80a | NegRisk CTF Exchange v1 (neg-risk markets) |
| 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296 | NegRisk Adapter (neg-risk conversions) |
| 0xE111180000d2663C0091e4f400237545B87B996B | CTF Exchange v2 (#600 — live CLOB collateral spender) |
| 0xe2222d279d744050d28e00520010520000310F59 | NegRisk CTF Exchange v2 (#600 — live CLOB collateral spender) |
Source-of-truth for v1: py_clob_client.config.get_contract_config(137, neg_risk=…). The NegRisk Adapter is a Polymarket protocol contract not exposed by py-clob-client. The v2 entries (issue #600) come from Polymarket's live CLOB get_balance_allowance response for AssetType.COLLATERAL — py-clob-client v0.34 still ships v1, but Polymarket migrated production allowance tracking to v2, so v2 must be pinned alongside v1 or auto-approve never reaches the addresses the CLOB actually checks. assert_pinned_spenders_match_py_clob_client() cross-checks the v1 entries at startup and refuses to start if py-clob-client ever drifts.
Defense-in-depth. build_usdc_approve_calldata() and build_ct_set_approval_for_all_calldata() refuse to encode for any spender not in the pinned set, so even if the orchestrator is somehow handed an attacker address it cannot sign approval to it. That refusal — not a CLI flag — is the trust boundary. The broadcast result is attached to the blocked envelope under polymarket_auto_approve (per-leg tx_hash + status), so the operator can audit every signed transaction.
Idempotency. broadcast_pinned_polymarket_approvals queries the current allowance / isApprovedForAll per spender and skips broadcasts where the on-chain state already shows approval. After the first successful cycle the auto-approve path is a constant-cost no-op (6 RPC reads, 0 signed transactions).
Issue #524: every placeOrder LIMIT submission locks USDC collateral
on Prophet's CTF order book. The Python runner runs a funds preflight
after scoring and before the placement loop fires. If protocol cash
can't cover the planned collateral, cmd_run returns:
{
"status": "blocked",
"reason": "funds_insufficient",
"payload": {
"action": "deposit_required",
"deposit": {
"chain": "polygon",
"chain_id": 137,
"usdc_contract_polygon": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
"available_usdc": 0.0,
"needed_usdc": 12.0,
"deficit_usdc": 12.0
}
}
}
When this envelope lands, the agent (Claude in Seren Desktop) drives Prophet's deposit UI before the next tick.
Prophet protocol cash (viewer.cashBalance.availableCents) is funded
by USDC deposited into the operator's Safe on Polygon (chainId 137):
On-chain USDC exists but not yet deposited. Query the Polygon
native USDC contract 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
for the operator's Safe via the seren-polygon publisher
(eth_call → balanceOf(address)). If on-chain ≥ deficit, drive
Prophet's /wallet → Deposit UI to move it into protocol cash.
No on-chain USDC available. Surface the Safe address and
deficit_usdc. Stop and let the operator fund externally.
PROPHET_WALLET_URL = "https://app.prophetmarket.ai/wallet"
DEPOSIT_BUTTON_SELECTOR = 'button:has-text("Deposit")'
DEPOSIT_AMOUNT_INPUT_SELECTOR = 'input[data-testid="deposit-amount-input"]'
DEPOSIT_CONFIRM_BUTTON = 'button:has-text("Confirm")'
deposit.safe_wallet_address (when surfaced) from the blocked envelope.seren-polygon.deposit.deficit_usdc, surface and stop.mcp__playwright__playwright_navigate,
mcp__playwright__playwright_click, and
mcp__playwright__playwright_fill) to navigate to
PROPHET_WALLET_URL, click Deposit, fill the amount =
deficit_usdc, click Confirm, and accept the Prophet signing prompt.viewer.cashBalance.availableCents until the deposit lands.agent.py --command run --json-output.funds_insufficient is not auto-paused like the publisher 402
low-SerenBucks case. The cron keeps firing; each tick re-checks the
balance. This is correct when the operator funds externally between
ticks. The blocker funds_insufficient_by_<deficit>_usdc is recorded
on the run so the operator can see the gap without re-running.
When execution_mode = "delta_neutral", every cycle follows this two-leg sequence:
Post-fill sweep (top of cycle). list_user_orders is called first.
Any previously-open Prophet order now reporting filled_shares > 0
triggers an immediate Polymarket hedge — opposite side, same notional,
marketable price snapped to live tick. The hedge order id, fill
quantity, and fill price are persisted to arb_orders alongside the
Prophet leg under hedge_status='hedged'. Latency target: <5s between
fill detection and Polymarket submission.
Pre-trade depth check. Each scored opportunity goes through
assess_polymarket_depth before the Prophet limit is posted. If the
visible Polymarket book can't cover size_usdc at acceptable slippage,
the opportunity is rejected this cycle with a polymarket_depth_*
blocker. This makes the "Prophet fills, Polymarket can't hedge" failure
path impossible at the source — we don't quote Prophet exposure we
can't immediately offset.
Two-venue funds preflight. Both Prophet protocol cash and
Polymarket CLOB collateral are checked. The blocked envelope returns
separate prophet_deficit_usdc and polymarket_deficit_usdc so the
deposit runbook routes to the right venue.
Hedge-failure path. If the hedge submission throws after a Prophet
fill (book moved between depth check and submission, CLOB rejection,
balance shortfall on Polymarket), the bot invokes Prophet's
cancelOrder for cleanup (no-op if already fully filled) and records
hedge_status='naked_exposure'. Prophet has no force-close on the maker
side, so naked exposure is honestly surfaced rather than silently
accumulated — the operator must unwind the Prophet leg manually via
the Prophet UI.
Schema: arb_orders carries four delta-neutral columns
(polymarket_filled_qty, polymarket_fill_price, polymarket_order_id,
hedge_status). These default to neutral values on legacy rows so
existing operators see no migration churn.
placeOrder is server-signed (#505 Phase 15)The Prophet placeOrder mutation is server-signed. Unlike
createMarketWithBet (which requires a client-signed
SignedOrderInput), PlaceOrderInput carries no signature — Prophet's
backend signs the CTF order on behalf of the user via the user's
embedded wallet. Just the Prophet session JWT in the Authorization
header is sufficient.
The mutation shape is live-validated against Prophet's production
GraphQL endpoint (2026-05-13) and pinned in
tests/fixtures/prophet_schema.json:
mutation PlaceOrder($input: PlaceOrderInput!) {
placeOrder(input: $input) {
order { id status side outcome type priceBps quantityShares filledShares remainingShares }
cashBalance { availableCents totalCents }
errors { field message code }
}
}
PlaceOrderInput = {marketId, outcome, type, side, priceBps, quantity, timeInForce}.
If Prophet drifts the schema later, capture the new shape with:
python3 scripts/agent.py --command probe-schema
Default schedule is 0 * * * * (every hour, on the hour, UTC). The schedule lives in seren-cron; a long-lived local poller on the user's machine claims due ticks and runs agent.py --command run locally.
# 1. Register the runner and the local-pull job. Run once after setup.
python3 scripts/setup_cron.py create \
--prophet-email "$PROPHET_EMAIL" \
--email-provider gmail \
--config config.json \
--yes-live
# 2. Start the local poller. Leave this process running on the machine
# that should execute the arb work (e.g. via launchd, pm2, or just
# leaving Seren Desktop open).
python3 scripts/run_local_pull_runner.py --config config.json
# 3. Pause / resume / delete the schedule.
python3 scripts/setup_cron.py list
python3 scripts/setup_cron.py pause --job-id <job_id>
python3 scripts/setup_cron.py resume --job-id <job_id>
python3 scripts/setup_cron.py delete --job-id <job_id>
Auto-pause triggers on a publisher 402 (low SerenBucks). Top up at https://serendb.com/serenbucks and setup_cron.py resume.
Transient failures (Prophet GraphQL down, polymarket-data 5xx, OTP not delivered) do not auto-pause; the cron keeps firing and seren-cron's execution_results table records the consecutive blocks for later inspection.
The skill writes to SerenDB project=prophet, database=prophet. On --command setup the agent resolves the project/database via the seren-db publisher's /projects + /databases endpoints, fetches a Postgres connection URI from /projects/{id}/connection_uri, and applies serendb_schema.sql over a psycopg2 connection. Every cycle then writes opportunities and orders to that database.
Tables (created on first setup):
arb_pairs — prophet ↔ polymarket bindingarb_runs — one row per --command runarb_opportunities — every scored opportunity (acted on or skipped)arb_orders — submitted orders + last-seen statusarb_positions — open holdings (computed from fills, populated in a future revision)arb_pnl_snapshots — daily mark-to-market (populated in a future revision)The seren-db publisher does not expose an HTTP run-sql endpoint; SQL execution happens via the connection URI it returns. psycopg2-binary is a runtime dependency (see requirements.txt).
Preconditions (operator does once, no shell required):
prophet/prophet-arb-bot.requirements.txt are installed.mcp__seren-mcp__list_projects), or SEREN_API_KEY lives in <skill-root>/.env, or SEREN_API_KEY is set in the current shell. The runner auto-loads <skill-root>/.env on startup, so a key in that file is sufficient on any platform — no export or cp required.Cycle commands (the agent runs these via python3):
# 1. Validate config and apply the schema. Auto-bootstraps config.json
# from config.example.json on first run with auto_discover.enabled=true,
# execution_mode=delta_neutral, and live_mode=true. Email + provider
# are persisted from flags; existing configs are never overwritten.
python3 scripts/agent.py --config config.json --command setup --json-output \
--prophet-email <prophet-login-email> --email-provider gmail
# 2. Validate the runner end-to-end before scheduling cron. Confirm the
# JSON output reports `"status": "ok"`. If it reports `blocked`
# (e.g. `funds_insufficient_for_seeds`), resolve the blocker using
# the deposit envelope and re-run before continuing.
#
# Before launching this command, arm a chat-side Monitor on
# `state/run_progress.jsonl` so each stage transition streams into
# chat (see "Live progress stream" above). The final JSON envelope
# still arrives on stdout when the cycle completes.
python3 scripts/agent.py --config config.json --command run --yes-live --json-output
# 3. Schedule and start the autonomous hourly runner. Use --yes-live
# only after step 2 returned status=ok.
python3 scripts/setup_cron.py create \
--config config.json \
--prophet-email <prophet-login-email> \
--email-provider gmail
python3 scripts/run_local_pull_runner.py --config config.json
When auto_discover.enabled = true, every pending_ui_submission
entry will cost the operator initial_bet_usdc on Prophet at confirm
time. In delta_neutral mode, it costs the same amount again on
Polymarket for the hedge (see below). Before emitting the list, the
runner now:
viewer.cashBalance and (in delta-neutral mode)
DirectClobTrader.get_cash_balance to compute
max_fundable = min(prophet_floor, polymarket_floor).assess_polymarket_depth)
when delta-neutral is active.max_fundable entries.If max_fundable == 0 the cycle returns
status=blocked, reason=funds_insufficient_for_seeds with a
deposit_required_for_seeds envelope carrying split deficits per venue.
Issue #636/#654: market creation is fully autonomous. During one
--command run --yes-live cycle, the Python subprocess opens one
Playwright-stealth MCP gateway and one browser context for the whole
pending_ui_submission batch, restores the cached Prophet session once
into that warm context (the JWT + refresh token are JSON-stringified
into the Prophet origin's localStorage — both keys are present since
#583), and drives /create end-to-end for each pending entry without
any mcp__playwright__* orchestration from the calling LLM.
The standalone per-entry command remains:
python3 scripts/agent.py \
--command create-market-via-ui \
--polymarket-condition-id "$POLY_CID" \
--question "$QUESTION" \
--initial-bet-usdc "$BET_USDC" \
--json-output
Inside --command run --yes-live, the bot calls the same inner driver
directly against the warm browser context, resets the page between
entries, then attaches a ui_submission_results: [{polymarket_condition_id, status, reason, prophet_market_id}] block
to the run envelope. If the warm context loses Prophet auth mid-batch, the
current entry is blocked with reason='warm_context_corrupted', the
gateway/browser are reopened, and the next entry continues on a freshly
restored context. The orchestrating LLM does NOT call this command
directly; it only re-runs --command run --yes-live to pick up the
newly-created pairs for arbing.
Per-entry sequence inside the subprocess:
/create, install a window.fetch wrapper that captures
startOddsCalculation's sessionId client-side, then click
Validate Question + Create Market.cmd_compute_seed_intent with the captured
sessionId — Prophet's 6-model AI calc runs 60–180s; on completion
the helper returns seed_side, hedge_price, and tick_size.cmd_record_created_market with --prophet-seed-side
--polymarket-marketable-price to submit the Polymarket hedge.hedge_status='hedge_failed_no_commit', abort without clicking
Confirm. The entry's ui_submission_results carries
reason='hedge_failed_no_commit'.hedge_status='hedged', click Prophet Confirm. Prophet's
embedded wallet auto-signs createMarketWithBet in-browser using the
restored session. Poll the URL for the /markets/<id> redirect.cmd_record_created_market again with
--prophet-confirm-declined to unwind the Polymarket leg. The
entry's ui_submission_results carries reason='prophet_confirm_failed'.cmd_record_created_market with the captured
--prophet-market-id to UPSERT the pair. The entry's
ui_submission_results carries reason='pair_created' + the new
prophet_market_id.Per-entry blocking reasons surfaced in ui_submission_results.reason:
prophet_session_unavailable — the cached Prophet session is missing
or could not be refreshed. The OTP cold-start path will fire on the
next --yes-live cycle. Since #658 the session-observability check
polls for a positive auth signal over an 8s budget (250ms interval),
and since #660 it accepts a surviving planted token plus a Prophet
origin URL as a positive signal — the same heuristic the mid-batch
health check uses — so a slow browser cold launch no longer
false-negatives a fresh cached session. When this block does fire,
the envelope now carries payload.observable_check with the
post-restore state (planted state presence, URL, budget used) so
the operator can tell which signal failed. Since #662 the
warm-context restore path also fails closed when the session
restore call itself raises (MCP add_init_script rejected,
navigate timed out, gateway stdio dropped, etc.) — the envelope
carries payload.restore_exception with the raw exception type
and message, and payload.auth_source reads
prophet_session_unavailable:restore_failed so operators can
distinguish a restore-time failure from an OTP-side failure
without burning an email round-trip. Since #664 the OTP
fall-through path also attaches payload.cache_check with the
decision inputs the cache-fresh guard actually saw (state,
is_fresh, jwt_present, refresh_token_present, jwt_expires_at)
so operators can tell whether the guard rejected the cache (one
of those fields was False) or whether it accepted the cache and
the failure was downstream (in which case observable_check or
restore_exception will also be present). Since #666 the
warm-context establish path is also tolerant of JWT-only sessions:
Prophet's auth provider retired its localStorage refresh-token
mechanism server-side, and the bot now treats the JWT alone as the
session. Capture, cache read, and the cache-fresh guard all
normalize the legacy refresh-token slot to empty, and the session
restore step plants only the JWT when no refresh token is
available. Existing operators with a stale cached refresh token
recover automatically on the next cycle — no manual cache wipe
required.seren_desktop_playwright_mcp_unavailable — no playwright-stealth
MCP command resolvable. Run on Seren Desktop or set
SEREN_PLAYWRIGHT_MCP_COMMAND.warm_context_corrupted — the shared browser context lost observable
Prophet auth after an entry. That entry is skipped; the bot tears down
and restores a fresh context before attempting the next pending entry.ocs_session_id_not_captured — the capture shim did not observe a
Prophet odds-session response within the poll window. Since #655 the
shim is installed via add_init_script at document_start (so the
wrapper is in place before Prophet's bootstrap fires), wraps both
window.fetch and XMLHttpRequest, matches any URL containing
graphql or odds, and walks responses recursively for a
sessionId/session_id/oddsSessionId field under any odds-shaped
ancestor — so this blocker should be rare. Since #695, the blocked
envelope carries payload.capture_observations (the diagnostic ring
buffer) and payload.capture_error (any JS error the shim caught)
directly, so the operator can tell the failure mode without re-driving:
an empty capture_observations list points to a transport Prophet
adopted that neither fetch nor XHR covers; a non-empty list with
ok: false rows points to schema drift beyond the recursive walker's
reach; a non-empty capture_error points to the wrapper itself
raising during interception.odds_session_not_completed / odds_session_timeout /
prophet_market_not_viable / no_edge — Prophet's AI rejected or
did not justify a seed bet. Abandon the entry; no exposure was created.polymarket_book_unavailable — Polymarket's CLOB book had no usable
bid/ask. Retry next tick.hedge_failed_no_commit — Polymarket rejected the hedge before Prophet
Confirm. No exposure on either side.prophet_confirm_failed — Polymarket hedge filled but the Prophet
createMarketWithBet signing flow did not redirect to /markets/<id>.
The Polymarket leg has been unwound; payload.unwind_status reports
the outcome (unwound_after_prophet_decline or a hedger error).Pass --skip-ui-submission on --command run only when explicitly
exercising the legacy code path for tests.
Every seed bet is hedged before Prophet Confirm. The flow:
The agent fills Prophet's /create UI through the bet form without
clicking Confirm.
The agent invokes:
python3 scripts/agent.py --command record-created-market \
--polymarket-condition-id "$POLY_CID" \
--prophet-seed-side buy \
--polymarket-marketable-price 0.001
If the runner returns hedge_status='hedged', the agent clicks
Prophet Confirm and captures the resulting prophet_market_id.
The agent persists the pair with
record-created-market --polymarket-condition-id "$POLY_CID" --prophet-market-id "$PROPHET_MID".
Seed hedge statuses:
hedged — Polymarket accepted the pre-confirm hedge; click Prophet
Confirm next.hedge_failed_no_commit — Polymarket rejected the hedge before
Prophet Confirm; abort this entry with no exposure.unwound_after_prophet_decline — Polymarket filled, Prophet Confirm
failed or was declined, and the Polymarket leg was reversed.naked_exposure — an unwind attempt after Prophet decline failed;
operator action is required on Polymarket.Free
npx skills add serenorg/seren-skillsSelect “Prophet Arb Bot” when prompted
Prophet
Added May 25, 2026