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.

Check

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

bash
API=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.

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
Image 1 of 1 from the last green run of the recipe A prediction market's week as an image

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 .

Ask a question... ⌘I