Skip to main content
B
Kalshi logo

How to Build a Kalshi Sports Betting Bot

Build your own automated trading bot for Kalshi MLB and NFL game lines on Supabase, step by step. It includes the exact risk limits our bot runs with.

Sign up for Kalshi

(affiliate link)

bot.config
unit_size:$100
daily_cap:$600
min_edge_vs_ask:2%
start_buffer:10 min
max_attempts:6
lookahead:4 days

How it works

1

Scan

Read your model picks and find the matching open Kalshi market for each one.

In Supabase: an Edge Function

2

Queue

Store every qualifying bet as a row with a status field, so nothing is lost between passes.

In Supabase: a table

3

Execute

Place the order, and retry on a network or rate-limit failure instead of dropping the bet.

In Supabase: an Edge Function

4

Grade

Settle each bet from the Kalshi market result, not from your own score feed.

In Supabase: an Edge Function

Supabase Cron runs the functions on a timer, several times a day.

The bot reads your model picks, finds the matching open Kalshi market, and places orders that pass your risk limits. It runs on a schedule several times a day so late-opening markets get caught. Every bet is queued, executed with retries, then graded from the Kalshi settlement. This guide shows the four stages, the setup, and the exact limits our bot runs with. It does not hand out our keys or our API.

Before you start

What you need

  • A Kalshi account, verified and funded.
  • A free Supabase account at supabase.com. Supabase gives you a database, a place to run your code, and a timer, all in one place.
  • Picks from a model. Use your own, or copy ours by hand.
  • About 2 hours for the first setup. You do not need your own server.

Words you will see

  • API key: A key ID and a private key file from Kalshi. Your bot uses them to prove it is you.
  • Edge Function: A small program that Supabase runs for you when it gets a request. Your bot is made of these.
  • Secret: A private value, like your Kalshi key, that Supabase stores and gives only to your functions.
  • Table: A list in your Supabase database, like a spreadsheet. The bot keeps its settings and its bets in tables.
  • Cron job: A timer that runs a function at set times each day.
  • Paper mode: The bot does everything except place real orders. Use it until the results look right.

Setup steps

  1. 1
    Open a Kalshi account , verify it and fund it.
  2. 2
    Make a free Supabase account at supabase.com. Click New project. Enter a name and a strong database password, and pick the region nearest to you. Wait about two minutes for the project to start.
  3. 3
    In Kalshi, open your account settings and find API Keys. Click Create key. Kalshi gives you the private key file one time only, so save it in a safe folder right away. Copy the key ID too.
  4. 4
    Test on the Kalshi demo site first. The demo site has its own account and its own API keys. Use real money only after the bot works on demo.
  5. 5
    Save your Kalshi keys in Supabase as secrets (Supabase step 2 below). Never put a key in website code, in a table, or in git.
  6. 6
    Build the tables, the functions and the timer in Supabase. The next section shows each click and gives you each file.
  7. 7
    Get picks from your model. Each pick needs the game date, the Kalshi market, the side (yes or no) and your model probability.
  8. 8
    Put each pick in the queue table. The bot checks the live Kalshi price, computes the edge (your probability minus the Kalshi ask price), and places the bet only if it passes your limits.
  9. 9
    Grade each bet from the settled Kalshi market result, not from your own score feed.

Check Kalshi's API docs at docs.kalshi.com for the current base URLs and endpoints.

Build it on Supabase, step by step

