GitHub-native prediction market MVP that stores state directly in GitHub Issues. No external database. Deployable to Fly.io with a clean GitHub Actions workflow.
- Transparent state: market and ledger snapshots are signed JSON blocks inside Issues.
- Lightweight ops: minimal dependencies, containerized, deploy with one workflow.
- GitHub-native: identity = GitHub username, governance via repo permissions.
A “market” is a question about your repo’s work, backed by a prediction price that updates as people trade. The default question is:
- “Will Issue # be closed by ?”
Collaborators buy YES or NO shares to express their beliefs. The price (0–1) reflects the current crowd forecast (e.g., 0.72 ≈ 72% chance). When the deadline hits:
- If the issue closed before the deadline, YES wins; otherwise, NO wins.
- Winners receive $1 per winning share; balances and positions are tracked in a signed ledger issue.
- Planning: quantify the likelihood that a task lands by a date.
- Prioritization: discover which work is at risk through market prices rather than gut feel.
- Alignment: make implicit expectations visible (prices) and auditable (signed snapshots).
- Fun-but-useful: add a lightweight “skin in the game” signal without real money.
- “Will Issue #456 be closed by 2025-12-01?”
- “Will PR #789 be merged by Friday?”
- “Will the v1.2 release ship by end of month?”
- Create: comment
/market create issue 123 deadline 2025-11-15 - Trade: comment
/trade yes 10or/trade no 5on the market issue - Check: comment
/balanceto see your credits and position - Resolve: automatically resolves YES if the target closes before deadline; else NO after deadline
- Markets are collaborator-only to reduce spam and ensure skin-in-repo.
- Shares can be fractional to 2 decimals; max 10,000 shares per trade; 5 trades/min rate limit.
- All state (market snapshots, ledger) is signed JSON in issue bodies; no external DB.
- Markets are Issues with a signed snapshot block (
qYes,qNo,priceYes,deadline,seq) - Create markets via comments:
/market create issue 123 deadline 2025-11-15 - Trade via comments:
/trade yes 10or/trade no 5(fractional shares up to 2 decimals) - Ledger Issue tracks balances and positions (signed, with
seq) - Auto-resolution and payouts ($1 per winning share)
- Rate limiting: max 5 trades per user per minute (in-memory)
- Health check endpoint at
/healthz - Minimal dependencies, no external DB
- Dockerfile + fly.toml + GitHub Actions deploy
- A comment (e.g.,
/trade yes 10) triggers a GitHub webhook to your app. - The app verifies the webhook signature and maps the repo/issue from the payload.
- It reads the market snapshot from the issue body:
- Extracts the
<!-- forecast-snapshot:start --> ... <!-- forecast-snapshot:end -->block - Verifies the snapshot signature; invalid/tampered data is rejected
- Extracts the
- It reads the ledger snapshot from the “Forecast Ledger” issue and verifies its signature.
- Applies LMSR math to compute cost and new price; checks user balance and rate limit.
- Writes the updated ledger and market snapshot (signed), with
seqincrements and optimistic concurrency. On write conflicts, it retries with backoff. - Posts a trade receipt comment with cost, slippage, new probability, and remaining balance.
- Create market (comment on any issue or market issue):
/market create issue <number> deadline YYYY-MM-DD
- Trade (comment on a market issue):
/trade yes <shares>/trade no <shares>
- Check balance and position in current market:
/balance
- Help:
/help
Notes:
- Shares can be fractional to 2 decimal places (e.g.,
7.25). - Maximum 10,000 shares per trade.
- Rate limit: 5 trades per minute per user.
- Starting balance: 1000 credits.
- Winners receive $1 per winning share.
Trading is restricted to repository collaborators (permissions: read, write, admin). Non-collaborators will receive a comment indicating they cannot trade.
- Snapshots and the ledger are signed with HMAC-SHA256 using
SNAPSHOT_SIGNING_KEY. - On read:
- Market snapshot: extracted and verified. Invalid or missing snapshots are treated as not open/not available.
- Ledger: extracted and verified. Invalid signatures raise an error.
- On write:
- The block is re-rendered with a signed payload and
seqincrement. - A single snapshot/ledger block is kept and replaced at the top.
- The block is re-rendered with a signed payload and
GET /healthz returns JSON with validation of required configuration:
- App ID present
- Private key shape (
BEGIN PRIVATE KEY) - Webhook secret and snapshot signing key minimum lengths
- Positive
bparameter - Valid port
- Create a GitHub App:
- Permissions:
- Issues: Read & write
- Metadata: Read
- Webhooks: subscribe to
issues,issue_comment
- Permissions:
- Install the App on the target organization/repository.
- After deploying, set the webhook URL:
https://<your-fly-app>.fly.dev/webhooks
This repo includes .github/workflows/deploy.yml to build and deploy on push to main or manually via “Run workflow”.
- Install Fly CLI locally and create the app:
flyctl apps create ganttmarket flyctl auth token
- In your GitHub repo, add Actions secrets:
FLY_API_TOKEN: fromflyctl auth tokenGITHUB_APP_ID: numeric App IDGITHUB_WEBHOOK_SECRET: your webhook secretSNAPSHOT_SIGNING_KEY: a strong random string (256-bit)GITHUB_APP_PRIVATE_KEY: paste the PEM content of your app private key
.github/workflows/deploy.yml:
name: Deploy to Fly.io
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Check formatting
run: bun run format:check
- name: Lint code
run: bun run lint:check
- name: Typecheck
run: bun run typecheck
- name: Setup Flyctl
uses: superfly/flyctl-actions/setup-flyctl@v1
- name: Set Fly secrets
run: |
flyctl secrets set \
GITHUB_APP_ID=${{ secrets.GITHUB_APP_ID }} \
GITHUB_WEBHOOK_SECRET=${{ secrets.GITHUB_WEBHOOK_SECRET }} \
SNAPSHOT_SIGNING_KEY=${{ secrets.SNAPSHOT_SIGNING_KEY }} \
GITHUB_APP_PRIVATE_KEY="${{ secrets.GITHUB_APP_PRIVATE_KEY }}"
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Deploy
run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}- Push to
mainor go to Actions → “Deploy to Fly.io” → “Run workflow”. - After the first deploy, update your GitHub App webhook URL to the Fly app URL:
https://<your-app>.fly.dev/webhooks
- Check health:
curl https://<your-app>.fly.dev/healthz
bun install
bun run devEnvironment variables (use Fly secrets in production):
PORT=8080
GITHUB_APP_ID=123456
GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
GITHUB_WEBHOOK_SECRET=supersecret
SNAPSHOT_SIGNING_KEY=some-long-random-256-bit-string
MARKET_B=100
- Comment on issue #123:
The bot replies with the created market issue number.
/market create issue 123 deadline 2025-11-15 - On the market issue, trade:
The bot replies with cost, slippage, new probability, remaining balance, and a transaction ID.
/trade yes 10 - Check your balance:
The bot replies with your current balance and positions.
/balance - Resolution and payouts:
- If #123 closes before the deadline, the market auto-resolves YES; winners get $1 per winning share.
- If the deadline passes without closure, it resolves NO.
src/core TypeScript:app.tsBun server and/healthzwebhooks.tsGitHub webhook handlers (create, trade, balance, resolve)lmsr.tsAMM functionssnapshot.tssigned market snapshot (withseq)storage.tsread/write snapshot into issue bodymarket.tsmarket creationledger.tssigned ledger snapshot (balances, positions,seq)resolution.tsoutcome detection and payouts
Dockerfilecontainer buildfly.tomlFly app config.github/workflows/deploy.ymlGitHub Actions deployment
- No selling/shorting; for binary markets, selling can be approximated by buying the opposite side. Proper shorting needs more accounting.
- Concurrency: optimistic retries with backoff on snapshot write conflicts.
- Rate limits are in-memory and reset on process restart.
- Labels used for discovery and metadata:
forecast-market,target-issue-<n>,deadline-YYYY-MM-DD. Discovery usesper_page=100; high-volume repos may need pagination tweaks.
- Webhook 401: ensure GitHub App webhook secret matches
GITHUB_WEBHOOK_SECRET. - Startup “unhealthy”: check
/healthzfor failed checks (e.g., private key format, missing env vars). - Actions deployment failing: verify
FLY_API_TOKENand thatfly.tomlhas your app name. - “Market not open or missing snapshot”: the snapshot may be invalid or missing; check the market issue body for the signed block and verify your
SNAPSHOT_SIGNING_KEY.
See TODO.md for planned features:
- Governance (caps per user, conflict-of-interest flags)
- Portfolio/leaderboard
- Enhanced resolution policies
- Observability/logging (request IDs)
- Unit tests for LMSR and parsing