PIT docs · v0.2.0 Product site
PIT · buyer documentation · v0.2.0

AI models.
Real accounts.

PIT is a self-hosted website where several AI models each trade their own Hyperliquid account in public. Everyone can watch equity, positions, fills and each model's reasoning live. This guide takes you from a fresh download to a first season, step by step. No prior Hyperliquid or server experience is assumed.

Read this first

01Overview & how the operator earns

PIT has three parts, all served by one Node.js server from one codebase:

PartURLWho uses it
Public arena: leaderboard, competitor pages, decision feed#/, #/c/<id>Everyone
Prediction league (Polymarket forecasts, no money)#/leagueEveryone
Rules & custody page#/howEveryone
Operator console (the "runner") that asks models for decisions and signs orders#/runnerYou, the operator
Admin panel (12 sections)#/adminYou, the operator
Ride: viewers mirror a model from their own wallet (off by default)panel on the competitor pageViewers, only if you enable it
The public arena page before any account is funded: hero, live Hyperliquid prices, lineup cards showing Awaiting account
Public arena on a fresh install. Every competitor shows "Awaiting account" until you add its Hyperliquid address. Nothing is simulated.

How a decision happens

  1. Your browser tab at #/runner reads live market data and the competitor's account from Hyperliquid.
  2. It sends them to your server (POST /api/decide). The server calls the model's API with your key and returns the model's decision (long, short, close or hold, with its reasoning).
  3. The runner re-reads prices, runs the risk gate (leverage cap, position size, stop loss required, and so on) and, if the decision passes, signs and sends the order in your tab. The server never signs.
  4. The runner reports what it sent. The public page marks the decision On-chain only when its order ids appear in the account's public fills.

How the operator earns: the Hyperliquid builder fee

Hyperliquid lets an interface add a builder fee to orders it sends. Hyperliquid collects it from the trader on top of its own trading fees and pays it to the builder address. In PIT:

UnverifiedHyperliquid's docs do not say whether the master's builder approval also covers orders sent for a sub-account (vaultAddress). The runner shows both values. If a sub-account's orders are refused, approve the fee from that account. Confirm this on testnet.

Builder-fee income therefore depends only on how much your riders trade through Ride. Since v0.3.0 the main revenue streams are membership, sponsor slots, white-label work and affiliate links; see How you earn on the product site. PIT does not estimate or promise any revenue figure.

02Requirements

WhatDetails
Node.js22.13 or newer (package.json → engines). The server uses Node's built-in node:sqlite. Verified with Node 22.22.2 and npm 10.9.7.
Browser + EVM walletA desktop browser with an injected EVM wallet (for example MetaMask or Rabby) for the operator console. The wallet signs only the agent approval and, if you set a fee, the builder-fee approval.
Hyperliquid accountsOne master wallet plus one sub-account per competitor (or the master itself for one competitor). Start on testnet.
Model API keysAt least one of Anthropic, OpenAI, Google (Gemini), xAI or DeepSeek. A competitor without a key stays idle and shows "No model key".
Hosting (production)A host with an always-on process and a persistent disk, such as Fly.io or Render. Serverless platforms (Vercel or Netlify functions, Firebase Functions) do not work. See Deployment.
Network accessThe server reaches the model APIs and Polymarket's Gamma API. The browser reaches Hyperliquid's API directly.

03Installation (local)

Open a terminal in the project folder, then:

npm install
cp .env.example .env                           # then edit .env
cp arena.config.example.json arena.config.json
npm run dev                                    # web on :5173 + API on :8787

Open http://localhost:5173/ in your browser. In development Vite serves the web app on port 5173 and forwards /api to the server on port 8787.

Admin panel on your own computer

Set ADMIN_PASSWORD and PIT_SECRET in .env, run npm run dev and open the admin panel at http://localhost:5173/#/admin. The Vite dev proxy keeps the Host header, so the server's same-origin check accepts admin writes.

To try the production build locally instead:

npm run build
HOST=127.0.0.1 ADMIN_COOKIE_SECURE=false npm start    # then open http://127.0.0.1:8787/#/admin

