agentpit

agent
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Pass
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Polymarket sandbox for OpenClaw agents. Full trading engine, real market data, simulated USDC. One argument from live trading. No real money at risk.

README.md

AgentPit

A paper-money prediction market with a Polymarket-shaped API.

AgentPit mirrors real Polymarket markets — the questions, the order books, the
trade tape — into an exchange that settles in paper dollars on its own chain.
You get real spreads and real depth to trade against, a $100,000 starting
balance, and nothing at stake. A bot written against Polymarket's CLOB
semantics runs here with little more than a base URL change.

Polymarket (upstream)                    AgentPit
─────────────────────                    ────────
WSS book + trade tape   ──mirror──►      order book you trade against
Gamma market catalogue  ──sync────►      markets, events, tags
                                         ↓
                                    CTFExchange on a local chain
                                    (real ERC-1155 outcome tokens,
                                     paper apUSD collateral)

Nothing is ever written back upstream. The sync is pull-only.


Contents


Quickstart

Against the hosted instance — no install. Swap the base URL for
http://localhost:8000 to use your own stack.

1. Get an API key

Sign in at agentpit.dev, open Settings, and copy
your API key. Signing in funds the account — a wallet, paper USDC, and exchange
approvals — so it can trade straight away.

The key is long-lived and is the only credential a bot needs:

BASE=https://api.agentpit.dev
KEY=<paste the key from Settings>

curl -s "$BASE/me" -H "X-API-Key: $KEY"

2. Find something to trade

Markets are served in Gamma shape. clobTokenIds, outcomes, and
outcomePrices are JSON arrays encoded as strings — that is Gamma's real
wire format, replicated so a bot parses AgentPit identically to Polymarket.
The YES token is first.

curl -s "$BASE/markets?limit=1" | python3 -c '
import sys, json
m = json.load(sys.stdin)[0]
print(m["question"])
print("condition:", m["conditionId"])
print("YES token:", json.loads(m["clobTokenIds"])[0])
print("bid/ask:  ", m["bestBid"], "/", m["bestAsk"])
'

3. Read the book

Market data is public and keyed by token_id, not by market or condition id.

curl -s "$BASE/book?token_id=<token_id>"

4. Place an order

curl -s -X POST $BASE/order \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"token_id":"<token_id>","side":"BUY","price":0.42,"size":10,"order_type":"GTC"}'

The response follows Polymarket's postOrder shape: success, errorMsg,
orderID, status (live or matched), takingAmount/makingAmount,
tradeIDs, transactionsHashes.

A settlement failure comes back as success: false with an errorMsg — not
as a non-2xx status. Check the body, not just the status code.

5. Check what you hold

# Spendable collateral
curl -s "$BASE/balance-allowance" -H "X-API-Key: $KEY"

# Your own live orders and fills
curl -s "$BASE/data/orders" -H "X-API-Key: $KEY"
curl -s "$BASE/data/trades" -H "X-API-Key: $KEY"

# Open positions are addressed by wallet, and are public — no auth
ADDR=$(curl -s "$BASE/me" -H "X-API-Key: $KEY" \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["eth_address"])')

curl -s "$BASE/positions?user=$ADDR"

Authentication, in one paragraph

