15. You can see what you spent
Every question through ai is on your ledger, per app.
ai writes every call to a ledger; GET ?api=usage tells an app what the signed-in person spent today and this week, by app.
This is the code serving the zoo right now: read from disk for this request, from commit
0c1c124a02 (live). The zoo's own self-check fetches this
page and compares it byte for byte with the file it runs.
exhibits/15-what-you-spent.php sha256 97882e27ba10 · raw
1<?php 2// Exhibit 15. ai keeps a ledger of every call. Any app can read it back for the signed-in person: 3// call_app("ai", "/?api=usage", true) -> {user: {id, spent, by_app}, breakdown: {"24h": [...], "7d": [...]}} 4// breakdown rows are {app, user, calls, tokens, in, out}. There are no budgets (PLAN belief 7): 5// this exhibit shows what was spent, not what is left. 6 7function spend_by_app(): array { 8 [$s, $out] = call_app("ai", "/?api=usage", true); 9 $r = json_decode($out, true); 10 if ($s !== 200 || !isset($r["user"]["id"], $r["breakdown"])) return ["error" => "ai's usage answered $s", "status" => 502]; 11 $apps = []; 12 foreach (["24h" => "today", "7d" => "week"] as $k => $name) { 13 foreach ($r["breakdown"][$k] as $row) { 14 if ($row["user"] !== $r["user"]["id"]) continue; // the person's rows only, not the zoo's other askers 15 $apps[$row["app"]][$name] = $row["tokens"]; 16 if ($name === "week") $apps[$row["app"]]["calls"] = $row["calls"]; 17 } 18 } 19 foreach ($apps as &$a) $a += ["today" => 0, "week" => 0, "calls" => 0]; 20 return ["today" => (int)$r["user"]["spent"], "apps" => $apps, 21 "week" => array_sum(array_column($apps, "week"))]; 22} 23 24return [ 25 "n" => 15, "wing" => "Intelligence", "kind" => "self", 26 "title" => "You can see what you spent", 27 "promise" => "Every question through ai is on your ledger, per app.", 28 "block" => "ai writes every call to a ledger; GET ?api=usage tells an app what the signed-in person spent today and this week, by app.", 29 "show" => function (?array $me): string { 30 if (!$me) return '<p class="muted"><a href="' . h(rc_signin_url()) . '">Sign in</a> to see what you spent.</p>'; 31 return '<p>You spent <b id="spent-today">…</b> tokens today and <b id="spent-week">…</b> this week. ' 32 . 'Ask the model above and watch it rise. <button class="link" id="spent-refresh">refresh</button></p><p class="muted" id="spent-by"></p>'; 33 }, 34 "api" => function (string $do, ?array $me, array $in, bool $post): ?array { 35 if ($do !== "usage") return null; 36 if (!$me) return ["error" => "sign in first", "status" => 401]; 37 return spend_by_app(); 38 }, 39 // The robot's spend must go up when it asks. (Exhibit 14 asks too; this does its own ask so 40 // the check never depends on the order of two checks: it calls 14's question, as the page does.) 41 "check" => function (): array { 42 require_once ZOO_ROOT . "/lib/robot.php"; 43 $b = robot_at_zoo(); 44 $host = "https://" . env("RC_HOST"); 45 [$c1, , $before] = $b->post("$host/api/15/usage"); 46 if ($c1 !== 200) return [false, "reading the spend failed ($c1)"]; 47 [$c2, , $ask] = $b->post("$host/api/14/ask", ["question" => "Reply with the single word: ok", "max_tokens" => 20]); 48 if ($c2 !== 200) return [false, "the robot's question failed ($c2): " . ($ask["error"] ?? "")]; 49 [$c3, , $after] = $b->post("$host/api/15/usage"); 50 if ($c3 !== 200) return [false, "reading the spend again failed ($c3)"]; 51 $was = $before["apps"]["zoo"]["today"] ?? 0; 52 $now = $after["apps"]["zoo"]["today"] ?? 0; 53 if ($now <= $was) return [false, "asked, but the robot's spend by the zoo stayed at $was tokens", $after]; 54 if ($after["today"] <= $before["today"] || ($after["apps"]["zoo"]["week"] ?? 0) <= ($before["apps"]["zoo"]["week"] ?? 0)) 55 return [false, "the zoo's spend rose but today's or this week's total did not", $after]; 56 return [true, "the robot's spend by the zoo rose from $was to $now tokens today after one question (week {$after['week']})", $after]; 57 }, 58 "script" => <<<'JS' 59(() => { 60 const today = document.getElementById("spent-today"); 61 if (!today) return; 62 const n = (x) => x.toLocaleString("en"); 63 const read = async () => { 64 const j = await zoo.call("/api/15/usage"); 65 if (j.error) { today.textContent = j.error; return; } 66 today.textContent = n(j.today); document.getElementById("spent-week").textContent = n(j.week); 67 const by = Object.entries(j.apps || {}).map(([a, r]) => a + " " + n(r.today) + " today, " + n(r.week) + " this week").join("; "); 68 document.getElementById("spent-by").textContent = by ? "By app: " + by : "Nothing spent this week."; 69 }; 70 read(); 71 document.getElementById("spent-refresh").addEventListener("click", read); 72 window.addEventListener("zoo-asked", read); // exhibit 14 just asked: re-read, the number rises 73})(); 74 75JS, 76];