A prediction market's week as an image
Two Polymarket actions read an open bitcoin market and its week of prices, the model writes one sentence, and render draws the card from HTML.
Actions read the data, the model writes the sentence, render draws the picture. The recipe searches Polymarket for
bitcoin markets with io.b3os/polymarket/search, keeps the open Yes/No market with the most 24-hour volume that ends
more than a week from now and is not decided, and reads that market's Yes price over the last week, every six hours,
with io.b3os/polymarket/get-price-history. Neither action needs a key.
The model gets the numbers already worked out (the price now and a week ago, the change in points, the volume) and
writes one sentence with no advice and no forecast. render takes the card as html, draws it with no network on a
background preset and returns the image. Nothing runs between the calls but your code.
Verified 2026-11-01: this recipe's last run passed against https://beta-api.b3os.org, label live-recorded (recorded from production), from commit 771a9508. The recipe harness runs it again, and a failed run takes this page out of the cookbook.
Run it
bashAPI=https://beta-api.b3os.orgAUTH="Authorization: Bearer $B3OS_API_KEY"act() { curl -s "$API/v1/actions/call" -H "$AUTH" -H "Content-Type: application/json" \ -d "$(jq -n --arg ref "$1" --argjson input "$2" '{ref: $ref, input: $input}')"; }act io.b3os/polymarket/search@1 '{"query": "bitcoin", "limit": 20}' > found.jsonjq '[.output.events[].markets[] | select(.closed == false and .outcomes == ["Yes","No"] and .outcome_prices[0] >= 0.05 and .outcome_prices[0] <= 0.95)] | sort_by(-(.volume_24h // 0)) | .[0]' found.json > market.jsonjq -r .question market.jsonact io.b3os/polymarket/get-price-history@1 \ "$(jq '{token_id: .token_ids[0], interval: "1w", fidelity_minutes: 360}' market.json)" > history.jsonjq '.output.history | {points: length, first: .[0].p, last: .[-1].p}' history.jsonjq -n --slurpfile m market.json --slurpfile h history.json '{ model: "b3os/qwen3.5-9b", temperature: 0, max_tokens: 80, chat_template_kwargs: {enable_thinking: false}, messages: [ {role: "system", content: "Write one sentence of at most 28 words describing how a prediction market'"'"'s Yes price moved this week. Use only the facts given. No advice, no forecast, no opinion."}, {role: "user", content: ({question: $m[0].question, yes_price_now: $h[0].output.history[-1].p, yes_price_a_week_ago: $h[0].output.history[0].p} | tojson)}]}' \| curl -s "$API/v1/infer/chat/completions" -H "$AUTH" -H "Content-Type: application/json" -d @- \| jq -r '.choices[0].message.content' > brief.txtjq -n --rawfile b brief.txt --slurpfile m market.json '{ html: ("<body style=\"margin:0;background:#0d1117;color:#e6edf3;font:24px system-ui;padding:48px\"><h1>" + $m[0].question + "</h1><p>" + $b + "</p></body>"), viewport: {width: 1120, height: 480}, full_page: true, style: {background: "midnight", frame: "none"}, format: "webp"}' \| curl -s "$API/v1/render/screenshots" -H "$AUTH" -H "Content-Type: application/json" -d @- \| jq -r '.artifacts[0].url' | xargs curl -s -o card.webp
The recipe's run.mjs draws the full card: the price, the change and the week as an SVG line.
tsconst API = "https://beta-api.b3os.org";const headers = { authorization: `Bearer ${process.env.B3OS_API_KEY}`, "content-type": "application/json" };const post = async (path: string, body: unknown): Promise<any> => (await fetch(`${API}${path}`, { method: "POST", headers, body: JSON.stringify(body) })).json();const act = (ref: string, input: unknown) => post("/v1/actions/call", { ref, input });const found = await act("io.b3os/polymarket/search@1", { query: "bitcoin", limit: 20 });const market = found.output.events .flatMap((e: any) => e.markets) .filter((m: any) => !m.closed && m.outcomes.join() === "Yes,No" && m.outcome_prices[0] >= 0.05 && m.outcome_prices[0] <= 0.95) .sort((a: any, b: any) => (b.volume_24h ?? 0) - (a.volume_24h ?? 0))[0];const { output } = await act("io.b3os/polymarket/get-price-history@1", { token_id: market.token_ids[0], interval: "1w", fidelity_minutes: 360,});const now = output.history.at(-1).p;const before = output.history[0].p;const chat = await post("/v1/infer/chat/completions", { model: "b3os/qwen3.5-9b", temperature: 0, max_tokens: 80, chat_template_kwargs: { enable_thinking: false }, messages: [ { role: "system", content: "Write one sentence of at most 28 words describing how a prediction market's Yes price moved this week. Use only the facts given. No advice, no forecast, no opinion." }, { role: "user", content: JSON.stringify({ question: market.question, yes_price_now: now, yes_price_a_week_ago: before }) }, ],});const brief = chat.choices[0].message.content;const shot = await post("/v1/render/screenshots", { html: `<body style="margin:0;background:#0d1117;color:#e6edf3;font:24px system-ui;padding:48px"><h1>${market.question}</h1><p>${brief}</p></body>`, viewport: { width: 1120, height: 480 }, full_page: true, style: { background: "midnight", frame: "none" }, format: "webp",});console.log(shot.artifacts[0].url);
pythonimport jsonimport osimport requestsAPI = "https://beta-api.b3os.org"s = requests.Session()s.headers["Authorization"] = f"Bearer {os.environ['B3OS_API_KEY']}"act = lambda ref, inp: s.post(f"{API}/v1/actions/call", json={"ref": ref, "input": inp}).json()found = act("io.b3os/polymarket/search@1", {"query": "bitcoin", "limit": 20})markets = [m for e in found["output"]["events"] for m in e["markets"] if not m["closed"] and m["outcomes"] == ["Yes", "No"] and 0.05 <= m["outcome_prices"][0] <= 0.95]market = max(markets, key=lambda m: m.get("volume_24h") or 0)history = act("io.b3os/polymarket/get-price-history@1", {"token_id": market["token_ids"][0], "interval": "1w", "fidelity_minutes": 360})["output"]["history"]chat = s.post(f"{API}/v1/infer/chat/completions", json={ "model": "b3os/qwen3.5-9b", "temperature": 0, "max_tokens": 80, "chat_template_kwargs": {"enable_thinking": False}, "messages": [ {"role": "system", "content": "Write one sentence of at most 28 words describing how a prediction market's " "Yes price moved this week. Use only the facts given. No advice, no forecast, no opinion."}, {"role": "user", "content": json.dumps({"question": market["question"], "yes_price_now": history[-1]["p"], "yes_price_a_week_ago": history[0]["p"]})}, ],}).json()brief = chat["choices"][0]["message"]["content"]shot = s.post(f"{API}/v1/render/screenshots", json={ "html": f'<body style="margin:0;background:#0d1117;color:#e6edf3;font:24px system-ui;padding:48px">' f'<h1>{market["question"]}</h1><p>{brief}</p></body>', "viewport": {"width": 1120, "height": 480}, "full_page": True, "style": {"background": "midnight", "frame": "none"}, "format": "webp",}).json()print(shot["artifacts"][0]["url"])
On the remote B3OS MCP server, ask your agent:
textSearch Polymarket for bitcoin markets with the polymarket search action. Take the open Yes/No market with the most24-hour volume whose Yes price is between 5 and 95 cents, and get its Yes price history for the last week every 6hours. Write one sentence on how the price moved, with no advice and no forecast. Then render a 1120 px wide card fromHTML with the question and that sentence on the midnight background, and give me the image URL.
The agent calls actions_call twice, writes the sentence itself and calls render_screenshot with html.
The market changes every run: the card below is the last green run's, drawn in production. A question's text is
Polymarket's; escape it before it goes into HTML, as run.mjs does.
Owner: Docs. Products: Docs, Actions and triggers, Infer, Render.
Output of the last green run
bash$ node run.mjs# exit 0
json{ "question": "Will Bitcoin reach $110,000 by December 31, 2026?", "market_url": "https://polymarket.com/event/what-price-will-bitcoin-hit-before-2027", "end_date": "2027-01-01T05:00:00Z", "volume_24h_usd": 33052, "markets_seen": 227, "points": 23, "yes_now_cents": 6.5, "yes_week_ago_cents": 7.5, "change_points": -1, "model": "b3os/qwen3.5-9b", "brief": "The prediction market's Yes price for Bitcoin reaching $110,000 by December 31, 2026, moved down 1 point to 6.5 cents this week.", "render_status": "succeeded", "card": "https://beta.b3os.org/gallery/recipes/market-brief-card/b8cc354061d2d5f0.webp", "card_bytes": 56898, "search_ms": 868.2, "history_ms": 454, "brief_ms": 277.6, "render_ms": 105.9, "total_ms": 2051.4}
The outputs this recipe publishes (its recipe.json publish.outputs), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.
Captured 2026-11-01 from commit
771a9508
, label
live-recorded
(recorded from production). Case
recipes/market-brief-card
.