Put waves rarely show up on a daily chart in time. The big institutional hedges that precede a drawdown print on the options tape hours - sometimes days - before the underlying breaks down. If you hold a concentrated equity portfolio, polling the unusual options activity API every few minutes for puts on your tickers turns the OptionsBell feed into a portfolio-grade risk monitor.
This guide walks through a working script you can drop into a Raspberry Pi, a free Render worker or a tiny VPS. The full code is under 50 lines and ships with no dependencies you wouldn't already have.
What we are building
A scheduled job that does three things on every tick: pull the latest unusual options activity for your holdings, keep only the puts that cross a conviction threshold, and post a single Slack message per new contract so you never see the same alert twice.
- Polls
/api/v1/options-flow/unusualevery 5 minutes using the?since=parameter so each call only returns contracts seen after the last poll. - Filters server-side for puts on your portfolio symbols, with Vol/OI ≥ 4 and premium ≥ $250k.
- Deduplicates by
contract_idin a local SQLite file so a restart cannot re-fire old alerts.
Step 1 - get an API key
Subscribe to OptionsBell Pro, open Settings, and create an API key. Store it as the OPTIONSBELL_API_KEY environment variable. Every request authenticates via the X-API-Key header; rate limits are returned in X-RateLimit-Remaining, so a 5-minute cadence is comfortably inside the 2,000/day Pro budget.
Step 2 - define your portfolio and risk thresholds
The script reads two pieces of state: the symbols you want monitored, and the Slack webhook to notify. Keep them in a tiny config file so you can change holdings without touching code.
# config.py
PORTFOLIO = ["AAPL", "MSFT", "NVDA", "META", "AMD", "TSM"]
# Conviction floor: vol/oi >= 4 AND premium >= $250k.
# Tighten these to cut noise, loosen to see more.
MIN_VOLOI = 4.0
MIN_PREMIUM = 250_000
SLACK_WEBHOOK = "https://hooks.slack.com/services/T0000/B0000/XXXX"
POLL_SECONDS = 300 # 5 minutesStep 3 - the polling loop
The endpoint accepts a since ISO timestamp so we only ever pull new contracts. We store the last seen timestamp in a SQLite file and ratchet it forward on every successful response.
# monitor.py
import os, time, json, sqlite3, urllib.request, urllib.parse
from datetime import datetime, timezone
from config import (PORTFOLIO, MIN_VOLOI, MIN_PREMIUM, SLACK_WEBHOOK, POLL_SECONDS)
API_BASE = "https://optionsbell.com/api/v1/options-flow/unusual"
API_KEY = os.environ["OPTIONSBELL_API_KEY"]
DB = sqlite3.connect("state.db")
DB.execute("CREATE TABLE IF NOT EXISTS seen (contract_id TEXT PRIMARY KEY)")
DB.execute("CREATE TABLE IF NOT EXISTS cursor (k TEXT PRIMARY KEY, v TEXT)")
def get_cursor() -> str:
row = DB.execute("SELECT v FROM cursor WHERE k='since'").fetchone()
return row[0] if row else (datetime.now(timezone.utc).isoformat())
def set_cursor(iso: str) -> None:
DB.execute("INSERT OR REPLACE INTO cursor(k, v) VALUES('since', ?)", (iso,))
DB.commit()
def fetch_unusual_puts(since: str) -> list[dict]:
qs = urllib.parse.urlencode({
"symbols": ",".join(PORTFOLIO),
"type": "p",
"min_voloi": MIN_VOLOI,
"min_premium": MIN_PREMIUM,
"since": since,
"limit": 200,
})
req = urllib.request.Request(f"{API_BASE}?{qs}", headers={"X-API-Key": API_KEY})
with urllib.request.urlopen(req, timeout=15) as r:
return json.loads(r.read())["data"]
def already_seen(contract_id: str) -> bool:
return DB.execute(
"SELECT 1 FROM seen WHERE contract_id=?", (contract_id,)
).fetchone() is not None
def remember(contract_id: str) -> None:
DB.execute("INSERT OR IGNORE INTO seen(contract_id) VALUES(?)", (contract_id,))
DB.commit()
def alert(c: dict) -> None:
msg = (
f":rotating_light: *Unusual PUT on {c['symbol']}* "
f"- strike ${c['strike']}, exp {c['expiry']}\n"
f"Vol/OI {c['voloi']:.1f}x, premium ${c['premium']:,.0f}, IV {c['iv']}%"
)
body = json.dumps({"text": msg}).encode()
urllib.request.urlopen(urllib.request.Request(
SLACK_WEBHOOK, data=body, headers={"Content-Type": "application/json"}
), timeout=10).read()
while True:
since = get_cursor()
try:
for c in fetch_unusual_puts(since):
if already_seen(c["contract_id"]):
continue
alert(c)
remember(c["contract_id"])
set_cursor(datetime.now(timezone.utc).isoformat())
except Exception as e:
print(f"[poll error] {e}")
time.sleep(POLL_SECONDS)Step 4 - what a real alert looks like
When the loop fires on something like a 30-day put on a name you own, the Slack message that lands in your channel is enough to act on without leaving the chat:
⚠️ Unusual PUT on NVDA - strike $140, exp Jul 19. Vol/OI 7.4x, premium $1,840,000, IV 52%.
From there, decide: trim the long, roll your existing protective puts forward, or buy a small downside spread to define risk through the same window the institutional hedger picked.
Step 5 - run it as a service
On Linux, a one-line systemd unit keeps the script alive across reboots. On free hosting, a single render.com worker with this file is enough. There is no database to manage and no queue to babysit.
# /etc/systemd/system/optionsbell-monitor.service
[Unit]
Description=OptionsBell portfolio risk monitor
After=network-online.target
[Service]
Environment=OPTIONSBELL_API_KEY=ob_live_xxxxxxxxxxxxxxxx
ExecStart=/usr/bin/python3 /opt/optionsbell/monitor.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetTuning the noise floor
The two knobs that matter are min_voloi and min_premium. Start at 4x and $250k - this typically gets you 1-3 alerts per day across a 10-stock book. If you trim to 6x and $500k you will only see the loudest prints, which is usually what an institutional hedger looks like. Loosening below 3x lets event-driven, single-strike noise back in.
Where to take this next
- Swap Slack for SMS via Twilio if you treat puts as an off-hours emergency signal.
- Add a second filter for calls on shorts you carry - the same script with
type=cflags institutional buying against your downside thesis. - Persist every alert to Postgres and chart the rolling hit rate (how often a flagged put preceded a 3% drop within 10 sessions) so you can defend the workflow with data.
The unusual options activity API was designed for exactly this kind of always-on monitoring. One endpoint, sensible filters, and a stable contract identifier - everything else is composition.