Drive Over the Fence from your own code
The one metered task, map, is available over HTTP. It takes what is going on between a person and a neighbour, what each side wants, how they get along and what has been tried, and returns a first move (knock, note or mediator), each issue with both positions and the needs underneath them sorted into common ground, trade or stuck, the ground both sides share, two to four trades, each side's fallback, questions to ask and an opener.
The division of labour first: the page owns the flashpoints, the heated-word check and the first-move rule; the model owns the judgement and the words; the page re-checks the reply. All three come from fencekit.js, which you can run yourself (see Build the input yourself). It is general information, not legal advice.
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":"over-the-fence"}), 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, issue, contact, tried, flashpoints) 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: mapping a dispute is metered and needs 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 and the first move
Every run sends task: map; a missing or different one is still answered as map. The first move (verdict) is checked in a fixed order and the first that applies wins:
- mediator -
safetyisyes, the text says someone was threatened, hurt or harassed,triedisthird-partyorformal, orcontactisnot-speaking. The opener is a message to a local community mediation service, never to the neighbour. - note -
contactistenseorstrangers, ortriedistense-talkornote-sent. The opener is a short written note ending[your name]. - knock - everything else. The opener is what to say at the door.
The page computes the same rule (FenceKit.expected) and flags a reply that disagrees.
The run input
Every value is a string (objects are never sent). app.js builds it with FenceKit.buildInput(form).input.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always map. |
issue | yes | What is going on, in the person's words (at most 1,500 characters; a longer text keeps its start and end with a [...] marker). |
your_side / their_side | no | What the person wants (600) and what the neighbour wants or has said (600), or not given / not known. |
contact | yes | Starts with friendly, nodding, strangers, tense or not-speaking, then a plain-words gloss. |
tried | yes | Starts with nothing, mentioned, tense-talk, note-sent, third-party or formal, then a gloss. |
safety | no | yes or no. |
flashpoints | yes | One line per topic the page's word list found, most-mentioned first, at most 5: <id> | <name>. Ids: leaves, trees, noise, parking, boundary, smoke, water, lights, bins, building, privacy, pets, costs. Or none found. |
clip_note | no | What the page shortened, or absent. |
retry_note | no | Sent only on the page's one reformat retry. |
The reply
The reply is one JSON object in job.output.output. The page parses it with FenceKit.parseReply and FenceKit.normalize (a cut-off stream is closed by FenceKit.closeJson and shown as "N of 11 sections recovered").
{"task": "map", "verdict": "knock | note | mediator", "verdict_reason": "...", "title": "...", "summary": "...",
"issues": [{"id": "trees", "title": "...", "you_want": "You want ...", "they_want": "Your neighbour ...",
"your_need": "You ...", "their_need": "Probably your neighbour ...", "status": "common | trade | stuck", "why": "..."}],
"shared": ["..."],
"options": [{"idea": "...", "for_you": "...", "for_them": "...", "issues": ["trees"]}],
"fallback": {"you": "You ...", "them": "Probably your neighbour ..."},
"order": ["trees", "leaves"], "ask_them": ["...?"],
"opener": {"kind": "knock", "text": "..."}}
| Field | Contract |
|---|---|
issues | One per flashpoints line, same ids and order, then at most 2 more as other-1, other-2 (1 to 3 when none was found). status in a fixed order: stuck if it needs a fact or rule neither neighbour can settle alone or one side has flatly refused, else common if both needs point the same way, else trade. A neighbour position nobody stated is exactly Not known yet.; a guessed need starts with Probably. |
options | 2 to 4 arrangements the neighbours could agree themselves - never lawyers, police, complaints, recording or watching. |
order | Every issue id once: common, then trade, then stuck. |
opener | kind equals verdict; knock 40 to 110 words, note 50 to 120 ending [your name], mediator 50 to 120 addressed to a mediation service. |
The page then checks the reply against its own facts (FenceKit.check): the first move against its rule, a panel for every flashpoint, extra issues it missed, a status on every panel, the raising order, the opener's kind, length and heated words, lawyers or police in a first word, the note's name line, positions marked not known, trades pointing at missing issues, questions without a question mark, and internal field ids in the prose.
Worked examples, one per first move
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited.
A friendly neighbour's maple, leaves and a branch - knock
Input:
{
"task": "map",
"issue": "The big maple on my neighbour's side drops most of its leaves into our front yard and the gutters on our side clog every year. One branch now hangs over our roof and scrapes the tiles when it is windy. I like the tree and do not want it cut down.",
"your_side": "Some help clearing the leaves this year, and the branch over our roof trimmed back before winter.",
"their_side": "When I mentioned the leaves last month they laughed and said the tree was here before both of us. They said they would not want the branch to damage our roof either, but they worry that cutting it the wrong way could hurt the tree.",
"contact": "friendly (we chat and get on)",
"tried": "mentioned it in passing",
"safety": "no",
"flashpoints": "trees | Trees and branches\nleaves | Leaves and gutters"
}
Reply:
{
"task": "map",
"verdict": "knock",
"verdict_reason": "You get on well with your neighbour and have only mentioned this in passing so far, so knocking on their door is the natural next step.",
"title": "The branch, the roof and the leaves",
"summary": "You and your neighbour differ on how to handle an overhanging branch and the leaves it drops into your gutters each year. Since you get on well and have only mentioned it briefly so far, the best first move is a friendly chat at their door.",
"issues": [
{
"id": "trees",
"title": "Branch over the roof",
"you_want": "You want the branch trimmed back before winter so it stops scraping your roof, without the tree itself being cut down.",
"they_want": "Your neighbour agrees the branch should not damage your roof, but worries that cutting it the wrong way could hurt the tree.",
"your_need": "You need the roof protected from damage and the tree left standing, since you like it.",
"their_need": "Your neighbour needs the tree to stay healthy and not be harmed by how the branch is cut.",
"status": "common",
"why": "Both you and your neighbour want the tree kept safe while the branch is dealt with, so one careful trim could suit everyone."
},
{
"id": "leaves",
"title": "Clearing the leaves",
"you_want": "You want some help clearing the leaves that clog your gutters each year.",
"they_want": "Your neighbour has pointed out that the tree was there before both of you, rather than offering to help with the leaves.",
"your_need": "You need the leaf build-up cleared so your gutters do not stay clogged.",
"their_need": "Probably your neighbour wants to be treated fairly and not be asked to fix something caused by a tree that was already there.",
"status": "trade",
"why": "You want help with the leaves while your neighbour feels the tree was there before both of you, so this is something to work out together rather than something you already agree on."
}
],
"shared": [
"Keeping the tree and your roof both free from damage.",
"Sorting this out in a way that keeps things friendly between you."
],
"options": [
{
"idea": "You and your neighbour share the cost of hiring someone experienced to trim the branch safely.",
"for_you": "The branch is trimmed properly without risking your roof.",
"for_them": "The tree is cut by someone careful, so it is not harmed.",
"issues": [
"trees"
]
},
{
"idea": "Your neighbour helps clear the leaves from your gutters each year.",
"for_you": "The gutters get cleared without you doing all the work alone.",
"for_them": "A straightforward way to help that does not cost much time or money.",
"issues": [
"leaves"
]
},
{
"idea": "You cover the cost of trimming the branch, and your neighbour helps with clearing the leaves.",
"for_you": "The branch is dealt with and you get extra help with the gutters.",
"for_them": "The branch is handled properly without your neighbour having to pay for it.",
"issues": [
"trees",
"leaves"
]
},
{
"idea": "You and your neighbour split the cost of gutter guards so leaves stop building up.",
"for_you": "Less time spent clearing the gutters each year.",
"for_them": "No need for the tree to be touched to solve the gutter problem.",
"issues": [
"leaves"
]
}
],
"fallback": {
"you": "You can pay to have your own gutters cleared when they clog, and bring up the branch again next time you speak.",
"them": "Probably your neighbour would leave the tree and branch as they are unless they decide in their own time to get it trimmed."
},
"order": [
"trees",
"leaves"
],
"ask_them": [
"Would you be open to splitting the cost of someone experienced trimming the branch safely before winter?",
"Is there a way we could share the job of clearing the leaves each year?",
"What would feel fair to you about handling the cost of trimming the branch?"
],
"opener": {
"kind": "knock",
"text": "Hi, do you have a minute? I wanted to ask about the branch that hangs over our roof - it scrapes the tiles when it's windy, and I'd love to get it trimmed back before winter without touching the rest of the tree, since I know you like it too. There are also a couple of other small things I wanted to mention. Would you be open to splitting the cost of someone experienced to trim it safely?"
}
}
Parking across the driveway, after a doorstep row - note
Input:
{
"task": "map",
"issue": "My neighbour's visitors often park across the end of our driveway, so I can't get the car out in the morning. Their bins also stay out on the pavement in front of our house for days after collection. Last week I knocked about the car and it turned into a row on the doorstep.",
"your_side": "A clear driveway, and their bins brought back in the day after collection.",
"their_side": "They said the street is public and anyone can park there, and that I was being dramatic.",
"contact": "tense (things are frosty)",
"tried": "tense-talk (we talked and it went badly)",
"safety": "no",
"flashpoints": "parking | Parking and driveways\nbins | Bins and rubbish"
}
Reply:
{
"task": "map",
"verdict": "note",
"verdict_reason": "You described the contact between you as tense, which points to sending a note rather than knocking again or contacting a local community mediation service.",
"title": "Parking across the driveway and bins",
"summary": "This disagreement is about visitors' cars blocking the end of your driveway and bins being left out on the pavement for several days after collection. Because things between you and your neighbour are currently tense, a short written note is a better next step than knocking again or contacting a local community mediation service.",
"issues": [
{
"id": "parking",
"title": "Parking across the driveway",
"you_want": "You want a clear driveway so you can get your car out in the morning.",
"they_want": "Your neighbour says the street is public and anyone can park there.",
"your_need": "You need to be able to get your car out in the morning without being blocked in.",
"their_need": "Probably your neighbour wants to be treated fairly when it comes to parking on a public street.",
"status": "stuck",
"why": "Whether anyone can park on this stretch of street depends on local parking rules, which neither of you can settle alone."
},
{
"id": "bins",
"title": "Bins left out too long",
"you_want": "You want the bins brought back in the day after collection.",
"they_want": "Not known yet.",
"your_need": "Probably you want the front of your house to look tidy without bins left out for days.",
"their_need": "Probably your neighbour wants to avoid the extra effort of bringing the bins in straight away.",
"status": "trade",
"why": "You want the bins brought in sooner and your neighbour would rather not add the extra task, so this looks like something to trade on."
}
],
"shared": [
"You and your neighbour likely both want to avoid another row like the one on the doorstep.",
"You and your neighbour may both want a clear answer about what the parking rules actually allow.",
"You and your neighbour would probably both prefer this to involve as little ongoing hassle as possible."
],
"options": [
{
"idea": "You and your neighbour look together into what the local parking rules actually allow at the end of a driveway.",
"for_you": "You get a clear, shared answer instead of each going on your own view of what is allowed.",
"for_them": "Your neighbour gets the same certainty, so it is not just your word against theirs.",
"issues": [
"parking"
]
},
{
"idea": "Your neighbour asks visitors to leave a gap in front of the driveway, whatever the parking rules turn out to allow.",
"for_you": "You can get the car out each morning without waiting on the rules question being settled.",
"for_them": "Your neighbour's visitors can still park on the street, just clear of the driveway opening.",
"issues": [
"parking"
]
},
{
"idea": "Your neighbour brings the bins in the day after collection each time.",
"for_you": "You get a tidier stretch of pavement in front of your house most of the time.",
"for_them": "Your neighbour knows exactly what is expected, rather than guessing when it matters to you.",
"issues": [
"bins"
]
}
],
"fallback": {
"you": "You adjust your own routine, such as parking your own car to leave room, and keep reminding your neighbour about the bins when you see them.",
"them": "Probably your neighbour will keep telling visitors the street is open to anyone and will bring the bins in when it suits them."
},
"order": [
"bins",
"parking"
],
"ask_them": [
"Would you be able to bring the bins in the day after collection?",
"Could your visitors leave the end of the driveway clear when they park?",
"Do you know if there is a local rule about parking across someone's driveway?"
],
"opener": {
"kind": "note",
"text": "Hi, I hope you're doing okay. I wanted to drop a quick note about a couple of things. The bins have been staying out on the pavement for a few days after collection, and it would help if they could come back in sooner. I also wanted to mention the parking at the end of our driveway, since it sometimes makes it hard for me to get the car out in the morning. I know our last chat about this did not go well, and I am sorry it got heated. Could we find a time to talk it through calmly? Thanks for reading this.\n[your name]"
}
}
A fence over the line and fire-pit smoke - mediator
Input:
{
"task": "map",
"issue": "Our neighbour put up a new fence in the spring and we believe it sits about half a metre onto our side of the line. They also burn garden waste in a fire pit near the fence and the smoke comes straight into our garden. The residents' association was asked to look at the fence and said it was a private matter.",
"your_side": "The fence moved back to the real line, and no burning when the wind blows our way.",
"their_side": "They say their own survey shows the fence is in the right place, and that the fire pit is on their land so it is their business.",
"contact": "tense (things are frosty)",
"tried": "third-party (a landlord, residents' association, council or police got involved)",
"safety": "no",
"flashpoints": "boundary | Fence and boundary\nsmoke | Smoke and fires"
}
Reply:
{
"task": "map",
"verdict": "mediator",
"verdict_reason": "You already brought an outside group into the fence question and they said it wasn't theirs to decide, so reaching out to a mediation service is a better next move than talking to your neighbour again right now.",
"title": "Fence Line And Garden Smoke",
"summary": "This disagreement is about where a new fence actually sits compared to the boundary line, and about smoke from a garden fire drifting into your garden. Since an outside group already looked at part of this without settling it, the best next step is reaching out to a mediation service rather than speaking to your neighbour again right now.",
"issues": [
{
"id": "boundary",
"title": "Fence and boundary",
"you_want": "You want the fence moved back to where the boundary line actually runs.",
"they_want": "Your neighbour says their own survey already shows the fence sitting in the right place.",
"your_need": "Probably you want a clear rule about where the boundary line truly runs.",
"their_need": "Probably your neighbour wants the same clear rule about where the boundary line truly runs.",
"status": "stuck",
"why": "Only an agreed survey or local rule can settle where the boundary runs, and neither neighbour can decide that alone."
},
{
"id": "smoke",
"title": "Smoke and fires",
"you_want": "You want the burning stopped on days when the wind would carry smoke into your garden.",
"they_want": "Your neighbour says the fire pit is on their own land, so burning there is their own choice.",
"your_need": "Probably you want to feel safe from the smoke that drifts into your garden.",
"their_need": "Probably your neighbour wants to be asked rather than told what to do on their own land.",
"status": "trade",
"why": "Your need for clean air and your neighbour's wish to decide what happens on their own land point in different directions, but there is still room for an arrangement that could suit you as well as your neighbour."
}
],
"shared": [
"A clear, agreed answer about exactly where the boundary line runs",
"Being able to use their own garden without smoke or conflict getting in the way",
"Being treated fairly and not just told what to do"
],
"options": [
{
"idea": "Share the cost of a single, agreed surveyor to mark exactly where the boundary line runs.",
"for_you": "You get a result both sides have agreed to trust, not just your own reading of the line.",
"for_them": "Your neighbour gets the same assurance, so the question is settled for good.",
"issues": [
"boundary"
]
},
{
"idea": "Agree a simple rule about which days or wind directions are off-limits for burning garden waste.",
"for_you": "You stop getting smoke blown into your garden on the days that matter most to you.",
"for_them": "Your neighbour can keep burning garden waste most of the time, just not when the wind carries it your way.",
"issues": [
"smoke"
]
},
{
"idea": "Your neighbour shifts some garden waste to composting or a collection option instead of burning it, while you drop the request for exact days.",
"for_you": "Less smoke overall without needing to track the wind every time.",
"for_them": "Fewer limits on their routine since they are not tied to checking wind direction.",
"issues": [
"smoke"
]
}
],
"fallback": {
"you": "You can focus on reducing how much the smoke reaches you, such as planning your outdoor time around when burning usually happens, while leaving the fence as it is for now.",
"them": "Probably your neighbour would keep the fence where it is and continue burning garden waste whenever they choose."
},
"order": [
"smoke",
"boundary"
],
"ask_them": [
"Would you be willing to share the cost of one surveyor we both trust to confirm exactly where the boundary sits?",
"Could we agree on certain days or wind conditions when burning garden waste should pause?",
"Would you be open to a different way of handling garden waste on days when the wind blows toward my garden?"
],
"opener": {
"kind": "mediator",
"text": "I'm dealing with an ongoing disagreement with my neighbour over two issues: where our shared boundary line actually sits, since a new fence may be about half a metre onto my side, and smoke from a garden fire pit that drifts into my garden when the wind blows that way. We already raised the fence question with an outside group, and they said it wasn't theirs to decide. Things between us are strained at the moment, and I would like to understand how mediation works and whether it could help us find a way forward together."
}
}
Build the input yourself
Load the script in node (global.window = global; require("./fencekit.js")), then:
const K = FenceKit;
const form = K.form({ issue: "Their security light shines into our bedroom all night.", your_side: "The light angled down or on a timer.",
their_side: "not known", contact: "nodding", tried: "nothing" });
K.flashpoints(form); // [{id: "lights", name: "Lights", hits: 2}]
K.heat(form); // heated words in your own text, with calmer swaps
K.expected(form); // {verdict: "knock", why: [...]} - the page's first-move rule
const built = K.buildInput(form);
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply) // the map 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://over-the-fence.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://over-the-fence.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"]
// over-the-fence.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://over-the-fence.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://over-the-fence.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
}
// OverTheFence.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 OverTheFence {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://over-the-fence.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://over-the-fence.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://over-the-fence.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://over-the-fence.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":"over-the-fence"}' | 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": "over-the-fence"})["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: "over-the-fence" })).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": "over-the-fence"}, 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", "over-the-fence"), 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: "over-the-fence" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "over-the-fence"])["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 = "over-the-fence" }))!["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": "map", "issue": "The big maple on my neighbour's side drops most of its leaves into our front yard and the gutters on our side clog every year. One branch now hangs over our roof and scrapes the tiles when it is windy. I like the tree and do not want it cut down.", "your_side": "Some help clearing the leaves this year, and the branch over our roof trimmed back before winter.", "their_side": "When I mentioned the leaves last month they laughed and said the tree was here before both of us. They seem to love that tree.", "contact": "friendly (we chat and get on)", "tried": "mentioned it in passing", "safety": "no", "flashpoints": "trees | Trees and branches\nleaves | Leaves and gutters"}
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": "map", "issue": "The big maple on my neighbour's side drops most of its leaves into our front yard and the gutters on our side clog every year. One branch now hangs over our roof and scrapes the tiles when it is windy. I like the tree and do not want it cut down.", "your_side": "Some help clearing the leaves this year, and the branch over our roof trimmed back before winter.", "their_side": "When I mentioned the leaves last month they laughed and said the tree was here before both of us. They seem to love that tree.", "contact": "friendly (we chat and get on)", "tried": "mentioned it in passing", "safety": "no", "flashpoints": "trees | Trees and branches\nleaves | Leaves and gutters"}
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: "map", issue, your_side, their_side, contact, tried, safety, flashpoints, clip_note} (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 over-the-fence:<task>:<inputHash>:a<attempt> (the one task is map), 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="over-the-fence:map:$(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 = "over-the-fence:" + 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("title"))
import { createHash } from "node:crypto";
const key = `over-the-fence:${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.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "over-the-fence:" + 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 = "over-the-fence:" + 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 = "over-the-fence:#{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 = "over-the-fence:" . $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 = $"over-the-fence:{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: over-the-fence:map:$(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": "map", "issue": "The big maple on my neighbour's side drops most of its leaves into our front yard and the gutters on our side clog every year. One branch now hangs over our roof and scrapes the tiles when it is windy. I like the tree and do not want it cut down.", "your_side": "Some help clearing the leaves this year, and the branch over our roof trimmed back before winter.", "their_side": "When I mentioned the leaves last month they laughed and said the tree was here before both of us. They seem to love that tree.", "contact": "friendly (we chat and get on)", "tried": "mentioned it in passing", "safety": "no", "flashpoints": "trees | Trees and branches\nleaves | Leaves and gutters"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "over-the-fence:map:" + 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": `over-the-fence:map:${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", "over-the-fence:map:"+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", "over-the-fence:map:" + 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" => "over-the-fence:map:#{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: over-the-fence:map:" . 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", $"over-the-fence:map:{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);