Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions SPRINT_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,58 @@ Only 2 test files exist: `src/__tests__/security.test.ts`, `wallet-eligibility.t
- **PublicAgent** now includes `slug` and `treasuryHealth`; still excludes all CRM tags. `/api/registry/agents` no longer leaks `outreachStatus`/`adminNotes`/`priority`.
- **Agent type** gained required `slug` + the eight tag fields; `rowToAgent`/`agentToRow`/static `data.ts` handle them. `approvePendingUpdate` deliberately does NOT accept tag fields.
- Tests: `src/__tests__/status-tags.test.ts` (bun harness still absent locally/CI — see §9.6).

---

## 11. Sprint completion (2026-07-27) — P0–P9 all code-complete

All phases merged to `main` (`7dda29b`). DoD green: typecheck 0 · lint 0 · **181 tests** · build clean (both flag states). CI runs the full gate on every PR.

| Phase | Delivered | Key files |
|---|---|---|
| P0 | Scope lock + tag system + 2 migrations | `focus.ts`, `20260715*` migrations |
| P1 | Data-quality audit route/script/skill | `bankr-audit.ts`, `scripts/bankr-data-audit.sh` |
| P2 | Registry financial terminal | `registry-client.tsx`, `filters.ts` |
| P3+P4 | Single-scroll profile + operator books | `books-presenter.ts`, `books-sections.tsx` |
| P5 | Pipeline observability, fail-loud | `pipeline-observability.ts`, `20260717*` migration |
| P6 | Bankr Queue (6 admin views) | luca-admin `BankrQueueSection` |
| P7 | Claim/outreach CTAs + pending states | profile `ProfileCtas`, `ClaimBanner` |
| P8 | Agent-scoped keys + isolation tests + docs | `v1-auth.ts`, `agent-scope.test.ts`, `docs/API_BANKR.md` |
| P9 | Messaging rules enforced in CI | `messaging-rules.test.ts` |

## 12. QA checklist run (3 Bankr agents: bankr, helixa, botcoin)

Spec named **Luca** as a test agent — Luca is ecosystem `Base` and intentionally 404s under the P0 scope lock (verified). Substituted a third Bankr agent.

| Check | Result | How verified |
|---|---|---|
| P3 layout order, bio before scores | ✅ | HTML marker positions: bio→status→metrics→verdict→books→attribution→data-quality. One documented deviation: for `no_books` agents the claim CTA renders directly after Books ("the CTA IS the next action") instead of position 11 — deliberate merged design. |
| Metrics real or explicitly missing, never fake zeros | ✅ | 0 occurrences of `$0.00` on all 3 profiles; unattributed metrics render "—" with reason |
| Luca verdict right after metrics, reflects known/missing | ✅ | verdict section immediately follows metrics; missing list populated (e.g. "Wallet attribution (no books-eligible manifest wallet)") |
| Wallet attribution shows source provenance | ✅ | source pills + legend (manifest/admin/inferred/candidate) render; bankr shows `candidate` |
| Token activity ≠ operating revenue | ✅ (by test) | presenter invariant tests; agent-token is its own row — no attributed agent exists in local fallback data to eyeball; re-verify once on prod |
| Books status / freshness / last-indexed / next-action populate | ✅ | Data Quality block fully rendered on all 3, concrete next action each |
| Registry row matches profile | ✅ | API `needs_verification/no_books/partial` == profile badges "Needs Verification/No Books/Partial" (bankr; spot-checked others) |
| Pipeline failure visible, no silent empty state | ✅ (by test) | data_status=failed overlay + admin section unit-tested; needs prod `pipeline_failures` table for a live example |
| Copy follows P9 rules | ✅ | `messaging-rules.test.ts` scans every public tsx in CI |
| Scoped key returns own data, denied for others | ✅ (by test) | `agent-scope.test.ts` security-property tests; live-key check needs a prod-issued `agent:{slug}` key |

**Caveat:** local data is the static fallback (no Supabase env), so "✅ (by test)" rows are enforced by the unit suite rather than observed live. One prod pass over a live-books agent completes these visually.

## 13. P0/P1 backlog — Bankr reliability (CTO deliverable #7)

**P0 (blocking full value of shipped work):**
1. Apply `supabase/migrations/20260717000001_pipeline_failures.sql` in prod (P5 has no table until then).
2. Run the first production data audit (`scripts/bankr-data-audit.sh` with prod secret); commit `reports/` artifact — this is the real P1 deliverable's numbers.
3. Add Hermes schedules: weekly `bankr-data-audit` (Mon 06:00 UTC).
4. Issue `agent:{slug}` keys to pilot Bankr teams; verify one live 403 cross-agent denial.
5. Fill bios for top Bankr agents (admin Edit Agent Profile) — every profile currently shows the empty state.

**P1 (reliability debt, next sprint):**
6. Tracked migrations for hand-created prod tables (`registry_manifest_submissions`, `luca_subagent_runs`, `registry_claims`) — schema currently not reproducible.
7. `outreach_status_legacy` cleanup migration once the new vocabulary is confirmed in practice.
8. Wallet-level v1 endpoints (`ledger-summary`, `full-report`, `scan`) are unscoped by design (public chain data) — consider verifying wallet-belongs-to-agent for scoped keys.
9. Gas tracking (no ledger category exists — expense breakdown honestly shows "not tracked").
10. `valueUsd` pricing for provider-normalized transactions (currently honest-blank).
11. Sync `docs/API_BANKR.md` into the GitBook (`DOCS_URL`).
12. Visual QA pass on a live-books agent post-deploy (attributed rendering was test-verified only).
119 changes: 119 additions & 0 deletions docs/API_BANKR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Zetta API for Bankr Agent Teams

How a Bankr agent team reads its own financial data from Zetta, what each
number means (actual vs. estimated vs. missing), and what an agent-scoped key
can and cannot do.

> Canonical hosted docs live at the GitBook (see `DOCS_URL`); this file is the
> source-of-truth spec for the Bankr-facing v1 endpoints and should be synced
> there.

---

## Authentication

Pass your key either way:

```
Authorization: Bearer <key>
X-API-Key: <key>
```

### Agent-scoped keys — `agent:{slug}`

Keys named `agent:<your-agent-slug>` (e.g. `agent:bankr-bot`) are **scoped**:
they can read that agent's data and nothing else. Scope is enforced centrally
in the API layer and covered by tests (`src/__tests__/agent-scope.test.ts`) —
a scoped key requesting another agent's data always receives:

```
403 { "error": "This API key is scoped to agent 'bankr-bot', not 'other-agent'. Use a key scoped to this agent, or an unscoped key." }
```

Scoped-key rules:
- **Read**: `agent-books`, `agent-books/history`, `agent-revenue`,
`agent-truth`, `agent-report`, `agent-events` — own slug only.
- **Write**: `POST /api/v1/agent-events` — a scoped key can only log events
under its own agent, never another's.
- Wallet-level endpoints (`ledger-summary`, `full-report`, `scan`,
`transactions`) query public on-chain data by wallet address and are not
agent-scoped — they never return Zetta's derived agent books.

Request a scoped key at `/developer` or via the Zetta team.

---

## Endpoints

### `GET /api/v1/agent-books/{slug}?range=30d`
The core product output: revenue, expenses, net income, and treasury across
the agent's **declared** wallets, internal transfers eliminated.
`range`: `7d | 14d | 30d | 90d` (default `30d`).

### `GET /api/v1/agent-books/{slug}/history`
Snapshots of past books periods (trend data).

### `GET /api/v1/agent-revenue/{slug}?chain=&classification=&since=`
Classified revenue events (settlement detection, per-transaction).

### `GET /api/v1/agent-truth/{slug}`
Observed-truth summary: classification counts, revenue candidates,
unresolved inflows — the raw evidence layer underneath books.

### `GET /api/v1/agent-report?agentName={name}&days=7`
Plain-language financial report generated from the event log.

### Not available (yet)
`/api/v1/agent-wallets/{slug}` and `/api/v1/agent-holdings/{slug}` **do not
exist**. Declared wallets and their roles appear inside the `agent-books`
response (`wallets`) and on the public profile; stablecoin treasury balance
appears as `financials.treasury_balance_usd`.

---

## Reading the numbers: actual · estimated · missing · unavailable

Zetta never fabricates a number. Every field falls into one of four classes:

| Class | What it means | How it appears |
|---|---|---|
| **Actual** | Computed from on-chain transactions of manifest-declared, books-eligible wallets. | `financials.revenue_usd`, `expenses_usd`, `net_income_usd`, `tx_count`, `classification.settlement_revenue_usd` (highest-confidence tier: stablecoin inflows from known counterparties). |
| **Estimated / lower-confidence** | Real observations whose classification is not certain. Check `confidence.*` (`high/medium/low`) per metric. | `classification.agent_token_revenue_usd` (agent-token inflows — **never counted as operating revenue**), `fee_received`-tier events in `agent-truth`, anything the response flags in `confidence.flags`. |
| **Missing** | Not tracked or not fetchable right now — reported as `null`, **never as `$0`**. | `treasury_balance_usd: null` (no treasury wallet, or the balance lookup failed), `runway_months: null` (needs treasury), gas costs (not tracked at all). |
| **Unavailable** | The agent has no attributed books. You get an honest `200` with `attributed: false` and a `reason` — not an empty ledger. | `{ "attributed": false, "reason": "no_manifest_wallets" \| "wallets_declared_not_scannable" \| "financials_under_review" }` |

Also relevant:
- `classification.quarantined_inflows_usd` — inflows deliberately **held out**
of revenue (suspected capital injections, grants, bridge receipts) pending
classification. Quarantined ≠ revenue.
- `generated_at` — books freshness; the cache refreshes on a ~4h cycle.
- `agent-truth` classifications marked `unknown` stay unknown — ambiguous
activity is never auto-upgraded to revenue.

---

## Errors

| Status | Meaning | Example |
|---|---|---|
| 400 | Bad parameter — message names the field and valid values | `"Range must be 7d, 14d, 30d, or 90d."` |
| 401 | No/invalid key | `"Missing API key. Pass it as Authorization: Bearer <key> or X-API-Key header."` |
| 403 | Scoped key requesting another agent | `"This API key is scoped to agent 'bankr-bot', not 'luca'. …"` |
| 404 | Unknown agent slug | `"Agent 'foo' not found in the registry. Browse agents at /registry."` |
| 503 | Registry/backing store unavailable | `"Registry unavailable"` |

Rate limits apply per key; usage is recorded per endpoint.

---

## Quick start (Bankr team, own data)

```bash
KEY="<your agent:bankr-bot key>"
curl -s -H "Authorization: Bearer $KEY" \
https://www.zettaai.co/api/v1/agent-books/bankr-bot?range=30d | jq '.financials'