Supabase runs the whole bot on its free plan. Tables hold the settings and the bets. Edge Functions run the code. Cron runs the functions on a timer. Do each step in order.

  1. 1

    Create the tables

    The bot needs two tables. bot_config holds your settings in one row. bet_queue holds one row for each bet. Change your limits in bot_config, not in the code.

    Supabase dashboard > SQL Editor > New query. Paste the code. Click Run.

    tables.sql
    -- One row of settings. Change the values here, not in the code.
    create table public.bot_config (
      id int primary key default 1,
      live_enabled boolean not null default false, -- false = paper mode, no real orders
      kill_switch boolean not null default false,  -- true = stop all orders now
      unit_cents int not null default 10000,       -- 100 dollars per bet
      daily_cap_cents int not null default 60000,  -- 600 dollars per game date
      min_edge numeric not null default 0.02,      -- 2 percent vs the Kalshi ask
      max_attempts int not null default 6          -- retries for a failed order
    );
    insert into public.bot_config (id) values (1);
    
    -- One row for each bet the bot wants to make.
    create table public.bet_queue (
      id bigint generated always as identity primary key,
      game_date date not null,
      market_ticker text not null unique,          -- one bet per market
      side text not null check (side in ('yes', 'no')),
      model_prob numeric not null,                 -- your model probability, 0 to 1
      status text not null default 'queued',       -- queued, placed, error
      attempts int not null default 0,
      price_cents int,
      contracts int,
      stake_cents int,
      order_id text,
      error_msg text,
      created_at timestamptz not null default now()
    );
    
    -- Lock both tables. Only your Edge Functions can read or write them.
    alter table public.bot_config enable row level security;
    alter table public.bet_queue enable row level security;

    You should see: Success. No rows returned. Both tables show in Table Editor.

  2. 2

    Save your Kalshi keys as secrets

    Supabase functions need the key in PKCS#8 format. The file starts with BEGIN PRIVATE KEY. If your Kalshi file starts with BEGIN RSA PRIVATE KEY, convert it one time with the command below. On Windows, run it in Git Bash. On a Mac, run it in Terminal.

    convert-key.sh
    openssl pkcs8 -topk8 -nocrypt -in kalshi-key.pem -out kalshi-key-pkcs8.pem
    NameValue
    KALSHI_KEY_IDThe key ID from Kalshi.
    KALSHI_PRIVATE_KEYThe full text of kalshi-key-pkcs8.pem, with the BEGIN and END lines.
    KALSHI_HOSThttps://api.elections.kalshi.com for real money. Use the demo host from docs.kalshi.com while you test.
    CRON_SECRETA long random password that you make up. The timer sends it, and the functions check it.

    Supabase dashboard > Edge Functions > Secrets > Add new secret.

    Supabase gives every function SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY automatically. Do not add them.

  3. 3

    Make a test function and check your connection

    This function signs one request and reads your Kalshi balance. It places no orders. If it works, your keys and your signing are correct.

    Edge Functions > Deploy a new function > Via Editor. Name it bot-test. Delete the sample code, paste this code, and click Deploy. Then open the function Details and turn off JWT verification. The CRON_SECRET check protects the function instead.

    bot-test/index.ts
    // bot-test: checks that your Kalshi keys work. It places no orders.
    const HOST = Deno.env.get("KALSHI_HOST") ?? "https://api.elections.kalshi.com";
    
    // Turn the PEM text into bytes for WebCrypto.
    function pemToDer(pem: string): ArrayBuffer {
      const b64 = pem.replace(/-----[^-]+-----/g, "").replace(/[^A-Za-z0-9+/=]/g, "");
      return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)).buffer;
    }
    
    // Kalshi signs: timestamp + METHOD + path. Sign the path WITHOUT the query string.
    async function kalshiHeaders(method: string, path: string) {
      const key = await crypto.subtle.importKey(
        "pkcs8", pemToDer(Deno.env.get("KALSHI_PRIVATE_KEY")!),
        { name: "RSA-PSS", hash: "SHA-256" }, false, ["sign"]);
      const ts = Date.now().toString();
      const sig = await crypto.subtle.sign(
        { name: "RSA-PSS", saltLength: 32 }, key,
        new TextEncoder().encode(ts + method + path));
      return {
        "KALSHI-ACCESS-KEY": Deno.env.get("KALSHI_KEY_ID")!,
        "KALSHI-ACCESS-TIMESTAMP": ts,
        "KALSHI-ACCESS-SIGNATURE": btoa(String.fromCharCode(...new Uint8Array(sig))),
        "Content-Type": "application/json",
      };
    }
    
    Deno.serve(async (req) => {
      if (req.headers.get("x-cron-secret") !== Deno.env.get("CRON_SECRET")) {
        return new Response("unauthorized", { status: 401 });
      }
      const path = "/trade-api/v2/portfolio/balance";
      const r = await fetch(HOST + path, { headers: await kalshiHeaders("GET", path) });
      return new Response(await r.text(), { status: r.status, headers: { "Content-Type": "application/json" } });
    });

    Run the test from your computer. Put in your project ref (it is in the dashboard address, and in Project Settings > General) and your CRON_SECRET. On Windows use curl.exe. On a Mac use curl.

    test.txt
    curl.exe -X POST "https://YOUR-PROJECT-REF.supabase.co/functions/v1/bot-test" -H "x-cron-secret: YOUR-CRON-SECRET"

    You should see: A JSON answer with your balance. The word unauthorized means the CRON_SECRET does not match. A 401 from Kalshi means the key ID or the key text is wrong. A DataError means the key is not PKCS#8, so do step 2 again.

  4. 4

    Make the execute function

    This function reads today's queued bets, checks the live Kalshi price, and places each bet that still passes your limits. It starts in paper mode, so it places nothing real until you turn live_enabled on.

    Deploy a new function > Via Editor. Name it bot-execute. Paste the code. Copy the pemToDer and kalshiHeaders functions from bot-test into the marked line. Click Deploy, then turn off JWT verification the same way as in step 3.

    bot-execute/index.ts
    // bot-execute: places queued bets that still pass your limits.
    // Paper mode until bot_config.live_enabled is true.
    import { createClient } from "jsr:@supabase/supabase-js@2";
    
    const HOST = Deno.env.get("KALSHI_HOST") ?? "https://api.elections.kalshi.com";
    
    // PASTE pemToDer AND kalshiHeaders FROM bot-test HERE.
    
    Deno.serve(async (req) => {
      if (req.headers.get("x-cron-secret") !== Deno.env.get("CRON_SECRET")) {
        return new Response("unauthorized", { status: 401 });
      }
      const db = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
      const { data: cfg } = await db.from("bot_config").select("*").eq("id", 1).single();
      if (!cfg || cfg.kill_switch) return Response.json({ ok: false, reason: "kill switch on" });
    
      // Kalshi tickers use Eastern time, so use Eastern time for game_date too.
      const today = new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" });
      const { data: rows } = await db.from("bet_queue").select("*")
        .eq("game_date", today).in("status", ["queued", "error"]).lt("attempts", cfg.max_attempts);
      const { data: placed } = await db.from("bet_queue").select("stake_cents")
        .eq("game_date", today).eq("status", "placed");
      let spent = (placed ?? []).reduce((sum: number, r: any) => sum + (r.stake_cents ?? 0), 0);
      const results: any[] = [];
    
      for (const row of rows ?? []) {
        const res = await fetch(HOST + "/trade-api/v2/markets/" + row.market_ticker);
        const market = (await res.json()).market ?? {};
        const askDollars = row.side === "yes" ? market.yes_ask_dollars : market.no_ask_dollars;
        const ask = Math.round(parseFloat(askDollars ?? "0") * 100); // price in cents
        if (ask <= 0 || ask >= 100) { results.push({ ticker: row.market_ticker, skip: "no price" }); continue; }
    
        const edge = Number(row.model_prob) - ask / 100;
        if (edge < Number(cfg.min_edge)) { results.push({ ticker: row.market_ticker, skip: "edge gone", edge }); continue; }
    
        const contracts = Math.min(Math.floor(cfg.unit_cents / ask), Math.floor((cfg.daily_cap_cents - spent) / ask));
        if (contracts < 1) { results.push({ ticker: row.market_ticker, skip: "daily cap reached" }); continue; }
    
        if (!cfg.live_enabled) { results.push({ ticker: row.market_ticker, paper: true, contracts, ask }); continue; }
    
        // This order type quotes from the YES side: "bid" buys YES, "ask" buys NO.
        const path = "/trade-api/v2/portfolio/events/orders";
        const body = {
          ticker: row.market_ticker,
          client_order_id: crypto.randomUUID(),
          side: row.side === "yes" ? "bid" : "ask",
          count: String(contracts),
          price: ((row.side === "yes" ? ask : 100 - ask) / 100).toFixed(4),
          time_in_force: "good_till_canceled",
          self_trade_prevention_type: "taker_at_cross",
        };
        const r = await fetch(HOST + path, { method: "POST", headers: await kalshiHeaders("POST", path), body: JSON.stringify(body) });
        const text = await r.text();
        if (!r.ok) {
          await db.from("bet_queue").update({ status: "error", attempts: row.attempts + 1, error_msg: (r.status + ": " + text).slice(0, 500) }).eq("id", row.id);
          results.push({ ticker: row.market_ticker, error: r.status });
          continue;
        }
        const order = JSON.parse(text);
        await db.from("bet_queue").update({
          status: "placed", attempts: row.attempts + 1, order_id: order.order_id,
          price_cents: ask, contracts, stake_cents: contracts * ask, error_msg: null,
        }).eq("id", row.id);
        spent += contracts * ask;
        results.push({ ticker: row.market_ticker, placed: contracts, ask });
      }
      return Response.json({ ok: true, live: cfg.live_enabled, results });
    });

    This is a starter. Before you go live, add a check that skips a game that starts in less than 10 minutes, and cap the contracts at the size available at the ask. See Our example constraints below.

  5. 5

    Add a test bet and run in paper mode

    Your scan function, or you by hand, adds rows to bet_queue. Add one row for a real game today. Get the market ticker from the markets endpoint at docs.kalshi.com. Then run bot-execute with the same curl command as step 3, with bot-execute in place of bot-test.

    SQL Editor > New query.

    test-bet.sql
    insert into public.bet_queue (game_date, market_ticker, side, model_prob)
    values ('2026-09-27', 'PASTE-THE-KALSHI-MARKET-TICKER', 'yes', 0.56);

    You should see: A result with paper: true and a contract count. Nothing is placed on Kalshi.

    When the paper results look right for a few days, turn on real orders:

    go-live.sql
    update public.bot_config set live_enabled = true where id = 1;
    -- To stop everything at once:
    -- update public.bot_config set kill_switch = true where id = 1;
  6. 6

    Put the bot on a timer

    Cron calls bot-execute at set times, so late markets get caught. The times are in UTC. 14:30 UTC is 9:30 AM Central time in the summer.

    Database > Extensions. Turn on pg_cron and pg_net. Then SQL Editor > New query. Put in your project ref and your CRON_SECRET, then click Run.

    schedule.sql
    -- Store the project URL and your cron secret in Vault (encrypted). Do this one time.
    select vault.create_secret('https://YOUR-PROJECT-REF.supabase.co', 'project_url');
    select vault.create_secret('YOUR-CRON-SECRET', 'cron_secret');
    
    -- Run bot-execute 4 times a day (UTC times).
    select cron.schedule('bot-execute', '30 14,17,20,23 * * *', $$
      select net.http_post(
        url := (select decrypted_secret from vault.decrypted_secrets where name = 'project_url') || '/functions/v1/bot-execute',
        headers := jsonb_build_object(
          'Content-Type', 'application/json',
          'x-cron-secret', (select decrypted_secret from vault.decrypted_secrets where name = 'cron_secret')),
        body := '{}'::jsonb,
        timeout_milliseconds := 60000);
    $$);
  7. 7

    Check that it worked

    A cron run that says succeeded only means Supabase sent the request. It does not mean a bet was placed. Check the function answer and the table.

    Edge Functions > bot-execute > Logs, and Table Editor > bet_queue.

    check.sql
    -- The answers from your functions, newest first.
    select status_code, content, created from net._http_response order by created desc limit 10;
    
    -- Bets that failed, with the reason.
    select market_ticker, attempts, error_msg from public.bet_queue where status = 'error';
  8. 8

    Optional: use your own computer instead of the dashboard

    If you like to work in files, the Supabase CLI does the same steps.

    cli.txt
    npx supabase login
    npx supabase link --project-ref YOUR-PROJECT-REF
    npx supabase functions new bot-execute
    npx supabase functions deploy bot-execute --no-verify-jwt
    npx supabase secrets set --env-file .env