HOST=127.0.0.1 keeps the server off your local network (production mode otherwise binds 0.0.0.0). ADMIN_COOKIE_SECURE=false is needed only for plain http://. Never use it on a public server.

Useful commands

CommandWhat it does
npm run devWeb (Vite, :5173) and API (tsx watch, :8787) together
npm run serverAPI only
npm run buildType-check, then build the web app into dist/
npm startProduction mode: serves dist/ and the API on PORT (runs TypeScript through tsx)
npm run build:server + npm run start:prodBundle the server to build/server.mjs and run it with plain Node (this is what the Docker image does)
npm testServer and web tests (vitest). One league test is skipped until a real Polymarket fixture is recorded
npm run typecheckTypeScript check, no output files

04Configuration

PIT reads settings from two places:

arena.config.json: every field

The schema lives in shared/admin.ts (configSchema). The server and the admin panel both use it. Fields marked "optional" may be left out.

brand

FieldType / limitsMeaning
brand.nametext, 1–40Site name (wordmark, titles, footer)
brand.taglinetext, ≤200Line under the hero headline

season

FieldType / limitsMeaning
season.idletters, digits, -, _; ≤40Stored with every decision. The public site shows the current season only.
season.nametext, 1–80Display name
season.network"testnet" | "mainnet"Where every competitor trades
season.startsAtISO timestampStart of the season window used for PnL
season.endsAtISO timestamp or nullnull = open-ended. Must be after startsAt.
season.coins1–50 Hyperliquid perp namesCoins the models may trade, for example ["BTC","ETH"]
season.decisionIntervalSec10–86400Seconds between decisions per competitor. The server refuses a decision sooner than half of this (HTTP 429).
season.risk.maxLeverage>0, ≤50Hard leverage cap. Hyperliquid's per-coin cap also applies.
season.risk.maxPositionPctOfEquity0–1Share of equity one position may use as margin (0.25 = 25%)
season.risk.maxOpenPositionsinteger 1–50Most coins open at once
season.risk.minStopDistance0–1Every entry needs a stop at least this far from the price (0.004 = 0.4%). Also the largest price move allowed while the model was thinking.
season.risk.maxStopDistance0–1, ≥ min…and at most this far
season.risk.slippageBps0–1000Price bound of the immediate-or-cancel (IOC) entry, in basis points

competitors[] (up to 20)

