Drive Rebalance Desk from your own code
Everything the page does is available over HTTP: send one household's holdings, targets, trades,
context and the engine's facts, name the lane with task, and get the same structured
envelope back. The natural use is a nightly job that runs the drift lane over every
household past its band and files the ones that need a trade ticket, or a pre-meeting script that
runs all three lanes in sequence and drops the review into the advisor's notes.
The task field comes first
Rebalance Desk is one app with one system prompt and three lanes. Every run body carries task:
| task | what it returns | body key | posture values |
|---|---|---|---|
drift | Class-by-class calls on the allocation, a note on every engine trade, sequence, asset location, tax note, watch list, client questions, deviations. | rebalance | in-band · rebalance-now · rebalance-with-caveats |
harvest | A decision per candidate lot (harvest / wait / skip) with replacement exposure and safe-rebuy date, the order, the benefit claimed, dated guardrails, a client explainer, questions. | harvest | nothing-to-harvest · harvest-selectively · harvest-now |
review | Agenda, five to nine talking points each carrying the figures it used, action items with owners, questions for the client, a follow-up email. | review | ready · ready-with-questions · not-ready |
The lanes are a pipeline: review accepts a handoff object carrying the drift and harvest envelopes from earlier runs, and its talking points are built from them.
Input fields
| field | type | required | meaning |
|---|---|---|---|
task | string | yes | drift, harvest or review. Missing or unknown: the model picks the closest lane and says so in assumptions. |
household | string | no | A label for the title. |
holdings | string | yes | The holdings table as text, header row first. CSV, TSV, pipes or Markdown. The page sends at most 300 rows; every figure in facts already covers all rows. |
targets | string | yes | Class-and-percent pairs in any format. |
trades | string | no | Recent trades; purchases inside the 61-day wash window are what matter. |
notes | string | no | Context: ages, bracket, refusals, what the client asked, performance figures for the review. Up to 6,000 characters. |
facts | object | yes, in practice | The engine's arithmetic for the lane, including flags. The model may quote only numbers that appear here or in your text, so without facts it has almost nothing it is allowed to say. Compute it with the same module the page uses — see below. |
handoff | object | no | review only: { "drift": <envelope>, "harvest": <envelope> } from earlier runs. |
retry_note | string | no | Sent by the page on its one automatic reformat retry. Use it the same way. |
Computing facts outside the browser
The engine is a dependency-free module served at /holdkit.js. It attaches
itself to window.HoldKit, so in Node it runs with a one-line shim:
// node compute-facts.js > facts.json
global.window = {};
require("./holdkit.js"); // curl -sO https://rebalance-desk.skillsafe.ai/holdkit.js
const H = window.HoldKit;
const a = H.analyze({
holdings: require("fs").readFileSync("holdings.csv", "utf8"),
targets: "US Equity 55\nIntl Equity 20\nBonds 20\nCash 5",
trades: "", // optional
settings: { asOf: "2026-09-21", band: 5, relBand: 25, minTrade: 500, minLoss: 500, stRate: 35, ltRate: 18.8, concentration: 25, cashFlow: 0, mode: "full" },
household: "Okafor household"
});
if (!a.ok) throw new Error(a.errors.join(" "));
process.stdout.write(JSON.stringify(H.facts(a, "drift"))); // or "harvest" / "review"
H.facts(a, lane) returns exactly what the page sends: the allocation, accounts, top positions,
settings, the lane's flags, and — depending on the lane — rebalance (the trade list with
ids T-01… and totals) and harvest (candidates keyed by lot id). The lane's
contract refers back to those ids, so use the same facts object for the estimate and the run.
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": { ... } } }
Send your token as Authorization: Bearer … on every call. Tokens are scoped to this app; there is no separate slug header.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. |
payment_required | 402 | The balance is below min_credits. Call /estimate first and top up. |
forbidden | 403 | A guest token tried a metered run. Sign in for a personal token. |
not_found | 404 | Unknown job id or collection. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
validation_error | 422 | The body is not an object, or a field is the wrong type. A body that is not JSON at all is a 400. |
rate_limited | 429 | Back off and retry; do not tight-loop. |
internal | 5xx | Retry with the SAME Idempotency-Key so you are not billed twice. |
1. Get a token
The shortest path is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. A guest token can call /me and /estimate; running a lane is metered and needs a personal token.
# Open https://rebalance-desk.skillsafe.ai/tokens.html and press "Copy shell export".
# It writes: export SKILLSAFE_TOKEN="aut_..."
export SKILLSAFE_TOKEN="YOUR_TOKEN"
curl -s https://api.skillsafe.ai/v1/app-api/me -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# Open https://rebalance-desk.skillsafe.ai/tokens.html, press "Copy token", paste it into an environment variable.
import os
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
// Open https://rebalance-desk.skillsafe.ai/tokens.html, press "Copy token".
const TOKEN = "YOUR_TOKEN"; // or read it from your own secret store
// Open https://rebalance-desk.skillsafe.ai/tokens.html, press "Copy token".
token := os.Getenv("SKILLSAFE_TOKEN")
if token == "" { token = "YOUR_TOKEN" }
// Open https://rebalance-desk.skillsafe.ai/tokens.html, press "Copy token".
String token = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
# Open https://rebalance-desk.skillsafe.ai/tokens.html, press "Copy token".
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
<?php
// Open https://rebalance-desk.skillsafe.ai/tokens.html, press "Copy token".
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
// Open https://rebalance-desk.skillsafe.ai/tokens.html, press "Copy token".
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
2. A tiny client
Every call is the same three things: the base URL, your bearer token, JSON in and out.
# One helper: base URL, bearer token, JSON in, JSON out.
call() { # call <method> <path> [json-body] [idempotency-key]
local m=$1 p=$2 b=$3 k=$4
curl -s -X "$m" "https://api.skillsafe.ai/v1/app-api/$p" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
${k:+-H "Idempotency-Key: $k"} ${b:+--data-binary "$b"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None, key=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method=method)
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
if key: req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
env = json.load(r)
if not env.get("ok"): raise RuntimeError(env["error"])
return env["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body, key) {
const headers = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
if (key) headers["Idempotency-Key"] = key;
const res = await fetch(`${BASE}/${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
const env = await res.json();
if (!env.ok) throw Object.assign(new Error(env.error.message), env.error);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http"; "os")
const base = "https://api.skillsafe.ai/v1/app-api"
func call(method, path string, body any, key 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("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
if key != "" { req.Header.Set("Idempotency-Key", key) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Ok bool; Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if !env.Ok { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
import com.fasterxml.jackson.databind.*;
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient http = HttpClient.newHttpClient();
static final ObjectMapper om = new ObjectMapper();
static JsonNode call(String method, String path, Object body, String key) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json");
if (key != null) b.header("Idempotency-Key", key);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(om.writeValueAsString(body)));
var env = om.readTree(http.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (!env.get("ok").asBoolean()) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body = nil, key = nil)
uri = URI("#{BASE}/#{path}")
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key if key
req.body = body.to_json if body
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s unless env["ok"]
env["data"]
end
<?php
$BASE = "https://api.skillsafe.ai/v1/app-api";
function call($method, $path, $body = null, $key = null) {
global $BASE, $token;
$h = ["Authorization: Bearer $token", "Content-Type: application/json"];
if ($key) $h[] = "Idempotency-Key: $key";
$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 (!$env["ok"]) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class Desk {
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string method, string path, object? body = null, string? key = null) {
var req = new HttpRequestMessage(new HttpMethod(method), $"{Base}/{path}");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
if (key != null) req.Headers.Add("Idempotency-Key", key);
if (body != null) req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var env = JsonDocument.Parse(await (await Http.SendAsync(req)).Content.ReadAsStringAsync()).RootElement;
if (!env.GetProperty("ok").GetBoolean()) throw new Exception(env.GetProperty("error").ToString());
return env.GetProperty("data");
}
}
3. Check the session and the balance
/me returns subject_type (user or guest), subject_id and credits. Compare credits against the estimate's min_credits before running.
call GET me
# {"ok":true,"data":{"subject_type":"user","subject_id":"...","credits":184220}}
me = call("GET", "me")
print(me["subject_type"], me["credits"]) # "user" means a personal token; a guest cannot run
const me = await call("GET", "me");
console.log(me.subject_type, me.credits);
me, err := call("GET", "me", nil, "")
if err != nil { panic(err) }
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = call("GET", "me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = call("GET", "me")
puts "#{me["subject_type"]} #{me["credits"]}"
<?php
$me = call("GET", "me");
echo $me["subject_type"], " ", $me["credits"], "\n";
var me = await Desk.Call("GET", "me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
4. Price the run - free
POST /estimate with the exact body you will run. No charge, no job. It returns hold_credits (reserved for the run, priced at the full output cap), min_credits (below which a run is refused), model, model_alias and markup_bps. Price each lane separately: the prompt sections differ, so the hold differs. Between min_credits and hold_credits the run still executes with a reduced output cap and comes back truncated: true.
# INPUT is the drift-lane body from the worked examples below, with facts from holdkit.js.
INPUT=$(cat drift-input.json)
call POST estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,
# "hold_credits":6120,"min_credits":612,"sponsor_enabled":false}}
INPUT = json.load(open("drift-input.json")) # the drift-lane body below
est = call("POST", "estimate", INPUT)
print(est["hold_credits"], "reserved;", est["min_credits"], "minimum;", est["model"])
assert est["model_alias"] == "gpt-terra"
if me["credits"] < est["min_credits"]:
raise SystemExit("top up before running")
const INPUT = JSON.parse(await fs.readFile("drift-input.json", "utf8"));
const est = await call("POST", "estimate", INPUT);
console.log(est.hold_credits, "reserved;", est.min_credits, "minimum;", est.model);
if (me.credits < est.min_credits) throw new Error("top up before running");
raw, _ := os.ReadFile("drift-input.json")
var input map[string]any
json.Unmarshal(raw, &input)
est, err := call("POST", "estimate", input, "")
if err != nil { panic(err) }
fmt.Println(est["hold_credits"], "reserved;", est["min_credits"], "minimum;", est["model"])
JsonNode input = om.readTree(new java.io.File("drift-input.json"));
JsonNode est = call("POST", "estimate", input, null);
System.out.println(est.get("hold_credits") + " reserved; " + est.get("min_credits") + " minimum; " + est.get("model"));
input = JSON.parse(File.read("drift-input.json"))
est = call("POST", "estimate", input)
puts "#{est["hold_credits"]} reserved; #{est["min_credits"]} minimum; #{est["model"]}"
<?php
$input = json_decode(file_get_contents("drift-input.json"), true);
$est = call("POST", "estimate", $input);
echo $est["hold_credits"], " reserved; ", $est["min_credits"], " minimum; ", $est["model"], "\n";
var input = JsonDocument.Parse(File.ReadAllText("drift-input.json")).RootElement;
var est = await Desk.Call("POST", "estimate", input);
Console.WriteLine($"{est.GetProperty("hold_credits")} reserved; {est.GetProperty("min_credits")} minimum; {est.GetProperty("model")}");
5. Run it, then poll
Always send an Idempotency-Key derived from the body and the task. A retried request with the same key returns the same job and is never billed twice; a deliberate re-run increments the attempt suffix.
# Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
KEY="rebalance-desk:drift:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(call POST run "$INPUT" "$KEY" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
until call GET "jobs/$JOB" | grep -q '"status":"\(succeeded\|failed\)"'; do sleep 2; done
OUT=$(call GET "jobs/$JOB")
printf '%s' "$OUT" | python3 -c 'import sys,json;d=json.load(sys.stdin)["data"];print(d["status"], d.get("charged_credits"), d.get("truncated"))'
import hashlib, time
# Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
key = "rebalance-desk:%s:%s:a1" % (INPUT["task"], hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16])
job = call("POST", "run", INPUT, key)
while True:
j = call("GET", f"jobs/{job['job_id']}")
if j["status"] in ("succeeded", "failed"): break
time.sleep(2)
print(j["status"], j.get("charged_credits"), "credits; truncated =", j.get("truncated"))
raw = j["output"]["output"] # one JSON object as a string - see "Parse the envelope"
import { createHash } from "node:crypto";
// Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
const key = `rebalance-desk:${INPUT.task}:${createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16)}:a1`;
const job = await call("POST", "run", INPUT, key);
let j;
for (;;) {
j = await call("GET", `jobs/${job.job_id}`);
if (j.status === "succeeded" || j.status === "failed") break;
await new Promise((r) => setTimeout(r, 2000));
}
console.log(j.status, j.charged_credits, "credits; truncated =", j.truncated);
const raw = j.output.output;
// Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
sum := sha256.Sum256(raw)
key := fmt.Sprintf("rebalance-desk:%s:%x:a1", input["task"], sum[:8])
job, err := call("POST", "run", input, key)
if err != nil { panic(err) }
var j map[string]any
for {
j, _ = call("GET", "jobs/"+job["job_id"].(string), nil, "")
if s := j["status"]; s == "succeeded" || s == "failed" { break }
time.Sleep(2 * time.Second)
}
fmt.Println(j["status"], j["charged_credits"], j["truncated"])
// Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(om.writeValueAsBytes(input));
String key = "rebalance-desk:" + input.get("task").asText() + ":" + java.util.HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = call("POST", "run", input, key);
JsonNode j;
while (true) {
j = call("GET", "jobs/" + job.get("job_id").asText(), null, null);
String s = j.get("status").asText();
if (s.equals("succeeded") || s.equals("failed")) break;
Thread.sleep(2000);
}
System.out.println(j.get("status") + " " + j.get("charged_credits") + " truncated=" + j.get("truncated"));
require "digest"
# Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
key = "rebalance-desk:#{input["task"]}:#{Digest::SHA256.hexdigest(input.to_json)[0, 16]}:a1"
job = call("POST", "run", input, key)
loop do
@j = call("GET", "jobs/#{job["job_id"]}")
break if %w[succeeded failed].include?(@j["status"])
sleep 2
end
puts "#{@j["status"]} #{@j["charged_credits"]} truncated=#{@j["truncated"]}"
<?php
// Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
$key = "rebalance-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = call("POST", "run", $input, $key);
do { sleep(2); $j = call("GET", "jobs/" . $job["job_id"]); } while (!in_array($j["status"], ["succeeded", "failed"]));
echo $j["status"], " ", $j["charged_credits"], " truncated=", var_export($j["truncated"] ?? false, true), "\n";
using System.Security.Cryptography;
// Idempotency-Key = rebalance-desk:<task>:<sha256 of the body>:a1 - the lane is part of the key, so two lanes over one paste are two runs, and a retried request with the same key is never billed twice.
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input.GetRawText())))[..16];
var key = $"rebalance-desk:{input.GetProperty("task").GetString()}:{hash}:a1";
var job = await Desk.Call("POST", "run", input, key);
JsonElement j;
while (true) {
j = await Desk.Call("GET", $"jobs/{job.GetProperty("job_id").GetString()}");
var s = j.GetProperty("status").GetString();
if (s == "succeeded" || s == "failed") break;
await Task.Delay(2000);
}
Console.WriteLine($"{j.GetProperty("status")} {j.GetProperty("charged_credits")}");
6. Or stream it
The same body to /run-stream returns server-sent events. Note that in a browser the platform currently sends heartbeat ticks rather than deltas; from a server-side client the deltas arrive as shown.
# Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
curl -sN -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" --data-binary "$INPUT"
# event: job data: {"job_id":"job_..."}
# event: delta data: {"text":"{\"lane\":\"drift\","}
# ...
# event: done data: {"status":"succeeded","charged_credits":2210,"truncated":false,"output":{"output":"{...}"}}
# Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
for k, v in {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", "Idempotency-Key": key}.items(): req.add_header(k, v)
raw, event = "", None
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event:"): event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:])
if event == "delta": raw += data.get("text", "")
elif event == "done": job = data
raw = job["output"]["output"] or raw
// Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
const res = await fetch(`${BASE}/run-stream`, { method: "POST", headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key }, body: JSON.stringify(INPUT) });
const reader = res.body.getReader(); const dec = new TextDecoder();
let buf = "", raw = "", done = null;
for (;;) {
const { value, done: end } = await reader.read(); if (end) break;
buf += dec.decode(value, { stream: true });
let i; while ((i = buf.indexOf("\n\n")) >= 0) {
const frame = buf.slice(0, i); buf = buf.slice(i + 2);
const ev = (frame.match(/^event:\s*(\S+)/m) || [])[1]; const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
if (!data) continue; const d = JSON.parse(data);
if (ev === "delta") raw += d.text || ""; else if (ev === "done") done = d;
}
}
raw = (done && done.output && done.output.output) || raw;
// Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(raw))
req.Header.Set("Authorization", "Bearer "+token); req.Header.Set("Content-Type", "application/json"); req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var event, out string
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "event:") { event = strings.TrimSpace(line[6:]) }
if strings.HasPrefix(line, "data:") {
var d map[string]any; json.Unmarshal([]byte(line[5:]), &d)
if event == "delta" { out += d["text"].(string) }
if event == "done" { fmt.Println("charged", d["charged_credits"], "truncated", d["truncated"]) }
}
}
// Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
var req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json").header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(om.writeValueAsString(input))).build();
var lines = http.send(req, HttpResponse.BodyHandlers.ofLines()).body().iterator();
String event = null; StringBuilder out = new StringBuilder();
while (lines.hasNext()) {
String line = lines.next();
if (line.startsWith("event:")) event = line.substring(6).trim();
else if (line.startsWith("data:")) {
JsonNode d = om.readTree(line.substring(5));
if ("delta".equals(event)) out.append(d.path("text").asText(""));
else if ("done".equals(event)) System.out.println("charged " + d.get("charged_credits") + " truncated " + d.get("truncated"));
}
}
# Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json", "Idempotency-Key" => key)
req.body = input.to_json
raw = ""; event = nil
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
event = line[6..].strip if line.start_with?("event:")
if line.start_with?("data:")
d = JSON.parse(line[5..])
raw << d["text"].to_s if event == "delta"
puts "charged #{d["charged_credits"]} truncated #{d["truncated"]}" if event == "done"
end
end
end
end
end
<?php
// Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
$ch = curl_init("$BASE/run-stream");
$raw = ""; $event = null;
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json", "Idempotency-Key: $key"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:")) { $d = json_decode(substr($line, 5), true);
if ($event === "delta") $raw .= $d["text"] ?? "";
elseif ($event === "done") echo "charged ", $d["charged_credits"], " truncated ", var_export($d["truncated"] ?? false, true), "\n"; }
}
return strlen($chunk);
}]);
curl_exec($ch);
// Server-sent events: `event: delta` frames carry chunks of the JSON envelope; the final `event: done` carries the job with charged_credits and truncated. Send the same Idempotency-Key as you would to /run.
var sreq = new HttpRequestMessage(HttpMethod.Post, $"{Desk.Base}/run-stream") { Content = new StringContent(input.GetRawText(), Encoding.UTF8, "application/json") };
sreq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); sreq.Headers.Add("Idempotency-Key", key);
using var stream = await (await Http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead)).Content.ReadAsStreamAsync();
using var rd = new StreamReader(stream);
string? ev = null; var raw = new StringBuilder();
while (await rd.ReadLineAsync() is string line) {
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:")) {
var d = JsonDocument.Parse(line[5..]).RootElement;
if (ev == "delta") raw.Append(d.TryGetProperty("text", out var t) ? t.GetString() : "");
else if (ev == "done") Console.WriteLine($"charged {d.GetProperty("charged_credits")}");
}
}
Worked examples, one per lane
These are the Okafor household bodies the page itself sends, with the holdings and the facts arrays abbreviated to their first entries, and the replies a model produced for them under this app's system prompt. Ids in the reply (T-01, L004, DRIFT-US-EQUITY) always refer back to ids in facts.
task: "drift" - the rebalance
Request body
{
"task": "drift",
"household": "Okafor household - Q3 2026 review",
"holdings": "account,account type,ticker,name,asset class,quantity,price,cost basis,acquired\nOkafor Joint Brokerage,taxable,VTI,Vanguard Total Stock Market ETF,US Equity,620,298.40,112400,2020-04-14\nOkafor Joint Brokerage,taxable,VTI,Vanguard Total Stock Market ETF,US Equity,140,298.40,39900,2026-01-22\n...(10 more rows)",
"targets": "US Equity 55\nIntl Equity 20\nBonds 20\nCash 5",
"trades": "date,side,ticker,quantity,account\n2026-09-03,buy,VXUS,4.2,Okafor Joint Brokerage\n2026-08-28,buy,BND,3.1,Ada Okafor Traditional IRA\n2026-07-15,sell,QQQ,20,Okafor Joint Brokerage",
"notes": "Ada and Emeka, both 54, retiring in about eight years. Household income puts them in the 35% bracket, 15% on long-term gains plus 3.8% NIIT. Emeka asked last quarter why international keeps lagging. They added $40,000 to the joint account in July and want it invested. Q3 performance: household +3.1% for the quarter, +9.4% year to date; the 55/20/20/5 benchmark blend did +2.8% and +8.9%.",
"facts": {
"as_of": "2026-09-21",
"household": "Okafor household - Q3 2026 review",
"total_value": 520320.5,
"value_after_cashflow": 520320.5,
"accounts": [
{
"account": "Okafor Joint Brokerage",
"type": "taxable",
"value": 354778.5,
"pct": 68.2,
"lots": 8
},
{
"account": "Ada Okafor Traditional IRA",
"type": "ira",
"value": 107977,
"pct": 20.8,
"lots": 3
},
"..."
],
"allocation": [
{
"cls": "US Equity",
"value": 385860.5,
"pct": 74.16,
"target": 55,
"drift_pp": 19.16,
"band_pp": 5,
"status": "over",
"delta_usd": -99684.22
},
{
"cls": "Intl Equity",
"value": 45344,
"pct": 8.71,
"target": 20,
"drift_pp": -11.29,
"band_pp": 5,
"status": "under",
"delta_usd": 58720.1
},
"..."
],
"max_drift_pp": 19.16,
"classes_outside_band": 3,
"top_positions": [
{
"ticker": "VTI",
"cls": "US Equity",
"value": 337192,
"pct": 64.8
},
{
"ticker": "BND",
"cls": "Bonds",
"value": 72956,
"pct": 14.02
},
"..."
],
"settings": {
"band_pp": 5,
"rel_band_pct": 25,
"min_trade_usd": 500,
"min_loss_usd": 500,
"short_term_rate_pct": 35,
"long_term_rate_pct": 18.8,
"cash_flow_usd": 0,
"mode": "full"
},
"flags": [
{
"id": "DRIFT-US-EQUITY",
"label": "US Equity is 74.2% against 55.0% - +19.2% with a 5.0% band; sell $99,684"
},
{
"id": "DRIFT-INTL-EQUITY",
"label": "Intl Equity is 8.7% against 20.0% - -11.3% with a 5.0% band; buy $58,720"
},
"..."
],
"rebalance": {
"trades": [
{
"id": "T-01",
"side": "sell",
"account": "Ada Okafor Traditional IRA",
"account_type": "ira",
"ticker": "VTI",
"cls": "US Equity",
"shares": 210,
"price": 298.4,
"amount": 62664,
"lot": "L009",
"term": "long",
"realized_gain": 18564,
"tax_est": 0,
"placeholder": false
},
{
"id": "T-02",
"side": "sell",
"account": "Emeka Okafor Roth IRA",
"account_type": "roth",
"ticker": "VTI",
"cls": "US Equity",
"shares": 124,
"price": 298.4,
"amount": 37001.6,
"lot": "L012",
"term": "long",
"realized_gain": 13441.6,
"tax_est": 0,
"placeholder": false
},
"..."
],
"sell_total": 99665.6,
"buy_total": 79779.55,
"turnover_pct": 19.15,
"realized_gain": 32005.6,
"realized_short_term": 0,
"realized_long_term": 0,
"tax_est": 0,
"leftover_cash": 26086.05,
"post_trade_max_drift_pp": 0.01,
"notes": [],
"post_trade_allocation": [
{
"cls": "US Equity",
"pct": 55,
"target": 55,
"drift_pp": 0
},
{
"cls": "Intl Equity",
"pct": 19.99,
"target": 20,
"drift_pp": -0.01
},
"..."
]
}
}
}
data.output.output, parsed
{
"lane": "drift",
"title": "Okafor household - Q3 2026 review — Drift rebalance",
"posture": "rebalance-now",
"verdict": "US Equity's 19.2pp overweight against its 5.0pp band (74.2% vs. 55.0%) forces action, and the engine's four trades — all inside the Traditional IRA and Roth — bring every class back in-band (post-trade max drift 0.01pp) at zero tax cost.",
"summary": "Three of four classes are outside band: US Equity is 19.2pp over (74.2% vs. 55.0%), Intl Equity is 11.3pp under (8.7% vs. 20.0%), and Cash is 3.8pp under its tight 1.3pp band (1.2% vs. 5.0%); Bonds is 4.1pp under but stays inside its 5.0pp band (15.9% vs. 20.0%). The plan sells $99,665.60 of VTI from the Traditional IRA and Roth (lots L009 and L012) and buys $58,664.40 of VXUS and $21,115.15 of BND with the proceeds, leaving $26,086.05 unspent as cash; every trade sits inside a tax-advantaged account, so the $32,005.60 of realized gain costs $0 in tax. The taxable Okafor Joint Brokerage account, and the CONC-VTI concentration flag, are both left untouched by this plan.",
"assumptions": [
"task was given as \"drift\"; produced the drift-lane contract only.",
"Treated the engine's leftover_cash of $26,086.05 as satisfying the Cash class rather than as an unfunded amount, since facts.rebalance.post_trade_allocation shows Cash at 5.01%, in-band.",
"..."
],
"open_questions": [
"The notes say $40,000 was added to the joint account in July and the client wants it invested, but the holdings only show $6,200 in the settlement fund — where that $40,000 sits now isn't shown in facts or holdings.",
"The CONC-VTI flag (VTI at 64.8% of the household, per top_positions) isn't resolved by this drift plan, since the taxable VTI lots (620 and 140 shares) aren't sold — whether the household wants a separate plan for that is open.",
"..."
],
"reconciliation": [
{
"id": "DRIFT-US-EQUITY",
"addressed": true,
"note": "T-01 and T-02 together sell $99,665.60 of VTI out of the Traditional IRA and Roth, bringing US Equity from 74.16% to the post-trade 55%."
},
{
"id": "DRIFT-INTL-EQUITY",
"addressed": true,
"note": "T-03 buys $58,664.40 of VXUS in the Traditional IRA, bringing Intl Equity to the post-trade 19.99%."
},
"..."
],
"rebalance": {
"class_calls": [
{
"cls": "US Equity",
"status": "over",
"call": "At 74.2% against a 55.0% target (5.0pp band), US Equity is 19.2pp over. The plan sells $62,664 of VTI (lot L009, long-term) from the Traditional IRA and $37,001.60 of VTI (lot L012, long-term) from the Roth IRA — $99,665.60 total — bringing it to the post-trade 55.0%.",
"rationale": "Selling inside the IRA and Roth realizes the $32,005.60 combined gain (T-01 $18,564, T-02 $13,441.60) without a taxable event (tax_est $0 on both), avoiding the household's 15% long-term rate plus 3.8% NIIT that a taxable sale of the same size would trigger."
},
{
"cls": "Intl Equity",
"status": "under",
"call": "At 8.7% against a 20.0% target, Intl Equity is 11.3pp under. The plan buys $58,664.40 of VXUS (996 shares) in the Traditional IRA, funded by the VTI sale proceeds, bringing it to the post-trade 19.99% — closing all but a dime of the $58,720.10 gap.",
"rationale": "VXUS is already the household's international holding, so adding to it in the IRA closes the gap directly; Emeka's question last quarter about international lagging can be answered with the fact that it's being brought up from 11.3pp under to essentially full target weight."
},
"..."
],
"trade_notes": [
{
"id": "T-01",
"keep": true,
"note": "Sell 210 VTI (lot L009, long-term, realized gain $18,564) from the Traditional IRA. Selling here rather than in the taxable brokerage means no tax on the gain (tax_est $0), making it the lowest-cost way to trim the US Equity overweight."
},
{
"id": "T-02",
"keep": true,
"note": "Sell 124 VTI (lot L012, long-term, realized gain $13,441.60) from the Roth IRA — again no tax on the gain (tax_est $0). Combined with T-01 this covers $99,665.60 of the $99,684.22 US Equity needs to come down by."
},
"..."
],
"sequence": [
"Sell 210 VTI (lot L009) from the Traditional IRA — T-01, $62,664, no tax.",
"Sell 124 VTI (lot L012) from the Roth IRA — T-02, $37,001.60, no tax.",
"..."
],
"asset_location": "All four trades sit inside the Traditional IRA and the Roth IRA; the taxable Okafor Joint Brokerage account is untouched. That's the tax-efficient placement here: selling $99,665.60 of the overweight VTI inside retirement accounts realizes $32,005.60 of gain at $0 tax (tax_est $0 on both T-01 and T-02) instead of the household's 15% long-term rate plus 3.8% NIIT that a taxable sale of the same size would trigger. The Intl Equity buy (T-03, VXUS) lands in the Traditional IRA and the Bonds buy (T-04, BND) lands in the Roth, both funded entirely by IRA/Roth sale proceeds, so no new cash is drawn from the taxable account or the $40,000 added there in July.",
"tax_note": "This plan realizes $32,005.60 of gain across T-01 and T-02 (both long-term per facts), but because both sales are inside the Traditional IRA and Roth, the engine's tax_est is $0 on each trade and the household-level realized_short_term and realized_long_term both come out to $0. Nothing in this plan is a taxable event this year, and it doesn't touch the 35% bracket or the 15% long-term rate plus 3.8% NIIT the notes mention.",
"watch": [
"US Equity drifting back over its 5.0pp band if it keeps outperforming — the taxable VTI lots (620 and 140 shares, per holdings) aren't touched by this plan and remain the household's largest single exposure.",
"Whether the $40,000 added to the joint account in July is still sitting uninvested somewhere the holdings don't show as cash — the settlement fund shows only $6,200.",
"..."
],
"client_questions": [
"Are you comfortable selling VTI out of the Traditional IRA and Roth specifically (T-01, T-02), rather than the taxable account, given the $99,684.22 US Equity overweight is mostly concentrated in the taxable VTI lots (620 and 140 shares)?",
"The notes mention $40,000 added to the joint account in July that you want invested — the holdings only show $6,200 in the settlement fund, so is that $40,000 already deployed, and if so into what?",
"..."
],
"deviations": []
}
}
task: "harvest" - the tax-loss lots
Request body
{
"task": "harvest",
"household": "Okafor household - Q3 2026 review",
"holdings": "account,account type,ticker,name,asset class,quantity,price,cost basis,acquired\nOkafor Joint Brokerage,taxable,VTI,Vanguard Total Stock Market ETF,US Equity,620,298.40,112400,2020-04-14\nOkafor Joint Brokerage,taxable,VTI,Vanguard Total Stock Market ETF,US Equity,140,298.40,39900,2026-01-22\n...(10 more rows)",
"targets": "US Equity 55\nIntl Equity 20\nBonds 20\nCash 5",
"trades": "date,side,ticker,quantity,account\n2026-09-03,buy,VXUS,4.2,Okafor Joint Brokerage\n2026-08-28,buy,BND,3.1,Ada Okafor Traditional IRA\n2026-07-15,sell,QQQ,20,Okafor Joint Brokerage",
"notes": "Ada and Emeka, both 54, retiring in about eight years. Household income puts them in the 35% bracket, 15% on long-term gains plus 3.8% NIIT. Emeka asked last quarter why international keeps lagging. They added $40,000 to the joint account in July and want it invested. Q3 performance: household +3.1% for the quarter, +9.4% year to date; the 55/20/20/5 benchmark blend did +2.8% and +8.9%.",
"facts": {
"as_of": "2026-09-21",
"household": "Okafor household - Q3 2026 review",
"total_value": 520320.5,
"value_after_cashflow": 520320.5,
"accounts": [
{
"account": "Okafor Joint Brokerage",
"type": "taxable",
"value": 354778.5,
"pct": 68.2,
"lots": 8
},
{
"account": "Ada Okafor Traditional IRA",
"type": "ira",
"value": 107977,
"pct": 20.8,
"lots": 3
},
"..."
],
"allocation": [
{
"cls": "US Equity",
"value": 385860.5,
"pct": 74.16,
"target": 55,
"drift_pp": 19.16,
"band_pp": 5,
"status": "over",
"delta_usd": -99684.22
},
{
"cls": "Intl Equity",
"value": 45344,
"pct": 8.71,
"target": 20,
"drift_pp": -11.29,
"band_pp": 5,
"status": "under",
"delta_usd": 58720.1
},
"..."
],
"max_drift_pp": 19.16,
"classes_outside_band": 3,
"top_positions": [
{
"ticker": "VTI",
"cls": "US Equity",
"value": 337192,
"pct": 64.8
},
{
"ticker": "BND",
"cls": "Bonds",
"value": 72956,
"pct": 14.02
},
"..."
],
"settings": {
"band_pp": 5,
"rel_band_pct": 25,
"min_trade_usd": 500,
"min_loss_usd": 500,
"short_term_rate_pct": 35,
"long_term_rate_pct": 18.8,
"cash_flow_usd": 0,
"mode": "full"
},
"flags": [
{
"id": "HARVEST-L004",
"label": "Lot L004: 410 VXUS in Okafor Joint Brokerage, short-term, unrealized loss $5,701 (19.1%), est. benefit $1,995"
},
{
"id": "HARVEST-L006",
"label": "Lot L006: 380 BND in Okafor Joint Brokerage, long-term, unrealized loss $2,983 (10.1%), est. benefit $561"
},
"..."
],
"harvest": {
"window_start": "2026-08-22",
"safe_rebuy_date": "2026-10-22",
"total_loss": 11024,
"short_term_loss": 5701,
"long_term_loss": 5323,
"est_benefit": 2996.07,
"clean_loss": 2340,
"clean_benefit": 439.92,
"wash_risk_lots": 2,
"skipped": {
"advantaged": 3,
"noBasis": 0,
"small": 1,
"gains": 3
},
"candidates": [
{
"lot": "L004",
"account": "Okafor Joint Brokerage",
"ticker": "VXUS",
"cls": "Intl Equity",
"shares": 410,
"value": 24149,
"basis": 29850,
"loss": 5701,
"loss_pct": 19.1,
"term": "short",
"acquired": "2025-10-06",
"rate_pct": 35,
"est_benefit": 1995.35,
"wash_risk": true,
"wash": [
"Bought 4.2 VXUS on 2026-09-03 in Okafor Joint Brokerage - inside the 30 days before a sale, so the loss on the matching shares is disallowed.",
"The rebalance plan BUYS VXUS; harvesting and buying the same security inside 61 days is a wash sale.",
"..."
],
"safe_rebuy_date": "2026-10-22",
"long_term_on": "2026-10-07"
},
{
"lot": "L006",
"account": "Okafor Joint Brokerage",
"ticker": "BND",
"cls": "Bonds",
"shares": 380,
"value": 26657,
"basis": 29640,
"loss": 2983,
"loss_pct": 10.1,
"term": "long",
"acquired": "2022-03-11",
"rate_pct": 18.8,
"est_benefit": 560.8,
"wash_risk": true,
"wash": [
"Bought 3.1 BND on 2026-08-28 in Ada Okafor Traditional IRA - inside the 30 days before a sale, so the loss on the matching shares is disallowed.",
"The rebalance plan BUYS BND; harvesting and buying the same security inside 61 days is a wash sale.",
"..."
],
"safe_rebuy_date": "2026-10-22",
"long_term_on": null
},
"..."
]
}
}
}
data.output.output, parsed
{
"lane": "harvest",
"title": "Okafor household - Q3 2026 review — Tax-Loss Harvest",
"posture": "harvest-selectively",
"verdict": "Only lot L005 (VWO, $2,340 long-term loss) is clear of wash-sale risk today; L004 (VXUS) and L006 (BND) each have an actual purchase of the same fund already inside the 30-day look-back window, so they wait until it clears on 2026-10-22.",
"summary": "Of the three loss lots the engine surfaced in the taxable account, only VWO (L005) can be harvested today, for an estimated $439.92 benefit on a $2,340 long-term loss. VXUS (L004) and BND (L006) both carry real wash-sale exposure — 4.2 shares of VXUS were bought 2026-09-03 in the same brokerage account, and 3.1 shares of BND were bought 2026-08-28 in Ada's Traditional IRA — so selling either now would disallow part of the loss; both wait for the window to clear on 2026-10-22. Three additional losing lots sit inside the IRA and Roth accounts, where a loss has no tax value and nothing is done with them.",
"assumptions": [
"No refusal to sell or trade was stated in notes for this household; harvesting was evaluated on wash-sale exposure and benefit alone.",
"The $40,000 added to the joint account in July is treated as already reflected in the holdings and facts shown (the engine's cash_flow_usd is 0), not as new cash to be deployed alongside this harvest."
],
"open_questions": [
"State tax treatment of the harvested loss is unknown — only federal rates (35% short-term, 18.8% long-term) were supplied.",
"Whether the household has capital-loss carryforward from prior years.",
"..."
],
"reconciliation": [
{
"id": "HARVEST-L004",
"addressed": true,
"note": "Marked wait, not harvested this cycle: the wash risk is a real, already-executed purchase (4.2 shares of VXUS on 2026-09-03 in the same account), not only a plan-buy collision, so a guardrail on new VXUS purchases applies instead of a sale today."
},
{
"id": "HARVEST-L006",
"addressed": true,
"note": "Marked wait, not harvested this cycle: 3.1 shares of BND were actually bought 2026-08-28 in Ada's Traditional IRA, inside the look-back window, so a guardrail on new BND purchases applies instead of a sale today."
},
"..."
],
"harvest": {
"lots": [
{
"lot": "L004",
"action": "wait",
"note": "Wash risk here is real, not just a plan collision: 4.2 shares of VXUS were bought 2026-09-03 in this same taxable account, inside the 30-day look-back, and VXUS is also held in Ada's Traditional IRA, where a purchase or reinvestment would trigger a wash sale across accounts. Separately, this lot turns long-term on 2026-10-07 — about two weeks before the wash window clears on 2026-10-22 — so by the time a sale is safe, the $1,995.35 estimated benefit (priced at the 35% short-term rate) will no longer apply; a long-term sale would price at the 18.8% rate instead, which the engine has not calculated.",
"replacement_exposure": "Not applicable now — no sale is being made. If a sale is revisited after 2026-10-22, the interim holding would be a broad international developed- and emerging-markets index fund that is not substantially identical to VXUS.",
"safe_rebuy_date": "2026-10-22"
},
{
"lot": "L006",
"action": "wait",
"note": "Wash risk here is real, not just a plan collision: 3.1 shares of BND were bought 2026-08-28 in Ada's Traditional IRA, inside the 30-day look-back and across accounts. BND is also held in Emeka's Roth IRA. This lot is already long-term, so its $560.80 estimated benefit is priced at the 18.8% rate and does not change with more waiting — only the wash exposure does.",
"replacement_exposure": "Not applicable now — no sale is being made. If revisited after 2026-10-22, the interim holding would be a broad investment-grade bond index fund that is not substantially identical to BND.",
"safe_rebuy_date": "2026-10-22"
},
"..."
],
"order": [
"L005"
],
"benefit_claimed": 439.92,
"guardrails": [
"No purchase, dividend reinvestment, or rebalance buy of VXUS in the Okafor Joint Brokerage or Ada Okafor Traditional IRA before 2026-10-22.",
"No purchase, dividend reinvestment, or rebalance buy of BND in the Okafor Joint Brokerage, Ada Okafor Traditional IRA, or Emeka Okafor Roth IRA before 2026-10-22.",
"..."
],
"client_explainer": "Harvesting a loss means selling a position worth less than you paid for it, so the loss can offset other capital gains this year or up to $3,000 of ordinary income, with anything unused carried forward. It does not change your investment mix by itself — the idea is to stay invested in something similar but not identical for at least 31 days, then decide whether to come back to the original fund. Right now only one lot, VWO, is clear to harvest today, for an estimated $439.92 benefit on a $2,340 loss. The other two candidates, VXUS and BND, both had real purchases of the same fund inside the last 30 days — in the brokerage account and in Ada's IRA — so selling either now would disallow part of the loss under the wash-sale rule; we wait until the window clears on 2026-10-22 and avoid any new purchases of those two funds until then. Losses sitting inside the IRA or Roth IRA are not eligible for this treatment at all, because a loss inside a tax-advantaged account has no tax value.",
"questions": [
"Do you have any capital-loss carryforward from prior years that this year's harvested loss would stack on top of?",
"What is your state's tax treatment of capital losses — the 35% and 18.8% rates used here are federal only?",
"..."
]
}
}
task: "review" - the meeting
Request body
{
"task": "review",
"household": "Okafor household - Q3 2026 review",
"holdings": "account,account type,ticker,name,asset class,quantity,price,cost basis,acquired\nOkafor Joint Brokerage,taxable,VTI,Vanguard Total Stock Market ETF,US Equity,620,298.40,112400,2020-04-14\nOkafor Joint Brokerage,taxable,VTI,Vanguard Total Stock Market ETF,US Equity,140,298.40,39900,2026-01-22\n...(10 more rows)",
"targets": "US Equity 55\nIntl Equity 20\nBonds 20\nCash 5",
"trades": "date,side,ticker,quantity,account\n2026-09-03,buy,VXUS,4.2,Okafor Joint Brokerage\n2026-08-28,buy,BND,3.1,Ada Okafor Traditional IRA\n2026-07-15,sell,QQQ,20,Okafor Joint Brokerage",
"notes": "Ada and Emeka, both 54, retiring in about eight years. Household income puts them in the 35% bracket, 15% on long-term gains plus 3.8% NIIT. Emeka asked last quarter why international keeps lagging. They added $40,000 to the joint account in July and want it invested. Q3 performance: household +3.1% for the quarter, +9.4% year to date; the 55/20/20/5 benchmark blend did +2.8% and +8.9%.",
"facts": {
"as_of": "2026-09-21",
"household": "Okafor household - Q3 2026 review",
"total_value": 520320.5,
"value_after_cashflow": 520320.5,
"accounts": [
{
"account": "Okafor Joint Brokerage",
"type": "taxable",
"value": 354778.5,
"pct": 68.2,
"lots": 8
},
{
"account": "Ada Okafor Traditional IRA",
"type": "ira",
"value": 107977,
"pct": 20.8,
"lots": 3
},
"..."
],
"allocation": [
{
"cls": "US Equity",
"value": 385860.5,
"pct": 74.16,
"target": 55,
"drift_pp": 19.16,
"band_pp": 5,
"status": "over",
"delta_usd": -99684.22
},
{
"cls": "Intl Equity",
"value": 45344,
"pct": 8.71,
"target": 20,
"drift_pp": -11.29,
"band_pp": 5,
"status": "under",
"delta_usd": 58720.1
},
"..."
],
"max_drift_pp": 19.16,
"classes_outside_band": 3,
"top_positions": [
{
"ticker": "VTI",
"cls": "US Equity",
"value": 337192,
"pct": 64.8
},
{
"ticker": "BND",
"cls": "Bonds",
"value": 72956,
"pct": 14.02
},
"..."
],
"settings": {
"band_pp": 5,
"rel_band_pct": 25,
"min_trade_usd": 500,
"min_loss_usd": 500,
"short_term_rate_pct": 35,
"long_term_rate_pct": 18.8,
"cash_flow_usd": 0,
"mode": "full"
},
"flags": [
{
"id": "REVIEW-DRIFT",
"label": "3 class(es) outside band; the largest drift is 19.2%; the plan trades $99,666 of sales and $79,780 of purchases"
},
{
"id": "REVIEW-HARVEST",
"label": "3 harvest lot(s) carrying $11,024 of losses; $2,340 is clear of wash-sale risk"
},
"..."
],
"rebalance": {
"trades": [
{
"id": "T-01",
"side": "sell",
"account": "Ada Okafor Traditional IRA",
"account_type": "ira",
"ticker": "VTI",
"cls": "US Equity",
"shares": 210,
"price": 298.4,
"amount": 62664,
"lot": "L009",
"term": "long",
"realized_gain": 18564,
"tax_est": 0,
"placeholder": false
},
{
"id": "T-02",
"side": "sell",
"account": "Emeka Okafor Roth IRA",
"account_type": "roth",
"ticker": "VTI",
"cls": "US Equity",
"shares": 124,
"price": 298.4,
"amount": 37001.6,
"lot": "L012",
"term": "long",
"realized_gain": 13441.6,
"tax_est": 0,
"placeholder": false
},
"..."
],
"sell_total": 99665.6,
"buy_total": 79779.55,
"turnover_pct": 19.15,
"realized_gain": 32005.6,
"realized_short_term": 0,
"realized_long_term": 0,
"tax_est": 0,
"leftover_cash": 26086.05,
"post_trade_max_drift_pp": 0.01,
"notes": [],
"post_trade_allocation": [
{
"cls": "US Equity",
"pct": 55,
"target": 55,
"drift_pp": 0
},
{
"cls": "Intl Equity",
"pct": 19.99,
"target": 20,
"drift_pp": -0.01
},
"..."
]
},
"harvest": {
"window_start": "2026-08-22",
"safe_rebuy_date": "2026-10-22",
"total_loss": 11024,
"short_term_loss": 5701,
"long_term_loss": 5323,
"est_benefit": 2996.07,
"clean_loss": 2340,
"clean_benefit": 439.92,
"wash_risk_lots": 2,
"skipped": {
"advantaged": 3,
"noBasis": 0,
"small": 1,
"gains": 3
},
"candidates": [
{
"lot": "L004",
"account": "Okafor Joint Brokerage",
"ticker": "VXUS",
"cls": "Intl Equity",
"shares": 410,
"value": 24149,
"basis": 29850,
"loss": 5701,
"loss_pct": 19.1,
"term": "short",
"acquired": "2025-10-06",
"rate_pct": 35,
"est_benefit": 1995.35,
"wash_risk": true,
"wash": [
"Bought 4.2 VXUS on 2026-09-03 in Okafor Joint Brokerage - inside the 30 days before a sale, so the loss on the matching shares is disallowed.",
"The rebalance plan BUYS VXUS; harvesting and buying the same security inside 61 days is a wash sale.",
"..."
],
"safe_rebuy_date": "2026-10-22",
"long_term_on": "2026-10-07"
},
{
"lot": "L006",
"account": "Okafor Joint Brokerage",
"ticker": "BND",
"cls": "Bonds",
"shares": 380,
"value": 26657,
"basis": 29640,
"loss": 2983,
"loss_pct": 10.1,
"term": "long",
"acquired": "2022-03-11",
"rate_pct": 18.8,
"est_benefit": 560.8,
"wash_risk": true,
"wash": [
"Bought 3.1 BND on 2026-08-28 in Ada Okafor Traditional IRA - inside the 30 days before a sale, so the loss on the matching shares is disallowed.",
"The rebalance plan BUYS BND; harvesting and buying the same security inside 61 days is a wash sale.",
"..."
],
"safe_rebuy_date": "2026-10-22",
"long_term_on": null
},
"..."
]
}
},
"handoff": {}
}
data.output.output, parsed
{
"lane": "review",
"title": "Okafor household - Q3 2026 review — review meeting prep",
"posture": "ready-with-questions",
"verdict": "This meeting can be run from the engine's facts as they stand, but the harvest decision and the tax picture on realized gains still need the tax preparer's and clients' answers before it's final.",
"summary": "US equity is 19.2pp over its band at 74.2% of the household versus a 55% target, and the plan's IRA/Roth trades bring it back to 55% with no tax owed. There is $11,024 of harvest-eligible loss, but only the $2,340 VWO lot is clear of wash-sale risk right now. VTI concentration (64.8%) and Emeka's question about international performance are worth raising as context, not as calls to act.",
"assumptions": [
"No handoff was provided from the drift or harvest lanes (handoff was empty), so this review is built directly from facts.rebalance and facts.harvest rather than from a prior lane's trade_notes, deviations, or lot-by-lot harvest calls.",
"The +3.1%/+9.4% household and +2.8%/+8.9% benchmark performance figures are taken verbatim from the advisor's notes, since the engine does not compute performance."
],
"open_questions": [
"What is the household's state tax treatment and are there capital-loss carryforwards from prior years the tax preparer should apply against this year's $11,024 of harvest losses?",
"Are there other 2026 realized gains or losses outside this household's accounts that change how much of the harvest is useful this year?",
"..."
],
"reconciliation": [
{
"id": "REVIEW-DRIFT",
"addressed": true,
"note": "Covered as the first talking point and in the trade/tax talking points and action items — the plan trades $99,665.60 of sales and $79,779.55 of purchases to bring all four classes back to target."
},
{
"id": "REVIEW-HARVEST",
"addressed": true,
"note": "Covered in the harvest talking point, an action item, and a client question — only the clear $2,340 VWO lot is queued to harvest now; the wash-risk lots wait for the safe rebuy date."
},
"..."
],
"review": {
"agenda": [
"Where the household sits against the 55/20/20/5 target",
"The $40,000 July contribution and the rebalance trades that use it",
"..."
],
"talking_points": [
{
"topic": "Where the portfolio sits against the 55/20/20/5 target",
"say": "Right now you're at 74.2% US equity against a 55% target, 8.7% international against 20%, 15.9% bonds against 20%, and 1.2% cash against 5%. US equity is the one furthest outside its 5pp band, at 19.2pp over.",
"why": "facts.allocation shows US Equity at 74.2% (target 55%, drift 19.2pp, band 5pp), Intl Equity at 8.7% (target 20%), Bonds at 15.9% (target 20%), and Cash at 1.2% (target 5%).",
"figures": [
"74.2%",
"55%",
"..."
]
},
{
"topic": "The $40,000 July contribution and where it's landed",
"say": "The $40,000 you added to the joint account in July is part of why cash is still under target — the plan leaves $26,086.05 in cash after its trades, versus a 5% target that's currently sitting at 1.2%.",
"why": "Notes state the $40,000 July addition; facts.rebalance.leftover_cash is $26,086.05 and facts.allocation shows Cash at 1.2% against a 5% target.",
"figures": [
"$40,000",
"$26,086.05",
"..."
]
},
"..."
],
"action_items": [
{
"owner": "ops",
"action": "Execute the four rebalance trades: sell VTI in Ada's IRA (T-01, $62,664) and Emeka's Roth (T-02, $37,001.60); buy VXUS in the IRA (T-03, $58,664.40) and BND in the Roth (T-04, $21,115.15).",
"by": "this week"
},
{
"owner": "ops",
"action": "Harvest the VWO lot (L005, $2,340 loss, $439.92 estimated benefit) and record the trade.",
"by": "this week"
},
"..."
],
"questions_for_client": [
"Any planned withdrawals, income changes, or other realized gains this year that would change the tax picture on the $32,005.60 of realized gain or the $11,024 of harvest losses?",
"Is it acceptable to have Ada's IRA and Emeka's Roth carry the VTI sales and VXUS/BND buys as the plan lays them out?",
"..."
],
"follow_up_email": {
"subject": "Okafor household — Q3 2026 review recap and next steps",
"body": "Hi Ada and Emeka,\n\nGood catching up. Recap: the household returned +3.1% this quarter (+9.4% year to date) versus +2.8% (+8.9%) for the 55/20/20/5 benchmark.\n\nAllocation has drifted: US equity is 74.2% against a 55% target, international is 8.7% against 20%, and cash is 1.2% against 5%. The plan sells $99,665.60 of VTI inside Ada's IRA and Emeka's Roth (no tax owed there) and buys $58,664.40 of VXUS and $21,115.15 of BND, bringing US equity back to 55%.\n\nOn losses: we can harvest the VWO lot now for a $2,340 loss ($439.92 estimated benefit). The VXUS and BND lots carry wash-sale risk from recent purchases, so we'll revisit those after the safe rebuy date of 2026-10-22.\n\nOne more note: VTI is 64.8% of the household total — not a call to sell, just something to keep watching.\n\nPlease let us know about any planned withdrawals or other gains this year before we execute the $79,779.55 of purchases.\n\nBest,\nYour advisor"
}
}
}
7. Parse the envelope
data.output.output is a string holding one JSON object. The page strips an optional code fence, takes
everything from the first { to the last }, parses it, and normalizes. Every lane shares one
outer envelope - lane, title, posture, verdict, summary,
assumptions[], open_questions[], reconciliation[] - plus one body keyed
rebalance, harvest or review.
What the normalizer does to it
- An unrecognised
laneis inferred from which body key is present, else from thetaskyou sent. The page flags a mismatch between the two. - An unrecognised
posturebecomes the lane's middle value (rebalance-with-caveats,harvest-selectively,ready-with-questions). - A reply with neither
verdictnorsummarythrows; the page then retries once with aretry_note, on a key with the attempt suffix incremented. Do the same. trade_notes[].keepbecomestrue,falseor"modify"; alots[].actionoutside harvest / skip / wait becomeswait; aclass_calls[].statusoutside over / under / in-band / untargeted becomesin-band.- Entries without an id (trade notes, lots, reconciliation rows) or without any text (class calls, talking points, action items, deviations) are dropped.
benefit_claimedis coerced to a number; the page recomputes the engine's sum over the lots markedharvestand shows both when they disagree.
Invariants worth asserting in CI
- Every id in
facts.flagsappears exactly once inreconciliation, and no id that was not sent appears there. drift: oneclass_callsentry perfacts.allocationentry, in order; onetrade_notesentry perfacts.rebalance.tradesentry, in order;postureis notin-bandwhenfacts.classes_outside_bandis above zero.harvest: onelotsentry per candidate;orderlists only lots markedharvest;benefit_claimedequals the sum ofest_benefitover those lots; no lot withwash_risk: trueis markedharvestunless every one of its wash notes is a plan-buy collision.review: five to nine talking points, each with a non-emptyfiguresarray; no percentage return unless one appears innotes.- Every ticker-like token in the reply appears in your request body. Every dollar or percentage figure appears in
factsor your own text, allowing for rounding - the page's tracer implements exactly this and marks the exceptions.
Truncation and partial results
When the balance sits between min_credits and hold_credits the run executes with a reduced
output cap and comes back truncated: true. What you hold then is a prefix of the envelope, not the envelope.
The page closes the open JSON in stages and renders whatever sections parsed with an honest "N of M recovered" note; a
CI caller should instead retry with a retry_note asking for shorter prose, on a key with the attempt suffix
incremented. Appending closing braces yourself produces something that parses and is not what the model meant.