Most of the institutional call buying that matters happens before lunch and goes unwatched until the next morning. A 4 p.m. cron job that hits the OptionsBell unusual options endpoint, filters for calls on a watchlist you actually care about, and posts a ranked digest to your inbox turns the API into something you can act on with a coffee in hand.
Below is a complete Node implementation - no framework, no build step, just a single file and one scheduled run per day.
Why the daily digest format works
Real-time alerts are great for risk. Idea generation works better as a list. Once a day, you want to see the ten biggest calls printed against names you already follow, sorted by premium, so you can pick one or two to research before the next session. That is the whole format.
Step 1 - the watchlist and the filters
Pick the universe you actually trade or track. A short watchlist beats a long one because the signal density goes up. The script takes the list as an env var so a separate cron job can repurpose the same code with a different universe.
// digest.mjs - run once a day, e.g. at 16:30 ET
const API = "https://optionsbell.com/api/v1/options-flow/unusual";
const KEY = process.env.OPTIONSBELL_API_KEY;
const SYMBOLS = (process.env.WATCHLIST ?? "AAPL,NVDA,META,GOOGL,AMD,TSM,AVGO,PLTR").split(",");
const TO = process.env.DIGEST_TO ?? "[email protected]";
async function fetchCalls() {
const params = new URLSearchParams({
symbols: SYMBOLS.join(","),
type: "c",
min_voloi: "5",
min_premium: "500000",
max_dte: "60",
limit: "100",
});
const res = await fetch(`${API}?${params}`, {
headers: { "X-API-Key": KEY },
});
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
const json = await res.json();
return json.data;
}Step 2 - rank and format the digest
We sort by premium descending and cap at ten lines - the goal is a digest you can read in 20 seconds, not a CSV dump. Each row carries enough metadata (vol/OI, IV, days to expiry) to know whether the print is a real conviction trade or just a quiet hedge.
function buildDigest(rows) {
const top = rows
.sort((a, b) => b.premium - a.premium)
.slice(0, 10);
const totalPremium = top.reduce((s, r) => s + r.premium, 0);
const date = new Date().toISOString().slice(0, 10);
const lines = top.map((r, i) => {
const prem = (r.premium / 1e6).toFixed(2);
return `${i + 1}. ${r.symbol.padEnd(5)} ${r.expiry} ${("$" + r.strike).padEnd(7)} C ` +
`Vol/OI ${r.voloi.toFixed(1)}x $${prem}M IV ${r.iv}% ${r.dte}d`;
});
return [
`Watchlist call-sweep digest - ${date}`,
"",
`Top ${top.length} unusual calls. Combined premium: $${(totalPremium / 1e6).toFixed(1)}M.`,
"",
...lines,
].join("\n");
}Step 3 - ship it somewhere you actually read
Pick whichever channel you live in - Resend, Postmark or SendGrid for email, a Telegram bot, a Discord webhook, or a Notion page. The example below uses Resend because their REST API is one fetch call and the developer plan is free for personal volume.
async function sendEmail(markdown) {
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "[email protected]",
to: TO,
subject: "Watchlist call-sweep digest",
text: markdown,
}),
});
if (!res.ok) throw new Error(`Resend ${res.status}`);
}
const rows = await fetchCalls();
if (rows.length === 0) {
console.log("No qualifying calls today - skipping email.");
} else {
await sendEmail(buildDigest(rows));
console.log(`Sent digest with ${Math.min(rows.length, 10)} rows.`);
}Step 4 - what shows up in the inbox
The Markdown body renders as a clean monospace block in any client. Each row tells you everything you need to triage before opening a chart:
Top 10 unusual calls. Combined premium: $48.2M.
1. NVDA Jul 19 $145 C Vol/OI 9.8x $14.20M IV 51% 29d
2. AVGO Aug 16 $1900 C Vol/OI 6.4x $7.10M IV 38% 57d
3. META Jul 19 $560 C Vol/OI 5.1x $5.85M IV 33% 29d
4. PLTR Jun 28 $48 C Vol/OI 12.0x $4.40M IV 64% 8d
5. AMD Jul 26 $185 C Vol/OI 7.2x $3.95M IV 47% 36d
...Step 5 - run it on a schedule
Drop the script on Vercel as a cron-protected route, run it from a GitHub Actions schedule, or just use a server-side cron. The script is idempotent: re-running it inside the same day reproduces the same digest, so a missed schedule is a non-event.
# .github/workflows/digest.yml
name: Options digest
on:
schedule:
- cron: "30 20 * * 1-5" # 16:30 ET, Mon-Fri (UTC)
jobs:
digest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- run: node digest.mjs
env:
OPTIONSBELL_API_KEY: ${{ secrets.OPTIONSBELL_API_KEY }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
WATCHLIST: "AAPL,NVDA,META,GOOGL,AMD,TSM,AVGO,PLTR"
DIGEST_TO: "[email protected]"Where the filters bite
The two filters doing the work here are min_voloi=5 and min_premium=$500k. Below those, the noise of normal market-making swamps the institutional signal you actually want. The max_dte=60 cap keeps the digest focused on positioning that has to express itself inside a tradable window - LEAPs and very long-dated calls are noise for a daily routine.
Three small tweaks that level it up
- Add a second API call with
type=pand a smaller premium floor, then print a 'Watch for downside' section underneath the call digest. - Persist each day's rows to a Postgres table and add a 'returning prints' badge when a contract from yesterday hits the digest again - those repeats are unusually high signal.
- Score each row by recent realised volatility relative to implied; a high vol/OI call on a name with collapsing realised vol is a different setup than the same print on a stock already moving.
One endpoint, a 60-line file, and a free cron is enough to turn the OptionsBell API into a research tool that lives where you read your email. From here, anything else you build is just a different filter on the same feed.