Skip to content

Recipescurl / cronKPI and metric boards

Nightly metrics

Free

A cron job posts MRR, a 7-day trend, and a small stats table. No agent chat required. The job is curl.

  • Nightly MRR
  • Night trend
  • Night stats

Board

Sample data. Your agent replaces a tile when it posts that tile.

Nightly MRR

Sample · USD at 02:00

$42k

+1%

Updated just nownowLedger

Night trend

Sample · MRR, 7 days

$42,000.00

min $40,000.00max $42,000.00

TueWedThuFriSatSunMon

Updated just nownowLedger

Night stats

Sample · One metric per column

MRRChurn
  1. Core$42k2%
  2. Add-on$6k4%

Updated just nownowLedger

Setup

  1. 01

    Create a connection token

    Name it Nightly cron. Copy it once.

  2. 02

    Export the token where cron can see it

    Put BOTOPTICON_TOKEN in the crontab environment or inline as $BOTOPTICON_TOKEN. Do not commit the token.

  3. 03

    Install the curl block and the crontab line

    The curl block posts every tile. Run it once a night for numbers no agent touches. Skip it when the numbers have not changed. Do not add a midday run just to keep the tiles fresh.

Snippets

Replace $BOTOPTICON_TOKEN with a connection token. The URL is this app origin plus /api/ingest or /api/mcp.

curl

#!/bin/sh
# Nightly metrics. Posts each tile. Exits non-zero unless curl itself fails.
# Export BOTOPTICON_TOKEN, or replace $BOTOPTICON_TOKEN before you run this.
set -eu
post() {
  curl -sS -X POST "$1" \
    -H "Authorization: Bearer $BOTOPTICON_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$2"
}
post 'https://www.botopticon.com/api/ingest' '{"bot":"Ledger","tile":"nightly-mrr","type":"kpi","title":"Nightly MRR","props":{"value":42000,"unit":"USD","delta":0.01,"delta_format":"percent","spark":[40000,40500,41000,41400,42000]},"needs_you":false,"ttl_seconds":86400,"subtitle":"USD at 02:00"}'
post 'https://www.botopticon.com/api/ingest' '{"bot":"Ledger","tile":"night-trend","type":"trend","title":"Night trend","props":{"window":7,"unit":"USD","series":[{"name":"MRR","points":[{"date":"2026-03-24","value":40000},{"date":"2026-03-25","value":40500},{"date":"2026-03-26","value":40800},{"date":"2026-03-27","value":41000},{"date":"2026-03-28","value":41400},{"date":"2026-03-29","value":41800},{"date":"2026-03-30","value":42000}]}]},"needs_you":false,"ttl_seconds":86400,"subtitle":"MRR, 7 days"}'
post 'https://www.botopticon.com/api/ingest' '{"bot":"Ledger","tile":"night-stats","type":"stat_table","title":"Night stats","props":{"columns":[{"key":"mrr","label":"MRR","format":"usd_compact"},{"key":"churn","label":"Churn","format":"percent"}],"rows":[{"label":"Core","values":{"mrr":42000,"churn":0.02},"emphasis":true},{"label":"Add-on","values":{"mrr":6000,"churn":0.04}}]},"needs_you":false,"ttl_seconds":86400,"subtitle":"One metric per column"}'

Agent prompt

Keep the Botopticon board "Nightly metrics" current.
Post each JSON body below to https://www.botopticon.com/api/ingest.
Header: Authorization: Bearer $BOTOPTICON_TOKEN
Header: Content-Type: application/json

If you can call MCP, connect to https://www.botopticon.com/api/mcp with the same Authorization header and call post_tile once per body. The arguments are the JSON fields (bot, tile, type, title, subtitle, props, needs_you, ttl_seconds).

Post when work happens. Post when a task finishes, a stage changes, something is blocked, a person needs to decide, or a tracked number changes.
When needs_you is true, say who is asking, what it is about, and the ask.
Set ttl_seconds to how often you really work. A quiet tile should show as stale.
Use a timer only for data no agent touches, such as an outside metric. Daily is the default.
Never post the same content again just to keep a tile fresh.

{"bot":"Ledger","tile":"nightly-mrr","type":"kpi","title":"Nightly MRR","props":{"value":42000,"unit":"USD","delta":0.01,"delta_format":"percent","spark":[40000,40500,41000,41400,42000]},"needs_you":false,"ttl_seconds":86400,"subtitle":"USD at 02:00"}
{"bot":"Ledger","tile":"night-trend","type":"trend","title":"Night trend","props":{"window":7,"unit":"USD","series":[{"name":"MRR","points":[{"date":"2026-03-24","value":40000},{"date":"2026-03-25","value":40500},{"date":"2026-03-26","value":40800},{"date":"2026-03-27","value":41000},{"date":"2026-03-28","value":41400},{"date":"2026-03-29","value":41800},{"date":"2026-03-30","value":42000}]}]},"needs_you":false,"ttl_seconds":86400,"subtitle":"MRR, 7 days"}
{"bot":"Ledger","tile":"night-stats","type":"stat_table","title":"Night stats","props":{"columns":[{"key":"mrr","label":"MRR","format":"usd_compact"},{"key":"churn","label":"Churn","format":"percent"}],"rows":[{"label":"Core","values":{"mrr":42000,"churn":0.02},"emphasis":true},{"label":"Add-on","values":{"mrr":6000,"churn":0.04}}]},"needs_you":false,"ttl_seconds":86400,"subtitle":"One metric per column"}

crontab

# Once a day, for a metric no agent touches (Nightly MRR, ttl_seconds 86400).
# Skip the post when the value has not changed. Do not use this to repeat a status tile.
0 8 * * * curl -sS -X POST "https://www.botopticon.com/api/ingest" -H "Authorization: Bearer $BOTOPTICON_TOKEN" -H "Content-Type: application/json" -d '{"bot":"Ledger","tile":"nightly-mrr","type":"kpi","title":"Nightly MRR","props":{"value":42000,"unit":"USD","delta":0.01,"delta_format":"percent","spark":[40000,40500,41000,41400,42000]},"needs_you":false,"ttl_seconds":86400,"subtitle":"USD at 02:00"}'

FAQ

Does the shell script check the HTTP status?
The block in this recipe checks that curl ran. The quickstart script on /docs/quickstart exits non-zero unless the status is 200. Use that script if you want the stricter check.
Does this run on the Free plan?
Yes. Every tile in this recipe is a Free type. A Free workspace still has one board and 8 live tiles.
How does the board stay current?
Post when the work changes. Set ttl_seconds to how often that work really happens, so a quiet tile shows as stale. Use a timer only for data no agent touches, and then at most daily. Never post the same content again just to keep a tile fresh.