The API
Tenali runs on your machine first. This API is the same cards, queued on tenali.dev instead, for when your agent runs somewhere your laptop isn't, or the reviewer isn't you. Pass, fail, or defer against a versioned rubric, and nothing else.
Tenali runs on your machine first; that's the product. This API is the same cards, queued on tenali.dev instead, for when your agent runs somewhere your laptop isn't, or the reviewer isn't you. Pass, fail, or defer against a versioned rubric, and nothing else.
Tenali runs on your machine first, and the local tool is the whole product. This API exists for the day your agent runs on a server and the reviewer is someone other than you. Same cards, same three keys, same refusal to accept anything vaguer than pass, fail, or defer.
# start here, in any project tenali setup # the store, the MCP server, and the coach skill for your agent tenali ui # 127.0.0.1:7847, keyboard only tenali stats # the hallmark for each criterion
"audience": {"mode": "me"} and it goes to nobody but you, signed in. Team and guest audiences aren't built yet (what's built, and what isn't).
1. Authenticate
Every call sends your API key. Name your client too: it shows up in our analytics as how you use the service, and helps when something breaks.
Authorization: Bearer gpk_… Tenali-Client: my-agent/0.4.1
Your wallet starts with a proof-of-concept grant of 250,000 GOLD. GET /api/v1/wallet shows the balance and what's in escrow.
2. Post a batch
The reward is per seat. Posting moves gold × seats into the batch's escrow wallet. Send the same Idempotency-Key again and you get the same batch back, never a duplicate.
curl -X POST https://tenali.dev/api/v1/batches \
-H "Authorization: Bearer $TENALI_KEY" -H "Tenali-Client: my-agent/0.4.1" \
-H "Idempotency-Key: run-2291-batch-1" -H "Content-Type: application/json" -d '{
"capability": "transcript",
"title": "Support calls, week 37",
"gold": 400, "median_seconds": 360, "seats": 2, "closes_in_hours": 48,
"external_ref": "run-2291", "metadata": {"model": "asr-v7"},
"tasks": [{
"case_key": "call-0192", "instructions": "Does the transcript say what the caller says?",
"model_verdict": "pass", "confidence": 0.71, "stratum": "gray",
"metadata": {"row": 192},
"artifacts": [
{"kind": "audio", "label": "Call audio", "data_base64": "SUQzBAAAAA…"},
{"kind": "text", "label": "Transcript", "text": "I'd like to change my billing address."}
]
}]
}'
201 Created
{"batch_id": 41, "replayed": false, "status": "open",
"escrow": {"address": "gp1…", "balance": 800, "committed": 0},
"wallet_balance": 249200, "url": "/api/v1/batches/41"}
3. Get notified
Pick either. A webhook gets a signed POST for every event, retried with backoff for about seven hours. The events feed works from anywhere, including a laptop behind a firewall: long-poll it with wait.
curl -X PUT https://tenali.dev/api/v1/webhook -H "Authorization: Bearer $TENALI_KEY" \
-H "Content-Type: application/json" -d '{"url": "https://example.com/goldpan"}'
# → {"webhook": {"url": "…", "secret": "whsec_…"}} the secret is shown once
curl "https://tenali.dev/api/v1/events?after=0&wait=25" -H "Authorization: Bearer $TENALI_KEY"
# → {"events": [{"id": 88, "type": "batch.completed", "batch_id": 41, "data": {…}}], "next_after": 88}
Check the signature before trusting a webhook:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",")) # Tenali-Signature: t=…,v1=…
if abs(time.time() - int(parts["t"])) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, parts["v1"])
| Event | When |
|---|---|
batch.seat_accepted | A reviewer took a seat. |
batch.seat_released | A reviewer let a seat go, or their 24-hour hold ran out. The seat is back on the board. |
batch.seat_finished | A seat decided every card. |
batch.completed | Every seat finished. Carries the full results and any refund. |
batch.cancelled | You cancelled it. Unused escrow is refunded; seats already working can finish. |
batch.expired | It closed before every seat finished. Unused escrow is refunded. |
4. Read the results
GET /api/v1/batches/41 returns every seat's verdicts with rationale and timing, the consensus per card (pass, fail, split, or null), and each reviewer as a wallet address with a reputation tier. Verdicts from an account removed for abuse come back with "excluded": true and don't count toward consensus.
{"batch_id": 41, "status": "complete", "done": true, "external_ref": "run-2291",
"seats": {"total": 2, "finished": 2, "live": 0},
"cards": [{"position": 1, "case_key": "call-0192", "metadata": {"row": 192},
"consensus": "fail", "agreement": true, "overturned_model": true,
"verdicts": [{"seat": 1, "verdict": "fail", "rationale": "Says shipping, not billing",
"duration_ms": 21400, "reviewer": "gp1…", "excluded": false}, …]}]}
Endpoints
| Call | What it does |
|---|---|
GET /api/v1/capabilities | Kinds of work, rubric versions, qualified reviewers, the token, limits, event types. |
PUT /api/v1/criteria/{key} | Define your own question, or version it. Private to your workspace. |
GET /api/v1/criteria · /{key} | Your criteria: every rubric version, and what's been asked against each. |
POST /api/v1/batches | Post and escrow a batch. Honors Idempotency-Key. |
GET /api/v1/batches | Your batches; filter with status, external_ref; page with after. |
GET /api/v1/batches/{id} | Status, seats, escrow, and full results. |
POST /api/v1/batches/{id}/cancel | Stop new seats and refund unused escrow. |
GET /api/v1/events | The events feed: after, limit, wait (up to 25 s). |
GET · PUT · DELETE /api/v1/webhook | Read, set (rotates the secret), or remove your webhook. |
GET /api/v1/wallet | Your address, balance, GOLD in escrow, recent transactions. |
Your own questions
A capability is a kind of work reviewers qualified for: few, curated by us, public. A criterion is your own question: unlimited, private to your workspace, and never on the board. Writing down what right means for your app is the whole point, so a criterion is where you do it.
curl -X PUT https://tenali.dev/api/v1/criteria/receipt-total \
-H "Authorization: Bearer $TENALI_KEY" -H "Content-Type: application/json" -d '{
"title": "Receipt total matches",
"question": "The extracted total equals the final total on the receipt, including tax."
}'
# → {"key": "receipt-total", "rubric_version": 1, "spec_hash": "80674949…", "reviewable": true}
curl -X POST https://tenali.dev/api/v1/batches -H "Authorization: Bearer $TENALI_KEY" \
-H "Content-Type: application/json" -d '{
"criterion": "receipt-total", "title": "Receipts, week 37",
"audience": {"mode": "me"}, "tasks": [ … ]
}'
The same question again is the same version; a changed one is the next, and no version is ever edited, so a batch you already posted keeps the question it actually asked. A batch names a criterion that must already exist and carries no rubric of its own — a typo would otherwise quietly split the answers you're accumulating into two half-built sets.
A criterion batch is always "me": your own question is yours to answer. Reaching reviewers means a capability they qualified for.
Audiences
A batch takes an audience saying who can see and take it. Leave it out and nothing changes: the batch is open work, exactly as before audiences existed.
{"capability": "transcript", "title": "Receipts, week 37",
"audience": {"mode": "me"},
"tasks": [ … ]}
"me" is a batch only you review, signed in. It never appears on the board, in the demand meters, or to another reviewer, and it's kept out of everyone's reputation and accuracy — including yours, since scoring you against your own answers would mean nothing. Nobody is paid for it, so gold, median_seconds and a second seat are refused rather than ignored, and no GOLD moves. Your key needs an owner: the account that reviews its private batches, which we set when we mint the key.
Not built yet, and refused with AUDIENCE_NOT_BUILT so you can tell: "team" for colleagues in your workspace, "invited" for one guest with a scoped link, and "targeted", which waits on an honest answer about pay.
Limits
- Rewards are whole GOLD and at least 1,800 GOLD an hour at
median_seconds. - 1 to 16 cards a batch, 1 to 5 seats, 1 to 4 artifacts a card, each up to 5 MB: WAV or MP3 audio, UTF-8 text, or JSON.
- Inline text fields and
metadataare 4 KB at most; pack anything bigger as an artifact with only the range a reviewer needs. - Content labels: Mild language, Mentions alcohol, Medical terms, Money and prices. Graphic, sexual, violent, and self-harm content can't be posted.
- Errors are
{"error": "CODE", "message": "…"}. Every response carries aTenali-Request-Id.
Capabilities open now
| Key | What reviewers judge | Qualified |
|---|---|---|
transcriptRubric v1 | Transcript matches the audioReal audiobook clips beside a speech-recognition transcript | 0 |
same_readerRubric v1 | Same reader?Two short clips from real audiobooks: is one person reading both | 0 |
entity_linkRubric v1 | Right entity?A real Wikipedia sentence and a proposed link for a name in it | 0 |
GOLD is a token with no cash value. Rewards, escrow, and refunds move GOLD, never money. See docs/API.md in the repository for every field.