Kanban
All tasks across all phases. Drag a card to change its status.
New package apps/cli (Bun + TS, dist via tsc to a single index.js). bin/aigarth.ts entry point that parses argv, resolves command path, dispatches. Global flags: --json / --human / --dry-run / --idempotency-key / --no-color / --quiet / --verbose / --output / --schema-version. lib/config.ts reads ~/.config/aigarth/config.json. lib/credentials.ts reads/writes credentials.json (mode 0600). lib/signing.ts runs the Qubic wallet-link flow: GET /v1/auth/wallet-link/start, sign nonce, POST /v1/auth/wallet-link/finish. Persist { api_key, bearer_token, wallet_address, user_id, org_id }. Commands: auth login, auth status, auth logout. ~1.5 SP.
lib/output.ts: success(data, meta) and failure(error) produce the standard envelope { schema_version, command, data, meta, error }. lib/errors.ts: error code catalog (ANN_NOT_FOUND, MISSING_FLAG, IDEMPOTENCY_KEY_REUSED, etc.) + exit code mapping (0/1/2/3/4/5/64/130). printHuman() pretty-printer: JSON-aware table renderer, color palette, summary line. No TUI, no paginator. ~1 SP.
commands/anns.ts: anns list [--category X] [--limit N], anns retrieve <slug>, anns run <slug> --input <json> --model <name>. commands/tissues.ts: same shape. Run is the gateway call (OpenAI-compatible). Output: success envelope with the inference result. ~1.5 SP.
commands/marketplace.ts: marketplace search <query> [--category X] [--limit N], marketplace retrieve <slug>. commands/nodes.ts: nodes reservations [--status X] (links to Phase 24 compute.nodeReservations.list), nodes reserve --tier 1 --yield-opt-in (creates a pending_funding row, returns the deposit details for the wallet to sign). ~1 SP.
The MVP execution surface. anns run <slug> --input <json> makes the actual OpenAI-compatible call through the gateway. Returns the inference result with token usage and QUBIC cost. Streams progress events on stderr (upload.started, inference.started, inference.completed). Tests cover a happy-path inference and a 404 / 401 / 429 failure path. ~1 SP.
lib/schema.ts: in-memory map of command_path -> { version, jsonSchema }. commands/schema.ts: aigarth schema <command-path> returns the JSON schema for that command's output. commands/help.ts: aigarth help <command-path> returns JSON help (synopsis, flags, examples, exit codes, output schema ref). On startup, the CLI prints a one-line machine-readable manifest so an agent can introspect the whole surface in one call. Schema-versioned. ~0.5 SP.
apps/cli/package.json: name @aigarth/cli, bin { aigarth: dist/index.js }, engines node >= 20. pnpm-workspace.yaml: add apps/cli. README.md with install + first-run instructions. pnpm --filter @aigarth/cli build produces the dist/. Local install via `pnpm link` for testing. npm publish to the @aigarth org (when the user is ready; for now the package is built and tested locally). Self-update is out of scope for Phase 25 (deferred to Phase 27). ~0.5 SP.
Vitest config + tests/commands/ (one file per command: auth, whoami, anns, tissues, marketplace, nodes, self) + tests/lib/ (config, http, output, errors unit tests). End-to-end smoke test that runs the full login -> whoami -> anns list -> anns run flow against a stubbed HTTP layer. ~30 vitest cases. docs/deliveries/phase-25-delivery.md following the _TEMPLATE.md pattern. ~1 SP.
Compile the AigarthPool contract. Deploy to Qubic testnet using qubic-cli. Run an integration test that exercises the full loop locally first (services/training + AigarthPool + Qearn), then promotes to testnet. Mainnet deployment is gated on an external audit. ~0.5 SP.
Fork services/qubic/src/workers/tx-monitor.ts into a Qearn-aware watcher. Polls tx history filtered to receiver === Qearn contract address; matches the lock procedure input type; writes aigarth_economy_locks rows. ~3 SP per ADR 002 §7. Triggers the compute grant in services/economy/splits.ts.
Add `material_domain` enum on `anns` (battery, alloy, polymer, catalyst, semiconductor, ...). Create `material_knowledge_nodes` (kind: material/property/paper/structure, label, payload) and `material_knowledge_edges` (from/to, kind: has_property/cites/derived_from/measured_by, weight).
Literature ANN: PDF ingestion → structured facts (composition, property value, measurement method, citation). Research Director ANN: LLM call returning a research plan (which stages to run, in what order, with what budget). Single Python process orchestrating both. Wire up a /dashboard/research page where a user types a research question and gets a structured answer with citations.
End-to-end smoke test: ingest 1,000 open-access material science papers, answer a 3-paragraph research question in under 5 minutes, with structured property values and citations. If this passes, commit to Phase 2 with real numbers. If not, the prototype becomes an internal tool.
Add 8 new compute kinds (lit_ingest, dft_relax, md_run, mlip_predict, design_generate, optimize_pareto, experiment_plan, validation_compare). Add `compute_simulation_results` table + `input_deck_hash` column. Build aigarth/worker-dft (VASP, Q-E), aigarth/worker-md (LAMMPS, GROMACS), aigarth/worker-mlip (MACE, Allegro) container images. 3-worker local cluster.
Add `material_predictions` (per-prediction provenance + uncertainty), `material_validation_results` (experimental + literature validation), `material_feedback_events` (positive + negative + cross-ANN corrections). Build Design + Optimization + Experiment + Validation ANNs. End-to-end 'find a battery cathode' workflow: ~52 QU, ~102 h wall-clock on 1x CPU.
Publish the 8 ANNs as real listings. Build `material_ann_quality` materialized view + cron (with uncertainty calibration score). Add per-ANN quality ranking in the marketplace. Reviews and ratings flow into `material_feedback_events`. Add `kind = 'research_pipeline'` listing type for pre-composed workflows.
Currently services/qubic is format-only. Replace the TODO in services/identity/src/lib/qubic.ts with real K12 verification using @noble/curves (already a dep). Required before any on-chain writes in Phase 16 OR Phase 17. Solve once, reuse.
Deploy 4 base Qubic smart contracts (shared with Phase 16) PLUS the new `discovery_attribution` contract for material findings. The publish action is one-way and immediate (not via daily rollup) — once a material discovery is published, on-chain attribution is fixed. QUBIC reward distribution: 50% worker, 30% ANN author, 10% treasury, 10% discovery pool.
Add `role` column on anns. Create ann_pipelines (ordered stages), ann_pipeline_runs (per-stage status + artifact URLs + per-stage cost), ann_feedback_events (attribution to ann_version + pipeline_run).
Director: LLM call returning a structured shot list. Camera: rule-based path generator. Motion: OpenCV affine transforms between keyframes. Render: FFmpeg h264 encoder. Single Python process orchestrating all four.
POST /v1/pipelines/:id/run that drives the workflow end to end. Target: a 30-second 'city at sunset' MP4 in under 10 minutes on a single CPU. Re-evaluation gate at the end.
Add `kind = 'pipeline'` to marketplace listings. Publish the 4 role ANNs as listings. Build the `ann_version_metrics` materialized view + cron. Add per-ANN quality ranking in the marketplace.
compute_workers table + /v1/workers/* namespace. Long-poll POST /v1/jobs/next + POST /v1/jobs/:id/progress. Docker image with Python + OpenCV + FFmpeg + PyTorch CPU. 3-worker local cluster for E2E.
Reputation = weighted average of (success_rate × recency) per (worker, ann_kind). Surfaces in the worker registry API. Misbehavior (high error rate + low user rating) drops the score; persistent bad behavior → flagged.
Currently services/qubic is format-only. Replace the TODO in services/identity/src/lib/qubic.ts with real K12 verification using @noble/curves (already a dep). Required before any on-chain writes.
Deploy 4 Qubic smart contracts. Off-chain rollup of ann_version_metrics commits to chain as state root once per day. Reward distribution: 70% worker, 20% ANN author, 10% protocol treasury.
✅ SHIPPED. New tables: compute_node_reservations (status, USD cents, QUBIC amounts, rates, yield opt-in, qearn lock id, tx_hash idempotency) and compute_qubic_usd_rates (price oracle mirror). Service: create / fund / list / get / openConfirmWindow / confirm / release / autoReleaseExpired. Routes: /v1/nodes/reservations (CRUD + fund/confirm/release) + /v1/internal/nodes/* (operator-only). TIER_SPECS table for tier 1/2/3 USD amounts; TIERS_OPEN_FOR_RESERVATION gates marketing. usdCentsToQubic math: usdCents * 10^8 / rateScaled. 24h auto-cancel for stale pending_funding. Idempotency on tx_hash_reserve + tx_hash_confirm unique indexes. SDK methods shipped: compute.nodeReservations.{create,list,retrieve,fund,confirm,release}. 27 vitest passing; typecheck clean across @aigarth/compute, @aigarth/sdk, @aigarth/web.
New services/economy service: node_escrow ledger (double-entry: debit user → escrow, credit escrow → user on release). Replaces the inline TODOs in node-reservations.ts fund/confirm/release. Multisig payout support — for Phase 24 the multisig is a stub (the real QPI multisig lands with Phase 20.6). Idempotency on tx_hash. Escrow balance = sum(debits) - sum(credits) per reservation; used for audit and partial-refund calc. ~1 SP on the service + ~1 SP on the route layer + tests. Stubs in 24.1 call a placeholder; this is the real implementation.
New worker services/qubic/src/workers/usd-price-oracle.ts (or extend the existing qearn-watcher). Polls CoinGecko + CoinMarketCap every 60s, computes the median, writes a row to compute_qubic_usd_rates with rateScaled (USD per 1 QUBIC, scaled by 10^10) + source label + fetched_at. The node-reservations service already reads from this table and refuses to charge if the most recent row is >5 min stale (RATE_MAX_AGE_MS). Dev fallback: QUBIC_USD_FALLBACK_RATE_SCALED env var (already added in 24.1) used only when NODE_ENV=development and the table is empty. ~0.5 SP. Worker package script: worker:usd-price-oracle.
New services/economy service: qearn-lock.ts that wraps the existing Qearn integration (Phase 23 qearn-watcher). lockDeposit({ reservationId, amountQubic }) creates a Qearn lock, returns qearn_lock_id stored on the reservation row. unlockDeposit({ reservationId }) releases the lock; returns the QUBIC principal + accrued yield. getLockYield(reservationId) returns the current yield (polled). Wires into fundNodeReservation (after the spot_held transition) and confirmNodeReservation (applies yield_credit_qubic to the balance). Graceful failure: if the lock call fails, the reservation still moves to spot_held; yield_opt_in is set to false; the dashboard surfaces the failure. ~1 SP. The Qearn rate table already exists in services/qubic from Phase 23.
New route apps/web/app/(marketing)/nodes/page.tsx. Hero: 'Reserve a tier 1 compute spot. $599 total, $30 deposit.' Sub: 'Your deposit earns yield while you wait. Pay the balance when we go live. Cancel any time before that for a full refund.' Tier 1 spec card, 3-card compare table (Hetzner / AWS / Aigarth, not Mac Mini — we compare on compute primitives not hardware), 8-10 FAQ items (what is a spot, what does $599 include, ongoing costs, activation timing, refunds, yield opt-in, QUBIC pricing, Hetzner/AWS comparison, raw-staking comparison, 14-day window). Schema.org Product with availability: PreOrder, price: 59900 cents, priceCurrency: USD. NO animated counter / fake urgency. Reserve CTA → ReserveForm.tsx (login → wallet link → yield opt-in toggle → confirm modal with current QUBIC amount). Brand voice: per AGENTS.md, no em-dashes, plain English, small uppercase labels under plain-English headlines. ~1.5 SP.
New page apps/dashboard/src/components/pages/node-reservations.tsx. List of user's reservations with status badges, deposit USD / QUBIC paid / current QUBIC value / yield accrued / balance due columns. Per-row actions: release (if spot_held or awaiting_confirm), confirm (if awaiting_confirm), view on Qubic explorer. Escrow ledger view (collapsible per reservation). Phase tracker auto-includes the new card once it's in the DB. ~0.5 SP.
apps/dashboard/scripts/phase-24-trigger-confirm-window.ts. Operator-facing: takes a reservationId (or all spot_held rows), calls the internal endpoint to open the 14-day window + log the email send. For Phase 24 this is the manual path. The auto-worker (triggered by Phase 20.6 mainnet activation) is Phase 25+. Operator script + audit log entry per call. ~0.5 SP.
Standard delivery doc following the _TEMPLATE.md pattern: time-to-ship, SP velocity, breakdown table, comparison to prior phases, acceptance criteria checklist, files created/modified, what's still blocking. Includes a smoke-test recipe (e.g. seed a reservation, fund it, advance time, open the confirm window, confirm) and the JSON shapes for each endpoint. ~0.5 SP doc-only.
Three new top-level routes on the dashboard, each backed by the existing Doc store (SQLite) + the hand-rolled markdown renderer. Discriminated by path prefix: docs/blog/* for blog, docs/tutorials/* for tutorials, docs/academy/* for paths. Each surface lists + details — blog shows date/author/tags from frontmatter, tutorials show step numbers, academy paths are a curated list of lessons (a path doc references lessons by path). Frontmatter is parsed out of the markdown source at render time. Sidebar nav gets Blog + Tutorials + Academy. Seeded content: 1 engineering blog post (extracted from the existing trinary-intelligence launch post) + 1 product blog post (tweet thread), 2 tutorials (First stake, Your first ANN), 2 academy paths (Getting Started with Aigarth, Building ANNs). 3 SP.
New tables in services/ann: proposals (id, kind, title, body, proposer, status, deadline_epoch, created_at) and proposal_votes (proposal_id, voter, weight_qubic, choice 'yes'|'no'|'abstain', created_at). Endpoints: POST /v1/proposals (auth, kind: 'general'|'treasury-spend'|'ecosystem-grant', stake threshold check), GET /v1/proposals (public, paged by status), GET /v1/proposals/:id (public, includes tally), POST /v1/proposals/:id/vote (auth, weight = user's AigarthPool stake, re-vote updates), POST /v1/proposals/:id/finalize (anyone, after deadline), POST /v1/proposals/:id/execute (treasury-spend kind only — submits to AigarthPool.submitTreasuryTransfer so it flows through M1+M2). Dashboard /governance/proposals page lists open + closed proposals. ~10 vitest cases. ~3 SP.
New shared package @aigarth/observability with a tiny Prometheus text-format renderer + a registerHttpMetrics(app, serviceName) Fastify hook. Wires /metrics into all 10 services (identity, ann, qubic, economy, billing, marketplace, tissue, dataset, compute, gateway). The endpoint exposes: service info, HTTP request count + duration histogram (per route + method + status), Node process metrics (uptime, rss, heap, pid), and service-specific business metrics (aigarthpool_position_opens_total, aigarthpool_yield_distributed_qubic, governance_pause_state, qearn_events_observed_total). Logging: every request gets an X-Request-Id correlation ID injected from the inbound header (or generated), and every log line carries {service, route, status, duration_ms}. infra/observability/aigarth-cloud-overview.json: a starter Grafana dashboard with 6 panels (request rate, p95 latency, error rate, process memory, AigarthPool totalStaked, Qearn events observed). ~3 SP.
Add MAX_STAKE_PER_TX (1,000,000 QUBIC) and MAX_STAKE_PER_USER_PER_EPOCH (5,000,000 QUBIC) to the AigarthPool contract + simulator. Enforce both in stakeForAnn. New error codes: AMOUNT_EXCEEDS_TX_CAP, AMOUNT_EXCEEDS_USER_EPOCH_CAP. Per-user-per-epoch tracking is O(1) — reset on epoch change. ~8 new vitest cases. The on-chain QPI build is hardware-gated; design + simulator land now.
Add a `paused` flag to AigarthPool state. New procedures: pause(caller) + unpause(caller) — governance-only, throws NOT_SIGNER otherwise. When paused, stakeForAnn / extendLock / unlock / claimRewards / setAnnSplits reject with PAUSED. Read-only queries (getPosition, getAnnSplits, getYieldOwed, getTotals, getGovernanceState) and the watcher subscription are unaffected. New events: PoolPaused, PoolUnpaused. New service route POST /v1/aigarthpool/governance/pause with action=pause|unpause. Dashboard /governance page gets a paused badge + pause toggle. ~10 new vitest cases.
Two pieces. (1) New worker services/qubic/src/workers/qearn-watcher.ts that polls the Qearn contract address for transactions, classifies them as lock/unlock/yield, writes to a new qubic_qearn_events mirror table, and publishes NATS events for services/economy to consume. (2) P3 stake-attribution audit script apps/dashboard/scripts/p3-stake-audit.ts: pick 10 random (user, ann) positions from the AigarthPool simulator + services/ann mirror + services/qubic mirror and verify every field agrees. ~3 SP. The watcher is hardware-gated for real Qubic RPC; the audit script runs against the simulator end-to-end.
Embed the governance sub-state (signers[16], threshold, pending_treasury_transfer, pending_signer_change) in AigarthPool. Spec: docs/aigarthpool/governance.md. Wire: packages/aigarthpool types + simulator + client; services/ann admin endpoints; services/qubic watcher; apps/dashboard command centre. setAnnSplits accepts either the ANN's creator OR a current signer. PENDING_OP_TTL_EPOCHS=4. Forward-compatible with a future split to a sibling contract. 36 new vitest cases in the simulator (66/66 package total).
Ship Option B end-to-end: tx parser + verifier in services/identity/src/lib/qubic.ts (parseQubicTransaction, verifyQubicTransactionSignature, AIGARTH_AUTH_INPUT_TYPE 0x4147). Discriminated WalletFinishSchema (kind: 'message' | 'transaction') in services/identity/src/services/walletAuth.ts. Client buildQubicSelfTransfer + signChallengeAsTransaction in apps/web/lib/wallet/snap.ts. End-to-end snap path in apps/web/components/marketing/connect-qubic-wallet.tsx. walletAuthStats gains by_kind (30d message/transaction counts), snap_active_30d flag, and recent_audit (last 10 wallet-auth events with kind). Command centre page updated: by-kind breakdown, snap active badge, transaction audit feed table. invokeSnapSignMessage reserved for the future Option A path (when upstream ships a signMessage RPC). The unused import and result.reason bug in walletAuth.ts are fixed.
Add a profile to infrastructure/docker-compose.yml that brings up aio-qubic-dev-kit (Core + Faucet + Wallet + RPC) on a separate port range. Requires Linux x86-64 + AVX2 + ~24 GB RAM. Update pnpm stack:dev to accept a --with-qubic flag that includes this profile. Document the resource requirements in README.md. ~1 SP.
Create contracts/aigarth-pool/ with the QPI contract source. Procedures: stakeForAnn(annId, amount, weeks), extendLock(amount, additionalWeeks), unlock(), claimRewards(). State: pool.totalStaked, pool.userStakes[userId], pool.annStakes[annId], pool.splits[annId] (configurable creator/user/treasury bps). Compiles with qubic-cli. ~2 SP.
On unlock, AigarthPool receives the principal + Qearn rewards back. Compute splits: (creator_bps × rewards) → creator, (user_bps × rewards) → user, ((10000-creator_bps-user_bps) × rewards) → treasury. Emit split events for each recipient. Qubic tx fee comes from treasury, not user. ~1 SP.
When a user submits a training job with auto_publish=true, services/training calls AigarthPool.stakeForAnn on their behalf. services/ann listens for the resulting 'ANN version published' event to update its version table. services/economy mirrors the split events as user/creator credit balances. ~1 SP.
Update services/qubic/src/workers/tx-monitor.ts to subscribe to AigarthPool events. Reconcile our Postgres mirror against the on-chain state. Surface discrepancies (e.g., a stake that exists on-chain but not in our DB → user bypassed our flow). Best-effort, not the primary path. ~0.5 SP.
New page at apps/web/app/dashboard/garden/page.tsx + client.tsx. Replaces the operator-style 'portfolio' as the user's home. Four widget zones: 'Your ANNs' (status, accuracy, revenue), 'Your tissues' (Phase 18 surface, but framed as composed intelligence), 'Training queue' (Phase 19C output, shows jobs in flight), 'Marketplace revenue' (listings + tissue decisions). Composes existing queries — no new endpoints. ~1.5 SP.
Four reusable card components in apps/web/components/garden/. Each: status dot, key metric, last-updated timestamp, action button. Tailwind, lucide icons, no animation beyond a hover state. Reuses existing Card from @aigarth/ui. ~1 SP.
Move the existing dashboard root from /dashboard to /dashboard/garden. Update apps/web/components/dashboard/nav-client.tsx to mark 'Garden' as the home item. Keep the existing pages (/dashboard/models, /dashboard/marketplace, /dashboard/tissues, etc.) — they remain at their current paths. ~0.5 SP.
When the user has 0 ANNs, 0 tissues, 0 listings: the garden shows a single CTA card 'Create your first intelligence' that deep-links to /dashboard/models/new. The 6 seeded ANNs from Phase 18 are suggested as 'start from a template' in the same surface. Per BRAND-VOICE: buttons say what they do, empty states invite action. ~0.5 SP.
New service services/dataset on port 7009. Same pattern as services/ann / services/marketplace: config, db, schema, migrate, 4 routes (health, datasets, datasetVersions, publicCatalog), JWT verify only, helmet + CORS. ~1 SP.
Tables: datasets (id, owner_id, name, slug, kind, license, source, status, created_at, updated_at), dataset_versions (dataset_id, version, row_count, size_bytes, schema_json, sample_uri, content_hash, created_at), dataset_access (dataset_id, grantee_id, mode='read'|'derive', expires_at). Enum: dataset_kind (tabular, text, image, audio, time_series, multimodal, other). Enum: dataset_status (draft, private, public, deprecated). Enum: dataset_license (open, cc-by, cc-by-sa, commercial, custom). ~1.5 SP.
POST /v1/datasets/:id/versions — multipart upload to MinIO (already running per AGENTS.md), compute SHA-256 content hash, peek first 1MB to infer schema (CSV header, JSON keys, Parquet footer), persist sample_uri + schema_json. The schema is the contract for Phase 19C training jobs. ~2 SP.
GET /v1/datasets?visibility=public&kind=...&license=... (paginated, faceted). Public catalog page at apps/web/app/(marketing)/datasets — browse by category, link to original source, license terms. Reuses marketplace listing card UI. ~1.5 SP.
Table: dataset_connectors (id, dataset_id, kind, config_json, last_sync_at, status). Built-in kinds: 'http_api' (poll URL with auth header), 'iot_mqtt' (subscribe to topic, batch into rows), 'huggingface_dataset' (snapshot a public HF dataset by ID), 'kaggle_dataset' (download by slug). Each connector type is a small TypeScript module under services/dataset/src/connectors/. ~3 SP.
packages/sdk/src/types/dataset.ts + packages/sdk/src/resources/datasets.ts. 6 methods. New service URL on the client. Drizzle-style type re-exports. ~0.5 SP.
New service services/training on port 7010. Receives TrainingJob submissions, queues them, dispatches to worker pool. Workers call services/compute for the actual compute allocation. Database table: training_jobs (id, ann_id, dataset_version_id, recipe_json, status, started_at, finished_at, metrics_json, error). ~2 SP.
Recipe JSON contract: { architecture: 'mlp' | 'cnn' | 'transformer' | 'gradient_boost' | 'custom_ref', hyperparams: { lr, batch_size, epochs, ... }, optimizer: 'adam' | 'sgd' | 'rmsprop', loss: '...', metrics: ['accuracy', 'f1'], early_stopping: {...} }. Built-in recipes in services/training/src/recipes/. ~1 SP.
Phase 18E deferred: 'Real LLM invocation in the ANN service. The trinary /decide path still uses a deterministic hash-based stub for envelope generation.' Replace services/ann/src/services/trinary.ts stub with a real call to a configurable model backend (OpenAI-compatible, or self-hosted via the gateway). Pluggable model client interface so any backend works. ~3 SP.
When a TrainingJob starts, services/training calls services/compute (existing scheduler from Phase 2) to reserve capacity. While the job runs, services/compute reports utilization + heartbeats. On finish/fail, release. Wire through internal tokens. ~2 SP.
services/training emits progress events (epoch, loss, val_loss, eta) on a NATS subject. apps/web subscribes via SSE for the Garden UX progress bar (Phase 19A's 'Training queue' widget). Reuses the existing NATS infra (per AGENTS.md: 4222). ~1.5 SP.
When a TrainingJob reaches status 'succeeded', services/training calls services/ann to create a new ANNVersion with the trained weights, run a validation pass against a held-out split, write metrics, and (optionally) auto-promote the version to 'active' if metrics beat the prior version. ~1.5 SP.
New table: ann_decision_outcomes (decision_id, ann_id, ann_version_id, outcome: 'success' | 'failure' | 'reverted' | 'unknown', confidence_in_label, recorded_by, recorded_at, source). API: POST /v1/anns/:id/decisions/:decision_id/outcome (caller reports what actually happened). Used by the marketplace (Phase 6) to surface an 'actual accuracy' alongside predicted accuracy. ~1.5 SP.
Cron in services/training: scan recent ann_decision_outcomes, compute rolling accuracy per ANN. If drift detected (rolling accuracy < baseline - threshold) OR outcome rate falls below floor, enqueue a retraining TrainingJob with the prior recipe + the latest dataset version. Configurable per ANN (opt-in, defaults off). ~2 SP.
When an ANN has multiple versions, callers can request 'split traffic 50/50 between v1 and v2' (A/B) or 'shadow v2 alongside v1, log without responding' (shadow). Decision log records which version served each call. Services/ann/versions.ts gains 'deployMode' (active, shadow, canary, deprecated) + per-version traffic weight. ~2 SP.
When 19D.1 has data, the Garden ANN card (from 19A.2) grows a second metric line: 'Predicted: 94% / Actual: 89% (1,247 calls)'. Same for tissues. Calibrates user trust in the platform. ~0.5 SP.
docs/architecture-decisions/004-dataset-licensing.md. Decision needed: (a) when a user trains on a public dataset, who owns the resulting weights — the trainer, the dataset author, or both? (b) When a public dataset is removed/deprecated, do existing trained ANNs lose access? (c) How is attribution surfaced in the marketplace listing? Proposing: trainer owns weights, dataset is referenced + attributed, removal of source dataset marks trained versions 'orphaned' (visible to caller) but does not delete. ~0.5 SP.
Before any Phase 19 surface hits marketing, the Phase 3 Vision proposal must be rewritten against docs/BRAND-VOICE.md. Specifically: 'decentralized ecosystem' → 'open marketplace'; 'AI sovereignty' → 'data ownership'; 'transform' → 'extend'; 'where humanity grows its own intelligence' → 'stake, build, deploy, earn'; drop the 'Intelligence Creation Interface' label, keep the working vocabulary (ANN, Tissue, Listing, Decision). Output: docs/proposals/phase-3-vision-marketing.md (brand-clean version). ~0.5 SP.
Port 7008. Same pattern as services/billing / services/qubic. JWT verify only. Helmet + CORS + cookie. 5 routes: health, contributors, locks, bundles, payouts. db:seed and worker:qearn-watcher scripts. ~1 SP.
economy_contributor_shares (bps per ANN), economy_payout_runs, economy_payout_recipients, economy_bundle_listings, economy_aigarth_locks (Qearn mirror), economy_audit_logs. Bigint amounts. ~1 SP.
Pure revenue-split math in src/services/splits.ts: applySplit() with platform fee, multi-recipient bps distribution, exact-integer math, full input validation. computeGrantUnits() for Qearn-lock-to-compute-credit. Splits smoke test (pnpm smoke) — 38 assertions, all green: single/multi recipient splits, platform fee, all rejection paths, computeGrantUnits scaling. ~2 SP.
contributors (CRUD + transactional replaceShares with bps sum = 10000 invariant), locks (Qearn lock ledger with state machine, compute grant on record), bundles (N-ANN bundle listings, the 'intelligence index' use case), payouts (assemble idempotent on ann+period, settle loop calling services/qubic for QUBIC transfer). ~3 SP.
Fastify routes with Zod validation + JWT auth. GET/POST/PUT/DELETE for contributors and bundles. GET for locks (list, by id, total-grant). POST /v1/economy/payouts/assemble + POST /v1/economy/payouts/:id/settle. ~3 SP.
Fork of services/qubic/src/workers/tx-monitor.ts into a Qearn-aware watcher. Polls services/qubic for tx history filtered to receiver === Qearn contract address. Parses lock/unlock procedures. Calls recordLock/markUnlocking/markEnded in services/economy/src/services/locks.ts. Heuristic procedure detection (amount > 0 = lock) until services/qubic exposes structured inputData over the wire. ~3 SP.
services/ann/src/db/seed-anns.ts registers 6 ANNs (Caribbean Crop Doctor, Qubic Treasury Sentinel, Trinidad Creole Translator, SME Compliance Advisor, Solar Yield Forecaster, SQL Cockpit Copilot) with real descriptions, capability lines, demo input examples, accuracy/latency, license. Idempotent. Each gets a v1.0.0 version with placeholder metrics; the gateway can route to stub backends. ~1.5 SP.
apps/web/lib/anns-data.ts updated: FEATURED_ANNS now reflects the 6 real ANNs from the seed. ANN type gains 'capability' and 'demoInputExample' for the new detail surfaces. ANN_CATEGORIES unchanged. ~0.5 SP.
apps/web/app/(marketing)/stake-access/page.tsx — public funnel: hero with 4 stat tiles, 3-step how-it-works, 6-ANN grid, economics block with worked example, CTA. Marketing nav updated with 'Stake to Access' link. ADR 002 referenced as the design rationale. ~2 SP.
docs/architecture-decisions/002-staking-contract-strategy.md. Decision: use the existing Qearn contract for staking; do not clone it. Reject the clone with 5 specific reasons (re-implementing audited code, splits TVL, app-vs-contract logic, can't enforce what we want, 1-3 month deploy lead time). Layer off-chain bridge in services/economy. Re-evaluate only if Aigarth compute becomes a first-class on-chain primitive. ~1 SP.
services/economy/CHECKPOINT.md — what's shipped, what's left, ~15 SP remaining. Notes for the next agent on the wallet_address join, the procedure-index mapping, the linear compute-grant formula, the bundle/marketplace FK-in-spirit, and the Drizzle migration generation step. ~0.5 SP.
tsc --noEmit green across the four packages touched in this phase. Splits smoke test green (38/38). All routes and services compile. Cross-service import in the Qearn watcher handled via .d.ts shim + inlined penalty table (per ADR 001, no peer service imports).
StubQubicClient now treats the Qearn contract address (60-char, QEARN...A) as a real destination. broadcastStake to that address records a QearnLockPosition; new unlockQearn(lockId, weeksHeld) applies the official Qearn week-bucket penalty (0/5/10/15/20/25/30/35/40/45/50/55/100% reward-kept by weeks-held bucket), splits the penalty 50/50 between burn and redistribution, and accrues reward linearly at ~12.5% APY. Also exports computeEarlyUnlockPenalty(weeksHeld, totalWeeks) as a pure function for reuse.
New services/qubic/src/db/seed.ts. Idempotent. Seeds AIGARTH_TREASURY_ADDRESS (60-A), Showcase Validator 0/1/2 (deterministic stub computor addresses), and a 'Qearn Staking Contract' entry (computorIndex 676, isActive=false) so dashboard alias lookups work out of the box. Also records an opening 1T QUBIC deposit in treasury_movements.
server.ts now calls seedDevDefaults() in start() when NODE_ENV=development. Wrapped in try/catch so a seed failure is non-fatal (logs a warn). The package.json db:seed script is also wired to the same module for manual runs.
Added an extended doc-block at the top of services/qubic/src/client/stub.ts with: Qearn contract reference (docs + qubic.org blog), week-bucket penalty schedule, and a link to the official contract registry URL (https://static.qubic.org/v1/general/data/smart_contracts.json). Future readers see the WHY before the code.
Compatibility assessment for ANN registry, compute, continuous learning, and Qubic integration. Each dimension gets a verdict (Ready/Extend/Greenfield/Blocked), a 1-10 score, the gap, and the affected service. Material-science-specific: per-prediction provenance, uncertainty calibration, negative feedback as first-class. Output: docs/use-cases/material-science-eval.md.
Forward-looking article on the Aigarth Cloud website. Introduces the 8-role specialist team (Director, Literature, Simulation, Physics, Design, Optimization, Experiment, Validation), explains the per-stage cost model (DFT dominates at 87% of a $52 QU workflow), and lays out the 5-phase roadmap with a re-evaluation gate. Output: docs/use-cases/material-science.md.
Static manifest view of the proposal: 4-dimension compatibility matrix, 8-role architecture diagram, 5-phase roadmap timeline, illustrative cost model, 8-row risk register. Same look-and-feel as /video. Output: apps/dashboard/src/app/material-science/page.tsx + components/pages/material-science.tsx.
scripts/register-material-science-use-case.ts. Inserts phase-17, inserts 13 phase tasks (5 done + 8 backlog), upserts 2 docs into the docs table, logs activity. Idempotent — safe to re-run.
docs/deliveries/phase-17-material-science-delivery.md. Time-to-Ship section, SP velocity, what-shipped, open items, link to the two use-case docs.
Compatibility assessment for ANN registry, compute, continuous learning, and Qubic integration. Each dimension gets a verdict (Ready/Extend/Greenfield/Blocked), a 1-10 score, the gap, and the affected service. Output: docs/use-cases/video-synthesis-eval.md.
Forward-looking article on the Aigarth Cloud website. Introduces the 'specialist team' model (Director / Camera / Motion / Depth / FX / Audio / Quality), explains cost + risk, lays out the 4-phase roadmap with a re-evaluation gate. Output: docs/use-cases/video-synthesis.md.
Static manifest view of the proposal: 4-dimension compatibility matrix, 7-role architecture diagram, 5-phase roadmap timeline, illustrative cost model, 8-row risk register. Same look-and-feel as /services. Output: apps/dashboard/src/app/video/page.tsx + components/pages/video.tsx.
scripts/register-video-use-case.ts. Inserts phase-16, inserts 5 phase tasks (one per roadmap phase), upserts 2 docs into the docs table, logs activity. Idempotent — safe to re-run.
docs/deliveries/phase-16-video-synthesis-delivery.md. Time-to-Ship section, SP velocity, what-shipped, open items, link to the two use-case docs.
Workspace SDK consumed by server components and route handlers. Started as `import { Aigarth } from "@aigarth/sdk"`; ended as a relative import to packages/sdk/dist/index.js after the .js-extension bundler rabbit hole.
lib/server/session.ts: HttpOnly + SameSite=Lax + 7-day TTL cookie holding the JWT. lib/server/aigarth.ts: getAigarth() returns an Aigarth instance pointed at all 7 services, or null if not authed.
apps/web/app/(auth)/{login,signup}/page.tsx — clean forms with error display. app/api/auth/{login,signup,logout}/route.ts — POST handlers that talk to identity service, set session cookie, return { ok, redirect }. middleware.ts — edge guard: unauth /dashboard/* → /login?next=, authed /login or /signup → /dashboard.
Server component fetches /v1/me using the session cookie. Client component renders search, theme toggle, notifications, user menu with logout. Sign-out clears cookie + redirects to /.
4 stat cards (total jobs, compute credit remaining, total spent, available models) + 4 quick action tiles + latest marketplace listings sidebar. Fetches 5 services in parallel via Promise.all.
Server component lists /v1/keys. Client form (modal) calls /api/keys POST → shows full_key once with copy button. Revoke via /api/keys/:id DELETE. Key reveal-on-click for last 4 digits.
4 stat cards (total requests, total tokens, total cost, compute jobs) + by-model breakdown + by-endpoint breakdown + last 20 requests table with status code + duration.
Type-aware icon (chat/embedding/image/code) + name + description + context window + max output + per-1K pricing. 3-column grid.
Active subscription card + 4-plan grid with features + invoices table. All from /v1/{plans,subscriptions,invoices,credits}.
Compute: stats + credit card + regions + clusters + jobs table (last 20). Clusters: regions + clusters + per-cluster members with status + last heartbeat.
Listings grid (kind/price/available/duration/seller) + auctions grid (kind/start/min/current price/status/ends_at). Kind-aware badges (dutch/english/sealed_bid).
Compute credit card (used/remaining/% with bar) + reservations list (principal/remaining/epochs/fee) + Qubic wallets list (network, stake_authorized).
Server fetches /v1/models. Client renders model picker + system prompt editor + chat log. POSTs to /api/chat (SSE proxy), reads chunks, supports stop (AbortController) and clear.
Browser can't reach the gateway directly (HttpOnly cookie). /api/chat reads the cookie, attaches Bearer, calls gateway, pipes SSE back to the client. The streaming is end-to-end real.
scripts/strip-js-extensions.mjs runs as part of `pnpm build`. Strips the .js extensions that tsc preserves in ESM emit. Without this, Next.js's bundler tries to follow workspace symlink to src/ and chokes on the extensions.
scripts/e2e.ts. Marketing reach, auth pages render, middleware guards /dashboard, signup + auto-login, 10 dashboard pages render with real data, key create + revoke, topbar shows /v1/me, sign out clears cookie. Cookie jar implementation in pure fetch.
docs/deliveries/phase-9-delivery.md. Time-to-Ship ~180 min, 38 SP, 4.7 min/SP. Velocity table across all 9 phases. The hard-won lesson: workspace packages + Next.js bundler + .js extensions = use a relative path to the dist.
client.request<T>(path, init, baseURL?) is public. BaseResource(client, baseURL) holds the per-service baseURL. toQueryString helper exported from _base.ts. chat.create replaces chat.completions.create for ergonomic symmetry. types/ann.ts split AnnReview from marketplace Review to avoid collision.
Anns extends BaseResource. Public list/retrieve/listReviews. Authenticated create/update/publish/deprecate/addReview. Deploy forwards to /v1/compute/jobs on services/compute. Analytics returns denormalized counters + rating distribution + benchmarks rollup.
Hits /v1/auth/* and /v1/me (not /v1/users/me — that path doesn't exist). User type aligned to actual /v1/me response: { id, email, name, avatar_url, status, email_verified, locale, timezone, created_at, last_seen_at }.
5 sub-resources. Wallets: list/retrieve/link/balance/authorizeStaking. Stakes: createIntent/submit/list/retrieve/cancel/release (3-step K12-signing flow). Treasury: createMovement/listMovements/sign/execute (multi-sig). Validators: list/onboard. Network: status (current tick + epoch).
4 sub-resources + 2 convenience. Regions: list/retrieve/create/stats. Clusters: list/retrieve/create/listMembers/addMember/removeMember. Jobs: submit/list/retrieve/cancel/broadcast/start/complete (last 3 are test helpers). Reservations: create/list/retrieve/release. credit() and stats() convenience methods.
4 sub-resources. Plans: public list/retrieve. Subscriptions: create/list/retrieve/changePlan/cancel/cancelAtPeriodEnd. Invoices: list/retrieve/preview/generate/pay. Credits: list/redeemCoupon/validateCoupon. Coupons: admin create. Stripe-style credit model preserved — credits only consumed at pay time.
4 sub-resources. Listings: list/retrieve/create/update/close/listOffers. Offers: create/accept/reject/cancel. Auctions: list/retrieve/create (discriminated union for dutch/english/sealed_bid)/listBids/placeBid. Reviews: list/create. Purchases: list. Pseudo-purchases for auction wins (listing_id=null, offer_id=null, 2.5% fee).
POST /v1/keys issues ak_live_<prefix>.<secret> with full_key returned once. GET /v1/keys lists. DELETE /v1/keys/:id revokes (requires JWT, not the API key being revoked — caller must use a JWT client).
bin/aigarth.mjs. Resolves SDK dist via createRequire(this-script) so it works whether installed via pnpm symlink or globally. Commands: login (interactive), whoami, chat (one-shot + REPL), keys (list/create/revoke), anns (list/deploy), usage, init (scaffold .env.aigarth.example + .aigarth/config.json). No deps beyond node:readline.
Minimal Express server (~200 LOC). POST /api/chat (sync), POST /api/chat/stream (SSE bridge from AsyncIterable<ChatCompletionChunk> to browser EventSource), GET /api/whoami. Single HTML file with embedded JS for the chat UI. Added examples/* to pnpm-workspace.yaml.
Quick start, resource table, examples (chat streaming, signup, keys, ANNs, marketplace, Qubic stakes, billing), error class hierarchy, custom request helper, CLI reference, sample app pointer, build instructions, roadmap. ~270 lines.
scripts/e2e.ts. Sections: client instantiation, identity signup/login/whoami, billing plans + free sub, anns list/retrieve, marketplace listings/auctions, compute regions/credit/stats, qubic wallets/network, gateway key issue/use/revoke, error mapping. Caught /v1/users/me vs /v1/me, snake_case vs camelCase schema mismatches, gateway key-revoke auth requirement.
docs/deliveries/phase-8-delivery.md. Time-to-Ship: ~135 min, 28 SP, 4.8 min/SP. Velocity table across all 9 phases. Known limitations. Files created/modified. Next steps (Phase 9 dashboard wiring or stub-closing).
Same pattern as identity/qubic/compute/gateway/billing/ann. JWT verify, CORS, helmet, pino. Global rate-limit hook (60 writes/60s/user).
mkt_listings (28 cols, 5 idx), mkt_offers (13 cols, 3 idx, 1 FK), mkt_purchases (15 cols, 4 idx, 2 FK, nullable listing_id), mkt_auctions (28 cols, 5 idx), mkt_bids (11 cols, 3 idx, 1 FK), mkt_reviews (10 cols, 2 idx), mkt_audit_logs (8 cols, 3 idx). 9 enums: mkt_capacity_kind, mkt_listing_status, mkt_listing_visibility, mkt_offer_status, mkt_purchase_status, mkt_auction_kind, mkt_auction_status, mkt_bid_status, mkt_review_target.
Slug auto-gen with collision suffix. Listings service creates draft, updates, closes. List with filters: kind, status, seller, region, price range. Search by ILIKE on title + description. capacity_remaining_qubic decrements on offer accept; hits 0 → status=sold_out.
Buyer places offer (validates listing is active, not own, amount >= min, <= remaining). Seller accepts → capacity decrements atomically + status flips to sold_out if 0. Seller rejects → offer rejected, capacity preserved. Buyer cancels → pending offer cancelled, capacity preserved.
Discriminated union schema validates per-kind inputs (Dutch needs decrementPerTick + tickInterval; English + sealed_bid just need minPrice). Dutch: compute current price = max(min, start - ticks * decrement). English: track current_winning_bid. Sealed: one bid per bidder, all revealed at end. Settle on read after ends_at.
Dutch: first bid >= current price wins, settles immediately. English: bid must be > current winning, outbid previous. Sealed: one bid per bidder. On win, create a mkt_purchases pseudo-purchase with platform fee (2.5% of bid).
One review per (target_type, target_id, reviewer_user_id). After every review, recompute target's rating_average and rating_count. Currently only listings have the rollup; auctions/users deferred.
Public: 9 endpoints (browse, search, list offers/bids/reviews). Authenticated: 10 endpoints (CRUD on listings/offers, create + bid auctions, add reviews). /v1/me: 4 endpoints (listings, offers, auctions, bids). /v1/admin: ping. Shared serializers in lib/serialize.ts.
Records every meaningful action: listing.created/updated/closed, offer.placed/accepted/rejected, auction.created/settled, bid.placed/won, review.added. Decoupled from any shared enum. 3 indexes (actor, action, created).
scripts/e2e.ts. 1) health, 2) signup + login (seller + buyer), 3) listings create + publish + browse + search + filter by kind, 4) offers place + accept (decrement) + reject + self-offer prevention, 5) auctions 3 kinds + bid (Dutch wins immediate, English ascending + lower-bid-rejected, sealed-bid one-per-bidder), 6) reviews + listing rollup, 7) /v1/me endpoints, 8) admin (non-admin → 403), 9) validation (bad input, not found).
docs/deliveries/phase-6-delivery.md. ~80 min, 22 SP, 3.6 min/SP. Includes full architecture, 3 auction kinds explained, decisions (settle-on-read, pseudo-purchase, env-based admin), known limitations, E2E verification output. Plus update-phase-6.ts (this script).
Removed silent stub fallback. Deploy is transactional with the cross-service call to services/compute. If COMPUTE_SERVICE_URL is set and the call fails, the deploy throws. No row is created. Fixed path /v1/compute/jobs and priority 5. JWT is forwarded in Authorization.
Open licenses (price_per_call_qubic = 0) → granted immediately, status=active. Priced licenses (commercial, restricted, custom) → caller must pass paidInvoiceId from services/billing. We verify the invoice exists, was paid, and is for the same user + license. When BILLING_SERVICE_URL is unset, dev mode allows priced licenses with a warning.
POST /v1/admin/anns/:id/suspend — admin sets status to suspended. POST /v1/admin/anns/:id/restore — back to published. POST /v1/admin/anns/:id/force-deprecate — bypasses creator check for ToS violations. isAdmin(userId) reads ANN_ADMIN_USER_IDS env var.
Added pg_trgm extension. Generated search_text column (name + tagline + description). GIN trigram index. ?fuzzy=true uses similarity() > 0.15 for typo tolerance. ?sort=relevance orders by best trigram match.
Global Fastify preHandler hook on POST/PUT/PATCH/DELETE. 30 writes per 60s per JWT sub. Sets X-RateLimit-Limit/Remaining/Reset headers. Returns 429 with Retry-After when exceeded. In-memory; move to Redis for multi-replica.
ann_idempotency_keys table. Idempotency-Key header. Same key + same body → replay cached response (200 with Idempotent-Replay: true). Same key + different body → 422. Default TTL 24h. Per-(user, key, route) unique.
GET /v1/anns/:idOrSlug/analytics. Returns denormalized counters, deployment status breakdown, 1-5 star rating distribution, benchmark rollup. In-service only; cross-service usage (gateway + compute per-ANN) is deferred.
Added 5 new sections: fuzzy search, idempotency, analytics, admin, rate-limit. Updated section 7 to test payment-gating (commercial license without paidInvoiceId → 400).
Same pattern as identity/qubic/compute/gateway/billing. JWT verify, CORS, helmet, pino. Fastify app.authenticate decorator (JWT-only).
ann_categories (8 cols), ann_licenses (15 cols, 1 unique idx), anns (29 cols, 6 idx, 2 FK), ann_versions (14 cols, 3 idx, 1 FK), ann_deployments (16 cols, 4 idx, 2 FK), ann_ratings (8 cols, 2 idx, 1 FK), ann_benchmarks (10 cols, 2 idx, 2 FK), ann_licenses_granted (13 cols, 4 idx, 2 FK), ann_audit_logs (8 cols, 3 idx). Every enum prefixed ann_. All bigint without defaults.
16 categories matching the existing marketplace UI taxonomy (Vision, Medical, Legal, Finance, Education, Government, Engineering, Creative, Agents, Research, Science, Coding, Enterprise, Search, Oracles, Language). 4 license types: open (free, modify+redistribute), commercial (100k QUBIC/call, 70% revenue share), restricted (1M QUBIC/call, 85% revenue share), custom (negotiated).
Slug auto-gen with collision suffix. getAnn(idOrSlug) resolver. ListAnns with category + license + creator + q (ILIKE) + sort. Publish requires ≥1 version. Deprecate excludes from default marketplace. Sign stores Qubic wallet + signature (format-only).
ann_versions.version is semver, unique per (ann_id, version). Adding a new version with setAsLatest=true flips prior is_latest=false and updates anns.current_version_id + denormalized accuracy + latency from metrics.
POST /v1/anns/:id/deployments. Picks the version (explicit, current, or latest). Cost per call set from license. Real path: POST /v1/jobs on services/compute. Stub path: synthetic compute_job_id, status=running. Stop forwards cancel to services/compute (best-effort).
ratings: 1 per (ann, user). verified_use=true if user has deployed. recomputeRatingAggregates re-pulls AVG+COUNT after every rate and writes to anns.rating_average / rating_count. benchmarks: separate concept for verified accuracy numbers (MMLU, HumanEval, internal).
POST /v1/anns/:id/license grants a license to the caller. Upsert on (ann, user, license). DELETE revokes. For now, no payment gating — real impl hooks into services/billing.
Public: 11 endpoints (browse, details, versions, ratings, benchmarks, deployments, categories, licenses). Authenticated: 12 endpoints (CRUD, publish, deprecate, sign, deploy, stop, rate, benchmark, license). /v1/me: 4 endpoints (anns, licensed-anns, licenses, deployments). Shared serializers in lib/serialize.ts.
Extracted all entity serializers to a shared lib/serialize.ts. Routes import from one place. This fixed the 'me.ts imports serializeDeployment from anns.ts' typecheck error and is now the convention for future services.
Records every meaningful action: ann.created, ann.updated, ann.published, ann.deprecated, ann.signed, ann.version_added, ann.deployment_started, ann.deployment_stopped, ann.rating_added, ann.benchmark_added, ann.license_granted. Decoupled from any shared enum. 3 indexes (actor, action, created).
scripts/e2e.ts. 1) health + 16 categories + 4 licenses, 2) signup + login, 3) create draft + version + publish (denormalized metrics) + 2nd version, 4) sign with Qubic wallet (format-only), 5) marketplace browse + search + filter, 6) deploy + stop, 7) benchmark + rate + license grant + revoke, 8) /v1/me endpoints, 9) deprecate, 10) 5 validation cases (400/400/400/400/404).
docs/deliveries/phase-5-delivery.md. ~145 min, 24 SP, 6.0 min/SP. Includes full architecture, decisions (slug auto-gen, denormalized aggregates, license grant semantics, deploy stub fallback, K12 deferred to identity), known limitations, E2E verification output. Plus update-phase-5.ts (this script).
Same pattern as identity/qubic/compute/gateway. JWT verify, CORS, helmet, pino. Fastify app.authenticate decorator (JWT-only).
9 domain tables + billing_audit_logs. Every enum prefixed billing_ (billing_plan_tier, billing_subscription_status, billing_invoice_status, billing_payment_status, billing_credit_kind, billing_credit_status, billing_coupon_kind, billing_coupon_status, billing_payment_method). All bigint amounts in QUBIC (no defaults).
free (0 Qu / 10K tok), pro (30 Qu / 1M tok, 30 Qu first-month-free signup credit), team (300 Qu / 10M tok, 50 Qu signup), enterprise (3,000 Qu / unlimited, 1,000 Qu signup). Overage per 1K = 1k/30k/20k/10k QUBIC (volume discount). Seed is upsert-aware so changing signup_credits_qubic re-runs the update.
Rolling 30-day periods anchored to subscription start. Subscribe grants signup credit. Change plan updates the planId. cancel-at-period-end sets a flag. cancelImmediately sets status=cancelled. Period rollover is on-demand (cron is future work).
buildPreview(userId, jwt, subscriptionId, { asActual }). Stripe-style: invoice total = subtotal, no credit applied at generation. Preview shows would-be credit (creditAppliedQubic = available < subtotal ? available : subtotal). Actual invoice has creditAppliedQubic = 0. Cross-service usage via getGatewayUsage + getComputeUsage with JWT forwarding.
method=qubic calls services/qubic POST /v1/qubic/treasury/movements, records tx_hash. method=credits debits user balance via applyCreditToInvoice, records credit_id. method=fiat is stub (returns pi_stub_<hex>, succeeded immediately). Single point: if TREASURY_ADDRESS not set, method=qubic generates a synthetic 60-char Qubic-style tx hash and marks succeeded.
Credit kinds: grant, promo, refund, earned, referral. Coupon kinds: percent_off (grants value*10 QUBIC), amount_off (value Qu), free_period (value days * 1 Qu/day). Per-user redemption limit, max redemptions, validFrom/validUntil. Credits expire 90 days after coupon redemption.
Fetches gateway + compute usage in parallel via getGatewayUsage(userId, jwt) and getComputeUsage(userId, jwt). Forwards user's JWT via extractJwt(req) helper. Returns source=live|partial|unavailable for graceful degradation. Aggregates total_cost_qubic = gateway.total_cost + compute.total_spent.
/v1/plans, /v1/plans/:id, /v1/subscriptions, /v1/subscriptions/:id, /v1/subscriptions/:id/{patch,cancel-at-period-end,cancel}, /v1/billing/usage, /v1/billing/upcoming, /v1/invoices, /v1/invoices/:id, /v1/invoices/preview, /v1/invoices/generate, /v1/invoices/:id/pay, /v1/payments, /v1/credits, /v1/credits/redeem, /v1/coupons/validate, /v1/admin/coupons, /healthz, /readyz. All under Fastify app.authenticate (JWT-only).
scripts/e2e.ts. 1) health + plans, 2) signup + JWT, 3) subscribe to pro (30 Qu signup credit), 4) 3 gateway calls (2 chat + 1 embed), 5) cross-service usage, 6) preview upcoming invoice, 7) generate actual invoice, 8) pay with credits, 9) coupon create+validate+redeem, 10) plan change + cancel, 11) validation (3 bad-input cases). Required a Stripe-style credit model re-design mid-flight (E2E section 8 caught the auto-apply footgun).
Records every meaningful action: subscription.created/changed/cancelled, invoice.generated/paid/voided, payment.succeeded/failed, credit.granted/exhausted, coupon.created/redeemed. Decoupled from identity's audit enum (free-form action text). 3 indexes (actor, action, created).
docs/deliveries/phase-4-delivery.md. ~130 min, 22 SP, 5.9 min/SP. Includes full architecture, Stripe-style credit model explanation, decisions, known limitations, E2E verification output. Plus update-phase-4.ts (this script) to wire the tracker.
Same pattern as identity/qubic/compute. Dual auth (JWT + API key). Audit log prefixed (gateway_audit_logs). Bigint without defaults (Phase 3 lesson). Enums prefixed with gateway_ (Phase 7 new lesson).
gateway_models (13 cols, 2 idx), gateway_deployments (7 cols, 2 idx, 1 FK), gateway_api_keys (16 cols, 3 idx), gateway_requests (18 cols, 5 idx, 1 FK), gateway_rate_limits (5 cols, 1 idx, 1 FK), gateway_audit_logs (8 cols, 3 idx).
src/backends/stub.ts. Deterministic responses keyed on sha256(input). Chat streams word-by-word with 5ms gaps. Embeddings produce 1536-dim unit vectors. Image returns picsum.photos URLs. Swap module for real provider integration.
ak_live_<8prefix>.<43secret> format. Secret stored as sha256. Verify on every request. Used by chat/embeddings/images for dual auth. /v1/keys endpoints for admin (JWT-only).
Every call logged to gateway_requests with tokens, cost, duration, status. GET /v1/usage aggregates by model + endpoint. Sliding-window rate limit per key per minute via INSERT...ON CONFLICT DO UPDATE. Defaults 60 RPM / 100K TPM; per-key overrides.
10 endpoints total. Chat supports sync (returns chat.completion) and SSE streaming (text/event-stream chunks). Unified authenticateRequest() helper accepts JWT or API key.
scripts/e2e.ts. Health, public models, JWT auth, key issue, chat sync (JWT + key), SSE streaming (27 chunks), embeddings (deterministic + batch), image generation, usage aggregate + recent, validation (4 cases), bad keys (2 cases), revoke + post-revoke rejection.
scripts/sdk-smoke.ts. Updated SDK package.json to point at dist (not src). Added .js extensions to all SDK source imports. Rebuilt SDK. Verified round-trip works: list returns 4 models, chat.create returns 34 tokens, chat stream works, embeddings returns 1536-dim vector.
Avoided reinventing: Qubic Node, qubic-cli, outsourced-computing contract, 676 computors, tick-based execution, ts-library-wrapper, qubic-dev-kit, QX (decentralized exchange). This changed Phase 2's design from a generic scheduler to a thin broker.
Same pattern as identity + Qubic. JWT verify only (no session table). Helmet + CORS + cookie + Zod. Schema audit log renamed to compute_audit_logs (Phase 3 lesson). Bigint columns without defaults (Phase 3 lesson).
compute_regions (8 cols, 1 idx), compute_clusters (9 cols, 2 idx, 1 FK), compute_cluster_members (6 cols, 2 idx, 1 FK), compute_jobs (25 cols, 5 idx, 2 FKs), compute_reservations (16 cols, 2 idx), compute_audit_logs (8 cols, 3 idx). All bigint amounts in QUBIC (smallest unit).
Admin-facing topology management. Idempotent member add. Region stats: cluster count, computor count, active job count.
Two-step credit hold + settlement. Cost = JOB_BASE_FEE_QUBIC + (duration_ms × JOB_COST_PER_MS_QUBIC). 6-state machine (queued → submitted → running → completed/failed/cancelled). Cancel refunds credit hold. Max 100 active jobs per user.
0.5% platform fee on creation. 0.1% penalty on early release. Credit is debited on job submit, refunded on cancel-while-queued. Active reservations aggregate into the user's capacity credit.
21 endpoints total. Includes test-helper endpoints (POST /jobs/:id/{broadcast,start,complete}) for stub mode + dev. /v1/compute/credits is derived from active reservations.
src/workers/job-monitor.ts. Subscribes to aigarth.compute.job. In stub mode, auto-progresses submitted→running after 3 ticks, running→completed after 6. Expires deadline-missed jobs to failed. NATS-down = poll-only mode.
Idempotent. global region = all 676 computors + general-pool cluster (every 4th). eu-west region = computors 0..225 + training-pool (0..99) + inference-pool (126..225).
scripts/e2e.ts. Health, auth, regions+stats, clusters+members, reservations+credits, free-tier jobs, reservation-charged jobs, full lifecycle (broadcast→running→completed with result), user stats, early-release with penalty, validation.
Original ROADMAP implied building a generic scheduler. Research showed Qubic already has tick-based execution + outsourced-computing contract + 676 computors. Phase 2 became a thin broker over those primitives instead. ~50% smaller surface, faster build, less to maintain.
Port 7002. Same patterns as identity. JWT verify only (no session-table check). Helmet + CORS + cookie + JSON schema validation via Zod.
qubic_wallets, qubic_balances, stakes, transactions, rewards, validators, treasury_movements, epoch_snapshots + qubic_audit_logs (free-form text action, decoupled from identity's enum). Bigint amounts.
QubicClient interface. StubQubicClient (deterministic, 676 computors, sha256-derived tx hashes). HttpQubicClient (graceful-degrading JSON). Factory by QUBIC_CLIENT_MODE env. TCP notes in tcp-client.ts.TODO.
POST /v1/qubic/wallets idempotent on (user, address). GET single + list. GET balance with 30s cache (?refresh=true to bypass). POST /:id/authorize-staking sets stake_authorized + expiry (default 365d).
Two-step: server builds canonical message, user signs, server broadcasts. Status: pending_signature → broadcast → confirming → active → unstaking → released. Release blocked before maturity epoch. Cancel only valid before broadcast.
POST movements (kind enum, signersRequired). POST :id/sign validates against TREASURY_SIGNERS env, dedupes duplicates, stores {signer, signature, at} in payload jsonb. POST :id/execute marks executedAt + txHash.
First call after 1h refreshes from listComputors (676 by default). Cached in DB. POST /:idx/onboard marks a specific computor as onboarded (alias, performance, stake).
Standalone worker (pnpm worker:tx-monitor). NATS subscribe on aigarth.qubic.tx (optional — falls back to poll-only). 10s poll of in-flight txs. Confirms after 5 ticks. Handles finalized/failed transitions and updates related stake/treasury rows.
scripts/e2e.ts. Covers health, auth, validators, wallets (idempotent link, balance, authorize), network status, staking (intent, submit, cancel, release-before-maturity), treasury (create, sign, threshold, duplicate rejected, unauthorized rejected, execute).
POST /v1/orgs (create), GET /v1/orgs (list), GET/PATCH/DELETE /v1/orgs/:id, member add/list/patch/remove with role hierarchy owner > admin > member > viewer, team create + add member. Authorization middleware requireOrgRole gates routes by role.
POST /v1/api-keys returns secret ONCE (ak_live_<prefix>.<secret>). Hashed with sha256 at rest. Status lifecycle active → rotated → revoked. POST /v1/api-keys/:id/rotate issues new + marks old rotated. DELETE /v1/api-keys/:id revokes with reason. Member removal cascades to revoke their keys.
Enroll: start returns base32 secret + otpauth URL. finish verifies first code, marks credential enrolled. List: GET /v1/mfa shows enrolled. Verify: POST /v1/mfa/totp/verify checks code against any of the user's enrolled credentials. Wrong code → 401.
POST /v1/mfa/webauthn/register/start returns RP info + challenge. POST /v1/mfa/webauthn/register/finish stores credential. Real client-side ceremony (navigator.credentials.create/get) deferred to the dashboard client; server-side is a clean swap-in point for @simplewebauthn/server.
POST /v1/wallets/link/start issues 32-byte nonce + canonical message. POST /v1/wallets/link/finish verifies the signed nonce, links the wallet to the user. The signature verifier is currently a format-validated STUB (the real K12-based verifier is a swap-in). Nonce single-use, 5min TTL. Address format validation (60 base-26 chars).
Filter by action (enum-checked), actor, date range, cursor pagination. GET /v1/audit-logs/stats returns counts by action. Viewers get 403. Backed by the same audit_logs table that the rest of the service writes to.
Moved Next.js app to apps/web and tracker to apps/dashboard. Set up pnpm-workspace.yaml, turbo.json, root package.json, tsconfig.base.json.
Moved 16 UI primitives (button, card, dialog, tabs, etc.) to packages/ui/src/primitives. Extracted globals.css to packages/ui/src/styles/. Created barrel exports.
cn, formatters (currency/compact/percent/bytes/duration/relative), theme helpers, strings (slugify/truncate/initials/pluralize).
tsconfig presets (base/library/nextjs/node), eslint configs, prettier config, tailwind preset with full design tokens.
Typed client with chat completions (streaming SSE), embeddings, models, ANNs, usage. Error class hierarchy. Retry with backoff. Phase 7 deliverable, but skeleton in Phase 0.
Postgres 16, Redis 7, NATS 2.10, MinIO, MailHog. Volume persistence, health checks, minio-init sidecar to create the aigarth bucket.
Single ci.yml workflow with install, checks, and build jobs. Caches pnpm and turbo. Plus preview.yml for affected builds on PR.
preview.yml runs on PRs that touch apps/** or packages/**. Uploads .next/ and packages/*/dist for review.
Set up the workspace, root scripts, task graph.
Move globals.css and tailwind tokens into @aigarth/ui.
Move the shadcn primitives and custom components into a shared package.
Typed client for the OpenAI-compatible gateway.
Postgres, Redis, NATS, MinIO, MailHog.
GitHub Actions for every service + monorepo root.
Operational Kanban + phase tracker for the whole build.
PRD, BRD, architecture, data model, API spec, security, sprint plan, risk register, team, governance, glossary, brand voice, contributing, dev guide, index.