Copy our Monte Carlo picks

No model of your own yet? You can follow the picks our Monte Carlo sim publishes and place them on Kalshi by hand.

  1. 1

    Create a free account

    Create a free BankrollSZN account. The sim is free with an account.

  2. 2

    Open the sim for your sport

    Open the MLB sim or the NFL sim.

  3. 3

    Read the recommended bets

    Read the model's recommended bets for the day. Each card shows the side, the line and the edge.

  4. 4

    Find the same line on Kalshi

    On Kalshi, find the same game and the exact same line. If Kalshi has no market at that line, skip the pick. Do not move it to a nearby line.

  5. 5

    Re-check the Kalshi price

    Check the Kalshi price before you buy. Lines move, so an edge that was there when we published can be gone.

  6. 6

    Use your own unit size

    Use your own unit size and a daily cap. Our limits are in the section below.

Sign up for Kalshi(affiliate link)
Read this first
  • This is not financial advice. You trade at your own risk.
  • 2026 is our first year modeling these markets. The record is short, and past results do not promise future results.
  • Every pick is graded and public. See the performance history for each sport before you follow anything: MLB pick history and NFL pick history (open the History tab).
  • Only bet what you can afford to lose. 21+. 1-800-GAMBLER.

Our example constraints

$100

Unit

