Prediction Stack docs Open the Stack
Operate

Data refresh job

.github/workflows/refresh.yml ("refresh board") fetches Kalshi, Polymarket and Polygon on a GitHub-hosted runner, writes the JSON files the pages read, checks them for fabricated content and commits them back to your repository.

Why it runs on GitHub, not on your computer#

  • Kalshi refuses browser requests. Any request with an Origin header gets HTTP 403, so the fetch must happen server-side. A scheduled job that writes static JSON is the cheapest server-side fetch there is.
  • Your network may block a venue. Some internet providers block *.polymarket.com by law. GitHub's runners are not behind your provider. Do not try to get around a block on your own network; let the runner fetch the data.
  • Nothing to host. No server, no database, no keys. Latency equals the schedule interval, which suits this product: the spreads it looks at last hours to days, not milliseconds.

Run it by hand#

The workflow ships with the schedule off. Only a manual start (workflow_dispatch) runs it.

  1. Open your repository on GitHub and click Actions. If GitHub asks, enable workflows for the repository.
  2. Pick refresh board in the list on the left.
  3. Click Run workflow, keep the branch (main) and confirm.
  4. When the run is green, pull the new commit (git pull). It is titled board: refresh YYYY-MM-DDTHH:MMZ and authored by mikodes-bot.

With the GitHub CLI the same start is gh workflow run "refresh board" and gh run list shows the result (the list command was run for this guide; the start command was not).

What each step does#

StepWhat it doesFails the run?
Probe venue reachabilityPrints the HTTP status of Kalshi, Gamma and the CLOB host from the runner.No
Build boardnode src/run.js in scanner/: both venues, then the category filter (markets in categories.hidden of config/public.json are dropped right after the fetch, so they are never scored, stored or written), matcher, spreads. Writes public/board.json, public/crossvenue-diag.json, data/pairs.jsonl.Yes, on a crash
Fail if the run fabricated anythingStops the run if an unreachable venue reports markets, or if cross-venue is not ok but carries rows.Yes
Read the Terminal tape from Polygonnode src/onchain.js 40: the last 40 blocks of the exchange contract's logs, from your POLYGON_RPC_URL first if set (below), else the public RPCs. Writes public/data/tape.json with every token and up to 5,000 fills of the window; the next step cuts it to size.Yes, on a crash
Attach market titlesnode src/titles.js: resolves token ids to questions through Gamma, then applies the category filter: tokens and fills of hidden markets are dropped, the file keeps the top 60 tokens and the latest 300 fills, and category_filter records how many were left out. If Gamma is unreachable it writes titles.status = "unavailable" and no titles (nothing can be classified, so the lists are only cut to size); it never fails the job.No
Fail if the tape carries titles it could not have fetchedChecks the titles block is consistent with its own status and counts.Yes
Commit the new boardCommits the four data files if they changed and pushes.Yes, if push is refused

Use your own Polygon RPC#

The Terminal tape is read from Polygon. Out of the box the job uses three public, keyless RPCs and falls through them in order. They are fine for a 40-block window; a paid or private endpoint gives you higher rate limits and one provider you chose.

  1. Get an HTTPS Polygon mainnet (chain 137) endpoint from any RPC provider. It must allow eth_getLogs over at least 40 blocks.
  2. In your GitHub repository open Settings → Secrets and variables → Actions and add a repository secret named POLYGON_RPC_URL with the full URL. Several URLs may be given, separated by commas.
  3. Run the workflow. The tape step passes the secret to node src/onchain.js.
  • Your endpoint is tried first; the public RPCs stay as the fallback. Set POLYGON_RPC_ONLY=1 as well to use only yours.
  • The URL is never written anywhere. Many providers put the key in the URL, so tape.json and the Terminal footer name it only as own RPC (POLYGON_RPC_URL), and error messages carry that label instead of the URL.
  • A value that is not an https:// URL stops the job with an error that does not print the value.
  • Locally: POLYGON_RPC_URL=https://… node src/onchain.js 40 from terminal/.
  • The Polygon RPC URL in the admin's Integrations section is separate: it feeds the admin's Status check only. Use the same URL in both places if you like.

Turn on the schedule#

Edit .github/workflows/refresh.yml and remove the # in front of the two schedule lines:

on:
  schedule:
    - cron: '*/15 * * * *'    # every 15 minutes (UTC)
  workflow_dispatch:

Commit and push. Choose the interval with the cost below in mind. GitHub may delay scheduled runs at busy times, and it pauses schedules in repositories with no activity for a long period (GitHub's documented behaviour; re-enable in the Actions tab if it happens).

What it costs in Actions minutes#

The workflow's own comment says every 15 minutes is "~2,900 Actions minutes/month". That figure is really the number of runs (96 a day × 30 days = 2,880) at one billed minute each. The real runs are longer:

Recent runs (from gh run list)Wall time
2026-09-23 11:01 UTC, success2 min 33 s
2026-09-22 20:27 UTC, success2 min 33 s
2026-09-22 20:22 UTC, success3 min 51 s

GitHub bills each job rounded up to the whole minute, so a run like these costs 3 to 4 billed minutes. With 3 minutes per run:

SchedulecronRuns / 30 daysBilled minutes / 30 days (3 per run)
Every 15 minutes*/15 * * * *2,880about 8,640
Every 30 minutes*/30 * * * *1,440about 4,320
Every hour0 * * * *720about 2,160
Every 2 hours0 */2 * * *360about 1,080
Every 6 hours0 */6 * * *120about 360

The workflow comment puts the free allowance for a private repository at 2,000 minutes a month; on that allowance every 15 minutes is roughly four times over and hourly is just over. Check the allowance and overage price for your own GitHub plan on GitHub's billing page before you turn the schedule on; plans and prices change. Public repositories are billed differently (see GitHub's Actions billing documentation).

Running the jobs locally#

verified
cd terminal
node src/onchain.js 40     # Polygon only; about 0.5 s; argument = blocks to read
runs on the GitHub runner
cd scanner
node src/run.js            # options: --contracts 1000 --max-events 1200 --no-depth --out public/board.json

cd terminal
node src/titles.js         # needs Polymarket's Gamma API

Keep the block window small. The exchange contract emits tens of megabytes of logs over a few hundred blocks; a 500-block request once returned a 51 MB response that was cut off. 40 blocks is about one minute of chain time.

MIKODES Prediction Stack documentation. Written from the code in this repository; where the code and older notes disagree, the code wins. Nothing here is financial, legal or tax advice.