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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field such as task, what or pitch is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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:
| Order | Vote | When |
|---|---|---|
| 1 | abstain | What 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. |
| 2 | nay | A worry in the seat's focus is plainly raised and nothing in the comment or the concessions answers it. |
| 3 | aye | Otherwise. |
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
| Field | Type | Notes |
|---|---|---|
task | string | hearing or rework. Required. |
title, what | string | A short name and the proposal in a sentence or two (cut at 1,200 characters). what is required. |
where, when, ask | string | The place, the dates or hours, and the one thing asked of the council. May be empty - the seats that need them then abstain. |
cost, currency | string | A number (empty = not given, 0 = free) and one of $ £ € ₹ ¥. |
who_pays, who_runs | string | Who pays, and who runs and looks after it. |
helps, worries, concessions | string (JSON) | Arrays of short strings, up to 8 each. |
pitch | string | The public comment as written. Past 600 words the middle is left out with a marker. Required. |
minutes, max_words | string | The speaking limit (1, 2 or 3) and the words that fit in it at 130 a minute. |
page_lean, page_outcome | string (JSON), string | hearing only: the page's vote guess per seat ({"1":"aye",...}) and the outcome it adds up to. |
hearing | string (JSON) | rework only: {outcome, seats: [{seat, vote, concern}], conditions, hard_question} from the hearing. |
people | string (JSON) | rework only: people you can talk to before the vote, up to 8 (may be []). |
retry_note | string | Only 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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://all-in-favor.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// all-in-favor.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://all-in-favor.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://all-in-favor.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// AllInFavor.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class AllInFavor {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://all-in-favor.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://all-in-favor.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://all-in-favor.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://all-in-favor.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "all-in-favor"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "all-in-favor" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "all-in-favor"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "all-in-favor"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "all-in-favor" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "all-in-favor"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "all-in-favor" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"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"}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "hearing", what, where, pitch, worries, ... (see the field table)}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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}'
import hashlib, time
key = "all-in-favor:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the hearing as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("outcome"))
import { createHash } from "node:crypto";
const key = `all-in-favor:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.outcome);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "all-in-favor:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "all-in-favor:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "all-in-favor:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "all-in-favor:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"all-in-favor:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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
rev = json.load(open("input.json")) # the same run input as input.json: {"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"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "all-in-favor:hearing:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `all-in-favor:hearing:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "all-in-favor:hearing:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "all-in-favor:hearing:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "all-in-favor:hearing:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: all-in-favor:hearing:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"all-in-favor:hearing:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);