Operator CLI
The layer command runs Layer's matching pipeline by hand. API users don't need it — it's for whoever operates Layer. The scheduled jobs run the same code every 6 hours.
Setup
cd ~/dev/layer
git pull && npm install
npm link # once: puts `layer` on your PATH
Every command says which database it's using, shows live progress, ends with ✓ or ✗, and suggests the next command. Add --local to use a local practice copy instead of the production database. Without npm link, run npm run layer -- <command>.
Keys
The CLI reads secrets from the macOS Keychain or the environment. Save each one once with savekey <name>:
| Keychain name | Used by | Order |
|---|---|---|
openrouter-api-key |
grade, settle, job: the OpenRouter key Claude grades with |
Keychain first, then LAYER_LLM_API_KEY in .env (which holds an older, nearly empty key) |
typesafe-api-key |
jev, job grade: Jev's key |
TYPESAFE_API_KEY first, then Keychain |
github-issues-token |
settle, terms, job settle, job terms: opens a GitHub issue for each mismatch or terms change |
GITHUB_ISSUES_TOKEN first, then Keychain |
Commands
| Command | What it does |
|---|---|
layer status |
Health check: database, last pull per venue, schedule, review queue, local and live site. |
layer ingest [kalshi|polymarket|polymarket-us] [--accept-drop] |
Pull every open market from each venue, or just the one named (about 3 minutes; Polymarket US takes about 20 seconds). Polymarket US is matched with Kalshi and served with ?venue=polymarket_us. Only changed rows are written. A pull that sees more than 30% fewer open events or markets than the last good one stops before closing anything; if the venue really has fewer, --accept-drop records the new level. |
layer candidates [--category sports] [--series KXFEDDECISION] [--limit 500] [--recheck] [--venue polymarket|polymarket-us] |
Find possible matches and apply the hard checks, on both Polymarkets (international first) or the one named. --recheck redoes a scope; decisions are kept. |
layer grade [--limit 50] [--category sports] [--apply] [--regrade] [--venue …] |
Claude grades waiting pairs (both Polymarkets unless --venue names one). --apply puts ≥90% same bets live and removes confident mismatches. --regrade redoes pairs that already have a grade. A pair whose grading fails 3 times is no longer retried automatically; it stays in layer review list, and --regrade tries it again. Running out of OpenRouter credit, or reaching the key's own spending limit, stops the run instead and isn't counted against any pair. Model: LAYER_GRADER_MODEL (default Sonnet 5). |
layer jev [--limit 500] [--apply] [--regrade] [--venue …] |
Jev gives a second opinion. Jev checks each confirmed outcome pairing on its own. --apply puts the lines both graders call the same bet (each ≥80%) live. |
layer review list [--limit 20] [--venue …] |
Waiting pairs, with Claude's one-line verdict under each, in the order they're graded: core series first, soonest first. Polymarket US pairs are marked [Polymarket US]; add --venue polymarket-us to review show, approve and reject for them. |
layer review show [n] or layer review show <kalshi_event> <poly_slug> |
Pair #n (top if omitted), or a named pair: both rules and the suggested outcome pairing. |
layer review approve <kalshi_event> <poly_slug> --basis identical|equivalent_with_caveats [--caveats a,b] [--map K=P,…] [--note "why"] |
Approve a pair by hand. Uses the suggested outcome pairing unless you pass --map KALSHI_TICKER=conditionId,…. equivalent_with_caveats needs at least one caveat. |
layer review reject <kalshi_event> <poly_slug> [--note "why"] |
Reject a pair; it won't be suggested again, and if it's live its lines come off the API. |
layer settle [--limit 500] |
Check how live pairs that closed resolved on both venues. Lists every pair where they disagreed, with the reason, and opens a GitHub issue for each (needs a token saved with savekey github-issues-token). |
layer venue [on|off <venue>] [--reason "…"] |
Per-venue off switch. off stops the API serving that venue's data (matches pause with venue_unavailable) and stops the jobs calling its API, within seconds and without a deploy. Switching off Kalshi pauses every match; switching off one Polymarket pauses only the matches with it. With no arguments, shows each venue's state. |
layer venue serve|hide <venue> |
Whether the API shows a venue's matches. Every venue is shown by default. hide keeps matching and grading it but answers its requests with 403 venue_not_served; serve shows it again. Takes effect within 10 seconds. |
layer terms |
Download Kalshi's Developer Agreement and Data Terms and Polymarket's Terms of Use, and compare each with the last saved copy. The first run saves a baseline. A change opens a GitHub issue labelled venue-terms that quotes each removed, added or reworded clause. Without a token the change is saved and its issue is opened on the next run that has one. |
layer coverage [--venue polymarket-us] |
Of upcoming events in core series (NFL, Premier League, Fed…), how many are live, and where the rest drop out of the pipeline. Against polymarket.com unless --venue names Polymarket US. |
layer job <ingest-kalshi|ingest-polymarket|ingest-polymarket-us|candidates|grade|settle|venue-docs|terms> |
Run one scheduled job now. Manual runs don't open or close job-failure issues. |
layer help |
All of the above. |
The schedule
Vercel runs the pipeline every 6 hours (UTC): :00 pull Kalshi, :05 pull Polymarket, :10 pull Polymarket US, :15 find candidates, :30 grade, :45 check settled matches and snapshot coverage. Grading also runs at :30 every other hour, so a big batch of new candidates is worked through within a day. Every Monday at 13:00 UTC, venue-docs checks each venue's API changelog and docs index (Kalshi, Polymarket, Polymarket US). layer status shows whether it's running on time.
The :30 grade job screens pairs with Jev first, then Claude grades what's left until this month's Claude budget is spent ($100 by default, set with LAYER_GRADE_MONTHLY_USD), then Jev gives its second opinion. The :15 candidates job searches polymarket.com first, then Polymarket US, each with an equal share of the time left. Settle checks both Polymarkets' results (Polymarket US through its /v1/markets/<slug>/settlement), at most 15 requests a second to each venue, retrying a 429 or 5xx up to three times; a check that still fails is retried next run, and the run summary (and layer settle) lists why each one failed. When a venue is switched off with layer venue off, its ingest job is skipped and its pairs aren't searched, graded or settled; Kalshi off stops candidates, grade and settle.
When something breaks
The schedule reports problems as GitHub issues, so nobody has to be watching layer status:
- A job fails → an issue titled
[job failed] <job>(labeljob-failure) with the error. More failures of the same job are added as comments. The job's next clean scheduled run closes it. A run that never records a result (the function timed out or crashed) counts as a failure, reported on the next scheduled call. - A venue suddenly lists far fewer markets → if open events or open markets fall more than 30% from the last good pull, the ingest stops before closing anything and fails, which opens the issue above. This catches silent venue changes (a new filter, broken paging) that don't return errors. If the drop is real, run
layer ingest <venue> --accept-drop. - A venue announces an API change → the weekly
venue-docsjob compares each venue's changelog and docs page list with the previous week and opens a[venue change]issue (labelvenue-change) quoting each new or edited entry and listing added or removed pages. Its first run only records a baseline.
Every new issue (any of the above, a terms change or a settlement mismatch) is also emailed to LAYER_ALERT_EMAIL through Resend (RESEND_API_KEY, sender LAYER_ALERT_FROM). A comment on an issue that's already open isn't emailed. Without the key or the address, issues still open but no email goes out.
Issues go to the repo in GITHUB_REPO using GITHUB_ISSUES_TOKEN. To check the failure path in production, call a job with ?fail=test; the job's next clean run closes the issue it opens.
Once a week (Mondays 13:50 UTC) the terms job runs the same check as layer terms. It reads the venues' public documents, not their APIs, so it runs even while a venue is switched off. If a document can't be downloaded, or the download looks like a block page instead of the terms, the run fails and shows in layer status.