tally-sh
v0.1.0
Published
Counters with history: curl a +1 and a live sparkline page exists one second later.
Maintainers
Readme
curl -d "+1" https://tally.legible.sh/my-agents-x7k2q/tasks-completed
# {"metric":"tasks-completed","value":1}Now open https://tally.legible.sh/my-agents-x7k2q/tasks-completed in a browser. That's a live page — big number, smooth sparkline, updating over SSE as the curls land. https://tally.legible.sh/my-agents-x7k2q is the dashboard of everything in the topic. No signup, no API key, no dashboard configuration — the URL is the chart.
The hosted API at tally.legible.sh is in soft launch. To run the identical API yourself,
npx tally-sh servestarts it onhttp://localhost:4189; every example below works with the hostname swapped.
Count what your agents actually do
Your hooks already curl ntfy for pings — now curl tally for numbers. Paste this into .claude/settings.json and every Claude Code session starts feeding a dashboard:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command",
"command": "curl -sd +1 https://tally.legible.sh/acme-agents-x7k2q/tasks-completed >/dev/null" }] }
],
"PostToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command",
"command": "jq -e '.tool_response | tostring | test(\"(?i)error\")' >/dev/null && curl -sd +1 https://tally.legible.sh/acme-agents-x7k2q/tool-errors >/dev/null; exit 0" }] }
]
}
}Add one line to the agent's own instructions (see Teach your agent) and it counts tests-fixed itself whenever it turns a red test green. Open https://tally.legible.sh/acme-agents-x7k2q on a second monitor: tasks completed, tool errors, tests fixed — every card a live sparkline. That's the fleet, at a glance, for zero infrastructure.
The pain this solves: agents produce numbers constantly — tasks done, tokens burned, retries, queue depths — and every existing home for those numbers wants a signup, an SDK, or a Grafana. The countapi clones will store a number but show you no history. tally keeps a time series behind every counter and puts a genuinely pretty chart at its URL.
The whole API
| Call | Does |
|---|---|
| POST /{topic}/{metric} | count. Body +1 (default when empty), +5, -2, bare 7, or =42 (gauge set) → {"metric","value"} |
| GET /{topic}/{metric} | curl: {metric, value, updated, series} · browser: live sparkline page · Accept: text/plain: bare value |
| GET /{topic}/{metric}?wait=30 | long-poll: responds on the next change, or after the timeout (max 60s) |
| GET /{topic} | curl: {topic, metrics:[{name, value, updated}]} · browser: live dashboard |
| GET /{topic}/sse | SSE: named update / delete events with JSON data, heartbeat comments every 25s |
| DELETE /{topic}/{metric} | remove a metric (token-guarded if the server runs --token) |
| GET /README.md · /llms.txt | these docs, as text, for agents. Never token-gated; GET / with Accept: text/markdown serves the README too |
series holds three dense windows of {t, sum, count, last} buckets (t = bucket start, epoch ms): minute (last 60), hour (last 48), day (last 90). sum adds up the payloads written in the bucket (a counter's rate; divide by count for a gauge's average), last is the running value the sparkline plots. Retention: minutes 24 h, hours 14 d, days 400 d. Topics match [a-zA-Z0-9_-]{1,64} and are created by first write; metrics match [a-z0-9_-]{1,64} (sse is reserved). Errors are {"error","code","hint"} with honest status codes — the hint names the correct next call (omitted where the message already is one). Every limit is an exported constant in src/limits.mjs.
curl -d "=42" https://tally.legible.sh/my-agents-x7k2q/queue-depth # gauges: set, don't add
curl https://tally.legible.sh/my-agents-x7k2q # list the topic
curl -H "Accept: text/plain" https://tally.legible.sh/my-agents-x7k2q/queue-depth # → 42
curl "https://tally.legible.sh/my-agents-x7k2q/queue-depth?wait=30" # block until it changes
curl https://tally.legible.sh/my-agents-x7k2q/sse # live event stream
curl -X DELETE https://tally.legible.sh/my-agents-x7k2q/queue-depthTeach your agent
Paste this into your CLAUDE.md / AGENTS.md and the agent knows the whole tool:
## tally — counters with history (https://tally.legible.sh)
- Count something: curl -d "+1" https://tally.legible.sh/<topic>/<metric> (also "+5", "-2", bare "7")
- Set a gauge: curl -d "=42" https://tally.legible.sh/<topic>/<metric>
- Read (JSON): curl https://tally.legible.sh/<topic>/<metric> → value + minute/hour/day series
- List a topic: curl https://tally.legible.sh/<topic>
- Wait for a change: curl "https://tally.legible.sh/<topic>/<metric>?wait=30"
- Live events: curl https://tally.legible.sh/<topic>/sse (SSE: update/delete)
- Delete: curl -X DELETE https://tally.legible.sh/<topic>/<metric>
- Topics are created by the first write. Pick an unguessable topic name (add ~5 random chars).
- Metric names: [a-z0-9_-]. Every metric URL is a live chart in a browser; /<topic> is a dashboard.
- When you fix a failing test, run: curl -sd +1 https://tally.legible.sh/<topic>/tests-fixedSelf-hosting
npx tally-sh serve # http://0.0.0.0:4189, in-memory
tally serve --port 4189 --host 127.0.0.1
tally serve --data-dir ./data # append-only JSONL, replayed on boot
tally serve --token s3cret # POST/DELETE require Authorization: Bearer s3cret
tally serve --base-url https://tally.example.com # absolute URLs in pagesOr clone and run: git clone … && cd tally && npm start. Zero dependencies — npm install has nothing to install. State is in-memory by default (restart = clean slate); --data-dir makes writes durable. With --token, reads stay public — metric pages and JSON never require the token, only writes and deletes do. If your numbers are sensitive, that flag is not enough; put the whole server behind something, or wait for private topics (below).
CLI
The CLI is sugar over the same HTTP API — curl remains the contract.
tally bump ci-x7k2q deploys # +1
tally bump ci-x7k2q deploys +5
tally bump ci-x7k2q queue-depth '=42' # quoted: zsh expands bare =42
tally get ci-x7k2q deploys # prints the value; --json for the series
tally ls ci-x7k2q # name, value, updated
tally watch ci-x7k2q # live tail over SSE
tally rm ci-x7k2q deploys
tally serveServer resolution: --url flag, else $TALLY_URL, else https://tally.legible.sh. Writes against a --token server: --token flag or $TALLY_TOKEN.
Pro
Planned for the hosted instance, never gating the core verbs (self-host stays complete):
- Longer retention — minutes beyond 24 h, days beyond 400.
- Private topics — read tokens; today reads are public-by-obscurity.
- Threshold alerts —
tool-errors > 10/hour→ webhook or ntfy topic. - CSV / JSON export of full retained history.
- Teams — reserved topic prefixes, shared dashboards.
Straight talk
- The topic name is the password. Anyone who guesses it can read and write your numbers.
my-agentswill be squatted;my-agents-x7k2qwon't. This is capability-by-obscurity, same trade as ntfy — real auth is--token(writes) today and private topics (reads) later. - Rollups, not raw events. Each bucket keeps
sum,count,last— that's it. No percentiles, no histograms, no labels/dimensions. If you needp99{region="eu"}, you need Prometheus, and that's fine. - Arrival time is the timestamp. Writes land in the bucket for now; there is no backfill API. A counter bumped offline stays unbumped.
- The JSONL grows forever.
--data-dirappends one line per write and never compacts. At hobby scale that's megabytes per year; still, check on it. - One process, one box. State lives in memory (Maps), fsync'd to JSONL if asked. No clustering, no HA. A restart without
--data-dirforgets everything — by design, and documented as such.
The family
tally is one of the legible primitives — same stack, same conventions, one curl each:
| | | |---|---| | gate (4180) | ntfy tells you things. gate asks you things. | | bigred (4181) | The big red button for your agent fleet. | | trail (4182) | The flight recorder for agent runs. | | slate (4183) | The blackboard from the multi-agent papers, as a URL. | | relay (4184) | The work queue your agents can provision themselves. | | mutex (4185) | flock(1) for agents that live on different machines. | | quorum (4186) | Coordination for agents that do not share a parent process. | | meter (4187) | The kill-brake for agent spend. 429-as-a-service. | | stash (4188) | ntfy moves signals. stash moves bytes. | | tally (4189) | StatHat reborn as ntfy. Three months too late — or right on time. |
License
MIT.