# Proof of isolation — any other slug returns 403:
curl -s -H "Authorization: Bearer $KEY" \
https://www.zettaai.co/api/v1/agent-books/luca | jq '.error'
```
38 changes: 38 additions & 0 deletions src/__tests__/agent-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,41 @@ describe("agentScopeViolation", () => {
expect(agentScopeViolation("aeon", undefined)).toBeNull();
});
});

// ── P8: agent:{slug} key parsing + Bankr end-to-end naming ────────────────────

import { parseAgentScope } from "@/lib/api-keys";
import { toSlug } from "@/lib/slug";

describe("parseAgentScope (agent:{slug} key convention)", () => {
test("agent:bankr-bot → scope bankr-bot", () => {
expect(parseAgentScope("agent:bankr-bot")).toBe("bankr-bot");
});

test("non-agent key names have no scope (unrestricted)", () => {
expect(parseAgentScope("my personal key")).toBeNull();
expect(parseAgentScope("")).toBeNull();
expect(parseAgentScope("agentx:bankr")).toBeNull();
});

test("the security property: a Bankr agent's scoped key is denied every other slug", () => {
const scope = parseAgentScope("agent:bankr-bot");
// own data — allowed
expect(agentScopeViolation(scope, "bankr-bot")).toBeNull();
// anyone else's data — denied, with a clear error naming both slugs
for (const other of ["helixa", "clawd", "luca", "bankr"]) {
const v = agentScopeViolation(scope, other);
expect(v).not.toBeNull();
expect(v).toContain("bankr-bot");
expect(v).toContain(other);
}
});

test("agentName inputs normalize through toSlug before the scope check (agent-report / agent-events routes)", () => {
const scope = parseAgentScope("agent:bankr-bot");
// A request for agentName "Bankr Bot" is the same agent → allowed
expect(agentScopeViolation(scope, toSlug("Bankr Bot"))).toBeNull();
// A request for agentName "Sleuth.AI" is not → denied
expect(agentScopeViolation(scope, toSlug("Sleuth.AI"))).not.toBeNull();
});
});
77 changes: 77 additions & 0 deletions src/__tests__/bankr-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { describe, expect, test } from "bun:test";
import {
buildActionBuckets,
buildAuditRow,
buildCounts,
buildDuplicateGroups,
Expand Down Expand Up @@ -183,3 +184,79 @@ describe("buildAuditRow", () => {
expect(row.duplicate_reasons).toEqual(["token_address", "x_handle"]);
});
});

// ── P6 queue buckets ──────────────────────────────────────────────────────────

describe("buildActionBuckets P6 views", () => {
const completeMeta = {
xHandle: "@dm-me",
website: "https://x.yz",
symbol: "$OK",
tokenAddress: "0x2222222222222222222222222222222222222222",
};

test("ready_for_outreach: complete metadata + no manifest + never contacted", () => {
const target = buildAuditRow(makeAgent({ ...completeMeta, slug: "dm-target", outreachStatus: null }), {}, {});
const contacted = buildAuditRow(
makeAgent({ ...completeMeta, slug: "already-dmed", outreachStatus: "dm_sent" }),
{}, {},
);
const incompleteMeta = buildAuditRow(
makeAgent({ slug: "no-website", website: null, outreachStatus: null }),
{}, {},
);
const hasManifest = buildAuditRow(
makeAgent({
...completeMeta,
slug: "has-manifest",
outreachStatus: null,
wallets: [makeWallet({ evidenceSource: "manifest" })],
}),
{}, {},
);
const buckets = buildActionBuckets([target, contacted, incompleteMeta, hasManifest]);
expect(buckets.ready_for_outreach).toEqual(["dm-target"]);
});

test("ready_for_outreach also includes explicit not_contacted", () => {
const row = buildAuditRow(
makeAgent({ ...completeMeta, slug: "explicit", outreachStatus: "not_contacted" }),
{}, {},
);
expect(buildActionBuckets([row]).ready_for_outreach).toEqual(["explicit"]);
});

test("ready_to_verify: manifest wallets + needs_verification profile", () => {
const ready = buildAuditRow(
makeAgent({
slug: "verify-me",
profileStatus: "needs_verification",
wallets: [makeWallet({ evidenceSource: "manifest" })],
}),
{}, {},
);
const alreadyVerified = buildAuditRow(
makeAgent({
slug: "done",
profileStatus: "verified",
wallets: [makeWallet({ evidenceSource: "manifest" })],
}),
{}, {},
);
const noManifest = buildAuditRow(
makeAgent({ slug: "no-manifest", profileStatus: "needs_verification" }),
{}, {},
);
const buckets = buildActionBuckets([ready, alreadyVerified, noManifest]);
expect(buckets.ready_to_verify).toEqual(["verify-me"]);
});

test("outreach/priority CRM fields flow onto the audit row", () => {
const row = buildAuditRow(
makeAgent({ outreachStatus: "replied", bankrPriority: "high" }),
{}, {},
);
expect(row.outreach_status).toBe("replied");
expect(row.bankr_priority).toBe("high");
});
});
Loading
Loading