Zoo

← back to the zoo

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];