# BTC 15-min Up/Down Arbitrage (Polymarket x Kalshi) (`suitable_sunspot/btc-updown-arbitrage`) Actor

Find executable Bitcoin 15-minute up/down arbitrage between Polymarket and Kalshi. Matches their BTC 15-min markets by close time, pulls live order-book depth, applies both platforms' documented fees, and returns only mismatches still profitable at the supported size.

- **URL**: https://apify.com/suitable_sunspot/btc-updown-arbitrage.md
- **Developed by:** [Dmitry Melikhov](https://apify.com/suitable_sunspot) (community)
- **Categories:** Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.01 / actor start

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## BTC Up/Down 15-Minute Arbitrage — Polymarket x Kalshi

**The same 15-minute BTC window, priced on two venues.** Polymarket and Kalshi both run a recurring "will BTC be higher than it was 15 minutes ago?" binary market that resets every 15 minutes. This Actor matches those windows across the two venues by close time, walks both order books, applies each platform's documented trading fee, and reports the spread on **every** matched window — flagging the (rare) cases where that spread is wide enough to be a real arbitrage.

**Be honest about what you're getting:** these two venues are both liquid, so a genuine cross-venue arbitrage is **rare and short-lived** — most runs return `isArbitrage: false`. The Actor's job is to watch the spread *always*, so that when a gap does open you see it immediately, and to tell you exactly what it saw on every run (never an empty dataset you can't distinguish from a crash).

Because both venues run the *same* 15-minute window on the *same* asset, a window on one can be matched to the other by close time alone — no fuzzy text matching. One cheap, exact call per window, so a run is fast and repeatable on a schedule.

### Use cases

- **Cross-venue arb scanning** — watch Polymarket x Kalshi 15-minute BTC up/down markets for the rare price gaps worth trading.
- **Spread monitoring** — every run reports the live spread (gross/net) on each matched window, so you can see the market even when there's nothing to trade.
- **Executable sizing, not notional** — order books are walked to your `orderSizeUsd`; `sizeContracts` is the size both books can actually fill, and each leg is marked with where its depth came from (`sizeSource`).
- **Fees and net edge per row** — gross profit, each venue's fee, and net profit are computed on every row, so a gap that dies after fees never shows as an opportunity.
- **Threshold filtering** — set `minNetProfitUsd`; only rows clearing it are flagged `isArbitrage: true`.
- **Continuous monitoring** — run on a schedule; each run re-scans the window that is currently open.

### Output

Every run emits **at least one row** (one per matched window × combo, plus a status row when a window can't be matched). `isArbitrage` is `true` only when net profit clears `minNetProfitUsd`; you are charged only for those rows.

```json
{
  "status": "ok",
  "isArbitrage": false,
  "fetchedAt": "2026-10-16T19:32:12+00:00",
  "windowCloseKalshi": "2026-10-16T19:30:00Z",
  "windowClosePolymarket": "2026-10-16T19:30:00Z",
  "kalshiTicker": "KXBTC15M-26OCT161530-30",
  "polymarketSlug": "btc-updown-15m-1792178100",
  "combo": "Kalshi YES + Polymarket DOWN",
  "sizeContracts": 288.0,
  "kalshiAvgPrice": 0.31,
  "polymarketAvgPrice": 0.6945,
  "kalshiSizeSource": "implied",
  "polymarketSizeSource": "book",
  "grossProfitUsd": -5.65,
  "estimatedFeesUsd": 0.0,
  "netProfitUsd": -5.65,
  "netProfitPct": -1.962,
  "oracleRiskNote": "Polymarket settles on Chainlink's BTC/USD TWAP stream; Kalshi settles on CF Benchmarks' BRTI. ..."
}
```

| Field | Meaning |
|---|---|
| `status` | `ok` (matched + priced), `no_polymarket_match`, `tokens_unavailable`, `no_liquidity`, `no_open_markets`, or `error`. |
| `isArbitrage` | `true` only when net profit after fees clears `minNetProfitUsd`. The pay-per-event is charged **only** for these rows. |
| `fetchedAt` | UTC timestamp when the books were read. |
| `kalshiTicker` / `polymarketSlug` | Exact identifiers on each venue. |
| `combo` | `Kalshi YES + Polymarket DOWN` or `Kalshi NO + Polymarket UP`. |
| `sizeContracts` | Contracts fillable on **both** legs at `orderSizeUsd` (smaller side caps it). |
| `kalshiAvgPrice` / `polymarketAvgPrice` | Volume-weighted average fill price after walking the depth. |
| `kalshiSizeSource` / `polymarketSizeSource` | `implied` vs `book`. Polymarket returns real asks (`book`); Kalshi exposes only bids, so its ask is **implied** from the opposite side and its size is discounted by `kalshiImpliedSizeFactor`. |
| `grossProfitUsd` | `sizeContracts x (1 - kalshiAvgPrice - polymarketAvgPrice)` — before fees. |
| `estimatedFeesUsd` | Sum of both venues' fees at those average prices. |
| `netProfitUsd` | `grossProfitUsd - estimatedFeesUsd`. |
| `netProfitPct` | Net profit as a percentage of the pair's $1-per-contract payout. |
| `oracleRiskNote` | Basis-risk disclosure, on every row. |

The example values above are a real (non-arbitrage) row shape from a live run, not a promise of profit.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `orderSizeUsd` | integer | `200` | Capital simulated per leg. Each book is walked to this budget; reported size/profit is capped by whichever side runs out of depth first. |
| `minNetProfitUsd` | number | `0.5` | Only rows whose net profit after both venues' fees clears this are flagged `isArbitrage=true`. All matched windows are still reported. |
| `timeToleranceMinutes` | integer | `2` | How close the two venues' close times must be to count as the same real-world window. `0`-`10`. |
| `maxResults` | integer | `20` | Stop after this many `isArbitrage=true` rows (spread/status rows are always emitted). |
| `kalshiFeeRate` | number | `0.07` | Kalshi fee coefficient: `rate x contracts x price x (1 - price)`, rounded up to the cent. |
| `polymarketFeeRate` | number | `0.0025` | Polymarket crypto taker-fee coefficient. |
| `polymarketFeeExponent` | number | `2` | Exponent in Polymarket's fee formula. |
| `kalshiImpliedSizeFactor` | number | `0.5` | Kalshi asks are implied from opposite bids (it exposes bids only); this factor discounts the implied size to stay conservative about executable depth. |

### Fees used

- **Kalshi** — documented general trading fee: `0.07 x contracts x price x (1 - price)`, rounded **up** to the next cent (exact decimal arithmetic).
- **Polymarket** — documented crypto-category taker fee: `size x price x 0.0025 x (price x (1 - price))^2`.

Both are applied to the average fill price after walking the depth, on the taker side. The rates are input-tunable so you can update them if the venues change their schedules.

### What this Actor is not

- **Not a guaranteed-profit machine.** Cross-venue arbitrage on two liquid venues is **rare and transient**. Most rows will be `isArbitrage: false`. This Actor shows you the spread honestly so you can act when a real gap appears.
- **Not risk-free.** The two venues settle against *different oracles* — Polymarket on [Chainlink's BTC/USD TWAP stream](https://data.chain.link/streams/btc-usd-twap-60s-streams), Kalshi on CF Benchmarks' BRTI. They normally track each other closely, but a real arbitrage here carries small **basis risk** from that divergence, disclosed on every row as `oracleRiskNote`.
- **It does not place trades.** This Actor only reports opportunities; execution is yours.
- **BTC only.** Kalshi also runs ETH/SOL/XRP/DOGE/BNB at 15 minutes, but Polymarket's equivalents are hourly or 4-hour, so there is no true same-horizon pair — hence BTC only.
- **Kalshi depth is implied, not a real ask book.** Kalshi's API exposes only bids; the ask side is synthesized from the opposite side and discounted. Real executable size on the Kalshi leg may differ. This is marked `kalshiSizeSource: "implied"` on every row.
- **A matched pair is a snapshot.** Order books move; a row is the state of the books when it was read (`fetchedAt`), not a guaranteed fill.

### Run tests

```bash
.venv/bin/python -m pytest tests/ -q
```

# Actor input Schema

## `orderSizeUsd` (type: `integer`):

How much you'd spend on each side of a matched pair. Order books are walked to this budget on each platform; the reported size/profit is capped by whichever side runs out of depth first.

## `minNetProfitUsd` (type: `number`):

Only rows whose profit after both platforms' trading fees clears this amount are flagged isArbitrage=true (and charged). All matched windows are still reported regardless.

## `timeToleranceMinutes` (type: `integer`):

How close two platforms' window close times must be to count as the same real-world 15-minute window.

## `maxResults` (type: `integer`):

Stop after this many isArbitrage=true rows (spread/status rows are always emitted).

## `kalshiFeeRate` (type: `number`):

Kalshi trading fee coefficient: fee = rate x contracts x price x (1 - price), rounded up to the cent. Update if Kalshi changes its schedule.

## `polymarketFeeRate` (type: `number`):

Polymarket crypto-category taker fee coefficient: fee = size x price x rate x (price x (1 - price))^exponent.

## `polymarketFeeExponent` (type: `number`):

Exponent in Polymarket's fee formula. Default 2 per the crypto-category schedule.

## `kalshiImpliedSizeFactor` (type: `number`):

Kalshi exposes bids only, so its ask depth is implied from the opposite side. This factor (< 1) discounts that implied size to stay conservative about executable depth. Marked in output as kalshiSizeSource='implied'.

## Actor input object example

```json
{
  "orderSizeUsd": 200,
  "minNetProfitUsd": 0.5,
  "timeToleranceMinutes": 2,
  "maxResults": 20,
  "kalshiFeeRate": 0.07,
  "polymarketFeeRate": 0.0025,
  "polymarketFeeExponent": 2,
  "kalshiImpliedSizeFactor": 0.5
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("suitable_sunspot/btc-updown-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("suitable_sunspot/btc-updown-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 '{}' |
apify call suitable_sunspot/btc-updown-arbitrage --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,suitable_sunspot/btc-updown-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/wch0Hh97RtqKsagGr/builds/qLzK8gv8aW97k5SZP/openapi.json
