If you landed here because you typed “how to create an OpenClaw skill that monitors stock prices”, you are in the right place. I just built one for my own portfolio and I wrote down every step, including the bits that cost me a Saturday morning. The goal: an agent that watches a handful of tickers, pings me on Slack when they cross thresholds, and sends a pre-market briefing at 07:30 every weekday.

Prerequisites and quick sanity checks

OpenClaw is Node 22+ only. Make sure you are on at least v22.2.0:

$ node -v v22.2.0

Other things you need:

  • OpenClaw gateway ≥ 0.13.4 running locally or on ClawCloud
  • A free Alpha Vantage API key (5 calls/minute is enough for < 60 symbols; premium tier later)
  • Optional: Yahoo Finance (RapidAPI) key for fallback quotes
  • A Slack, Telegram, or email connector configured in the gateway
  • Basic TypeScript/JavaScript comfort — the skill is one file

You do not need a brokerage API for this tutorial. We are only reading market data and issuing notifications.

OpenClaw skill skeleton in 30 seconds

A skill is a CommonJS/ESM module that exports at least two things:

  • manifest – metadata: name, description, available commands
  • One or more functions referenced by manifest.commands

Create a folder skills/stock-sentinel under your agent directory and drop in index.ts:

// skills/stock-sentinel/index.ts import { SkillContext } from 'openclaw'; import fetch from 'node-fetch'; export const manifest = { name: 'stock-sentinel', description: 'Monitors equities, sends alerts, and compiles morning briefings.', version: '0.1.0', commands: { watchStocks: { description: 'Check current prices and compare against thresholds', parameters: { type: 'object', properties: { symbols: { type: 'array', items: { type: 'string' } } }, required: ['symbols'] } }, morningBriefing: { description: 'Push a summary for configured portfolio', parameters: { type: 'object', properties: {} } } }, schedule: { morningBriefing: '30 7 * * 1-5' // 07:30 on weekdays, cron format } }; // environment variables we will reference later const AV_KEY = process.env.ALPHA_VANTAGE_KEY!; const YF_KEY = process.env.RAPIDAPI_KEY; // optional fallback export async function watchStocks(ctx: SkillContext, { symbols }: { symbols: string[] }) { return checkAndAlert(ctx, symbols); } export async function morningBriefing(ctx: SkillContext) { const symbols = await ctx.memory.get('portfolio') || []; return compileBriefing(ctx, symbols); }

This compiles with ts-node or esbuild (the gateway ships both). The meat lives in checkAndAlert and compileBriefing, which we will write next.

Fetching stock quotes from Alpha Vantage & Yahoo Finance

Alpha Vantage keeps its free tier simple: one endpoint per symbol. That is a bottleneck but fine for ±20 names. For larger baskets we will batch through Yahoo Finance’s v11/finance/quoteSummary, but only when we hit AV rate limits.

