Sports spreads and lag#
Beta. These routes are new and may still change.
Three routes for game winners in the NBA, NFL, NHL and NCAAF, on Kalshi and Polymarket US:
/v0/sports/spreads: the spread between the two venues for every game right now, after both venues' taker fees./v0/sports/lead: over the last days, which venue moved first, how often the other followed, and how long it took./v0/sports/events: one day's recorded spreads and moves, one by one, to check against your own bot.
See all three on one page at uselayer.sh/sports. Every row there shows the call that returns it.
Only games where both venues list the identical market are included. A pair still under review is left out.
GET/v0/sports/spreads#
One row per game that's live or starts within 24 hours: of its two questions, one per team, the one with the better trade. Closest to a profit first, so the list is never empty.
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/sports/spreads?league=nba"{
"as_of": "2026-11-10T22:00:00.123Z",
"games": [
{
"event_id": "lyr_evt_…",
"market_id": "lyr_…",
"league": "nba",
"title": "…",
"question": "…: … wins",
"starts_at": "…",
"status": "live",
"kalshi": { "ticker": "…", "yes_bid": 0.39, "yes_ask": 0.40, "ask_size": 100, "as_of": "…" },
"polymarket_us": { "slug": "…", "yes_bid": 0.46, "yes_ask": 0.47, "ask_size": 300, "as_of": "…" },
"direction": "kalshi_yes_pmus_no",
"edge_cents": 1.96,
"arbitrage": true,
"contracts": 100,
"net_dollars": 1.96
}
]
}| Query | Default | Meaning |
|---|---|---|
league |
all four | nba, nfl, nhl or ncaaf. |
| Field | Meaning |
|---|---|
event_id, market_id |
Layer's ids for the game and for its market, the same as in /v0/markets. Before Layer has listed the game, event_id is Kalshi's event ticker and market_id is null. |
question |
What YES means on both venues: which team wins. |
status |
live or upcoming. |
kalshi, polymarket_us |
Each venue's best YES bid and ask in dollars, the size at the ask, and as_of, when Layer read that venue's book. |
direction |
The cheaper way round: kalshi_yes_pmus_no buys YES on Kalshi and NO on Polymarket US; pmus_yes_kalshi_no is the reverse. |
edge_cents |
Profit per contract in cents at both best asks, after both venues' taker fees (before the venues round fees to the cent). Below zero is a loss. |
arbitrage |
true when at least one whole contract makes a profit after both venues' fees, rounded up to the cent as the venues charge them. It can be false while edge_cents is just above zero, when the rounding eats the edge. |
contracts |
How many contracts you could buy that way round and still make a profit, walking both books. |
net_dollars |
The profit on those contracts, after fees. |
Books are cached for 5 seconds per game.
GET/v0/sports/lead#
Over the last days days: for each pair of venues, how often one moved first, how often the other followed, and how long it took. Plus how often spreads appeared and how long they lasted.
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/sports/lead?league=nba&days=7"{
"league": "nba",
"days": 7,
"from": "2026-11-03",
"to": "2026-11-09",
"days_done": 7,
"move_days": 7,
"games": 0,
"pairs": [
{
"leader": "kalshi",
"follower": "polymarket_us",
"moves": 0,
"follow_rate": 0,
"together": 0,
"together_rate": 0,
"lag_ms_p50": 0,
"lag_ms_p90": 0,
"lags_2s_plus": 0,
"lags_5s_plus": 0
}
],
"spreads": { "per_day": 0, "length_ms_p50": 0, "length_ms_p90": 0, "cents_p50": 0, "over_1s": 0 }
}| Query | Default | Meaning |
|---|---|---|
league |
all four | nba, nfl, nhl or ncaaf. |
days |
7 |
How many days back (1 to 30), ending on the last day Layer has processed. A day is the games that started on it, US Eastern. |
| Field | Meaning |
|---|---|
from, to |
The first and last day counted. null before the first day is processed. |
days_done |
Days in that range Layer has processed. |
move_days |
Of those, days whose moves are counted once per move. pairs comes from these days only. Days processed before 12 November 2026 counted each move twice, so their moves are left out; their spreads still count. |
games |
Games recorded in those days. |
pairs[].moves |
Price moves leader made first. A move is counted once: either venue's mid price moved 3¢ or more within 2 seconds, and the leader is the venue whose price got 2¢ past where it was 2 seconds earlier first. Both venues need a price. |
pairs[].follow_rate |
Share of those moves the follower also made: 2¢ or more the same way within 30 seconds, counting together. |
pairs[].together |
Moves the follower made within 1 second of the leader. Each venue's updates can reach Layer up to about 0.1 seconds apart, so these count as simultaneous, not as a lead. |
pairs[].together_rate |
together as a share of moves. When it is high, the two venues mostly move at the same time and neither leads. |
pairs[].lag_ms_p50, lag_ms_p90 |
Median and 90th-percentile time the follower took, in milliseconds, over every follow, together included (moves in the same millisecond left out; null if none). |
pairs[].lags_2s_plus, lags_5s_plus |
Follows that took 2 seconds or more, and 5 seconds or more. |
spreads.per_day |
Spreads that cleared both venues' fees, per day. Spreads less than 250 ms apart count as one. |
spreads.length_ms_p50, length_ms_p90 |
Median and 90th-percentile time a spread stayed open, in milliseconds. |
spreads.cents_p50 |
Median profit per contract after fees, in cents, taking each spread at its best moment. |
spreads.over_1s |
Spreads that stayed open for 1 second or more. |
The numbers come from Layer's recordings of both venues' order books, processed each night at about 10:15 UTC. Each game is read from an hour before its start to the end of play. A move is measured on one team's price; the other team's is the same market the other way up. When a venue's recording goes quiet for more than 5 seconds (an outage of the feed or of Layer's recorder), its prices count as unknown until it resumes, so no spread or move is measured across the gap. The full method, findings and limits: How we measure sports gaps.
GET/v0/sports/events#
One day's recorded spreads and moves, each with its time, so you can line them up against your own logs.
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/sports/events?league=nba&date=2026-11-09"{
"league": "nba",
"date": "2026-11-09",
"status": "done",
"spreads": [
{ "event_id": "lyr_evt_…", "market_id": "lyr_…", "league": "nba", "title": "…", "question": "…: … wins",
"started_at": "…", "length_ms": 0, "edge_cents": 0, "contracts": 0, "dollars": 0 }
],
"moves": [
{ "event_id": "lyr_evt_…", "market_id": "lyr_…", "league": "nba", "title": "…", "question": "…: … wins",
"at": "…", "leader": "kalshi", "follower": "polymarket_us",
"move_cents": 3, "followed": true, "lag_ms": 0, "together": true }
]
}| Query | Default | Meaning |
|---|---|---|
league |
all four | nba, nfl, nhl or ncaaf. |
date |
required | YYYY-MM-DD: games that started that day, US Eastern. |
| Field | Meaning |
|---|---|
status |
done once Layer has processed the day; running, failed or not processed otherwise, with empty lists. |
event_id, market_id, title, question |
The game and the team's market, as in /v0/sports/spreads. |
spreads[] |
Each spread that cleared both venues' fees: when it opened (started_at), how long it stayed open (length_ms), the best profit per contract while it was open (edge_cents), and the number of contracts that made the most profit (contracts) with that profit (dollars). Spreads on one game less than 250 ms apart count as one. |
contracts, dollars |
On paper: what buying contracts on both books at that moment would have locked, after fees. Most spreads close within a fraction of a second, often before an order could reach both venues, so a real fill is usually smaller or none. |
moves[] |
Each price move of 3¢ or more within 2 seconds, counted once: which venue moved first (leader), by how much, whether the other followed within 30 seconds (followed), after how long (lag_ms, 0 when both moved in the same millisecond), and whether that was within 1 second (together). Days processed before 12 November 2026 list no moves. |
Worked example#
Kalshi's best YES ask on a game is 40¢ for 100 contracts. Polymarket US's best NO ask on the same game is 54¢, also for 100 contracts. YES on one venue plus NO on the other pays $1 whoever wins.
| Kalshi YES | Polymarket US NO | Total | |
|---|---|---|---|
| Cost of 100 contracts | $40.00 | $54.00 | $94.00 |
| Taker fee | $1.68 | $2.36 | $4.04 |
| Payout | $100.00 | ||
| Profit | $1.96 |
/v0/sports/spreads returns this game with direction: "kalshi_yes_pmus_no", edge_cents: 1.96, arbitrage: true, contracts: 100 and net_dollars: 1.96.
The fees are each venue's published taker fee: Kalshi's is 0.07 × contracts × price × (1 − price), rounded up to the cent. Polymarket US's is its fee coefficient (0.095 at the lowest volume tier) × contracts × price × (1 − price). A Polymarket US NO ask is 1 minus its YES bid, so a YES bid of 46¢ is a NO ask of 54¢.
A smaller gap can lose money. At 42¢ + 55¢ the gap is 3¢, but the fees are about 4.1¢ per contract, so edge_cents is −1.06 and arbitrage is false.