Flat per bet

$600

Daily cap

Per game date, all sports

2%

Edge floor

Vs Kalshi ask, re-checked

10 min

Start buffer

No bets before start

6

Retries

Placement attempts max

4 days

Lookahead

Queue ahead, place on edge

Model gate first

Only takes picks our model marks live. Paper-mode picks are skipped.

Fill cap

Contracts capped at size available at the ask. A thin book gives a small fill, not an error.

No line shopping

If Kalshi has no strike at the exact line the model published, skip the pick. Never move it to the nearest strike.

One bet per market

Skip a market that is already queued.

Switches

A live on/off flag, an auto-execute flag, and a kill switch that stops all placement at once.

Markets

MLB game totals and run line, NFL game totals and moneyline. MLB moneyline and NFL spread are turned off in our setup.

These are our settings, not advice. Size your own unit to your own bankroll.

Gotchas we hit

Collateral is per exchange shard

Kalshi runs markets on more than one shard. Cash on one shard cannot back an order on another. The Kalshi website moves the money for you, the API does not. Turn on Kalshi's automatic balance allocation across the shards you trade. A "user not found" or "insufficient balance" error on one sport only usually means this.

Cancels are shard-routed too

Pass the exchange index when you cancel, or the cancel lands on the wrong shard.

Signing with the query string attached

