# Kalshi Arbitrage Finder — Groups Priced Against $1 (`jpmarketdata/kalshi-odds-arbitrage`) Actor

Pick Kalshi categories and get each event where at most one outcome can win, priced as a group: sell one contract of each. Returns money back from selling all, profit % before and after fees, and cash tied up. $0.02 per group priced — 17 in one 200-event run; unpriceable ones are free. Unofficial.

- **URL**: https://apify.com/jpmarketdata/kalshi-odds-arbitrage.md
- **Developed by:** [h ichi](https://apify.com/jpmarketdata) (community)
- **Categories:** Automation, Developer tools, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 basket priced for arbitrages

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Kalshi Arbitrage Finder — Groups Priced Against $1

**What it does:** Prices each Kalshi event where at most one outcome can win as a whole group: what you get for selling one contract of every outcome, after Kalshi's fees.

**You enter:** Kalshi categories, e.g. `Politics`, `Economics`; or specific series tickers such as `KXNEWPOPE`.

**You get:** One row per group: money back from selling one contract of every outcome, profit % before and after fees (at one contract and at large size), cash tied up and return on it, outcomes priced and why any were not — from up to 200 open events scanned per run.

**Price:** $0.02 per group priced — a default run scans 200 open events and charges for every group it prices: 17 groups in the measured run (200 events, 3 categories), and it varies by category. +$0.002 per outcome if you also want each one (off by default). Groups that can't be priced, or that your minimum-profit setting drops, are never charged.

**Example:** enter series `KXGDPYEAR` → 11 open events read, 1 group priced (US real GDP growth in 2035, real run 2026-08-18): selling one contract of every outcome returns $0.86 · -14.0% before fees · -28.0% after fees at one contract · -19.6% at large size, the limit where the fee rounding washes out

> Unofficial — not affiliated with Kalshi. Reads public pages only.

### What you get, one row per group

A **group** here is a set of Yes/No markets on Kalshi that can't all happen: the exchange marks the event so that at most one of its outcomes can win. Each of those outcomes is a **leg**. This Actor prices the whole group at once — what you would get for selling one contract of every leg — and returns one row per group. In the data those rows are typed `basket` and the field names keep that word.

| field | meaning |
|---|---|
| `sumBestBid` | proceeds from selling one contract of **every** leg |
| `sellAllEdgePct` | `(sumBestBid - 1) x 100` — the gross locked-in edge |
| `totalFeeUsd` / `sellAllEdgeNetPct` | Kalshi's fee and the surviving edge **at one contract** — the conservative bound |
| `totalFeeAtScaleUsd` / `sellAllEdgeNetAtScalePct` | the same at large order size, where Kalshi's per-order cent rounding washes out |
| `feeBasis` | one sentence on why there are two figures and where the truth sits |
| `maxCollateralUsd` / `returnOnMaxCollateralPct` | capital actually at risk, and the return on it |
| `collateralReturnType` / `collateralBasis` | Kalshi's own margining flag for this event, and which basis the capital figure used |
| `sumBestAsk` | cost of buying every leg (a fact — see below) |
| `legsTotal` / `legsPriced` / `legsUnpriceable` | how much of the group was actually sellable |
| `legsUnpriceableBreakdown` | *why* each leg was dropped: `no-bid`, `not-active`, `bid-size-below-min` |
| `legsAskSideThinBreakdown` | ask-side thinness (`no-ask`, `ask-size-below-min`) — disclosed, never a refusal |
| `spreadStats`, `openInterest`, `volume24h`, `minLegSizeAtTouch` | whether the quote is real depth or a lonely market maker |

Every run also returns one `arbitrage_summary` row first: how many events were read, how many groups were evaluated, priced and refused, why each refusal happened, and the best group of the run at one contract and at size.

### Output example — one row per group

Measured on 2026-08-10 (real run; the four long explanation fields are shortened here with `...`)

```json
{
  "type": "basket",
  "eventTicker": "KXGDPYEAR-31",
  "seriesTicker": "KXGDPYEAR",
  "eventTitle": "US real GDP growth in 2031?",
  "subTitle": "2031",
  "category": "Economics",
  "mutuallyExclusive": true,
  "legsTotal": 14,
  "legsPriced": 14,
  "legsUnpriceable": 0,
  "legsUnpriceableBreakdown": {},
  "legsAskSideThin": 0,
  "legsAskSideThinBreakdown": {},
  "minLegSizeAtTouch": 30.0,
  "sumBestBid": 1.0,
  "sumBestAsk": 1.14,
  "sellAllEdgePct": 0.0,
  "sellAllEdgeNetPct": -14.0,
  "sellAllEdgeNetUsd": -0.14,
  "totalFeeUsd": 0.14,
  "sellAllEdgeNetAtScalePct": -6.34,
  "sellAllEdgeNetAtScaleUsd": -0.0634,
  "totalFeeAtScaleUsd": 0.0634,
  "maxCollateralUsd": 0.14,
  "returnOnMaxCollateralPct": -100.0,
  "collateralReturnType": "MECNET",
  "collateralBasis": "mecnet-netted",
  "buyAllEdgePct": null,
  "buyAllEdgeWithheldReason": "Kalshi's mutually_exclusive flag guarantees that AT MOST one leg ...",
  "feeModel": "per order: ceil(0.07 x C x P x (1 - P)) rounded up to the next c ...",
  "feeBasis": "Kalshi rounds the fee up to the next cent once per ORDER, not on ...",
  "capitalBasis": "Edge percentages are per $1 of basket notional, the same convent ...",
  "spreadStats": {
    "min": 0.01,
    "p25": 0.01,
    "median": 0.01,
    "p75": 0.01,
    "max": 0.01,
    "average": 0.01
  },
  "volume24h": 0,
  "openInterest": 8830.26,
  "liquidityUsd": 0,
  "closeTime": "2032-02-29T13:29:00Z",
  "url": "https://kalshi.com/markets/kxgdpyear"
}
```

### The two traps this Actor refuses to fall into

**1. "At most one can win" does not mean "one of them must win".** Kalshi's `mutually_exclusive` flag guarantees that *at most one* leg resolves YES — not that one of them must. Measured 2026-08-10, the event "Who will the next Pope be?" lists 7 cardinals whose asks sum to **$0.286**. Read naively that is a 71% arbitrage. It is not: the next Pope need not be one of the seven, so buying all seven can pay out **$0**. Kalshi publishes no flag for "one of these must happen" and nothing in the data implies one.

So `sumBestAsk` is reported as a plain fact and **`buyAllEdgePct` is always `null`**, with `buyAllEdgeWithheldReason` saying why. Nothing in the output invites you to trade a number that does not exist. Selling every leg is the side that stays valid when all you know is "at most one can win", and it is the side this Actor prices.

**2. Fees are not a rounding error — and they are not a per-contract cent either.** Kalshi charges `ceil(0.07 x C x P x (1 - P))` rounded up to the next cent **once per order**, where `C` is that order's contract count. At `C = 1` every leg pays a full cent, so a 14-leg group owes $0.14 against a $1 notional. At size the single ceiling amortises away and the same group owes only `sum(0.07 x P x (1 - P))`.

Both bounds are published on every priced group, because reporting only one of them is a lie in one direction or the other. Measured 2026-08-10, `KXGDPYEAR-31` (US real GDP growth in 2031) has bids summing to exactly $1.0000: flat gross, **-14.0% net at one contract, -6.3% at scale**. Gross-only numbers would have called that group free; one-contract-only numbers would bury every group whose real edge lives between the two. Both figures are **taker** fees, because selling every leg hits the resting bid. Kalshi's maker fee is lower on the series that charge one, but that is not this trade at a cheaper rate — a maker sell rests at the ask rather than hitting the bid, so it is a different trade at a different price that may never fill. No maker figure is computed here. The coefficient is the `feePctOfNotional` input rather than a constant, because a stale fee schedule would quietly overstate every edge forever.

**Read `returnOnMaxCollateralPct` next to `minLegSizeAtTouch`.** Under MECNET the exchange holds only the worst-case loss, so a group that clears $1 ties up little more than the fee — and the return percentage against that tiny base gets very large. It is arithmetically true and it is the point of arbitrage, but the trade is bounded by the size resting at the touch, not by your capital. A large return on $0.14 of collateral is not a large trade, and it is a per-settlement figure, not an annualised rate.

### Capital: Kalshi nets it, and says so in the payload

Every event carries `collateral_return_type`. In the 2026-08-10 run above, all 27 groups the exchange had marked "at most one outcome can win" came back as `MECNET` — Kalshi's collateral netting for exactly these groups, under which the exchange holds your **maximum possible loss on the position**, not `(1 - price)` on each short leg separately.

So when you sell every leg you collect `sumBestBid`, at most one leg ever pays $1, and `maxCollateralUsd = max(0, 1 - sumBestBid) + totalFeeUsd`. A three-leg group bidding $1.07 ties up **$0.06**, not $1.99. The flag is read from that event's own payload and never assumed: anything other than `MECNET` falls back to the conservative per-leg figure and `collateralBasis` says which one you got.

### What gets refused, and why that is the product

Selling every leg consumes the **bid** and only the bid, so priceability is judged on the bid side alone:

- an event Kalshi does **not** mark as "at most one outcome can win" gets `mutuallyExclusive: false` and no edge figure on either side;
- fewer than `minLegs` priceable legs is refused outright;
- **any** unpriceable leg — inactive, no bid, or less size resting on the bid than `minSizeAtTouch` — voids the edge for the whole group, even when the rest clears `minLegs`. This guard is **conservative, not corrective**: dropping a leg *lowers* `sumBestBid`, so a subset would read more negative, not more positive. It is refused because selling a subset is a different position with a different payout (the missing leg can resolve YES with nothing paying you for it) — not the group you asked about.

A leg nobody is *offering* is still perfectly sellable, so `no-ask` and `ask-size-below-min` are reported in `legsAskSideThinBreakdown` as disclosure and never refuse a group. `sumBestAsk` is still collected wherever every leg is offered.

Every refusal carries a human-readable reason naming the input you would change (`minLegs`, `minSizeAtTouch`) and the full breakdown of what was dropped. `minEdgePct` is applied only **after** every group has been priced, so the summary counts never shrink just because you set a threshold — and it is keyed off the conservative one-contract figure, with `positiveNetEdgeAtScaleBaskets` in the summary counting what clears at size.

### Price

**$0.02** per group priced. Individual legs are $0.002 each and **off by default** — turn on `includeIndividualMarkets` if you want them. You pay per group priced; there is no monthly fee. **Refused groups are never charged, and neither are groups filtered out by `minEdgePct`** — you pay for the priced groups actually delivered in your results, and nothing else.

### Input

This is the input that produced the measured run shown above; `feePctOfNotional` and `includeIndividualMarkets` were left at their defaults.

```json
{
  "categories": ["Politics", "Economics", "Elections"],
  "maxEvents": 200,
  "minLegs": 3,
  "minSizeAtTouch": 1,
  "minEdgePct": "-100"
}
```

Set `seriesTickers` (e.g. `["KXNEWPOPE"]`) to scan specific series instead; it takes precedence over `categories`.

### What the live exchange actually looks like

Measured on 2026-08-10 (real run): 200 open events read, 83 of them in the chosen categories, 66 groups evaluated, 28 events marked "at most one outcome can win", 17 groups priced and 49 refused (39 because Kalshi does not mark them that way, 10 because a leg had no sellable bid). **The best gross sell-all edge in the whole set was 0.0%** — median -16.0%. That is what a functioning exchange looks like from outside: the bid side of a complete group sums below $1 because of the spread. Use this to find the groups that are closest to crossing, to measure how wide a series is quoted, and to know immediately when one does cross.

Read-only public Kalshi API, no login, throttled requests. Nothing is stored between runs — every call re-reads the live book.

### If something goes wrong

- **Wrong number or a failed run?** Open a ticket on the **Issues** tab. I read every one and reply within 2 business days (Japan time).
- **You never get a fake "empty" result.** If the site can't be read, the run fails and says so.
- **A group that can't be priced = no charge.** A group that does get a price costs $0.02 even when its profit comes out negative — that number is the result you asked for.
- **Checked every week.** An automatic test runs this tool weekly; if the site changes, I fix it.
- **Public pages only.** No login, no personal data, and it goes easy on the site.

### More tools by the same author

- [Polymarket Odds, Volume & Top Markets by Topic](https://apify.com/jpmarketdata/polymarket-market-stats)
- [Polymarket Markets — Odds, Bid/Ask, Volume, One Row Each](https://apify.com/jpmarketdata/polymarket-markets)
- [Polymarket Arbitrage Finder — Groups Priced Under $1](https://apify.com/jpmarketdata/polymarket-odds-arbitrage)
- [Polymarket vs Kalshi Arbitrage — Same Question, Two Prices](https://apify.com/jpmarketdata/prediction-market-arbitrage)

All tools (Japan marketplaces, real estate, jobs, racing, prediction markets): <https://apify.com/jpmarketdata>

### Disclaimer

Unofficial, independent tool — **not affiliated with, endorsed by, or sponsored by Kalshi**. Product names and logos belong to their owners and only say where the data comes from. Data is read from public pages, for market research; check before you act on it.

# Actor input Schema

## `categories` (type: `array`):

Kalshi event categories to scan, e.g. "Politics", "Economics", "Climate and Weather", "Sports". Leave empty to scan every open event within the event budget.

## `seriesTickers` (type: `array`):

Restrict the scan to specific Kalshi series tickers (e.g. "KXNEWPOPE"). Takes precedence over categories when set.

## `maxEvents` (type: `integer`):

How many open events to pull before pricing. Each page of the Kalshi events endpoint returns up to 200 events with their markets nested.

## `minLegs` (type: `integer`):

Refuse to price a basket with fewer outcomes than this. A two-leg "basket" is usually a yes/no market where the edge figure means something different.

## `minSizeAtTouch` (type: `integer`):

A quote with no size behind it is not a price. The sell-all hits the BID, so this is applied to bid size only: legs whose resting bid size is below this are counted as unpriceable. Ask-side thinness is reported as disclosure (legsAskSideThinBreakdown) and never voids a leg.

## `minEdgePct` (type: `string`):

Only return baskets whose fee-adjusted sell-all edge is at or above this, keyed off the conservative one-contract figure (sellAllEdgeNetPct). Applied after every basket has been priced, so the summary counts stay honest, and only delivered baskets are charged. Use a negative value to see everything.

## `feePctOfNotional` (type: `string`):

Kalshi charges a trading fee of ceil(coefficient x contracts x price x (1 - price)) cents, rounded up once per ORDER. The published coefficient is 0.07 (taker; the maker fee is roughly a quarter of it). Exposed as an input because exchange fee schedules change and a stale constant would quietly overstate every edge.

## `includeIndividualMarkets` (type: `boolean`):

Also return every leg of every reported basket (charged per market). Off by default: the basket summary is the product and a default run should not bill several times the scan itself.

## Actor input object example

```json
{
  "categories": [
    "Politics",
    "Economics"
  ],
  "maxEvents": 200,
  "minLegs": 3,
  "minSizeAtTouch": 1,
  "minEdgePct": "-100",
  "feePctOfNotional": "0.07",
  "includeIndividualMarkets": false
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "categories": [
        "Politics",
        "Economics"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jpmarketdata/kalshi-odds-arbitrage").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "categories": [
        "Politics",
        "Economics",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("jpmarketdata/kalshi-odds-arbitrage").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "categories": [
    "Politics",
    "Economics"
  ]
}' |
apify call jpmarketdata/kalshi-odds-arbitrage --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jpmarketdata/kalshi-odds-arbitrage"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/rdG4IHNYBEPj9CG0t/builds/WSngRLTZs9YXcCejy/openapi.json
