← All in Favor
API
Tokens

Drive All in Favor from your own code

Both metered lanes are available over HTTP. hearing takes one local proposal - what, where, when, the ask, the cost and who pays, who runs it, who it helps, the worries and the concessions - and the public comment as written, and returns a mock hearing in front of seven fictional seats: a remark and a vote per seat, three public speakers, conditions or things to bring back, the hardest question to be ready for, and an outcome - passes, continued or fails. rework takes the same proposal plus that hearing and returns a fix - polish, fill-gaps or pivot - a power/interest grid of everyone still to win over with one move each, the changes to make, the seats that could flip, and the comment rewritten to fit the speaking limit. A natural use is a neighbourhood group's bot that rehearses every agenda item before the real meeting. The council is fictional: a rehearsal, never a forecast.

The division of labour first: the page owns the proposal, the timer and the leans; the model holds the hearing; the page re-checks the reply. The checks, the speaking time at 130 words a minute, and each seat's lean from the wording come from councilkit.js, which runs in node as well as in the browser. Its check function holds each reply to the proposal: seven seats, the outcome matching the vote count, an abstaining seat saying what is missing, every holdout on the rework's grid, the fix following the hearing, the comment fitting the limit, and no number in prose that the proposal does not contain.

Get a token on the token page (step 2 below also shows how to mint a guest token in code).

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"all-in-favor"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field such as task, what or pitch is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field

Every run sends task: hearing or rework. One system prompt routes on it; a missing or unknown task is treated as hearing. The seven seats are fixed: 1 Member Achterberg (Budget), 2 Member Bello (Next door), 3 Member Castellanos (Safety), 4 Member Dunmore (Green), 5 Member Esposito (Main Street), 6 Member Fairweather (Process), 7 Member Grady (Families). Each seat's vote is decided by three ordered rules - the first that applies wins:

OrderVoteWhen
1abstainWhat the seat needs is missing or vague: Budget the cost (0 counts) and, if it costs anything, who pays; Next door where and when; Safety, Green and Main Street where; Process who runs it; Families who it helps.
2nayA worry in the seat's focus is plainly raised and nothing in the comment or the concessions answers it.
3ayeOtherwise.

The outcome follows the votes alone: four or more aye is passes; otherwise four or more nay is fails; otherwise continued. A rework's fix follows the hearing's outcome alone: passes gives polish, continued gives fill-gaps (the comment carries [blanks] for missing facts), fails gives pivot (two to four changes that make the proposal smaller or a one-time trial).

