Starter bot#
Each starter is one Python file, under 80 lines, that reads live markets with your Layer API key. None of them places an order. Pick the one closest to what you're building:
| Starter | What it does | File |
|---|---|---|
| Price gaps | The same market on Kalshi and Polymarket US side by side, biggest YES price gap first, refreshing every 30 seconds. | price_gaps.py |
| Ask about markets | A find_markets tool for your AI agent: a tool definition plus an MCP server, returning each venue's link and YES price. |
ask_markets.py |
| Matched markets to a spreadsheet | Every market that's the same bet on both venues, written to matched_markets.csv. |
matched_markets_csv.py |
The fastest way in is the Starter bot page in your dashboard. Each starter runs live there with your account. Copy prompt for your coding agent gives Claude Code, Cursor or ChatGPT everything it needs to set the bot up, run it and explain the output, and Make a key for this bot puts a fresh key into that prompt.
Run one yourself#
You need Python 3.11 or newer (or uv) and a Layer API key from your dashboard.
mkdir layer-price-gaps && cd layer-price-gaps
curl -fsSL https://uselayer.sh/starters/price_gaps.py -o price_gaps.py
python3 -m venv .venv && .venv/bin/pip install uselayer
export LAYER_API_KEY=lyr_...
.venv/bin/python price_gaps.pySwap in ask_markets.py or matched_markets_csv.py for the other two.
Layer says which markets are the same bet. The prices come from each venue's public order book, read on your machine: no Kalshi or Polymarket account needed. A price gap is a starting point, not a profit. Check fees, how many shares are on offer, and the rule differences Layer lists for each match (caveat_notes, see How matching works).
Price gaps#
"""Price gaps: the same market on Kalshi and Polymarket US, side by side, most you could save first.
Layer says which markets are the same bet; this script reads both venues' order books on your machine and
prints what YES costs on each. You need a Layer API key, nothing else. It only reads data, never orders.
pip install uselayer
export LAYER_API_KEY=lyr_... # from uselayer.sh/dashboard/keys
python price_gaps.py # refreshes every 30 seconds; Ctrl+C to stop
python price_gaps.py --once # print the table once and exit
python price_gaps.py --include-started # also list games already under way (their prices go stale)
"""
import sys
import time
from datetime import datetime, timezone
from uselayer import Client
from uselayer.http import Http
from uselayer.venues.kalshi import KalshiMarkets
from uselayer.venues.polymarket_us import PolymarketUSPublic
MATCHES = 20 # how many matched markets to check each round
REFRESH_SECONDS = 30
INCLUDE_STARTED = "--include-started" in sys.argv
client = Client() # reads LAYER_API_KEY; paper mode, so nothing is ever traded
kalshi, polymarket_us = KalshiMarkets(Http()), PolymarketUSPublic(Http()) # public order books, no venue keys
def yes_asks(m):
"""The cheapest YES on offer on each venue (price and shares), read from the venues on your machine."""
k = kalshi.read_book(m.kalshi.market_id).book.outcome("yes").best_ask
u = polymarket_us.book(m.polymarket_us.market_id).outcome("yes").best_ask
return k, u # None: nobody is selling YES there
def started(m):
"""True once the game has begun: one venue's price is often stale then, so its gap isn't real."""
t = m.kalshi.event_time or m.polymarket_us.event_time
return bool(t) and datetime.fromisoformat(t) <= datetime.now(timezone.utc)
def cents(price):
return " -- " if price is None else f"{price * 100:4.0f}¢"
def gaps():
"""One row per upcoming match, soonest first (games under way only with --include-started)."""
rows = []
for m in [m for m in client.matches(limit=MATCHES * 3) if INCLUDE_STARTED or not started(m)][:MATCHES]:
try:
k, u = yes_asks(m)
except Exception as err: # one venue not answering shouldn't stop the table
print(f"skipped {m.kalshi.market_id}: {err}", file=sys.stderr)
continue
gap = abs(k.price - u.price) if k and u else None
shares = min(k.size, u.size) if k and u else None # a big gap on a few shares isn't worth much
save = gap * shares if gap is not None and shares is not None else None # dollars, before fees
rows.append((save, gap, m, k and k.price, u and u.price, shares))
# Most you could save first; markets missing a price on one side go last.
rows.sort(key=lambda r: -1 if r[0] is None else r[0], reverse=True)
return rows
def show(rows):
print(f"\n{time.strftime('%H:%M:%S')} YES price on each venue (cost to buy one share)")
print(f"{'save':>6} {'gap':>5} {'Kalshi':>6} {'Poly US':>7} {'shares':>6} market")
for save, gap, m, kalshi, poly, shares in rows:
name = ("[started] " if started(m) else "") + f"{m.kalshi.event or m.kalshi.market_id} · {m.kalshi.outcome or ''}"
size = " --" if shares is None else f"{shares:6.0f}"
dollars = " --" if save is None else f"${save:5.2f}"
print(f"{dollars} {cents(gap)} {cents(kalshi):>6} {cents(poly):>7} {size} {name[:56]}")
print("shares: shares on offer at both prices. save: gap x shares, before fees.")
if __name__ == "__main__":
while True:
show(gaps())
if "--once" in sys.argv:
break
time.sleep(REFRESH_SECONDS)Ask about markets#
Run it once to see what your agent gets back:
python ask_markets.py nbaTo give it to Claude Code as a tool, install mcp in the same environment and add it as an MCP server:
.venv/bin/pip install mcp
claude mcp add layer -e LAYER_API_KEY=lyr_... -- "$PWD/.venv/bin/python" "$PWD/ask_markets.py" --mcpFor other agents, pass the file's TOOL definition to your model (it's in the Claude API's shape; for OpenAI, rename input_schema to parameters) and call find_markets when the model asks for it.
"""Ask about markets: a tool your AI agent can call to find a market on Kalshi and Polymarket US.
It searches Layer's matched markets ("fed", "chiefs", "bitcoin"), then reads both venues' YES prices
on your machine. It only reads data: it never places an order.
pip install uselayer
export LAYER_API_KEY=lyr_... # from uselayer.sh/dashboard/keys
python ask_markets.py chiefs # run the tool once and print what an agent would get
python ask_markets.py --mcp # serve it over MCP (needs: pip install mcp), e.g.
claude mcp add layer -- python /full/path/to/ask_markets.py --mcp
"""
import json
import sys
from uselayer import Client
from uselayer.http import Http
from uselayer.venues.kalshi import KalshiMarkets
from uselayer.venues.polymarket_us import PolymarketUSPublic
# The tool definition, in the shape the Claude API takes. For OpenAI, rename input_schema to parameters.
TOOL = {
"name": "find_markets",
"description": "Find prediction markets that are the same bet on Kalshi and Polymarket US. "
"Returns each venue's market, link and current YES price, plus any rule differences.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "A few words, e.g. 'fed rates' or 'chiefs'"},
"limit": {"type": "integer", "description": "How many matches to return, 1 to 10", "default": 5},
},
"required": ["query"],
},
}
client = Client() # reads LAYER_API_KEY; paper mode, so nothing is ever traded
kalshi, polymarket_us = KalshiMarkets(Http()), PolymarketUSPublic(Http()) # public order books, no venue keys
def yes_prices(m):
"""What one YES share costs right now on each venue, read from the venues on your machine."""
k = kalshi.read_book(m.kalshi.market_id).book.outcome("yes").best_ask
u = polymarket_us.book(m.polymarket_us.market_id).outcome("yes").best_ask
return (k.price if k else None), (u.price if u else None) # None: nobody is selling YES there
def find_markets(query: str, limit: int = 5) -> list[dict]:
"""Find prediction markets that are the same bet on Kalshi and Polymarket US, with YES prices."""
results = []
for m in client.matches(q=query, limit=max(1, min(limit, 10))):
try:
k, u = yes_prices(m)
yes = {"kalshi": k, "polymarket_us": u}
except Exception: # a venue that doesn't answer leaves its price out
yes = {"kalshi": None, "polymarket_us": None}
results.append({
"event": m.kalshi.event,
"outcome": m.kalshi.outcome,
"event_date": m.event_date,
"kalshi": {"market_id": m.kalshi.market_id, "url": m.kalshi.url, "yes_price": yes["kalshi"]},
"polymarket_us": {"market_id": m.polymarket_us.market_id, "url": m.polymarket_us.url,
"yes_price": yes["polymarket_us"]},
"rule_differences": m.caveat_notes, # where the two venues' rules don't say the same thing
})
return results
if __name__ == "__main__":
if "--mcp" in sys.argv:
from mcp.server.fastmcp import FastMCP
server = FastMCP("layer")
server.tool()(find_markets)
server.run() # talks to the agent over stdin/stdout
else:
query = " ".join(a for a in sys.argv[1:]) or "fed"
print("Tool definition:\n" + json.dumps(TOOL, indent=2))
print(f"\nfind_markets({query!r}) returned:\n" + json.dumps(find_markets(query), indent=2))Matched markets to a spreadsheet#
--prices adds each venue's YES price (two order books per row, so it's slower).
"""Matched markets to a spreadsheet: every market that's the same bet on Kalshi and Polymarket US, as a CSV.
Open the file in Excel, Numbers or Google Sheets. Add --prices to also read each market's YES price
from both venues (slower: two order books per row). It only reads data: it never places an order.
pip install uselayer
export LAYER_API_KEY=lyr_... # from uselayer.sh/dashboard/keys
python matched_markets_csv.py # writes matched_markets.csv
python matched_markets_csv.py --prices # same, with YES prices
"""
import csv
import sys
from uselayer import Client
from uselayer.http import Http
from uselayer.venues.kalshi import KalshiMarkets
from uselayer.venues.polymarket_us import PolymarketUSPublic
MAX_ROWS = 1000 # stop after this many matches
PAGE = 200 # Layer returns at most 200 matches per request
OUT = "matched_markets.csv"
client = Client() # reads LAYER_API_KEY; paper mode, so nothing is ever traded
kalshi, polymarket_us = KalshiMarkets(Http()), PolymarketUSPublic(Http()) # public order books, no venue keys
def yes_prices(m):
"""What one YES share costs right now on each venue, read from the venues on your machine."""
k = kalshi.read_book(m.kalshi.market_id).book.outcome("yes").best_ask
u = polymarket_us.book(m.polymarket_us.market_id).outcome("yes").best_ask
return (k.price if k else None), (u.price if u else None) # None: nobody is selling YES there
with_prices = "--prices" in sys.argv
def all_matches():
"""Page through Layer's matches, soonest event first."""
offset = 0
while offset < MAX_ROWS:
page = client.matches(limit=PAGE, offset=offset)
yield from page
if len(page) < PAGE:
return
offset += PAGE
def row(m):
k, u = m.kalshi, m.polymarket_us
r = {
"event_date": m.event_date, "category": m.category,
"kalshi_event": k.event, "kalshi_outcome": k.outcome, "kalshi_market_id": k.market_id, "kalshi_url": k.url,
"polymarket_us_event": u.event, "polymarket_us_outcome": u.outcome,
"polymarket_us_market_id": u.market_id, "polymarket_us_url": u.url,
"confidence": m.confidence,
"rule_differences": " | ".join(m.caveat_notes.values()), # where the venues' rules differ
}
if with_prices:
try:
r["kalshi_yes_price"], r["polymarket_us_yes_price"] = yes_prices(m)
except Exception: # a venue that doesn't answer leaves the price blank
r["kalshi_yes_price"] = r["polymarket_us_yes_price"] = None
return r
if __name__ == "__main__":
rows = [row(m) for m in all_matches()]
if not rows:
sys.exit("No matches came back. Check LAYER_API_KEY.")
with open(OUT, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
print(f"Wrote {len(rows)} matched markets to {OUT}")
for r in rows[:5]:
print(f" {r['event_date']} {r['kalshi_event']} · {r['kalshi_outcome']}")