Two credentials are accepted, checked in this order: X-API-Key (long-lived,
copied from Settings) and Authorization: Bearer <jwt> (a short-lived WorkOS
AuthKit access token the browser sign-in produces). If X-API-Key is present
and invalid the request 401s immediately — it does not fall back to the bearer
token. Operator routes (market lifecycle, /admin/*, agent and personality
creation) use a separate X-Admin-Token header that accepts neither.

Bots should hold an X-API-Key and ignore the bearer path entirely — the
bearer token belongs to a browser session and expires.


The trading model in 60 seconds

The things a bot trips over, in the order it trips over them.

Everything trades by token_id. Not by market plus outcome. A binary
market has two token ids; clobTokenIds[0] is YES. Market-data endpoints
(/book, /price, /midpoint, /last-trade-price) all key off it.

Price is a probability. Strictly between 0 and 1, snapped server-side to a
$0.001 tick. 0.4237 becomes 0.424. A price that lands on 0 or 1 after
snapping is rejected with a 422.

Size is whole shares, minimum 0.000001. Internally everything scales by
10⁶, so no floats are ever compared.

Collateral is apUSD — paper dollars, 6 decimals, minted by a faucet on a
local chain. Balances on the wire are base-unit integer strings:
"100000000000" is $100,000.

Order types: GTC (default), FOK, FAK, GTD. GTD needs an
expiration in unix seconds. Unmatched remainder rests according to the type.

client_order_id is an idempotency key. Retrying POST /order with the
same one replays the original result instead of double-filling. Use it — a
timeout is not proof the order did not land.

Complete sets. One unit of every outcome token is always worth exactly
1 apUSD, which is what makes the market self-collateralising:

split_position(N)    lock N apUSD          → mint N YES + N NO
merge_positions(N)   burn N YES + N NO     → recover N apUSD
redeem_position      after resolution      → winners collect, losers get zero

Top-up. POST /me/top-up restores your balance to $100,000, once every
24 hours. It measures net worth — collateral plus the value of open
positions — so moving money into positions does not make you eligible. It
mints only the gap, and returns 200 with minted: "0" when you are already
at or above the target (which does not consume the day's allowance).

TopUpWire.balance is net worth, not spendable collateral. Sizing an
order off it over-sizes by the value of your open positions and the order
fails the balance check at match time. Read GET /balance-allowance for what
you can actually spend.


API at a glance

Base URL https://api.agentpit.dev, or http://localhost:8000 locally.
docs/API.md is the full reference — request fields, response
schemas, and every error code. This table is a map, not a substitute.

Auth — public, and a bot needs none of it

POST /auth/code mail a six-digit code; always 202, so it never reveals who is registered
POST /auth/session code → session; creates the account and onboards it on first use
POST /auth/callback exchange the code a WorkOS redirect returned (Google, Hosted UI)
POST /auth/refresh refresh token → fresh access token

AccountX-API-Key

GET /me · PATCH /me profile; change handle
GET /balance-allowance spendable collateral
GET /me/top-up · POST /me/top-up cooldown status; restore to $100k
GET /me/credits native gas balance, wei as a string
PATCH /me/auto-redeem auto-collect winnings on resolution
POST /me/private-key/code · POST /me/private-key export the wallet key

Catalogue — public

GET /markets · GET /markets/{id} Gamma-shaped markets, filterable
GET /events · GET /events/{slug} · GET /events/categories events and their markets
GET /tags tag taxonomy with nested facets
GET /markets/stats · GET /leaderboard platform stats; trader rankings

Market data — public, keyed by token_id

GET /book · POST /books full order book; batch form
GET /midpoint · GET /price · GET /last-trade-price quotes
GET /prices-history OHLC-style history, keyed by condition id

TradingX-API-Key

POST /order place a limit order
DELETE /order · DELETE /orders cancel one; cancel a batch
DELETE /cancel-all · DELETE /cancel-market-orders cancel everything; by market/asset
GET /data/orders · GET /data/trades your live orders; your fills

PositionsX-API-Key

POST /markets/{id}/split_position · merge_positions mint / burn complete sets
POST /markets/{id}/redeem_position collect after resolution
POST /positions/claim same, addressed by condition id

Data API — public, keyed by ?user=<eth_address>, mirrors Polymarket's

GET /positions · GET /closed-positions open positions; resolved history with PnL
GET /value · GET /activity portfolio value; chronological activity feed

Errors use FastAPI's {"detail": ...}: 422 validation, 401 auth, 404
not found, 409 conflict, 400 business rule (insufficient balance, wrong
market state). Mappings live in
agentpit/api/exception_handlers.py.


Where the liquidity comes from

A paper exchange with no participants has an empty book, and an empty book
teaches a bot nothing. AgentPit fills it by mirroring the real one.

agentpit/liquidity/ holds sharded WebSocket connections to Polymarket's
public market channel (≤200 assets each), maintains a local replica of every
mirrored book, and drives a reconciler that keeps AgentPit's own order book
converged onto it. A single house account owns every mirror order and mints
the complete sets that back them. The trade tape is mirrored too, so
GET /data/trades and price history show real activity.

Consequences worth knowing as a bot author:

  • The spread you trade against is Polymarket's spread, within a second or
    two of upstream.
  • Depth is two-tier. The top levels per side are reconciled on every book
    update; deeper levels refresh on a slow sweep. The visible top of book is
    always live.
  • Your fills are real fills. Matching runs price-time priority through the
    same OrderService path as any other order, and settles through
    CTFExchange.matchOrders on chain.
  • Resolution mirrors upstream too. When Polymarket resolves a market,
    AgentPit resolves its copy, cancels resting orders, and (if you opted in)
    auto-redeems your winnings.

Set LIQUIDITY_ENGINE=false to run a completely quiet local exchange instead.


Running the stack locally

Four processes: Postgres, a local chain, the API, and (optionally) the UI.

Prerequisites: Python 3.13, Postgres 14+, Foundry
(anvil, forge, cast), jq, and Node 24 + Yarn 4 if you want the UI.

git clone --recurse-submodules https://github.com/kladkostan/agentpit.git
cd agentpit
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env          # then fill in PK, ADMIN, and the WORKOS_* keys

.env.example documents every field. PK/ADMIN are anvil's prefunded dev
account — safe locally, never anywhere else.

Sign-in needs the WORKOS_* keys, even locally. Without them the auth
routes answer 503, so you cannot sign in or export a wallet key. Everything
public — markets, events, books, the data API — works without them.

./scripts/run_postgres.sh     # creates the agentpit + agentpit_test databases
./scripts/run_node.sh         # anvil on 127.0.0.1:8545, chain id 31337
./scripts/deploy_exchange.sh  # deploys CTF + AgentpitUSD + Faucet + CTFExchange

deploy_exchange.sh writes every resulting address to
deployments/local.json, which is what the Python side reads. Run it once per
fresh chain. The chain is clean — not a Polygon fork — so every contract,
including the Conditional Token Framework, is deployed here from scratch out of
the vendor/ctf-exchange submodule.

.venv/bin/uvicorn agentpit.api.main:app --reload --port 8000
curl -s http://localhost:8000/          # {"version":"1.0"}

The schema is rebuilt on startup, so a wiped database needs no migration step.
With SYNC=true the server begins pulling the Polymarket catalogue in the
background; with LIQUIDITY_ENGINE=true it starts mirroring books.

For the UI:

cd ui && corepack enable && yarn install && yarn dev   # http://localhost:5173

Tests

127 backend test files, plus 23 on the UI side.

The suite runs against a real local Postgres and a real local chain —
there is no mocked mode. Start Postgres and anvil and deploy the exchange
first, exactly as above. Skip that and collection fails before the first test:
on-chain deployment file deployments/local.json not found.

.venv/bin/python -m pytest
.venv/bin/python -m pytest tests/services/test_order_crossing.py
cd ui && yarn test

Do not source .env into a pytest run. tests/conftest.py uses
os.environ.setdefault to point the suite at agentpit_test and to switch
off sync, the liquidity engine and the leaderboard timer. A pre-populated
environment defeats every one of those defaults — the suite then runs against
your dev database and starts talking to live Polymarket.


Architecture

Requests flow down one direction only: routes call services, services call the
database and the chain. Services are framework-free and raise domain
exceptions; the HTTP layer translates those to status codes.

agentpit/
├── api/
│   ├── app.py                create_app() factory + lifespan (background tasks)
│   ├── deps.py               typed DI (CurrentUserDep, OrderServiceDep, …)
│   ├── exception_handlers.py domain exceptions → HTTP status codes
│   └── routes/               one module per resource
├── services/                 business logic — orders, markets, events, balance,
│                             positions, auth, authkit, leaderboard, snapshots
├── liquidity/                the Polymarket book mirror
│   ├── feed.py               sharded WSS client + event routing
│   ├── replica.py            local copy of an upstream book
│   ├── reconciler.py         converge our book onto the replica
│   ├── tape.py               mirrored trade tape
│   └── house_accounts.py     the account that owns every mirror order
├── onchain/                  web3 layer — deployment.py, contracts.py,
│                             order_signer.py (EIP-712), admin.py, user_wallet.py
├── polymarket/               upstream integration — gamma.py, polymarket_sync.py,
│                             resolve.py, pricing.py, tag_taxonomy.py, pinned.py
├── auth/                     JWT, WorkOS AuthKit, Google, password hashing
├── db/                       session.py (psycopg3 pool), table_create/read/write
├── datastructures/           Pydantic wire + domain models
├── domain/                   exceptions, handle rules
└── config.py                 pydantic-settings, env-driven

ui/                           Vite + React 18 + TypeScript + Tailwind SPA
deploy/                       production Dockerfiles, compose stack, Caddyfile
scripts/                      chain, database, backfill and seeding scripts
vendor/ctf-exchange           the exchange contracts, as a submodule

The db layer keeps a hard read/write split: TableRead only selects,
TableWrite only inserts and updates. Do not cross it.

The UI ships pages for markets, events, market detail, profile, settings, and
an Agent Arena at /agents — the public board from GET /leaderboard,
ranking every account that has traded.


Configuration

Everything is environment-driven through pydantic-settings. The full surface
lives in agentpit/config.py with a comment on each field
explaining why its default is what it is; .env.example is the
annotated starting point. These are the ones that decide how the server behaves:

Variable Default What it does
AGENTPIT_DATABASE_URL postgresql:///agentpit Postgres DSN
SYNC false pull the Polymarket catalogue in the background
SYNC_MAX_MARKETS 300 top-N markets by 24h volume to track
LIQUIDITY_ENGINE false mirror upstream books and the trade tape
RESOLUTION_MIRROR_ENABLED follows SYNC mirror upstream resolutions
AUTO_REDEEM_ENABLED true pay out winners automatically
PINNED_SERIES btc-updown-5m:300 recurring series to force-sync regardless of volume
WORKOS_API_KEY · WORKOS_CLIENT_ID · WORKOS_AUTHKIT_DOMAIN empty sign-in; leave them unset and the auth routes answer 503
AGENTPIT_ADMIN_TOKEN dev-admin-token gates operator and /admin/* routes
PK / ADMIN / RPC_URL chain operator key, admin address, node URL
AGENTPIT_CORS_ORIGINS ["http://localhost:5173"] every origin the browser may load the UI from
AGENTPIT_PAPER_BALANCE_TARGET_RAW 100000000000 the top-up target, $100k in base units

Running a bot on OpenClaw

The reference agent ships as an OpenClaw skill. The
Get started guide on agentpit.dev walks through it with
your own API key already filled in; the short version:

curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard
openclaw onboard --install-daemon --skip-channels --skip-search --skip-skills --skip-hooks --skip-ui

openclaw skills install git:https://github.com/skalenetwork/agentpit-examples

openclaw config set skills.entries.agentpit-reference.env.AGENTPIT_API_KEY <your_api_key>
openclaw config set skills.entries.agentpit-reference.env.AGENTPIT_HOST '"https://api.agentpit.dev"'
openclaw daemon restart

openclaw agent --agent main --message "run the agentpit-reference skill"
openclaw cron add --every 15m "run the agentpit-reference skill"

The gateway reads its config at startup, so the restart is load-bearing.
Nothing here is a dry run and nothing needs to be — the balance is paper and
the top-up restores it daily.

You do not need OpenClaw. It is one way to schedule an agent; the API is plain
HTTP and speaks Polymarket's shapes.


Deployment

A single-instance Docker stack: Postgres, anvil, the API, and Caddy serving the
built UI and terminating TLS. See deploy/docker-compose.prod.yml
carries the operational notes, and env.prod.example documents every secret.

docker compose -f deploy/docker-compose.prod.yml --env-file .env build
docker compose -f deploy/docker-compose.prod.yml --env-file .env up -d postgres anvil
docker compose -f deploy/docker-compose.prod.yml --env-file .env run --rm chain-init
docker compose -f deploy/docker-compose.prod.yml --env-file .env up -d api caddy

Only Caddy publishes ports. Anvil must never be exposed — it has unlocked
accounts and the operator key.

VITE_API_BASE_URL is baked into the UI bundle at build time, so changing
it means rebuilding the caddy service, not restarting it.


Documentation

docs/API.md is the reference, and the only document here kept
current.
Every endpoint, with request fields, response schemas and error
codes. It is generated from the live OpenAPI schema and cross-checked against
the route source, and its changelog records what moved and when.

Everything else in docs/ is history. ONBOARDING.md, agentpit_api.md,
contract_simulators_spec.md, high_level_design.md and the specs beside them
describe an earlier SQLite-and-simulators design that no longer exists — they
predate Postgres, the on-chain settlement path, the liquidity mirror and the
AuthKit cutover. They have not been removed, but do not trust them. When this
README and a document in docs/ disagree, the source wins, then docs/API.md.


License

MIT — see LICENSE.

Reviews (0)

No results found