async function quoteAlpha(symbol: string) { const url = `https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${symbol}&apikey=${AV_KEY}`; const r = await fetch(url); const data = await r.json(); // API returns { 'Global Quote': { '05. price': '123.45', ... } } const price = parseFloat(data['Global Quote']['05. price']); return price; } async function quoteYahoo(symbols: string[]): Promise> { if (!YF_KEY) throw new Error('Yahoo Finance key missing'); const url = 'https://apidojo-yahoo-finance-v1.p.rapidapi.com/market/v2/get-quotes'; const params = new URLSearchParams({ region: 'US', symbols: symbols.join(',') }); const r = await fetch(`${url}?${params}`, { headers: { 'X-RapidAPI-Host': 'apidojo-yahoo-finance-v1.p.rapidapi.com', 'X-RapidAPI-Key': YF_KEY } }); const data = await r.json(); const out: Record = {}; for (const row of data.quoteResponse.result) { out[row.symbol] = row.regularMarketPrice; } return out; }

Now glue them together with a naive cache to spare the rate-limit gods:

const cache = new Map(); const TTL = 60 * 1000; // 1 minute async function getPrice(symbol: string): Promise { const hit = cache.get(symbol); if (hit && Date.now() - hit.ts < TTL) return hit.price; try { const price = await quoteAlpha(symbol); cache.set(symbol, { price, ts: Date.now() }); return price; } catch (err) { // fallback to Yahoo — batch in groups of 10 const batch = await quoteYahoo([symbol]); const price = batch[symbol]; cache.set(symbol, { price, ts: Date.now() }); return price; } }

This is enough to read the market. Next we need thresholds.

Adding alert thresholds and routing messages

I keep thresholds in OpenClaw’s persistent memory (PostgreSQL or SQLite behind the scenes). Each symbol has an upper and lower bound plus a last-notified flag to reduce spam.

type Threshold = { up?: number; down?: number; last?: number }; async function checkAndAlert(ctx: SkillContext, symbols: string[]) { const thes: Record = await ctx.memory.get('thresholds') || {}; const notifications: string[] = []; for (const s of symbols) { const price = await getPrice(s); const t = thes[s] || {}; let violated = false; if (t.up && price >= t.up && price !== t.last) { notifications.push(`${s} hit $${price} (≥ $${t.up})`); t.last = price; violated = true; } if (t.down && price <= t.down && price !== t.last) { notifications.push(`${s} dropped to $${price} (≤ $${t.down})`); t.last = price; violated = true; } if (violated) thes[s] = t; } if (notifications.length) { await ctx.notify(notifications.join('\n')); await ctx.memory.set('thresholds', thes); } return { ok: true, messages: notifications.length }; }

Notes:

  • ctx.notify fans out to every configured channel unless you pick one explicitly (ctx.notify({text, channel: 'slack'})).
  • I store thresholds as a JSON blob; feel free to normalise in SQL if you enjoy schemas.
  • We ignore minor oscillations by writing last.

Thresholds are set with a plain language prompt — that’s the whole point of an agent. My command last week:

"Start watching AAPL at $170-up and $155-down, plus MSFT at 450-up."

The LLM parsed it, called watchStocks with the symbols, and wrote the numbers into ctx.memory using a function_call. You can also seed memory manually:

// one-off init in the playground await ctx.memory.set('thresholds', { AAPL: { up: 170, down: 155 } });

Persistent portfolio tracking for context

Only price hits are boring. The agent should understand why a stock matters to you. I keep a simple portfolio object:

type Lot = { shares: number; cost: number }; // memory key: 'portfolio' { 'AAPL': { shares: 50, cost: 159.20 }, 'MSFT': { shares: 10, cost: 436.00 } }

Two benefits:

  • The LLM can answer “What is my unrealised P/L today?” without calling an external brokerage.
  • The morning briefing can add colour: biggest winner, loser, total delta.

Add a helper:

async function loadPortfolio(ctx: SkillContext) { return (await ctx.memory.get>('portfolio')) || {}; }

For updating lots I use another command in manifest (updatePortfolio) but that’s outside the monitoring scope.

Scheduling morning market briefings

We already put schedule.morningBriefing in the manifest. When the daemon loads the skill it registers a cron job. Implementation:

async function compileBriefing(ctx: SkillContext, symbols: string[]) { if (symbols.length === 0) return ctx.notify('Morning briefing: no portfolio configured.'); const lines: string[] = []; const pnl: Record = {}; let total = 0; const portfolio = await loadPortfolio(ctx); for (const s of symbols) { const lot = portfolio[s]; const price = await getPrice(s); if (!lot) continue; const delta = (price - lot.cost) * lot.shares; pnl[s] = delta; total += delta; lines.push(`${s}: $${price.toFixed(2)} (P/L $${delta.toFixed(2)})`); } const sorted = Object.entries(pnl).sort((a, b) => b[1] - a[1]); const best = sorted[0]; const worst = sorted[sorted.length - 1]; lines.push(''); lines.push(`Total unrealised P/L: $${total.toFixed(2)}`); if (best) lines.push(`Top mover: ${best[0]} $${best[1].toFixed(2)}`); if (worst) lines.push(`Laggard: ${worst[0]} $${worst[1].toFixed(2)}`); await ctx.notify(lines.join('\n')); return { ok: true }; }

The notification shows up in Slack like:

AAPL: $173.12 (P/L $196.00)
MSFT: $445.32 (P/L $93.20)

Total unrealised P/L: $289.20
Top mover: AAPL $196.00
Laggard: MSFT $93.20

That’s enough signal for my commute.

Putting it together: the community “trading-research” skill pattern

If you browsed the community showcase you saw the trading-research pattern: isolate IO into helpers, let the LLM handle reasoning. Our stock-sentinel follows the same ideas:

  1. Pure functions for data (getPrice, quoteAlpha) — deterministic, testable.
  2. Agent memory for state (thresholds, portfolio).
  3. Scheduled tasks for routine stuff (briefings).
  4. Chat commands for ad-hoc queries (“/claw What’s my P/L on MSFT?”).

The LLM is great at glue: it converts natural language into structured params, calls our code, and formats the answer back to humans. Everything numerically critical (prices, math) stays outside the model. This avoids hallucinated quotes, a complaint we saw in #finance-nerds on Discord last month.

Deploying the skill on ClawCloud in 60 seconds

If you developed locally under ~/.claw/agents/stonks-bot and your git remote is ready, push and flip the switch:

$ git push origin main $ claw login # if not already, opens browser $ claw deploy stonks-bot --branch main --env ALPHA_VANTAGE_KEY=... --env RAPIDAPI_KEY=...

The CLI zips the repo, uploads it, and restarts the gateway in the cloud. Webhooks & connectors carry over from the dashboard. First morning briefing should appear tomorrow at 07:30 in the selected channel. Want to test now? Trigger manually:

$ claw run stonks-bot.morningBriefing

Live logs stream to the terminal, handy when Yahoo decides it’s weekend maintenance time.

Security and rate-limit caveats

Two gotchas before you trust this with serious money:

  • Do not commit API keys. Use claw secrets set or an .env ignored by Git.
  • Alpha Vantage free tier can silently throttle. If you care, instrument X-Call-Limit and back off.

For bigger portfolios switch to IEX Cloud or Polygon.io; the refactor is just swapping quoteAlpha.

Next step: share your tweaks

This tutorial covers the 80 %. If you automate option greeks, or hook into Interactive Brokers for real orders, open a PR to openclaw/skills-community. The finance crowd is tiny but opinionated — contributions get reviewed fast.

Until then, enjoy the peace of hearing about price swings before Twitter does.