The declared input schema carries scalars only, so array- and object-valued fields travel JSON-encoded as strings (CouncilKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringhearing or rework. Required.
title, whatstringA short name and the proposal in a sentence or two (cut at 1,200 characters). what is required.
where, when, askstringThe place, the dates or hours, and the one thing asked of the council. May be empty - the seats that need them then abstain.
cost, currencystringA number (empty = not given, 0 = free) and one of $ £ € ₹ ¥.
who_pays, who_runsstringWho pays, and who runs and looks after it.
helps, worries, concessionsstring (JSON)Arrays of short strings, up to 8 each.
pitchstringThe public comment as written. Past 600 words the middle is left out with a marker. Required.
minutes, max_wordsstringThe speaking limit (1, 2 or 3) and the words that fit in it at 130 a minute.
page_lean, page_outcomestring (JSON), stringhearing only: the page's vote guess per seat ({"1":"aye",...}) and the outcome it adds up to.
hearingstring (JSON)rework only: {outcome, seats: [{seat, vote, concern}], conditions, hard_question} from the hearing.
peoplestring (JSON)rework only: people you can talk to before the vote, up to 8 (may be []).
retry_notestringOnly on a reformat retry.

The replies

The job's output.output is a string holding one JSON object. For hearing: task, outcome, title, summary, seats ([{seat, vote, remark, concern}], seats 1-7), speakers ([{label, stance, says}], three), conditions and hard_question. For rework: task, fix, title, summary, grid ([{who, seat, power, interest, stance, move}]; power and interest are high or low), changes, pitch and flips (seat numbers). Parse with CouncilKit.parseReply, then normalize(reply, task), then check(reply, proposal, hearing), which returns the page's notes, the tally and the outcome by the votes.

Worked examples

Replies produced against the app's prompt for the six sample runs over three made-up proposals (made-up streets, people and numbers; long fields and lists shortened; the full replies ship in example-result.js). Together they cover every outcome and every fix.

hearing - a community garden with every worry answered: passes

Input:

{
 "task": "hearing",
 "title": "Community garden on the empty lot",
 "what": "Turn the fenced empty lot next to the library into 12 raised garden beds that neighbours sign up for, with a shared tool ... (138 characters)",
 "where": "The empty lot at Larkmere Avenue and Fennick Street, next to the library",
 "when": "Daylight hours, 7am to 8pm, March to November",
 "ask": "Permission to use the lot for a 2-year trial",
 "cost": "1800",
 "currency": "$",
 "who_pays": "A neighbourhood grant we already won, plus $20 a year per bed from gardeners",
 "who_runs": "A garden committee of six neighbours, with me as the contact",
 "helps": "[\"Families in the apartments with no yard\",\"Older neighbours who want to garden at waist height\",\"The library's kids' re ... (135 characters)",
 "worries": "[\"Water use\",\"Rats from compost\",\"Parking on workdays\",\"Looks messy in winter\"]",
 "concessions": "[\"Two rain barrels and a drip line, so no water from the hydrant\",\"Closed compost bins only, turned weekly\",\"Workdays on ... (207 characters)",
 "pitch": "Good evening. I'm here about the empty lot at Larkmere and Fennick, next to the library. It has been fenced and empty as long as most of us can remember. We are ... (852 characters)",
 "minutes": "2",
 "max_words": "260",
 "page_lean": "{\"1\":\"aye\",\"2\":\"aye\",\"3\":\"aye\",\"4\":\"aye\",\"5\":\"aye\",\"6\":\"aye\",\"7\":\"aye\"}",
 "page_outcome": "passes"
}

Reply:

{
 "task": "hearing",
 "outcome": "passes",
 "title": "Community garden trial approved unanimously",
 "summary": "The council votes 7 to 0 to let the community garden go ahead as a 2-year trial. Every worry on the table - water use, rats from compost, parking on workdays and how the lot looks come winter - was already met by a concession in the pitch, so no seat held back support. Budget and process held up too, with the grant, the per-bed fee and the committee of six all spelled out.",
 "seats": [
  {
   "seat": 1,
   "vote": "aye",
   "remark": "The $1800 is already covered by the grant you've won, and the $20 a year per bed should keep paying for upkeep after that. The budget's clear enough for me - aye.",
   "concern": "the per-bed fee keeps covering upkeep"
  },
  {
   "seat": 2,
   "vote": "aye",
   "remark": "The hours and the months you've given me are specific enough to work with, and doing workdays on foot or by bike takes care of the parking worry. Aye from me.",
   "concern": "parking stays off the street on workdays"
  },
  "... (7 in all)"
 ],
 "speakers": [
  {
   "label": "a parent",
   "stance": "for",
   "says": "Families in the apartments with no yard could really use this, and the waist-height option means older neighbours aren't left out either. Having the library's reading program folded in makes it even better."
  },
  "... (3 in all)"
 ],
 "conditions": [
  "Keep the grant and the per-bed fee covering the garden's costs through the trial.",
  "Keep workdays on foot or by bike to avoid parking problems.",
  "Keep compost in closed bins and tidy the beds by the end of November."
 ],
 "hard_question": "What happens to the garden, the shed and the beds if the 2-year trial isn't renewed?"
}

hearing - a monthly street party with no concessions: fails

Input:

{
 "task": "hearing",
 "title": "Monthly street party on Wrenfold",
 "what": "Close Wrenfold Street every month for a big Saturday party with a DJ, food trucks and a bounce house.",
 "where": "Wrenfold Street, the whole stretch between Calloway and Pinmarsh, both lanes closed",
 "when": "The second Saturday of every month, 6pm to 1am",
 "ask": "A standing monthly street-closure permit",
 "cost": "2500",
 "currency": "$",
 "who_pays": "Wristband sales at the barrier",
 "who_runs": "Me and a few friends",
 "helps": "[\"Everyone on the street\",\"Young people with nowhere to go on weekends\"]",
 "worries": "[\"Noise late at night\",\"Parking for the whole street\",\"Trash the next morning\",\"Ambulance access\",\"The two shops on the  ... (128 characters)",
 "concessions": "[]",
 "pitch": "Hi everyone. I think this neighbourhood needs more fun. Everyone loves music, and a party brings people together like nothing else. Young people have nothing to ... (278 characters)",
 "minutes": "2",
 "max_words": "260",
 "page_lean": "{\"1\":\"aye\",\"2\":\"nay\",\"3\":\"nay\",\"4\":\"nay\",\"5\":\"nay\",\"6\":\"aye\",\"7\":\"aye\"}",
 "page_outcome": "fails"
}

Reply:

{
 "task": "hearing",
 "outcome": "fails",
 "title": "Standing closure permit voted down",
 "summary": "The hearing ends with the proposal failing, 5 nay to 2 aye. Noise late at night, parking, ambulance access, next-morning trash and the two shops on the corner were all raised and none of them were answered, and asking for a standing permit with no trial run sealed it for Process. With that many seats against, the council closes the hearing without attaching any conditions.",
 "seats": [
  {
   "seat": 1,
   "vote": "aye",
   "remark": "I see $2500 covered by wristband sales at the barrier, so there's a plan behind the cost. I'll be watching whether those sales keep pace with that figure every month.",
   "concern": "whether wristband sales keep covering the cost each month"
  },
  {
   "seat": 2,
   "vote": "nay",
   "remark": "The closure runs 6pm to 1am between Calloway and Pinmarsh, but nothing here says what happens with noise late at night or where the street's residents are meant to park.",
   "concern": "noise late at night and parking for the street"
  },
  "... (7 in all)"
 ],
 "speakers": [
  {
   "label": "a resident",
   "stance": "for",
   "says": "I think this neighbourhood could use more going on, and a monthly night like this gives people something to look forward to."
  },
  "... (3 in all)"
 ],
 "conditions": [],
 "hard_question": "Before this becomes a standing permit, what's the plan for the noise, parking, trash and ambulance access once the street closes every month, and would a trial run come first?"
}

hearing - something for the kids, somewhere in the park: continued

Input:

{
 "task": "hearing",
 "title": "Something fun for the kids",
 "what": "Put something fun in the park for the kids, like a little play area or maybe a splash pad.",
 "where": "Somewhere in the park",
 "when": "",
 "ask": "",
 "cost": "",
 "currency": "$",
 "who_pays": "",
 "who_runs": "",
 "helps": "[\"Kids in the neighbourhood\",\"Parents\"]",
 "worries": "[\"It might cost a lot\"]",
 "concessions": "[]",
 "pitch": "Hello. I have lived here a long time and I think the kids deserve something fun to do. The park is kind of boring right now. A play area or a splash pad would b ... (201 characters)",
 "minutes": "1",
 "max_words": "130",
 "page_lean": "{\"1\":\"abstain\",\"2\":\"abstain\",\"3\":\"abstain\",\"4\":\"abstain\",\"5\":\"abstain\",\"6\":\"abstain\",\"7\":\"aye\"}",
 "page_outcome": "continued"
}

Reply:

{
 "task": "hearing",
 "outcome": "continued",
 "title": "Hearing continued for missing basics",
 "summary": "With one aye and six seats abstaining, the hearing is continued rather than decided. Most members held back on the same gap - no exact spot in the park - while Member Achterberg also wants a cost and Member Fairweather wants someone named to run it. Member Grady was the one aye, convinced by what this would do for kids and parents nearby.",
 "seats": [
  {
   "seat": 1,
   "vote": "abstain",
   "remark": "I can't vote on this without knowing what it costs. Tell us a number, even a rough one, before we come back to this.",
   "concern": "no cost given"
  },
  {
   "seat": 2,
   "vote": "abstain",
   "remark": "We need to know exactly where in the park and when this would happen before we can weigh in on noise or hours.",
   "concern": "no exact spot or time given"
  },
  "... (7 in all)"
 ],
 "speakers": [
  {
   "label": "a parent",
   "stance": "for",
   "says": "Kids around here would get a lot of use out of this, and it's long overdue for the park."
  },
  "... (3 in all)"
 ],
 "conditions": [
  "A cost for the project",
  "An exact spot in the park, and when it would happen",
  "Someone named to run and look after it"
 ],
 "hard_question": "Where exactly in the park would this go?"
}

rework - after the garden passed: polish

Input:

{
 "task": "rework",
 "title": "Community garden on the empty lot",
 "what": "Turn the fenced empty lot next to the library into 12 raised garden beds that neighbours sign up for, with a shared tool ... (138 characters)",
 "where": "The empty lot at Larkmere Avenue and Fennick Street, next to the library",
 "when": "Daylight hours, 7am to 8pm, March to November",
 "ask": "Permission to use the lot for a 2-year trial",
 "cost": "1800",
 "currency": "$",
 "who_pays": "A neighbourhood grant we already won, plus $20 a year per bed from gardeners",
 "who_runs": "A garden committee of six neighbours, with me as the contact",
 "helps": "[\"Families in the apartments with no yard\",\"Older neighbours who want to garden at waist height\",\"The library's kids' re ... (135 characters)",
 "worries": "[\"Water use\",\"Rats from compost\",\"Parking on workdays\",\"Looks messy in winter\"]",
 "concessions": "[\"Two rain barrels and a drip line, so no water from the hydrant\",\"Closed compost bins only, turned weekly\",\"Workdays on ... (207 characters)",
 "pitch": "Good evening. I'm here about the empty lot at Larkmere and Fennick, next to the library. It has been fenced and empty as long as most of us can remember. We are ... (852 characters)",
 "minutes": "2",
 "max_words": "260",
 "hearing": "{\"outcome\":\"passes\",\"seats\":[{\"seat\":1,\"vote\":\"aye\",\"concern\":\"the per-bed fee keeps covering upkeep\"},{\"seat\":2,\"vote\":\"aye\",\"concern\":\"parking stays off the street on workdays\"},{\"seat\":3,\"vote\":\"ay ... (817 characters)",
 "people": "[\"Amina Quellbrook (leads the library's kids' reading program)\",\"Theo (lives next to the lot)\"]"
}

Reply:

{
 "task": "rework",
 "fix": "polish",
 "title": "Small polish before the vote",
 "summary": "The council passed the proposal at the rehearsal, so only small polish is needed before the real hearing: write the conditions from the hearing - the per-bed fee, workdays on foot or by bike, and closed compost bins - into the plan, and be ready to say what happens to the garden, the shed and the beds if the 2-year trial is not renewed. Amina Quellbrook and Theo are worth a quick word so they hear the plan directly and can back it.",
 "grid": [
  {
   "who": "Amina Quellbrook",
   "seat": null,
   "power": "high",
   "interest": "high",
   "stance": "unsure",
   "move": "Meet with Amina Quellbrook before the vote to show how the garden ties into the library's kids' reading program."
  },
  {
   "who": "Theo",
   "seat": null,
   "power": "low",
   "interest": "high",
   "stance": "unsure",
   "move": "Keep Theo informed about the plan and ask for their support as a neighbour."
  }
 ],
 "changes": [
  "Write the conditions from the hearing - the per-bed fee, workdays on foot or by bike, and closed compost bins - into the plan so the council can see them in writing.",
  "Add a line to the plan about what happens to the garden, the shed and the beds if the 2-year trial is not renewed."
 ],
 "pitch": "Good evening. I'm here about the community garden for the empty lot at Larkmere Avenue and Fennick Street, next to the library: 12 raised beds, a shared tool shed and a bench. We are asking for permission to use the lot for a 2-year trial. The $1800 cost stays ... (942 characters)",
 "flips": []
}

rework - after the street party failed: pivot

Input:

{
 "task": "rework",
 "title": "Monthly street party on Wrenfold",
 "what": "Close Wrenfold Street every month for a big Saturday party with a DJ, food trucks and a bounce house.",
 "where": "Wrenfold Street, the whole stretch between Calloway and Pinmarsh, both lanes closed",
 "when": "The second Saturday of every month, 6pm to 1am",
 "ask": "A standing monthly street-closure permit",
 "cost": "2500",
 "currency": "$",
 "who_pays": "Wristband sales at the barrier",
 "who_runs": "Me and a few friends",
 "helps": "[\"Everyone on the street\",\"Young people with nowhere to go on weekends\"]",
 "worries": "[\"Noise late at night\",\"Parking for the whole street\",\"Trash the next morning\",\"Ambulance access\",\"The two shops on the  ... (128 characters)",
 "concessions": "[]",
 "pitch": "Hi everyone. I think this neighbourhood needs more fun. Everyone loves music, and a party brings people together like nothing else. Young people have nothing to ... (278 characters)",
 "minutes": "2",
 "max_words": "260",
 "hearing": "{\"outcome\":\"fails\",\"seats\":[{\"seat\":1,\"vote\":\"aye\",\"concern\":\"whether wristband sales keep covering the cost each month\"},{\"seat\":2,\"vote\":\"nay\",\"concern\":\"noise late at night and parking for the stre ... (789 characters)",
 "people": "[\"Lena (runs the corner bakery)\",\"The tenants' group at 12 Wrenfold\"]"
}

Reply:

{
 "task": "rework",
 "fix": "pivot",
 "title": "A one-time trial for Wrenfold Street",
 "summary": "The hearing failed because several seats raised concerns about noise, parking, trash, ambulance access, deliveries for the two shops on the corner, and a standing commitment with no trial first. Before the next hearing, the proposal needs to shrink into a one-time trial with shorter, quieter hours, part of the street left open, and a plan for parking and trash, so the council has something smaller to say yes to.",
 "grid": [
  {
   "who": "Member Bello",
   "seat": 2,
   "power": "high",
   "interest": "high",
   "stance": "against",
   "move": "Meet with Member Bello before the vote and bring the plan for noise late at night and parking for the street."
  },
  {
   "who": "Member Castellanos",
   "seat": 3,
   "power": "high",
   "interest": "high",
   "stance": "against",
   "move": "Meet with Member Castellanos before the vote and bring the plan for ambulance access during the closure."
  },
  "... (7 in all)"
 ],
 "changes": [
  "Make it a one-time trial instead of a standing monthly permit, with a repeat considered only after the trial runs its course.",
  "Shorten the hours so the party ends earlier in the evening instead of running late into the night.",
  "Keep part of the street open instead of closing both lanes, so deliveries and emergency vehicles still have a way through.",
  "Arrange parking away from the street for guests, and set up a plan for who clears the trash the next morning."
 ],
 "pitch": "Hi everyone. I'm back about the street party on Wrenfold Street, and this time I'm asking for a one-time trial instead of a standing monthly street-closure permit, so the council can see how it goes before anything becomes standing. On cost, wristband sales at ... (1112 characters)",
 "flips": [
  2,
  3,
  4,
  5,
  6
 ]
}

rework - after the park item was continued: fill-gaps

Input:

{
 "task": "rework",
 "title": "Something fun for the kids",
 "what": "Put something fun in the park for the kids, like a little play area or maybe a splash pad.",
 "where": "Somewhere in the park",
 "when": "",
 "ask": "",
 "cost": "",
 "currency": "$",
 "who_pays": "",
 "who_runs": "",
 "helps": "[\"Kids in the neighbourhood\",\"Parents\"]",
 "worries": "[\"It might cost a lot\"]",
 "concessions": "[]",
 "pitch": "Hello. I have lived here a long time and I think the kids deserve something fun to do. The park is kind of boring right now. A play area or a splash pad would b ... (201 characters)",
 "minutes": "1",
 "max_words": "130",
 "hearing": "{\"outcome\":\"continued\",\"seats\":[{\"seat\":1,\"vote\":\"abstain\",\"concern\":\"no cost given\"},{\"seat\":2,\"vote\":\"abstain\",\"concern\":\"no exact spot or time given\"},{\"seat\":3,\"vote\":\"abstain\",\"concern\":\"no exact ... (697 characters)",
 "people": "[\"Parents at the school pickup\"]"
}

Reply:

{
 "task": "rework",
 "fix": "fill-gaps",
 "title": "Fill the gaps before voting",
 "summary": "Most of the council abstained because the proposal does not yet give a cost, an exact spot and time in the park, or someone to run it. Member Grady was ready to support it for how it helps kids in the neighbourhood and parents, but the rest need those details before they can decide. Bring back a cost, an exact spot and time, and someone named to run it, then come back for a second hearing.",
 "grid": [
  {
   "who": "Member Achterberg",
   "seat": 1,
   "power": "high",
   "interest": "high",
   "stance": "unsure",
   "move": "Meet with Member Achterberg before the vote and bring a cost for the project."
  },
  {
   "who": "Member Bello",
   "seat": 2,
   "power": "high",
   "interest": "high",
   "stance": "unsure",
   "move": "Meet with Member Bello before the vote with an exact spot and time."
  },
  "... (7 in all)"
 ],
 "changes": [
  "Bring back a cost for the project.",
  "Bring back an exact spot in the park, and when it would happen.",
  "Bring back someone named to run and look after it."
 ],
 "pitch": "This is a proposal to put something fun in the park for the kids, like a play area or a splash pad. This asks the council for [the ask]. The cost for this is [cost]. It would happen at [exact spot in the park], on [when]. That same spot would also let the coun ... (544 characters)",
 "flips": []
}

Build the input yourself

Load the script in node (global.window = global; require("./councilkit.js")), then:

const K = CouncilKit;
const proposal = K.proposal({ title: "Bike corral outside the bakery", what: "Swap one parking space for a ten-bike corral.",
  where: "Outside the bakery on Main", when: "All year", cost: "0", who_runs: "The bakery", helps: "Cyclists\nThe bakery's customers",
  worries: "Losing a parking space", pitch: "I am asking the council to approve one bike corral ...", minutes: "2" });
console.log(K.flags(proposal));                                   // the proposal's own checks
console.log(K.lean(proposal), K.leanOutcome(proposal));           // each seat's lean from wording, and the outcome it adds up to
const wire = K.toWire(K.buildInput("hearing", proposal).input);  // POST this to /estimate and /run
// then: const hearing = K.normalize(K.parseReply(job.output.output), "hearing");
// const { notes, tally, outcome } = K.check(hearing, proposal);   // the page's re-check of the reply
// and for the rework: K.toWire(K.buildInput("rework", proposal, hearing).input)

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://all-in-favor.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"all-in-favor"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "hearing", "title": "Community garden on the empty lot", "what": "Turn the fenced empty lot next to the library into 12 raised garden beds ...", "where": "The empty lot at Larkmere Avenue and Fennick Street, next to the library", "when": "Daylight hours, 7am to 8pm, March to November", "ask": "Permission to use the lot for a 2-year trial", "cost": "1800", "currency": "$", "who_pays": "A neighbourhood grant ...", "who_runs": "A garden committee of six neighbours ...", "helps": "[\"Families in the apartments with no yard\", ...]", "worries": "[\"Water use\", ...]", "concessions": "[\"Two rain barrels and a drip line ...\", ...]", "pitch": "Good evening. I'm here about the empty lot ...", "minutes": "2", "max_words": "260", "page_lean": "{\"1\":\"aye\", ...}", "page_outcome": "passes"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses all-in-favor:<task>:<inputHash>:a<attempt> (the lanes are hearing and rework), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="all-in-favor:hearing:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: all-in-favor:hearing:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json