Drive Hear Me Out from your own code
The one metered task, scheme-ladder, is available over HTTP. It takes 1 small business, its 1 stubborn problem, who comes now, what the owner already has, anything off limits and the money and hours they can give, plus the page's problem kind and caps, and returns a ladder of exactly 5 schemes from sensible to outlandish - each with a 3-step logic chain, a rough cost and hours, a risk line, a verdict and a first step - with the lead scheme and why it leads.
The division of labour first: the model invents the schemes and estimates their cost and hours; a fixed rule on the page weighs every verdict and picks the lead from those numbers. After a run the page checks that every verdict and the lead match its rule, that rungs 1 to 5 come back in order, that rungs 1 to 3 fit both caps, that each scheme has 3 logic steps, that no number in the prose is invented and that no ids leak into the prose.
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": "..." } }
The token is minted for this app (the guest call takes {"slug":"hear-me-out"}), 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 (task, business, problem, regulars, have, off_limits, budget, hours, facts) 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: a ladder is metered and needs a personal sign-in. |
| 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, the input and the verdicts
Every run sends task: scheme-ladder (the one task is scheme-ladder); a missing or different one is still answered as scheme-ladder. Rungs run 1 sensible, 2 small twist, 3 playful, 4 odd, 5 outlandish; rungs 1 to 3 must fit both caps. Each scheme's verdict, first match wins: park-it if its cost is over the money cap or its hours over the hours cap; pilot-it if it is rung 4 or 5 or names a risk; otherwise try-it. The lead (pick) is the cheapest try-it, then fewest hours, then the lower rung; else the same among pilot-it; 0 if all are parked.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always scheme-ladder. |
business | yes | The business in the owner's words, up to 80 characters. |
problem | yes | The 1 problem that keeps happening, up to 140 characters. |
regulars, have, off_limits | yes | Who comes now, what they already have, what they will not do - or not given / none given. |
budget, hours | yes | The money cap with its currency sign (£60) and the hours cap (6). |
facts | yes | The page's lines: problem kind:, money cap: 60 in £ | hours cap: 6, the rung meanings and which rungs must fit the caps. |
retry_note | no | Sent only by the page's single reformat retry. |
The reply
{"task": "scheme-ladder", "summary": "...", "schemes": [{"rung": 1, "name": "...", "scheme": "...", "logic": ["...", "...", "..."], "builds_on": "...", "cost": 0, "hours": 1, "risk": "none", "verdict": "try-it", "first_step": "..."}], "pick": 1, "pick_why": "...", "closing": "..."}
| Field | What it holds |
|---|---|
summary | The problem in the owner's words and the lead scheme as "rung N", at most 35 words. |
schemes | Exactly 5, rungs 1 to 5 in order: name (at most 6 words), scheme (at most 35), logic (3 steps), builds_on, cost and hours (whole numbers, rough), risk (none or at most 15 words), verdict, first_step (at most 25 words). |
pick, pick_why | The lead scheme's rung and why it leads, at most 30 words. |
closing | 1 friendly sentence, at most 20 words. |
The page then checks the reply (PitchKit.check) and lists any disagreement under "The page's check"; where the reply's verdict or lead differs from the rule, the page shows its own.
Worked examples - every verdict
These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free.
One-chair barbershop, empty weekday mornings - money cap £60, 6 hours
Input:
{
"task": "scheme-ladder",
"business": "a one-chair barbershop on a side street",
"problem": "the chair sits empty on weekday mornings",
"regulars": "office workers at lunch and dads on Saturdays",
"have": "a big front window, a chalkboard and a good radio",
"off_limits": "no discounts",
"budget": "£60",
"hours": "6",
"facts": "problem kind: some times are quiet (the page matched: morning, weekday, empty)\nmoney cap: 60 in £ | hours cap: 6\nrungs: 1 sensible, 2 small twist, 3 playful, 4 odd, 5 outlandish but logical\nrungs 1 to 3 must cost at most 60 and need at most 6 hours; rungs 4 and 5 may go over a cap"
}
Reply:
{
"task": "scheme-ladder",
"summary": "For your one-chair barbershop on a side street, where the chair sits empty on weekday mornings, rung 1, Window Chalkboard Morning Invite, could give passersby a reason to look in.",
"schemes": [
{
"rung": 1,
"name": "Window Chalkboard Morning Invite",
"scheme": "Each weekday morning, before opening, write a short line on the chalkboard in the front window inviting anyone passing by to step in for a cut.",
"logic": [
"Passersby may notice the chalkboard message on their way past each morning.",
"Some could step inside to ask about a cut.",
"This may ease the chair sitting empty on weekday mornings."
],
"builds_on": "a big front window and a chalkboard",
"cost": 0,
"hours": 1,
"risk": "none",
"verdict": "try-it",
"first_step": "Write a short weekday-morning invite on the chalkboard in the window before you open."
},
{
"rung": 2,
"name": "A Different Morning Radio Sound",
"scheme": "On weekday mornings, switch the radio to a different sound than usual and keep the door open so the change can be heard outside.",
"logic": [
"People walking past may notice the change in sound from the doorway.",
"That could make some pause and look inside out of curiosity.",
"That could ease the chair sitting empty on weekday mornings."
],
"builds_on": "a good radio",
"cost": 0,
"hours": 2,
"risk": "none",
"verdict": "try-it",
"first_step": "Pick a different sound for the radio and prop the door open on a weekday morning."
},
{
"rung": 3,
"name": "A Visible Morning Grooming Routine",
"scheme": "On weekday mornings, turn the chair to face the big front window and go through your usual tool care in full view, like a small window display.",
"logic": [
"People passing the window may notice the chair facing outward.",
"Watching the routine could draw some inside to look closer.",
"This could ease the chair sitting empty on weekday mornings."
],
"builds_on": "a big front window",
"cost": 0,
"hours": 3,
"risk": "none",
"verdict": "try-it",
"first_step": "Turn the chair to face the window and do your usual tool care in view during a weekday morning."
},
{
"rung": 4,
"name": "A Quiet Morning Cut",
"scheme": "On weekday mornings, turn the radio off and offer a hushed, no-talking cut to anyone who wants a calm visit instead of the usual sound.",
"logic": [
"Someone wanting a quiet start may notice the hushed shop while passing.",
"That calm could bring them in for the quiet cut.",
"That may ease the chair sitting empty on weekday mornings."
],
"builds_on": "a good radio",
"cost": 0,
"hours": 2,
"risk": "someone expecting the usual sound might find the hush unexpected",
"verdict": "pilot-it",
"first_step": "Turn the radio off for a weekday morning and see how the hushed shop feels."
},
{
"rung": 5,
"name": "An Open Chair Morning",
"scheme": "On weekday mornings, let anyone sit in the chair to listen to the radio or look out the big front window, with no cut needed.",
"logic": [
"Someone curious may sit in the chair without planning a cut.",
"Spending that time there could lead them to also book a cut.",
"This may still ease the chair sitting empty on weekday mornings."
],
"builds_on": "a good radio and the one-chair barbershop",
"cost": 0,
"hours": 3,
"risk": "some may sit without ever booking a cut",
"verdict": "pilot-it",
"first_step": "Let someone sit in the chair without booking a cut during a weekday morning and see what happens."
}
],
"pick": 1,
"pick_why": "It needs no money from your budget and takes the fewest of your spare hours among the ideas you could try.",
"closing": "Try writing a weekday-morning message on the chalkboard before you open and see who looks in."
}
Hot-sauce market stall, people taste and walk off - money cap $0, 1 hour
Input:
{
"task": "scheme-ladder",
"business": "a hot-sauce stall at a weekend market",
"problem": "people taste a sample and walk off without buying",
"regulars": "the same few stallholders and some families",
"have": "a folding table, a cool box, a friend who draws",
"off_limits": "nothing online",
"budget": "$0",
"hours": "1",
"facts": "problem kind: people come but spend little (the page matched: walk off, without buying)\nmoney cap: 0 in $ | hours cap: 1\nrungs: 1 sensible, 2 small twist, 3 playful, 4 odd, 5 outlandish but logical\nrungs 1 to 3 must cost at most 0 and need at most 1 hours; rungs 4 and 5 may go over a cap"
}
Reply:
{
"task": "scheme-ladder",
"summary": "At your hot-sauce stall at a weekend market, people taste a sample and walk off without buying; rung 1, Ask for the jar now, leads the ladder.",
"schemes": [
{
"rung": 1,
"name": "Ask for the jar now",
"scheme": "Keep the jars right beside the sample tray on the folding table, and ask each taster directly if they would like a jar to take home.",
"logic": [
"Tasting beside the jars may remind a taster a jar is close by.",
"Asking directly for a jar may prompt a quick yes before they leave.",
"This may turn tasting a sample into buying, instead of walking off empty handed."
],
"builds_on": "a folding table",
"cost": 0,
"hours": 0,
"risk": "none",
"verdict": "try-it",
"first_step": "Move the jars beside the sample tray on the folding table, ready for the next taste."
},
{
"rung": 2,
"name": "Let them pick their jar",
"scheme": "After someone tastes, invite them to open the cool box themselves and choose the jar they want, rather than handing it to them yourself.",
"logic": [
"Opening the cool box themselves may keep someone standing at the stall longer.",
"Choosing their own jar may make the sample feel like something they picked.",
"This may stop someone from trying the sauce and leaving with nothing bought."
],
"builds_on": "a cool box",
"cost": 0,
"hours": 1,
"risk": "none",
"verdict": "try-it",
"first_step": "Tell the next person who tastes to open the cool box and pick their own jar."
},
{
"rung": 3,
"name": "A doodle with every taste",
"scheme": "After a taste, have the friend who draws sketch a tiny doodle on scrap paper as a free keepsake, then mention the jar is ready for them to take home.",
"logic": [
"Getting a quick doodle may make the tasting moment feel playful and worth remembering.",
"Remembering the doodle may bring the jar back to mind afterward.",
"This could help a taste become a jar going home instead of leaving."
],
"builds_on": "a friend who draws",
"cost": 0,
"hours": 1,
"risk": "none",
"verdict": "try-it",
"first_step": "Ask the friend who draws to keep scrap paper and a pen ready by the table."
},
{
"rung": 4,
"name": "Trade tasting spots nearby",
"scheme": "Ask the same few stallholders if you can place the cool box on their table for a while, offering tastes to people near them, then do the same for them at yours.",
"logic": [
"Placing the cool box with the same few stallholders may reach people walking past.",
"People near that table may taste without having walked to your stall at all.",
"This may turn a taste there into a jar bought, not a walk off."
],
"builds_on": "the cool box and the same few stallholders",
"cost": 0,
"hours": 1,
"risk": "needs permission from the same few stallholders to share their table",
"verdict": "pilot-it",
"first_step": "Ask the same few stallholders if you could share table space with them."
},
{
"rung": 5,
"name": "Let them pay you later",
"scheme": "When someone among the same few stallholders and some families tastes and starts to leave, let them take a jar now and pay you back next time you see them at the market.",
"logic": [
"Offering a jar to someone you recognise may stop a quick walk away.",
"Trusting them to pay later may turn that walk into taking a jar home.",
"This may turn leaving after a taste into taking a jar home instead."
],
"builds_on": "the same few stallholders and some families",
"cost": 0,
"hours": 2,
"risk": "they may not come back to pay for the jar",
"verdict": "park-it",
"first_step": "The next time the same few stallholders and some families taste and turn to leave, offer them a jar now."
}
],
"pick": 1,
"pick_why": "It needs no money, costs the fewest of your spare hours, and carries no named risk.",
"closing": "Give the jars a new spot next to your samples, and see how a quick ask goes."
}
Launderette nobody knows does service washes - money cap €200, 10 hours
Input:
{
"task": "scheme-ladder",
"business": "a launderette near the station",
"problem": "nobody knows we also do service washes",
"regulars": "students and people from the flats nearby",
"have": "a noticeboard, free wifi and a long folding counter",
"off_limits": "none given",
"budget": "€200",
"hours": "10",
"facts": "problem kind: people do not notice or know about it (the page matched: nobody knows)\nmoney cap: 200 in € | hours cap: 10\nrungs: 1 sensible, 2 small twist, 3 playful, 4 odd, 5 outlandish but logical\nrungs 1 to 3 must cost at most 200 and need at most 10 hours; rungs 4 and 5 may go over a cap"
}
Reply:
{
"task": "scheme-ladder",
"summary": "At a launderette near the station, nobody knows you also do service washes; rung 1, A Notice About Service Washes, leads the way to help with that.",
"schemes": [
{
"rung": 1,
"name": "A Notice About Service Washes",
"scheme": "You write a short notice for the noticeboard saying you also do service washes, placed where people waiting or passing by can see it.",
"logic": [
"People who stop at the noticeboard may read the new notice there.",
"Readers may notice you also do service washes for the first time.",
"This may ease how nobody knows you also do service washes."
],
"builds_on": "the noticeboard",
"cost": 0,
"hours": 1,
"risk": "none",
"verdict": "try-it",
"first_step": "Write a short line for the noticeboard stating you also do service washes."
},
{
"rung": 2,
"name": "A Folded Sample On The Counter",
"scheme": "You keep a neatly folded sample load on the long folding counter so anyone waiting can see what a service wash actually looks like.",
"logic": [
"People waiting at the counter may see the folded sample load there.",
"Seeing it may lead them to ask what a service wash involves.",
"That could chip away at nobody knowing you also do service washes."
],
"builds_on": "the long folding counter",
"cost": 5,
"hours": 2,
"risk": "none",
"verdict": "try-it",
"first_step": "Set aside a finished load to keep folded and visible on the counter."
},
{
"rung": 3,
"name": "A Welcome Message On Your Wifi",
"scheme": "You change the message people see when they join your free wifi so it mentions that you also do service washes.",
"logic": [
"People joining your free wifi may see the new welcome message.",
"Seeing it may make them aware of your service washes.",
"That may help more people learn you also do service washes."
],
"builds_on": "free wifi",
"cost": 0,
"hours": 1,
"risk": "none",
"verdict": "try-it",
"first_step": "Update the welcome message your wifi shows when someone joins it."
},
{
"rung": 4,
"name": "A Loud Commentary During A Wash",
"scheme": "You run a demonstration load through a service wash and fold it at the counter while giving a loud, lively commentary for anyone waiting nearby.",
"logic": [
"Students and others waiting nearby may hear your lively commentary.",
"Hearing it may make them curious about the service wash happening.",
"This could help people see you also do service washes."
],
"builds_on": "students and the long folding counter",
"cost": 10,
"hours": 3,
"risk": "may feel too loud or strange for some customers nearby",
"verdict": "pilot-it",
"first_step": "Choose a load to use as a demonstration for your next service wash."
},
{
"rung": 5,
"name": "A Laundry Cart By The Flats",
"scheme": "You wheel a demonstration load on a cart to fold just outside the flats nearby, clear of the path, once the building gives permission, so residents passing by can see a service wash.",
"logic": [
"People from the flats nearby may see you folding outside their building.",
"Watching may lead them to ask what a service wash is.",
"That may finally show nearby people you also do service washes."
],
"builds_on": "people from the flats nearby",
"cost": 80,
"hours": 14,
"risk": "needs the building's permission to be there; neighbours could object",
"verdict": "park-it",
"first_step": "Ask the building for permission to fold a demonstration load just outside."
}
],
"pick": 1,
"pick_why": "It needs no money, keeps the fewest hours, carries no named risk, and wins the tie by being the lower rung.",
"closing": "Whenever you're ready, try adding that notice to the noticeboard and see what happens."
}
Build the input yourself
Load the engine in node (global.window = global; require("./pitchkit.js")), then:
const K = PitchKit;
const built = K.buildInput(K.form({ business: "a one-chair barbershop on a side street", problem: "the chair sits empty on weekday mornings",
regulars: "office workers at lunch", have: "a big front window and a chalkboard", off_limits: "no discounts", budget: "60", currency: "£", hours: "6" }));
built.input.facts; // the page's lines: the problem kind, the caps, the rung meanings
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// reply.schemes.map(s => K.verdictOf(s, built.facts)) // the verdicts the page's rule gives
// K.pickOf(reply.schemes, built.facts) // the lead scheme's rung
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, "my shop") // the ladder as Markdown
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://hear-me-out.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://hear-me-out.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"]
// hear-me-out.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://hear-me-out.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://hear-me-out.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
}
// WhatWasThat.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 WhatWasThat {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://hear-me-out.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://hear-me-out.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://hear-me-out.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://hear-me-out.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":"hear-me-out"}' | 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": "hear-me-out"})["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: "hear-me-out" })).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": "hear-me-out"}, 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", "hear-me-out"), 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: "hear-me-out" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "hear-me-out"])["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 = "hear-me-out" }))!["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": "scheme-ladder", "business": "a one-chair barbershop on a side street", "problem": "the chair sits empty on weekday mornings", ..., "budget": "£60", "hours": "6", "facts": "problem kind: some times are quiet ..."}
# - see the field table
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": "scheme-ladder", "business": "a one-chair barbershop on a side street", "problem": "the chair sits empty on weekday mornings", ..., "budget": "£60", "hours": "6", "facts": "problem kind: some times are quiet ..."}
# - see the field table
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: "scheme-ladder", business, problem, regulars, have, off_limits, budget, hours, facts} (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 hear-me-out:<task>:<inputHash>:a<attempt> (the one task is scheme-ladder), 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="hear-me-out:scheme-ladder:$(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 = "hear-me-out:" + 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 reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("pick"))
import { createHash } from "node:crypto";
const key = `hear-me-out:${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.pick);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "hear-me-out:" + 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 = "hear-me-out:" + 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 = "hear-me-out:#{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["pick"]].inspect
$key = "hear-me-out:" . $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"], " ", json_encode($reply["pick"]), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"hear-me-out:{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: hear-me-out:scheme-ladder:$(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": "scheme-ladder", "business": "a one-chair barbershop on a side street", "problem": "the chair sits empty on weekday mornings", ..., "budget": "£60", "hours": "6", "facts": "problem kind: some times are quiet ..."}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "hear-me-out:scheme-ladder:" + 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": `hear-me-out:scheme-ladder:${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", "hear-me-out:scheme-ladder:"+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", "hear-me-out:scheme-ladder:" + 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" => "hear-me-out:scheme-ladder:#{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: hear-me-out:scheme-ladder:" . 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", $"hear-me-out:scheme-ladder:{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);