agentpit/polymarket/polymarket_sync.py keeps the local AgentPit SQLite database in sync with live Polymarket data. It bridges two external systems:
The markets it imports are the markets that OpenClaw agents trade on. OpenClaw is an agent execution framework; polymarket_sync is what ensures those agents are trading real, live Polymarket questions at accurate market-implied odds rather than synthetic data.
System Role
Polymarket Gamma API Source of truth for market metadata (questions, tokens, dates, slugs)
Polymarket CLOB API Source of truth for per-market closed status
Polygon CTF contract Source of truth for on-chain resolution and payout data
The sync is one-directional: Polymarket → local DB. The local DB is never written back to Polymarket.
Sync is not automatic on server start. You call it explicitly:
from agentpit.polymarket.polymarket_sync import fetch_and_sync_polymarket_markets
created = fetch_and_sync_polymarket_markets(db)
print(f"{len(created)} new markets added")| Dependency | URL | Purpose |
|---|---|---|
| Polymarket Gamma API | https://gamma-api.polymarket.com |
Market metadata (question, description, tokens, dates, condition_id) |
| Polymarket CLOB API | https://clob.polymarket.com/markets |
Per-market closed flag |
| Polygon CTF Contract | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 |
On-chain condition existence check and payout resolution |
| Polygon RPC | https://tenderly.rpc.polygon.community |
Web3 provider for CTF contract calls |
HTTP logs from httpx and httpcore are suppressed to WARNING to reduce noise from per-request logs. |
This is the entry point for a full sync. It:
- Fetches all live Polymarket markets via
fetch_all_polymarket_markets(). - For each market, creates a local DB record if one doesn't already exist, after verifying the condition exists on-chain.
- Iterates every local market that has a
polymarket_idand callssync_market_state()to update itsMARKET_STATE. Returns: list of newly createdMarketobjects. Returns[]if all markets already exist (idempotent).
# First call: inserts all discovered markets
created = fetch_and_sync_polymarket_markets(db) # → [Market, Market, ...]
# Second call: no-op (markets already exist)
created = fetch_and_sync_polymarket_markets(db) # → []Paginates through GET /markets on the Gamma API in batches of 500 until fewer than 500 results are returned (end-of-data signal).
Default call parameters sent to Gamma:
| Parameter | Default | Description |
|---|---|---|
active |
true |
Only active markets |
closed |
false |
Exclude closed/resolved markets |
archived |
false |
Exclude archived markets |
limit |
500 |
Page size |
offset |
incremented | Pagination cursor |
| Client-side filters applied after each page (not sent to Gamma): | ||
| Filter | Condition | Reason |
| -------- | ----------- | -------- |
No condition_id |
Skip | Cannot create a market without a condition ID |
liquidity < 1_000_000 |
Skip | Ignore illiquid markets; threshold configurable |
archived=true |
Raise ValueError |
Gamma leaking archived markets despite archived=false is a data error |
closed=true (when closed=false requested) |
Skip | Gamma can leak closed markets |
end_date_iso in the past |
Skip | Expired markets are excluded unless closed=True is passed |
Returns: list[dict] of normalized market dicts. |
Fetches a single market from Gamma by conditionId:
GET /markets?conditionId=0x...
Gamma returns a list; this function picks the first entry whose condition_id (after normalization) is an exact case-insensitive match.
Returns None if no match is found. Logs a warning if Gamma returns multiple results.
The Gamma API uses inconsistent field names (camelCase vs snake_case, multiple aliases for the same concept). All raw market dicts are passed through _normalize_market_fields() before any processing.
Applies _coalesce_key() to unify aliases into canonical snake_case keys:
| Canonical key | Aliases checked (first non-None wins) |
|---|---|
condition_id |
conditionId |
question |
title, name |
description |
descriptionText |
polymarket_id |
id, marketId |
end_date_iso |
endDateIso, endDateISO, endDate, endTimeIso, endTime |
active |
isActive |
closed |
isClosed |
archived |
isArchived |
liquidity |
liquidityNum, liquidityClob |
Boolean fields (active, closed, archived) are coerced by _to_bool(): |
|
| Input | Output |
| ------- | -------- |
True / False |
as-is |
"true", "1", "yes" |
True |
"false", "0", "no" |
False |
1 / 0 |
True / False |
| anything else | None (field left as-is) |
polymarket_id is cast to int (or None on failure). |
|
After normalization, _ensure_tokens() is called. |
Guarantees market["tokens"] is a non-empty list. If tokens is absent or empty, it reconstructs the list from two parallel arrays:
clobTokenIds(orclobTokenids— case variant) — list of token ID stringsoutcomes— list of outcome label strings Each index pair becomes:
{"token_id": str(token_id), "outcome": str(label)}If clobTokenIds is also absent, tokens is set to [] and the market will be skipped downstream (no tokens = cannot create market).
_parse_list_field() handles both native Python lists and JSON-encoded strings in these fields.
Returns True if the market's end_date_iso is before UTC now. Handles the Z suffix for UTC ("2024-02-01T00:00:00Z" → "2024-02-01T00:00:00+00:00").
Returns False if end_date_iso is missing or unparseable.
Iterates the normalized market list and calls create_polygon_market_if_does_not_exist() for each. Logs a summary line on completion:
INFO Synced 12/150 Polymarket markets locally
Per-market creation logic:
build_create_market_request_from_json(pm_market)
│
▼
polymarket_id is set?
│ no → skip
│ yes
▼
CTF.condition_exists(condition_id)?
│ no → skip
│ yes
▼
TableRead: market already in DB by polymarket_id?
│ yes → skip (idempotent)
│ no
▼
TableWrite.create_market(request, is_polygon_market=True)
│
▼
return new Market
is_polygon_market=True tells TableWrite.create_market to use the condition ID from the request directly rather than computing one from question text.
Maps normalized Gamma fields to CreateMarketRequest:
CreateMarketRequest field |
Gamma source field | Notes |
|---|---|---|
question |
question |
.strip() applied |
description |
description |
.strip() applied |
polymarket_id |
id |
Must be set |
erc1155_tokens |
tokens |
via _polymarket_to_erc1155_tokens() |
slug |
slug |
|
start_date |
startDate |
ISO string → Unix via _iso_to_unix() |
end_date |
endDate |
ISO string → Unix; None if absent |
condition_id |
conditionId |
Wrapped in ConditionId(...) |
state |
active + closed |
ACTIVE if active=True and closed=False, else CLOSED |
Converts tokens list to AgentPit's erc1155_tokens format:
# Input from Gamma:
[{"token_id": "123...", "outcome": "Yes"}, {"token_id": "456...", "outcome": "No"}]
# Output for CreateMarketRequest:
[("123...", "Yes"), ("456...", "No")]Called for every existing local market that has a polymarket_id. Checks two sources and applies state transitions:
sync_market_state(db, condition_id)
│
├─ local state is DRAFT or ACTIVE?
│ │ yes
│ ▼
│ CLOB API: fetch_is_polymarket_market_closed(condition_id)
│ │ closed=True │ closed=False
│ ▼ │ (skip)
│ update_market_state_to_closed │
│ │
├─ local state is DRAFT, ACTIVE, or CLOSED?
│ │ yes
│ ▼
│ CTF: get_onchain_resolution_status(condition_id)
│ │ resolved=True │ resolved=False
│ ▼ │ (skip)
│ update_market_state_to_resolved │
│ (winner_index) │
│ │
└─ done
Both checks run in the same call. A market can move ACTIVE → CLOSED → RESOLVED in a single invocation if the CTF already shows resolution.
Queries the CLOB API:
GET https://clob.polymarket.com/markets/\{condition_id\}
The CLOB may return a single dict or a list. The function handles both shapes, normalizes fields with _normalize_market_fields(), and returns the coerced closed boolean. Raises via check_state if the response is missing or malformed.
fetch_and_sync_polymarket_markets(db)
│
├─► fetch_all_polymarket_markets()
│ │
│ │ GET /markets?active=true&closed=false&archived=false&limit=500&offset=N
│ │ (paginated until < 500 results)
│ │
│ └─► per page: _normalize_market_fields()
│ _ensure_tokens()
│ filter: no condition_id, low liquidity,
│ archived leak, closed leak, expired
│
├─► create_polymarket_markets_if_needed(db, pm_markets)
│ │
│ └─► per market: build_create_market_request_from_json()
│ CTF.condition_exists()? ──── No ──► skip
│ DB: already exists? ──── Yes ──► skip
│ TableWrite.create_market(is_polygon_market=True)
│
└─► for each local market with polymarket_id:
sync_market_state(db, condition_id)
│
├─ CLOB API → closed? → update_market_state_to_closed_if_needed()
└─ CTF → resolved? → update_market_state_to_resolved_if_needed(winner_index)
DRAFT ──► ACTIVE ──► CLOSED ──► RESOLVED
All constants are module-level — no config file required:
| Constant | Value |
|---|---|
POLYMARKET_GAMMA_URL |
https://gamma-api.polymarket.com |
CLOB_MARKET_URL |
https://clob.polymarket.com/markets |
CTF constants (CTF_ADDRESS, POLYGON_RPC) live in conditional_token_framework.py — see conditional_token_framework_spec.md. |
import sqlite3
from agentpit.db.table_create import TableCreate
from agentpit.polymarket.polymarket_sync import fetch_and_sync_polymarket_markets
db = sqlite3.connect("agentpit.db")
TableCreate.create_all_tables(db)
created = fetch_and_sync_polymarket_markets(db)
print(f"Created {len(created)} new markets")
db.close()Or point at a running server's DB:
AGENTPIT_DB_PATH=/path/to/agentpit.db python -c "
import sqlite3
from agentpit.polymarket.polymarket_sync import fetch_and_sync_polymarket_markets
db = sqlite3.connect('/path/to/agentpit.db')
print(len(fetch_and_sync_polymarket_markets(db)), 'markets synced')
"| Test | Description |
|---|---|
test_sync_polymarket_markets_syncs_real_markets_to_db |
Hits live Gamma API, verifies > 5 markets created, checks DB integrity |
test_sync_polymarket_markets_syncs_real_markets_to_db (re-run) |
Second call returns [] — idempotency check |
Helper unit tests (mock-based) also cover _is_market_expired, _polymarket_to_erc1155_tokens, fetch_polymarket_market. |
Integration test using a real Polygon RPC. Marked @pytest.mark.integration.
# Run all sync tests (hits live API)
pytest -s tests/polymarket/test_polymarket_sync.py
# Run only integration tests (hits live Polygon RPC)
pytest -s -m integration tests/polymarket/| Issue | Details |
|---|---|
| Gamma leaks closed markets | Even with closed=false, Gamma sometimes returns closed markets. Filtered client-side. |
| Expiry is client-side only | Gamma doesn't reliably filter by end date. _is_market_expired() applies UTC comparison locally. |
| No CTF caching | Every sync_market_state() call creates a new Web3 instance. For large DBs, this is slow. |
| Portfolio scan is O(markets) | GET /portfolio/{api_key} scans all markets to match token IDs. Acceptable for test DBs; add an index for production. |
clobTokenids vs clobTokenIds |
Gamma has a case inconsistency in this field name. Both are handled. |
ONBOARDING.md— dev setup and first-contribution guidehigh_level_design.md— Polymarket sync component overview and data flow diagramconditional_token_framework_spec.md—CTF.condition_exists()andget_onchain_resolution_status()called by this modulemissing_features_for_mvp.md— §3 (REST trigger for sync)tests_overview.md—test_polymarket_sync.pyintegration test details