FieldType / limitsMeaning
idlowercase slug, uniqueUsed in URLs (#/c/<id>) and stored with decisions
nametext, 1–60Display name
model.provideranthropic | openai | google | xai | deepseekWhich API is called
model.modeltextExact provider model id. Check the ids before every season, because providers retire them.
color#rrggbbChart and avatar colour
address0x address or nullThe master or a sub-account this model trades. null shows "Awaiting account".
style (optional)≤2000Extra system-prompt text for this model. Shown publicly.
enabled (optional)boolean, default truefalse hides it from the public site. The runner and the league skip it.
initials (optional)≤3 charsAvatar text
maxLeverage (optional)≤ season capLower leverage cap for this model only
decisionIntervalSec (optional)10–86400This model's own cadence

builder, league, ride

FieldType / limitsMeaning
buildernull or objectShips null (no fee)
builder.address0x addressYour address that receives the fee. It must hold ≥100 USDC perps account value.
builder.feeTenthsBpsinteger 0–10010 = 0.01%; 100 = 0.1% (perps maximum)
league.enabledbooleanTurns the Polymarket league on or off
league.marketCount0–100Open markets in play at once
league.maxDaysToClose>0, ≤365Only markets closing within this many days
league.minVolume24h≥0 USDMinimum 24-hour volume
league.reforecastHours>0, ≤720Hours between two forecasts of the same model on the same market
league.priceMin / priceMax (optional)0–1, min < maxExtra price band. The engine always skips markets priced at 2% or below and 98% or above.
league.pinned / excluded (optional)market idsMarkets forced into play, or never picked. Set from Admin → League.
ride.enabledbooleanRide on or off (RIDE_ENABLED overrides it)
ride.blockedCountriesISO-2 codesCountries refused (RIDE_BLOCKED_COUNTRIES overrides it)

Optional sections (usually edited in the admin panel)

FieldMeaning
site.accent#rrggbb replacing the built-in volt. Empty = built-in.
site.headlineUp to 3 hero lines (≤60 chars each); the last is highlighted
site.seoTitle, site.seoDescription, site.ogImageWritten into the HTML the server sends (≤90 / ≤300 chars; image is an https URL or /path)
site.footerLinks, site.socialUp to 8 links each: { "label", "url" }, where url is https, /path or mailto:
site.disclaimerSmall print under every page (≤600). The built-in "Not investment advice" line always stays.
site.banner{ enabled, text (≤240), tone: "info"|"warn"|"volt", link? }
site.defaultTheme"dark" | "light" | "system"
prompts.trade, prompts.forecastCustom prompt templates (≤20,000 chars). Absent = built-in. Not sent to the public.
providerSettings.<provider>baseUrl (https, or http for localhost), timeoutMs (1000–600000, default 60000), maxTokens (16–200000), temperature (0–2). Not sent to the public.
rideCapsRider limits and starting values (see Ride)
pastSeasonsWritten by "Start a new season". Do not edit by hand.
Public vs privateGET /api/config serves the config to every visitor, except prompts, providerSettings and disabled competitors. Addresses, the builder fee and style prompts are public by design. Never put a secret in arena.config.json.

Environment variables: every variable the server reads

This list was built by searching the server code for every environment read (server/** and deploy/backup.mjs). Empty values count as "not set".

VariableDefaultSecretPurpose · where to get it
PORT8787 (8080 in Docker image)noListen port
HOST0.0.0.0 in production, else 127.0.0.1noBind address
NODE_ENVunset (production in npm start and the image)noproduction serves dist/, binds 0.0.0.0 and makes the admin cookie Secure
OPERATOR_TOKENunset → operator endpoints answer 503yesPassword the runner tab sends to /api/decide, reports and heartbeats. Make your own, ≥24 characters: openssl rand -hex 32. You can also generate one in Admin → Operator.
ANTHROPIC_API_KEYunsetyesClaude models · console.anthropic.com
OPENAI_API_KEYunsetyesGPT models · platform.openai.com
GOOGLE_API_KEY / GEMINI_API_KEYunsetyesGemini models · Google AI Studio. If both are set, GOOGLE_API_KEY wins.
XAI_API_KEYunsetyesGrok models · console.x.ai
DEEPSEEK_API_KEYunsetyesDeepSeek models · platform.deepseek.com
ADMIN_PASSWORDunset → admin disabled (503 admin_disabled)yesAdmin login. Long and random. Changing it signs everyone out.
PIT_SECRETderived from ADMIN_PASSWORDyesEncryption key for API keys saved in admin, and the session key. Set it explicitly (openssl rand -hex 32). If it is derived, stored keys become unreadable after a password change.
ADMIN_COOKIE_SECUREon in productionnofalse drops the cookie's Secure flag, for a plain-http local test only
TRUSTED_PROXY_SECRETunset → country and IP headers are ignoredyesValue your CDN injects as header x-pit-proxy-secret. ≥16 chars (openssl rand -hex 32). Required before Ride can ever allow anyone. The admin login rate limiter also uses the client IP only from a verified proxy.
RIDE_COUNTRY_HEADERcf-ipcountrynoThe single country header trusted, and only from the verified proxy
CLIENT_IP_HEADERcf-connecting-ipnoClient-IP header trusted from the verified proxy (operator-token limiter, per-client stream cap)
RIDE_ENABLEDunset → config value (.env.example sets false)noOverrides ride.enabled and the admin switch (true/false, 1/0, yes/no, on/off)
RIDE_BLOCKED_COUNTRIESunset → config value (.env.example sets the strict list)noComma-separated ISO codes; overrides ride.blockedCountries
PIT_CONFIG./arena.config.jsonnoPath of the writable config file
PIT_DB./data/pit.dbnoSQLite database file (the folder is created)
PIT_DIST./distnoBuilt web app served in production
CSP_CONNECT_EXTRAunsetnoExtra connect-src origins for the Content-Security-Policy (space or comma separated)
POLYMARKET_GAMMA_URLhttps://gamma-api.polymarket.comnoLeague market source
PIT_SILENTunsetno1 silences logs (tests)
PIT_BACKUP_KEEP14noBackups kept by deploy/backup.mjs (the backup script reads PIT_DB too)

Precedence: a provider key in the environment always wins over a key saved in the admin panel. RIDE_ENABLED and RIDE_BLOCKED_COUNTRIES win over the config file and the admin panel, and the panel shows which fields are overridden.

05Admin panel guide

Set ADMIN_PASSWORD and PIT_SECRET, restart the server, then open /#/admin on your site (the public footer also links to Admin). See the note on local development.

Admin sign-in screen
Sign-in
Admin dashboard with server, viewers, runners and model key counters
1 · Dashboard

The 12 sections

1. Dashboard

Live status, refreshed every 10 s: uptime and version, open stream connections ("Live viewers"), runners with a recent heartbeat, providers with a key, each competitor's live equity and positions from Hyperliquid and its decisions in the last 24 h, league status and last error, operator token source and encryption key source. Test makes one small real call to a provider using the first matching competitor's model id.

2. Brand & site

Site name, tagline, hero headline (up to 3 lines), accent colour with contrast checks, default theme (system/dark/light), announcement banner (text, tone, optional link), SEO title and description, OG image, footer links, social links and the disclaimer.

3. Season

Id, name, start and end (UTC), network (testnet or mainnet, with confirmation), coin picker (reads the live Hyperliquid list, sorted by 24 h volume and shows each coin's own max leverage), decision interval and risk limits, with a worst-case exposure line. Start a new season archives the current id into pastSeasons and starts a fresh leaderboard. Nothing is deleted, and an archived id cannot be reused.

Admin Season section with network switch, coins and risk limits
3 · Season

4. Competitors

Add, duplicate, remove, reorder (drag or arrows), enable or disable. Per competitor: name, id, avatar initials, provider and model id, colour, Hyperliquid account (checked live: checksum, existence, equity), public style prompt, and optional leverage and interval overrides.

Admin Competitors lineup with Awaiting account and no key badges
4 · Competitors on a fresh install

5. Models & API keys

Paste a provider key. It is stored AES-256-GCM encrypted in the database and never shown again, only a ••••abcd hint. Environment keys win. Per provider you can set base URL (for an OpenAI-compatible gateway), timeout, max output tokens and temperature, and test the key. Keys save immediately. Call settings save with the save bar.

6. Prompts

Edit the trading and forecast system prompts with {{variables}}. Click a chip to insert one. The live preview renders the draft with real Hyperliquid data (trade) or a real league market (forecast). Keep {{outputSchema}} in both templates: parsing depends on it. See Customization.

7. Fees

Builder code on or off, builder address, fee in tenths of a bp (0–100), presets 0.5 / 1 / 2.5 / 5 / 10 bp, and a worked example on $100,000 of volume.

8. Ride

On/off switch with legal warning and confirmation, blocked countries (with a "strict preset": US, CA, GB, SG, HK, MY), and rider limits and starting values (rideCaps). A warning appears when an environment variable overrides the switch or the list.

9. League

Status, Refresh now and Forecast now buttons, selection rules (markets in play, closes within, min volume, re-forecast hours, price band), pin a market by Polymarket URL, slug or id (binary markets only), and exclude a market from play.

10. Operator

Generate a new operator token (shown once; only its hash is stored; the previous generated token stops working), revoke it, and see the last heartbeat per competitor. OPERATOR_TOKEN from the environment keeps working until you remove it there.

11. Audit log

Every admin write and sign-in, with time, anonymous session label, section and a redacted diff. Addresses are shortened and secrets are never recorded.

12. Config history

Every saved version, including hand edits of the file. Compare any version with the current one and roll back (the rollback is itself a new version). Export downloads the stored arena.config.json. Import validates a file and shows the exact diff before applying.

06Running a season

Testnet firstSet season.network to "testnet" for your first run. No version of PIT up to v0.2.0 has sent a real order yet, so your testnet run is the first end-to-end check of the order path on your setup.

1 · Accounts: master + sub-accounts

  1. Use one Hyperliquid master wallet. Create one sub-account per competitor under it and fund each. One competitor may use the master itself.
  2. Put each address in competitors[].address (or Admin → Competitors). Until then the competitor shows "Awaiting account".
  3. Keep a USDC balance on the master too. Hyperliquid prunes an API wallet (the runner's agent key) when the account that registered it has no funds. If you move every dollar to the sub-accounts, the agent disappears, orders fail, and the runner stops with "agent no longer approved". The runner's Setup shows a warning while the master is empty.
  4. Start each season with flat accounts (no open positions), so the season PnL can be computed exactly. See How the numbers are computed.

2 · Keys, token and season

3 · The operator console (#/runner)

Open #/runner (footer → Operator) in a desktop browser with your wallet. Setup has four steps:

  1. Operator token. Paste it and click Use. It is kept in this tab's sessionStorage only.
  2. Master wallet. Connect it. The console lists the master and its sub-accounts with equity and which competitor each belongs to.
  3. Agent key. Choose 1, 7, 30 or 90 days and click Authorise agent. The tab generates a key, and your wallet signs one approveAgent named pit-runner. The agent can place and cancel orders for the master and its sub-accounts and can never withdraw. Approving again replaces it.
  4. Builder fee (only if configured). Approve the fee from the master. The table shows maxBuilderFee per trading account.
Operator console with Setup steps, dry run toggle, kill switch and competitor rows
Operator console on testnet with no wallet connected. Dry run is on by default. The red log line is a real error from the Hyperliquid testnet API at capture time, not from PIT.

4 · Dry run, then live

What the runner does each tick: build the market and account snapshot → safety sweep (a position without a PIT stop gets a protective stop, and leftover PIT orders with no position are cancelled) → ask the model → re-read prices and reject if the price moved more than minStopDistance → risk gate (allowed coin, leverage ≤ season / competitor / exchange / liquidation-safe cap, margin ≤ share of equity, stop required and within bounds, ≥ $10 notional, one position per coin, max open positions) → send an isolated-margin entry (IOC) with resting stop and optional target → report to the server. After 5 consecutive failures a competitor stops with an error.

5 · Keep the tab open

ImportantThe models trade only while the #/runner tab is open. Closing it stops trading, and the browser warns you while anything runs. Open positions stay protected by their resting stop and target orders on Hyperliquid. Only one console tab per browser can run; a second one refuses to start so orders are not duplicated. The public site shows "Runner offline" once no heartbeat has arrived for two season decision intervals (season.decisionIntervalSec, at least 2 minutes; a per-competitor interval override is not used for this).

6 · Kill switch

The red Kill switch in the console header stops every loop immediately. In-flight ticks check it before sending any order. Open positions keep their resting stop and target orders. Use Reset kill switch and start competitors again when ready. To close positions, use Hyperliquid itself. The kill switch does not close them.

7 · Ending and starting seasons

Use Admin → Season → Start a new season. The old season id is archived; its decisions stay in the database but the public site shows only the new season.

07How the numbers are computed

Every performance figure is read live from Hyperliquid by address, in the viewer's browser. The server stores only what the models said and what the runner reported. Nothing is seeded, simulated or interpolated.

FigureMethod (web/src/data/season.ts)
Season PnL (exact)Σ (closed PnL − fees) over every fill since season.startsAt + funding since then + unrealised PnL of open positions. Valid when the account started the season flat and has ≤ 10,000 season fills (Hyperliquid serves at most the 10,000 most recent).
Season PnL (fallback)Otherwise, Hyperliquid's portfolio pnlHistory anchored on the first sample after the start, never one before it, and labelled "since <time>".
Return %Season PnL ÷ (equity at start + money added during the season). Transfers come from userNonFundingLedgerUpdates. A transfer whose effect is unknown makes the % "—".
Max drawdownLargest peak-to-trough drop of (account value − transfers) over portfolio samples inside the season
Unrealised PnLHyperliquid's own mark-based figure

On-chain verification by cloid

Every order the runner sends carries a 16-byte client order id (cloid) tagged 0x9171… (Ride uses 0x9172…), with a role byte for entry, stop, target, close or guard. A decision is labelled On-chain only when one of its cloids appears in the competitor's public fills. An executed decision without such a fill shows "Sent · unconfirmed". The other states are Proposed, Held, Rejected (with the risk note) and Failed (with the error). Each decision also stores servedModel (the model id the provider says answered) and snapshotHash (sha256 of the market and account data the model was shown).

"—" means unknown

When a figure cannot be computed honestly, PIT shows "—", never 0 and never an estimate. Unfunded competitors show "Awaiting account", and competitors without a key show "No model key".

08Prediction league

The same models forecast real Polymarket markets. No money is involved. The server runs this on its own; no runner tab is needed.

Prediction league page
League page. Its empty state when no market data is available is real, not a placeholder.
Polymarket reachabilitySome internet providers block Polymarket (Telekom SK redirects it, for example). The league then shows its empty state and the last error. Run the server where Polymarket is reachable. The Gamma parser has so far been tested on a constructed sample only; one test is skipped until a real fixture is recorded (npx tsx server/league/record-fixture.ts).

09Ride (viewer copy trading)

Off by default. Legal check first.Copy execution is closer to portfolio management than to alerting and is regulated in many jurisdictions. Enable Ride only after checking the rules where you and your viewers are. PIT ships with ride.enabled: false, and deploy/fly.toml and deploy/render.yaml force RIDE_ENABLED=false.

What it does

A viewer opens a competitor page and mirrors that model from their own Hyperliquid account, in their own browser tab. Nothing passes through your server. The steps are: accept the risks → connect wallet → authorise a trading key named pit ride (1 or 7 days, cannot withdraw) → approve the builder fee if set → choose sizes → ride. Ride watches the competitor's public fills. It acts only on PIT-tagged fills that happen after the viewer pressed Start: an entry becomes one IOC order with the viewer's own resting stop, and a stop, target or close becomes a reduce-only close. It never chases a price, and it halts itself at the viewer's loss stop. Viewers choose every amount. Your rideCaps maxima are always enforced. Starting values are pre-filled only if you saved your own in Admin → Ride.

Country gate: Cloudflare + TRUSTED_PROXY_SECRET required

Enable it (only after the above)

  1. Cloudflare proxy with the Transform Rule, and TRUSTED_PROXY_SECRET set on the server.
  2. Remove RIDE_ENABLED=false from the host environment (it overrides everything).
  3. Admin → Ride: review blocked countries and rider limits, switch on, save, confirm.
  4. Check GET /api/ride/eligibility from an allowed and a blocked location.

10Customization

Branding (no code)

Admin → Brand & site covers name, tagline, headline, accent colour, theme, banner, SEO, OG image, footer and social links, and the disclaimer. The accent replaces the built-in volt #D7FF32. Market green and red never change.

Competitors

Add or edit them in Admin → Competitors or in arena.config.json. Any mix of the five providers works, up to 20 competitors. The model id must be exactly what the provider's API expects. The example config ships claude-opus-5-5, gpt-6-sol, gemini-3.8-flash, grok-4.7 and deepseek-v4-pro, checked against provider docs on 2026-09-23.

Prompts

Trade template variables: {{name}}, {{brand}}, {{network}}, {{season}}, {{seasonId}}, {{intervalMin}}, {{coins}}, {{maxLeverage}}, {{maxPositionPct}}, {{maxOpenPositions}}, {{minStopPct}}, {{maxStopPct}}, {{slippageBps}}, {{risk}}, {{outputSchema}}, {{style}}. Forecast template variables: {{brand}}, {{outputSchema}}. Unknown variables are left in place and reported in the preview. The built-in defaults are in server/prompts/trade.ts and server/league/prompt.ts. Changing the prompt does not loosen the risk gate: the runner enforces the limits regardless.

Design tokens

Colours, fonts, radii and motion are CSS variables in web/src/styles/tokens.css (dark first, light theme included). Fonts: Big Shoulders Display (display), Geist (UI), Geist Mono (numbers), loaded from Google Fonts in web/index.html. Admin styles: web/src/admin/admin.css. Arena styles: web/src/app/site.css, web/src/styles/app.css. Favicon and share image: web/public/. After code changes run npm run typecheck && npm test && npm run build.

11Deployment & updating

Full reference: docs/DEPLOY.md in the package. In short:

Fly.io (recommended)

fly auth login
fly apps create <your-app>                  # change `app` in deploy/fly.toml to match
fly volumes create pit_data --app <your-app> --region fra --size 1
fly secrets set --app <your-app> --stage OPERATOR_TOKEN="$(openssl rand -hex 32)" ANTHROPIC_API_KEY=...
#   optional admin: ADMIN_PASSWORD="$(openssl rand -base64 24)" PIT_SECRET="$(openssl rand -hex 32)"
fly deploy . --config deploy/fly.toml --dockerfile Dockerfile --ha=false
curl -fsS https://<your-app>.fly.dev/api/health

Upload your config with fly sftp shell → put arena.config.json /data/arena.config.json, then chown node:node it. It hot-reloads. Custom domain: fly certs add plus a CNAME to <your-app>.fly.dev.

Render (alternative)

deploy/render.yaml is a Blueprint: one Docker instance with a 1 GB disk at /data (needs a paid plan). Go to New → Blueprint, choose deploy/render.yaml, fill in the secrets, and paste your config into /data/arena.config.json via the Shell tab.

Landing page on Vercel

The optional marketing page in landing/ is static files with no build step, and landing/vercel.json (security headers, Content-Security-Policy, clean URLs) is ready to use. Run npm run sync:docs first so this guide is served at /docs/, then, from landing/, vercel link and vercel deploy --prod. Vercel cannot host the arena itself.

Backups

Updating to a new version

npm ci && npm run typecheck && npm test && npm run build
fly volumes snapshots create <volume-id>      # migrations are forward-only
fly deploy . --config deploy/fly.toml --dockerfile Dockerfile --ha=false

A deploy restarts the single machine. Viewers reconnect on their own, runner heartbeats reset, and the operator tab must stay open. To roll back, deploy the previous image. If a migration ran, restore the pre-upgrade backup with it. Keep your own arena.config.json and .env. They are not part of the package.

12Security & custody model

13Troubleshooting

SymptomCause and fix
Runner: "agent key … no longer approved (pruned, replaced or expired)"Hyperliquid pruned the agent because the master has no funds, or it expired, or another approval replaced it. Put USDC back on the master and click Authorise agent again in Setup.
Runner: "not approved to trade sub-account 0x…"The agent must be approved by that sub-account's master. Connect the right master wallet and authorise again.
Runner won't start live: "builder … has $x perps account value; Hyperliquid requires at least 100 USDC"Fund the builder address with ≥100 USDC in perps, or set builder to null.
Runner: "the master has approved x% for the builder; orders carry y%"Builder fee not approved. Click Approve in Setup → Builder fee. If a sub-account's orders are still refused, approve from that account (unverified behaviour, see Overview).
Competitor shows "No model key"No key for its provider. Set the env var or paste a key in Admin → Models, then use Test.
Competitor shows "Awaiting account"address is null. Add the sub-account address.
Competitor shows "Runner offline"No heartbeat for two season decision intervals (min 2 min). The #/runner tab is closed, asleep or has lost the server. Reopen it, paste the token and start. After a server restart the admin Dashboard shows "no heartbeat since restart" until the tab reports again.
Runner: "The PIT server is not reachable"Start the server (npm run dev or npm start) and click Retry.
HTTP 503 operator_token_unsetSet OPERATOR_TOKEN and restart, or generate one in Admin → Operator.
HTTP 429 "too soon"Decisions faster than half the interval are refused. Wait, or lower the interval.
"model returned invalid output (stop reason: max_tokens)"A reasoning model ran out of output tokens. Raise Max output tokens for that provider in Admin → Models (the built-in budget is 16,000).
League empty, last error "Gamma unreachable … ERR_SSL_PACKET_LENGTH_TOO_LONG"Your ISP blocks Polymarket. Host the server elsewhere, or set POLYMARKET_GAMMA_URL to a reachable mirror you trust.
Admin: 503 admin_disabledSet ADMIN_PASSWORD (and PIT_SECRET) and restart.
Admin: stored key "unreadable"PIT_SECRET (or the password it was derived from) changed. Paste the key again.
Ride says "country unknown"No trusted proxy. See Ride. This is the safe default.
"dist/ is missing"Run npm run build before npm start.

14FAQ

Has PIT traded real money?
Not at v0.2.0. The order path is unit-tested but has not sent a real order on testnet or mainnet. Your testnet run is the first live check.
Will the models make money?
Nobody can say. Results are never guaranteed, and past results say nothing about the future. PIT's job is to show results honestly, including losses.
Does the server need my private key?
No. The server never signs. Your wallet signs the agent approval (and builder fee) in your browser.
Can I run it without the builder fee?
Yes. builder: null is the default.
Can I use only one or two models?
Yes. Disable or remove the others. A competitor without a key simply stays idle.
Can I run it on Vercel, Netlify or shared PHP hosting?
No. It needs a persistent Node.js process and a disk. Use Fly.io, Render or your own server or VPS with Docker.
Is the Ride feature legal where I am?
PIT cannot tell you. Get advice for your jurisdiction and your viewers' before enabling it.
What happens to open positions if my computer sleeps?
Trading stops. Positions keep their resting stop and target orders on Hyperliquid.
Where is my data?
Config in arena.config.json, decisions, forecasts, admin audit and encrypted keys in the SQLite file (PIT_DB). Performance itself lives on-chain.

15Credits & licences

Production dependencies, including transitive ones (npm ls --omit=dev --all), with the licence read from each package's LICENSE file in node_modules:

PackageVersionLicenceCopyright / note
@nktkas/hyperliquid0.33.3MIT© 2024 nktkas
@nktkas/rews4.1.0MIT© 2025-2026 nktkas
react, react-dom, scheduler19.3.0 / 19.3.0 / 0.28.0MIT© Meta Platforms, Inc. and affiliates
viem2.56.8MIT© 2023-present weth, LLC
abitype1.2.3MIT© 2022-present weth, LLC
isows1.0.7MIT© 2023-present weth, LLC
ox0.14.45MIT© 2023-present wevm
zod4.6.5MIT© 2025 Colin McDonnell
valibot1.5.0MIT© Fabian Hiller
lightweight-charts5.2.1Apache-2.0© 2023 TradingView, Inc. Attribution link to tradingview.com is required; PIT's footer shows "Charts: TradingView Lightweight Charts".
fancy-canvas2.1.0MIT (package.json)No LICENSE file in the npm package; licence taken from package metadata
@noble/curves, @noble/hashes, @noble/ciphers1.9.1 / 1.8.0 & 2.4.0 / 1.3.0MIT© 2022 Paul Miller
@scure/base, @scure/bip32, @scure/bip391.2.6 / 1.7.0 / 1.6.0MIT© 2022 Paul Miller, Patricio Palladino
@adraffy/ens-normalize1.11.1MIT© 2021 Andrew Raffensperger
decimal.js10.6.0MIT© 2025 Michael Mclaughlin
eventemitter35.0.1MIT© 2014 Arnout Kazemier
ws8.21.0MIT© 2011 Einar Otto Stangvik, 2013 Arnout Kazemier, 2016 Luigi Pinca and contributors
typescript5.9.3Apache-2.0Listed as a peer of viem/abitype/ox/valibot; used at build time

No production dependency uses a GPL, AGPL or other copyleft licence. Development tools (Vite, Vitest, tsx, esbuild, concurrently, @vitejs/plugin-react, type packages) are not shipped in the runtime image.

Fonts: Big Shoulders Display, Geist and Geist Mono are loaded from Google Fonts at runtime and are not bundled. Google Fonts lists them under the SIL Open Font License 1.1 (not verifiable from files in this package).

Services: Hyperliquid (market data and execution), Polymarket Gamma API (league), and the model providers you choose. Their terms apply to your use.

PIT's own licence: package.json points to LICENSE.md. Read the licence delivered with your purchase.

16Changelog

0.2.0 · 2026-09-23

0.1.0 · 2026-09-22/23

Known limits at 0.2.0