Including the query string in the signed path gives a signature error. Sign the path only.

Kalshi ticker dates are Eastern time

Match games on the team pair, not only the date, or a late game rolls into the next day.

Team codes differ between sources

For example JAX vs JAC, LA vs LAR. Map them or you drop teams with no error.

NFL total strikes do not step by 0.5

Match on the numeric strike, not on a half-point ladder like a sportsbook.

A tied NFL moneyline settles at 50 cents a side

Grade ties as 50, not as a loss, or your record drifts.

Retry errored rows

If the bot only picks up pending rows, one failure means a missed bet for the day. Re-scan errored rows each pass.

A scheduled job that says "ok" can still have failed every item inside it

Check what it wrote, not just the run status.

Kalshi key in the wrong format

Supabase WebCrypto reads only PKCS#8 keys (BEGIN PRIVATE KEY). A key that starts with BEGIN RSA PRIVATE KEY fails with a DataError. Convert it one time with openssl.

Free Supabase projects can pause

A free project can pause after a period with no activity. Check that your project is active before game days, or use a paid plan.

FAQ

Kalshi is a CFTC-regulated exchange and offers an API for automated trading. Check availability in your state.

21+. Bet within your means. If you or someone you know has a gambling problem, call 1-800-GAMBLER. This guide is educational and is not financial advice.

How to build a Kalshi sports betting bot

A step by step guide to building your own Kalshi trading bot for MLB and NFL game lines on Supabase. Covers the four stages, the tables, the Edge Functions, the Cron timer, the exact risk limits our bot runs with, and the gotchas we hit in production.

It also shows how to copy our free Monte Carlo sim picks by hand, with the caveat that 2026 is our first modeling season.

This guide is educational. It does not give access to our API, our keys, or our bot. Use your own model and your own Kalshi account.

Frequently asked questions

Is a Kalshi bot legal?

Kalshi is a CFTC-regulated exchange and offers an API for automated trading. Check availability in your state.

Do I need to know how to code?

A little. You paste the code on this page, change your limits in one table, and read the logs. You do not need to run a server.

Why Supabase?

The free plan has a database, Edge Functions and Cron in one place. The bot needs all three, and nothing else.

Can I use BankrollSZN picks?

Yes. Our Monte Carlo picks for MLB and NFL are free with an account. See the Copy our Monte Carlo picks section above. This guide does not give access to our API or our bot. Not financial advice.

How much money do I need?

Enough that your unit is a small part of your bankroll. Our example is $100 units with a $600 daily cap.

Does the bot win?

Not guaranteed. Track every bet and grade it from settlement.