A whole business, run by one agent.
OAC turns a description of a business into a fully-wired agent that operates it — sales, marketing, support, inventory, payments — and a dashboard that shows you what's actually working. This page documents every feature on the live site.
Overview
OAC is two cooperating systems:
- OAC — the company layer — owns all business data (its own D1), the schema-driven module API, first-party auth, the dashboard + admin + docs, and the integration surface (connectors + SDK). Exposes business data to the agent as MCP tools.
- Uranus — the agent runtime — the per-business
DashboardAgentDurable Object (LLM, gateways, payments, browser, memory). It operates the business by calling OAC's tools, and provisions agents from a Blueprint spec.
From a user's point of view, the surface is just a website: describe a business, get an agent that runs it. Products and external systems can also feed data in via connectors (pull) or the SDK (push).
Architecture
OAC is the company layer — it owns the business data and exposes it as tools. Three things support OAC and meet it at three explicit boundaries: Uranus drives the agent runtime that operates the data, external software (accounting, ads, CRM…) is pulled in via the connector framework, and product code (POS, SaaS, marketplaces) pushes events in via the SDK. All four sides converge on the same canonical module records — so the agent, the dashboard, the synced data, and the pushed data treat each other identically.
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ External SaaS │ │ Your products │
│ (QuickBooks, Xero, Stripe, │ │ (POS, SaaS app, ecommerce, │
│ Shopify, Salesforce, │ │ marketplace, mobile app) │
│ Meta/Google Ads, …) │ │ │
└─────────────┬───────────────┘ └─────────────┬───────────────┘
OAuth + │ scheduled Bearer │ realtime push
PULL │ fetch+map INGEST │ via @oneagentcompany/sdk
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ OAC (Worker `oac-api` + own D1 `oac-db`) │
│ ──────────────────────────────────────────────────────── │
│ • System of record for ALL business data (keyed by │
│ OAC business id): businesses, module items, contacts, │
│ transactions, + everything synced/pushed from products │
│ • First-party OTP auth · schema-driven CRUD │
│ • Connector framework (PULL) + SDK ingest (PUSH) │
│ • Categories aggregate across leaves (P&L, AR, AP, …) │
│ • Exposes an MCP server: per-business tools ◄────────┐ │
│ Frontend (Pages): / landing+wizard · /app · /admin · /docs │ │
└────────────────────────────────────────────────────┐ │ │
▲ upsert / provision / send-otp │ │ MCP │
service secret │ (Uranus → OAC, vice versa) ▼ │ │
┌──────────────────────────────────────────────────────────────┐
│ Uranus (Worker `cloudflare-agents-pages`) │
│ • DashboardAgent DO (one per business): LLM, gateways, │
│ browser, memory, workflows — the runtime │
│ • Calls OAC's MCP tools to read/operate business data │
│ (incl. payments — post 2026-05 cutover) │
│ • Provisions agents from a Blueprint spec (any source) │
│ • Retains: x402/MPP gate, approval gate, Stratos wallet │
│ ops, bank-detect trigger, gateway pipelines │
│ • Lends OAC its verified Resend sender for OTP email │
└──────────────────────────────────────────────────────────────┘
Data, auth, and the module API live in OAC's own D1. The agent reaches business data only through OAC's MCP tools (scoped per business). The dashboard talks to oac-api for data/auth/CRUD; agent-dependent calls (chat, stats) are proxied through OAC to the Uranus agent.
How Uranus supports OAC
Uranus is a separately-deployed worker; OAC delegates to it for everything that needs an agent runtime or a deliverability surface it shouldn't have to re-own.
- Agent runtime. Each business has a Durable Object (
DashboardAgent) on Uranus that runs the LLM loop, manages gateways (Telegram/WhatsApp/email/web), drives the browser, holds memory, and runs workflows. OAC owns the data; Uranus owns the loop that acts on it. - MCP client → server. The DO calls OAC's per-business MCP server (
POST /mcp) with a Bearer key (oacmcp_…).tools/listis generated live from the business's enabled modules + categories, so toggling a module on the dashboard changes the agent's toolset on the next call — no re-provision. - Approval gate. Uranus's dispatcher pauses any tool call flagged with
annotations.requiresApproval— both for local tools (declared intool-constants.ts) and remote MCP tools (declared by the server, e.g. OAC'soac_payments_send_payment). Asks the owner via gateway, resumes on YES, returns denial to the agent on NO. Single mechanism, generalized 2026-05. - Provisioning. OAC's wizard (or any source-agnostic spec) hits Uranus's
/api/internal/oac/provisionwith the Blueprint — Uranus spins up the DO, registers the MCP server row, and hands back the agent id. OAC stores it on the business. - OTP email. OAC's auth code is sent through Uranus's verified Resend sender via
/api/internal/oac/send-otp, so OAC doesn't have to verify its own domain to ship login. - Dashboard proxying. The
/appcallsoac-apifor everything;oac-apiforwards agent-dependent reads (chat, stats, browser screenshot) to Uranus's/api/internal/oac/agent/<id>/…. The browser only ever sees one origin. - x402 / MPP runtime gate. Uranus retains the HTTP-402 payment gate for paid agent endpoints — pay first → workflow executes → result returned. This is access billing (metered API calls), distinct from business income. Receipts can optionally be pulled into OAC's P&L via the future
uranus_x402connector. - Stratos wallet ops. On-chain signing + transfers run on Uranus (
stratos_sign,stratos_transfer); the wallet state record (address, last balance) lives in OAC'spayments.walletitem type. - Bank-detect trigger. The gateway pipeline runs the regex match + LLM parse on inbound SMS/email locally on Uranus (it's where the message lands), then calls
oac_payments_log_notification+oac_payments_log_paymentover MCP to persist the parsed record on OAC. - Service secret. Both directions are authenticated by a shared
OAC_FRONTDOOR_SECRET(header on every cross-service call). No public access to either internal surface.
What moved from Uranus to OAC (2026-05 payments cutover)
The full payment surface — transactions, payment methods, payment notifications, Stripe checkout sessions, Stripe Issuing virtual cards, and Stratos wallet state — now lives on OAC's data layer, exposed to the agent as oac_payments_* MCP tools that drop into tools/list the moment the payments module is enabled.
| Was on Uranus | Now on OAC | Agent tool name |
|---|---|---|
transactions table | payments.payment item type | oac_payments_log_payment, list_transactions, check_balance |
payment_methods table | payments.method item type | oac_payments_list_methods |
payment_notifications table | payments.notification item type | oac_payments_log_notification |
stripe_checkout_sessions | payments.stripe_session | oac_payments_create_checkout |
stripe_issuing_cards | payments.issuing_card | (REST: /api/oac/business/:id/stripe/cards) |
| Stratos wallet state | payments.wallet | oac_payments_send_payment (requires approval) |
POST /stripe-webhook on Uranus DO | POST /webhooks/stripe/:businessId on oac-api (per-tenant URL, per-tenant webhook secret) | n/a |
The cutover is freeze + cutover: pre-cutover rows on Uranus's D1 are preserved as a read-only archive (no DROP TABLE); the old DO routes (/transactions, /payment-methods, /payment-notifications, /stripe, /stripe-webhook, /stratos) now return HTTP 410 Gone with a pointer to the OAC equivalent. New payment events from any source land on OAC.
Cred storage. Per-tenant Stripe + Stratos credentials live in oac_businesses.payment_settings (JSON column added in migration 0006). The PaymentSettings shape: { stripe: { secretKey, publishableKey, webhookSecret, accountId }, stratos: { apiBase, apiKey, walletNetwork, walletAddress }, defaultCurrency, requireApprovalAbove, x402FacilitatorUrl }. No env-level Stripe secret on OAC — every business plugs in their own account.
How external software supports OAC (pull)
Existing SaaS that already has the data — accounting, CRM, ads, ecommerce — is reached via the in-house connector framework. A connector is a single ConnectorDef file: auth method, entities to pull, and a map that reshapes each provider row into a canonical module item.
- Accounting (QuickBooks, Xero, FreshBooks). Invoices, bills, payments, expenses →
finance+paymentsmodule items, tagged with the synceddepartment/projectdimensions so they roll into the Accounting → P&L / AR / AP reports automatically. - Payments / commerce (Stripe, Shopify, Square). Charges, refunds, customers, SKUs →
payments,sales,crm,inventory. - CRM & sales (Salesforce, HubSpot, Pipedrive). Contacts, leads, deals →
crm+saleswith therefback to the originating campaign / contact preserved. - Ads & social (Meta Ads, Google Ads, LinkedIn Ads, Twitter/X). Campaigns, ad sets, spend, conversions →
campaigns, so Marketing's ROAS report has real numerator and denominator. - OAuth tokens are AES-GCM encrypted at rest. Sync is incremental and idempotent by
(source, externalId)— re-running a sync updates, doesn't duplicate.
Connectors are optional: the data path works without any of them, and adding a new provider is one file in server/src/connectors/registry.ts. See Integrations & SDK for the full API.
How products support OAC (push)
When the source of truth is your own code — a POS, a SaaS, a marketplace, a mobile app — you push as events happen, using @oneagentcompany/sdk and a per-business ingest key (oacingest_…, push-only, scoped to one business, minted in Settings → Companies → SDK keys).
- One record per real-world event. A POS sale →
oac.transaction(...)→finance+payments. A new customer →oac.contact(...)→crm. An inventory delta →oac.pushBatch([...])→inventory. The SDK has typed helpers for the common cases plus a genericpush/pushBatchfor any module. - Idempotent by
(source, externalId). Re-firing with the same id updates the existing row instead of duplicating — safe to retry, safe to replay. - Dimensions on every record.
department,project,costCenter,sourceare first-class on every push, so pushed product data slices the same way synced data does and shows up in the same category reports. - Zero dependencies, runs anywhere (Node 18+, browsers, Workers, Deno, Bun).
One canonical surface for the agent
Whether a record arrived via the wizard, the dashboard, a connector pull, an SDK push, or the agent's own MCP create call, it lands in the same oac_module_items table tagged with its source and created_by. The agent's tools, the category reports, and the dashboard see one timeline — provenance preserved, but treated uniformly.
Provisioning
A business + its agent are created from a Blueprint spec — a JSON structure describing the business, persona, profile, enabled modules, gateways, and human setup tasks. Provisioning is source-agnostic: the conversational wizard is just one source. A template, a partner API, or a connector can hand over a spec and get a fully-wired agent.
What one provision does
- Creates the business record in OAC (
oac-db) — visible in the dashboard. - Creates the Uranus agent (modules / gateways / payments configured from the spec).
- Mints a per-business MCP key and registers OAC as an MCP server for the agent — so the agent immediately has scoped read/write tools over its modules.
Sources
- Wizard — the conversational onboarding at oneagentcompany.com builds a spec interactively, then deploys (email → OTP → provision).
- Direct / template / connector —
POST /api/internal/oac/provision(service-authed) with{ spec, email, source }→{ businessId, agentId }. This is how non-wizard sources “define the whole agent setup based on specific requirements.”
The agent's toolset is a live projection of the business's enabled modules — toggle a module and the agent's tools change on the next list, no re-provision.
Authentication
Email + 6-digit OTP, 30-day first-party session cookie — OAC's own (server/src/auth.ts on oac-api), separate from Uranus's passkey auth. OAC owns the OTP lifecycle + sessions; the email send is delegated to Uranus's Resend so the verified sender/domain is reused.
- Request OTP —
POST /api/oac/auth/request-otpwith{ email }. The worker hashes a fresh 6-digit code intooac_otps(email, code_hash, expires_at, attempts)and emails it via Resend. TTL = 10 minutes. - Verify OTP —
POST /api/oac/auth/verify-otpwith{ email, code }. Compares hash, decrements attempt counter (max 5), deletes the row on success, creates a row inoac_sessions, and returns a session cookie. - Cookie —
oac_session, HttpOnly, Secure, SameSite=None, Domain=manage.uranus.im, Path=/. Max-Age = 30 days. - Whoami —
GET /api/oac/meresolves the cookie to a user row fromoac_users. - Logout —
POST /api/oac/auth/logoutdeletes the session row and clears the cookie.
The same oac_otps table is reused by the wizard's blueprint email-verification step.
CORS: only the OAC origins (oneagentcompany.com, www., the Pages preview hostnames, and a few localhost ports) are allowed credentials.
Dashboard
The per-business operating view at /app. Single-page app driven by js/app.js with hash routing.
Routes
| Hash | View |
|---|---|
/app | Overview: persona banner, performance KPIs, merged activity feed (Uranus stats + OAC audit log), modules grid |
/app#board | To-dos — shared human↔agent Trello-style kanban over projects.task (drag-drop, quick-add, 12s live poll) |
/app#m/<key> | Per-module page — a purpose-built canvas + List toggle, live KPIs, a guide panel (on marketing/ads/social/seo/content), Data / Agent log / Settings tabs |
/app#cat/<cat>[/<report>] | Category page + report runner (Payments lives at #cat/accounting/payments) |
/app#settings | Settings landing: modules count, account, businesses |
/app#settings/modules | Per-module toggle list |
/app#settings/integrations | Integrations hub — every external service in one view (status + connect) |
Per-module canvases
Each module renders as its own little app rather than a generic table — the primary view is a purpose-built canvas, with a List toggle for the raw table and KPI tiles computed live from the same fetch:
- Scheduling → month calendar · Sales → drag-stage pipeline · Finance → 6-month cashflow + outstanding invoices · Marketing → funnel + ROAS cards · Inventory → stock-level bars · Support → priority lanes · CRM → people cards w/ lifecycle filter · Orders → status flow + live queue · Catalog → product grid · Goals → progress bars + insights · Content → drafts + newsletters with a Send button.
Guidance layer
The expert modules (Marketing, Advertising, Social, SEO/GEO, Content) carry a coaching panel so a non-expert can operate them: a stateful checklist (reads your real records → tells you the next step), plain-language tips, a glossary (ROAS, CPA, GEO…), and one-tap “let the agent do it” prompts — all tailored to the business archetype/profile. Record forms hide optional fields under “More details”, use brand-logo/colour pill selectors, and show field-level hints.
Shell elements
- Sidebar — Overview + To-dos, then enabled modules grouped under their category, then Account (Modules · Integrations · Settings · Docs · Agent runtime ↗). Mono Lucide-style SVG icons; active state is a soft mint gradient.
- Performance card — KPI tiles + refresh; pulls
/stats. - Activity feed — merges Uranus runtime activity with the OAC audit log (
/activity), so real create/update/delete/send events show with who-did-it badges (🤖 agent / 👤 you / 🔌 product). - Chat FAB — posts to
/chat; agent reply + tool-call summaries render. Guide prompts pre-fill it. - Mobile drawer — sidebar slides in; board columns swipe horizontally.
The 21 modules
Every business draws from the same catalog of 21 modules (a business enables the subset it needs). Each module is a typed ModuleSchema (server/src/modules.ts) — that one declaration drives the CRUD API, the agent's MCP tools, validation, the relation graph, and the dashboard's forms. Most modules hold more than one item type (e.g. Orders = order + queue ticket + subscription), and every record captures both intent and actuals so performance is measurable.
The set spans the whole company: find customers (Marketing/SEO/Social/Ads), convert them (Sales/CRM/Catalog/Storefront/Orders), serve them (Support/Scheduling/Payments), run the back office (Finance/Procurement/Inventory/People/Projects/Documents), and improve continuously (Goals/Insights). That coverage is what lets a single agent plausibly operate a business rather than assist with parts of it.
Cross-cutting: dimensions & relations
- Dimensions — every record can carry
department,project,costCenter, andsource, so spend & revenue roll up by initiative/department/project. - Relations (
ref/refsfields) — records link across modules (an ad set → its campaign, a lead → its contact, an order → its customer + payment + storefront). The relation graph is derived from the schema across all item types, so the whole business reads as one graph, not 21 silos. The agent traverses it with oneoac_relatedcall.
📣 Marketing — get found & get attention
💰 Accounting — the books
🤝 Sales & Customer — win them, keep them
🛒 Commerce — the storefront economy
🏢 Workspace & Knowledge — running the company
🎯 Goals & Insights — what makes it self-driving
Combined with the oac_business_snapshot tool (one call = a cross-module KPI digest: revenue MTD vs prior, pipeline, new leads, tickets+CSAT, low stock, orders, MRR, marketing ROAS, goal progress, open insights), this turns the ERP from a passive datastore into an optimization loop: snapshot → compare to goals → log insight → act → record outcome.
How data gets in
Module records (oac_module_items) are typed and schema-validated, and can be written four ways — all landing in the same store, visible to the dashboard and the agent identically:
- Human — via the dashboard (schema-driven forms, with per-type primary fields + brand-icon pickers).
- Agent — via its MCP tools (
oac_<module>_create/update/list/delete+ named tools likeoac_payments_*,oac_ads_*,oac_analytics_*). - Connector (pull) — OAC fetches from a provider and maps into the module. See § Integrations & SDK.
- Product SDK (push) — your POS/SaaS pushes records as events happen.
Every write is recorded in an append-only audit log (who/what/when), and deletes are soft (recoverable) — so the dashboard activity feed and history are truthful.
End-to-end data flows by business type
The same modules compose into very different businesses. A few worked examples (→ = a ref link; arrows show how one action cascades):
RESTAURANT / FOOD ORDERING
Catalog(menu_item) ──rendered by──► Storefront(food_ordering surface)
│ │ customer orders
▼ ▼
Orders(order) ──payment──► Payments(payment) ──► Finance(transaction) → P&L
│ └─queue_ticket (walk-in board) ▲
└─customer──► CRM(contact) ──► Content(newsletter blast: "today's specials")
Inventory(sku) ◄─movement─ decremented on each sale; low stock → Procurement(po → supplier)
SaaS
Marketing(campaign)+Advertising(ad_set)+SEO ──leads──► CRM(contact) ──► Sales(lead pipeline)
│ spend/ROAS │ won
▼ ▼
Storefront(saas_app + pricing landing) ──signup──► Orders(subscription) ──► MRR report
│ │ recurring
Catalog(plan) ──priced──┘ ▼
Support(ticket + kb_article) · Google Analytics ──traffic/conversions──► Marketing reports
E-COMMERCE
Catalog(product) ──► Storefront(ecommerce surface) ──cart/checkout──► Orders(order)
│ │ Stripe
Inventory(sku) ◄──decrement──────────────────────────────┘ ──► Payments + Finance
Shopify connector ──pull──► Catalog + Orders + CRM (kept in sync)
SERVICE / BOOKINGS (salon, clinic, consultant)
Catalog(service) ──► Storefront(booking surface) ──► Scheduling(event + Google Meet)
│ │ links contact
Scheduling(availability rules) ──offerable slots──┘ ▼
Event outcome ──► Payments(payment) + Finance ; CRM(contact + interaction history)
In every case the chain ends in Finance/Payments (so it's on the books) and runs through CRM (so it's attributable to a customer) — and Goals/Insights sits above all of it, watching the numbers and proposing the next move.
Categories & reports
The modules are leaves — they own typed records. Above them sit 7 categories: overarching functional layers that own no records; they project from the leaves on demand. Reports are live (no longer P1/P2 stubs).
| Category | Aggregates (leaves) | Live reports |
|---|---|---|
| Accounting | finance, payments, procurement, sales, inventory, orders | P&L · AR · AP (currency-aware) |
| Marketing | campaigns, advertising, social, content, seo | Spend & ROAS · Reach & Funnel |
| Sales | sales, crm, catalog, orders, payments, scheduling | Pipeline by stage |
| Operations | inventory, procurement, scheduling, projects, orders | Inventory on hand · Task throughput |
| Customer | crm, support, orders, payments, scheduling | LTV · Support & CSAT · MRR |
| Commerce | storefront, catalog, orders, payments, inventory | Orders & revenue · Queue throughput |
| Workspace | people, projects, documents, goals | Headcount & cost · Project status |
A leaf can appear under multiple categories — they're projections, not partitions. The leaf formerly called marketing was renamed to campaigns (the old key still aliases for back-compat) so the Marketing name is reserved for the category. Money measures group by currency first, so multi-currency totals never blend. Measures can also restrict to one item type (e.g. MRR sums only orders.subscription).
Reports
Each report is a declarative ReportSpec (measures × group-by × period). The aggregation engine resolves it into D1 queries against oac_module_items, using json_extract on the item data. Live-query only for now — no materialized snapshots.
- Dimensions drive group-by:
department,project,costCenter,source,period, or any{ field: "<name>" }. - Period bucketing:
day/week/month/quarter/year(uses the report'speriodField, defaultoccurredAt). - Measures:
sum/count/avg/min/maxon a data field, with optional filters.
Inter-module connectivity
Records link across modules via ref fields (e.g. sales.lead.campaign → campaigns, sales.lead.contact → crm). The relation graph is derived from the schema and powers two things:
- Traversal —
GET /api/oac/business/:id/m/:mod/items/:itemId/relatedwalks refs in both directions. Agent tool:oac_related(module, id). So "given a campaign, find the leads it drove" and "given a payment, find the customer + invoice" are one-call queries. - Category cross-joins (P1) — measures on one leaf can group by a
reffield that points to another (e.g.payments.amountgrouped byleads.campaignfor ROAS attribution).
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/oac/categories | Category catalog |
GET | /api/oac/relations | Full inter-module relation graph |
GET | /api/oac/business/:id/category/:cat | Category summary for a business |
GET | /api/oac/business/:id/category/:cat/report/:report?groupBy=&period= | Run a report |
GET | /api/oac/business/:id/m/:mod/items/:itemId/related | Linked records (in + out) |
Agent tools auto-generated from this registry: oac_<category>_<report>(groupBy?, period?) + oac_related(module, id) — appear in tools/list whenever any of a category's leaves is enabled.
Integrations & SDK
External systems feed the same canonical modules. Everything lands in oac_module_items — instantly visible to the dashboard and the agent's tools. A single Integrations hub (/app#settings/integrations) collects every service with its connection status, organized by which modules each powers — the same connect logic the per-module Settings tabs use.
First-party integrations
Beyond the generic connector framework, OAC ships deep integrations operable from the dashboard and as agent MCP tools (spend/send actions are owner-approval gated):
| Integration | Powers | What it does in-portal |
|---|---|---|
| Google Calendar & Meet | Scheduling | OAuth connect; online bookings auto-mint a Meet link + calendar invite to the contact. |
| Google Analytics (GA4) | Storefront, Marketing, Content | Create GA4 properties + web data streams via the Admin API (returns the G- measurement id — no GA dashboard) and pull traffic/conversions via the Data API. |
| Google Ads | Advertising | Create & manage Search campaigns (budget→campaign→ad group→responsive ad, all paused until enabled), pull spend/ROAS. (Needs a Google Ads developer token configured server-side.) |
| Payments (Stripe + Stratos) | Payments, Finance, Orders | Checkout links, card charges, virtual cards, crypto sends — via payments-svc. |
| Newsletters (Cloudflare email) | Content | Blast a content newsletter to a CRM segment via the Cloudflare send_email binding. (Cloudflare delivers to verified destination addresses via Email Routing.) |
Google products share one OAuth client across three “products” (calendar / analytics / ads), each its own connection row + scopes.
Connectors (pull)
OAC fetches from a provider on a schedule. A connector is one self-contained ConnectorDef (auth, entities, and a map from each provider entity into a module's canonical shape) in server/src/connectors/. The generic engine handles the rest: fetch → map → validate → idempotent upsert by (source, externalId); creds/tokens are AES-GCM encrypted at rest. A cron (every 6h) re-syncs every connected source.
| Connector | Auth | Feeds |
|---|---|---|
| Stripe | secret key | charges→payments · customers→crm · subscriptions→orders |
| Shopify | admin token | products→catalog · customers→crm · orders→orders |
| Square | access token | catalog→catalog · customers→crm · payments→payments |
| WooCommerce | key + secret | products→catalog · customers→crm · orders→orders |
| HubSpot | private-app token | contacts→crm · deals→sales |
| Mailchimp | API key | members→crm · campaigns→campaigns |
| Klaviyo | private key | profiles→crm · campaigns→campaigns |
| Zendesk | email + token | tickets→support · users→crm |
| Meta Ads | long-lived token | campaigns+insights→advertising |
| QuickBooks · Xero · Salesforce | OAuth2 | invoices/payments/contacts→finance/crm · contacts/deals→crm/sales |
The key/token connectors work on connect. The three OAuth2 connectors use a generic auth-code flow (connectors/oauth.ts: a /connections/oauth/:id/start popup → public callback → refresh-token stored encrypted → access token refreshed before each sync) and activate once the platform registers an app per provider and sets its client id/secret as Worker secrets. Google Ads and Google Analytics are not connectors here — they're first-party integrations (create/manage campaigns, create GA4 properties). A connector declares its connect form via connectFields (secret fields → encrypted creds, non-secret → config).
| Method | Path | Purpose |
|---|---|---|
GET | /api/oac/connectors | Connector catalog (id, category, status, which modules it feeds) |
POST | /api/oac/business/:id/connections | Connect a provider (creds encrypted) |
POST | /api/oac/business/:id/connections/:cid/sync | Run a sync (backfill / incremental) |
SDK (push)
Any product — a POS system, a SaaS app, a marketplace — pushes records into a business's modules using a per-business ingest key (push-only, scoped to one business, minted in Settings → Companies → SDK keys). Records are schema-validated and stored once per (source, externalId) — re-pushing the same id updates rather than duplicates, so it's safe to fire on every event. Everything is tagged source + created_by: "product".
Package: @oneagentcompany/sdk — zero dependencies (Node 18+, browsers, Workers, Deno, Bun).
import { OAC } from "@oneagentcompany/sdk";
const oac = new OAC({ key: process.env.OAC_INGEST_KEY });
// a POS sale → Finance (idempotent by your sale id)
await oac.transaction(
{ direction: "income", amount: 11.5, currency: "USD" },
{ externalId: "sale_7781", title: "Latte x2" },
);
// a new customer → CRM
await oac.contact({ name: "Mei", email: "mei@x.com" }, { externalId: "cust_55" });
// an inbound lead → CRM contact + linked Sales lead, in one idempotent call
await oac.captureLead({
email: "jordan@acme.com", // identity + the Sales↔CRM link
name: "Jordan Lee", company: "Acme",
stage: "new", value: 1200, source: "homepage-form",
});
// anything → any module, or batch in one round-trip
await oac.pushBatch([
{ module: "inventory", type: "sku", externalId: "SKU-9",
data: { sku: "SKU-9", onHand: 12, reorderAt: 4 } },
]);
Linking records: set a ref field to the target record's externalId (or id) — refs resolve by either. Use a stable key like a contact's email so links survive across pushes. captureLead does this for you (contact ↔ lead by email); for other links set them explicitly, e.g. a payment's contact / lead. Ref fields: sales.lead.contact→crm & .campaign→campaigns; payments/finance/support/scheduling .contact→crm & .lead→sales.
| Method | Path | Purpose |
|---|---|---|
POST | /api/oac/business/:id/ingest-keys | Owner mints an ingest key (shown once) |
POST | /api/oac/ingest | Push records (Bearer ingest key). Body { records: [{ module, type?, externalId?, title?, data }] } (max 500). Returns { ingested, errors? }. |
SDK helpers (@oneagentcompany/sdk v0.3):
- Push:
transaction·payment·contact·sku·lead·product·service·menuItem·plan·order·subscription·queueTicket·booking·ticket·member·task· genericpush/pushBatch. - Linked captures (idempotent by email):
captureLead(CRM contact + Sales lead) andcaptureOrder(CRM customer + Order) — the right way to ingest a signup or an e-commerce/POS order so the records are connected, not orphaned. - Read:
catalog()— the one read an ingest key may do (otherwise push-only). Returns active catalog items + live stock so a web app can derive its storefront from OAC. Call server-side.
This is the data layer behind the web-app path: a storefront reads its catalog with catalog() and pushes signups/orders back with captureLead/captureOrder — exactly how a derived shop (e.g. maxxing.im) integrates with OAC. Note: ingest keys are strictly push-only apart from catalog(); the generic module read endpoints reject them.
Connectors vs SDK
- Connector (pull) — use when OAC can reach the provider's API and you want OAC to fetch on a schedule.
- SDK (push) — use when your own product is the source of truth and emits events; you push as they happen.
- Either way the data lands in the same modules, so the agent and dashboard treat product data, synced data, agent-created data, and human-entered data identically.
Payments service
payments-svc is an isolated backend worker that owns every payment surface for both OAC and Uranus. No UI, no public self-onboarding. Two trusted callers, two route prefixes, one shared service secret.
At a glance
| URL | https://payments.uranus.im |
| Source | /data/uranus/payments-svc/src/index.ts |
| Data | oac-db D1 — own tables: payment_accounts, payment_items, stripe_event_log |
| Auth | Shared SERVICE_SECRET (also set as PAYMENTS_SVC_SECRET on oac-api + cloudflare-agents-pages); Stripe-Signature HMAC on the webhook |
Two-route ownership model
Every payment account is owned by either an agent or a business:
- Route A — Agent direct (
owner_type='agent'): a Uranus agent has its own payment account, no OAC business required. Used when the agent runs standalone. - Route B — OAC business (
owner_type='business'): an OAC business owns the account; the agent (if linked) operates it. Used when the business is the unit of accounting capture.
The agent-scoped route auto-resolves: if the agent has a linked business with an account, Route B is preferred (so OAC accounting captures it). Otherwise it falls back to Route A.
Authentication
Three different principals authenticate against payments-svc, each with a distinct mechanism:
| Principal | How | Allowed surface |
|---|---|---|
| OAC frontend (browser) | Never calls payments-svc directly. Hits oac-api with the user's first-party session cookie. oac-api validates canAccessBusiness(ctx, businessId), then forwards to payments-svc with Authorization: Bearer ${PAYMENTS_SVC_SECRET}. |
by-business/<id>/* only — scoped to whatever the cookie owns. |
| Uranus dashboard (browser) | Never calls payments-svc directly. Hits manage.uranus.im/api/agents/<id>/payments-bridge/* with its session cookie. The Uranus worker validates the user owns this agent, then forwards to payments-svc with Authorization: Bearer ${PAYMENTS_SVC_SECRET} + X-Owner-Email for connect calls. |
by-agent/<id>/* only — scoped to the agent ID resolved from the URL. |
| Stripe (webhook) | Public POST to /webhooks/stripe/<accountId>. Authenticated by Stripe-Signature HMAC against the per-account webhook secret stored in payment_settings.stripe.webhookSecret. Deduped by event id via stripe_event_log. |
Webhook endpoint only. |
Provisioning constraint: "Integration request or payment account is generated directly and only via Uranus agent or OAC company." Enforced by the service-secret gate — only the two trusted services have the secret, so no public self-onboarding is possible.
┌──────────────────────────┐
Browser (cookie) ───► │ oac-api / manage worker │
│ validates user can │
│ touch business / agent │
└────────────┬─────────────┘
│
Authorization: Bearer
${PAYMENTS_SVC_SECRET}
▼
┌──────────────────────────┐
│ payments-svc │
Stripe webhook ─────► │ /webhooks/stripe/<id> │
(Stripe-Signature) │ + /api/payments/* (auth)│
└──────────────────────────┘
Routes
Health
| Method | Path | Auth | Returns |
|---|---|---|---|
| GET | /health | none | { ok: true, service: "payments-svc" } |
Stripe webhook (public)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /webhooks/stripe/:accountId |
Stripe-Signature | Receives Stripe deliveries. Verifies HMAC against the account's webhook secret. Dedupes by event id. Handles checkout.session.completed (creates linked payment row), issuing_authorization.request, issuing_transaction.created. Returns 200/410. |
Agent-scoped (Route A) — for Uranus bridge
All require Authorization: Bearer ${SERVICE_SECRET}. X-Owner-Email header forwarded by the Uranus bridge for connect.
| Method | Path | Description |
|---|---|---|
| GET | /api/payments/by-agent/:agentId/status | Returns { connected, provisioned, accountId, ... }. connected=false if the agent has no account (neither direct nor via linked business). |
| POST | /api/payments/by-agent/:agentId/connect | One-step setup. Creates a payment account for the agent (business-owned if the agent has a linked OAC business, agent-direct otherwise). Idempotent. |
| POST | /api/payments/by-agent/:agentId/provision | Alias of /connect for callers that prefer the verb. |
| any | /api/payments/by-agent/:agentId/<sub> | Resolves the agent's account, then dispatches to the same routes as the account-scoped layer below. |
Business-scoped (Route B) — for OAC frontend
| Method | Path | Description |
|---|---|---|
| GET | /api/payments/by-business/:businessId/status | Returns the business's account state. |
| POST | /api/payments/by-business/:businessId/connect | Creates the business's payment account if not present. Idempotent. |
| POST | /api/payments/by-business/:businessId/provision | Alias of /connect. |
| any | /api/payments/by-business/:businessId/<sub> | Resolves the business's account, then dispatches to the account-scoped layer. |
Account-scoped (after resolution)
These are the actual operations. Both Route A and Route B resolve down to these by mapping agentId/businessId → accountId.
Provisioning & settings
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | .../status | — | Full status object: provisioned, accountId, ownerType, ownerId, webhookUrl, defaultCurrency, requireApprovalAbove, stripeConnected, stripePublishable, stratosConnected, stratosWalletNetwork, stratosWalletAddress. |
| PATCH | .../settings | { defaultCurrency?, requireApprovalAbove?, stripe?, stratos? } | Updated status. Patches merge into payment_settings; stripe/stratos sub-objects deep-merge. |
Transactions (the unified ledger)
| Method | Path | Body | Description |
|---|---|---|---|
| GET | .../transactions | — | Lists payment items in legacy shape: { transactions: [{ id, type, status, amount, currency, from_address, to_address, tx_hash, description, created_at, ... }] }. ?limit=50 supported. |
| POST | .../transactions | { type, amount, currency, from_address?, to_address?, description?, contact_id?, tx_hash?, status? } | Manually record a payment. type like crypto_in, card_out, bank, manual — parsed into direction + rail internally. |
Payment methods (stored cards / banks / wallets)
| Method | Path | Body | Description |
|---|---|---|---|
| GET | .../methods | — | { paymentMethods: [{ id, type, provider, label, last4, brand, network, address, enabled, ... }] } |
| POST | .../methods | { type, label, provider?, last4?, brand?, network?, address?, spending_limit?, contact_id? } | Store a method. |
| DELETE | .../methods/:id | — | { deleted: 0|1 } |
Notifications (parsed bank SMS/email)
| Method | Path | Body | Description |
|---|---|---|---|
| GET | .../notifications | — | Parsed notification log. |
| POST | .../notifications | { source, rawMessage, parsedAmount?, parsedCurrency?, parsedFrom?, parsedReference?, matchStatus? } | Log a parsed notification. Called by Uranus's gateway-layer bank-detect (LLM-parsed message → here). |
| PATCH | .../notifications/:id | { status?, matched_transaction_id? } | Update match state. |
Stripe
| Method | Path | Body | Description |
|---|---|---|---|
| POST | .../stripe/checkout | { amount, currency?, description?, customerEmail?, successUrl?, cancelUrl? } | Creates a Stripe Checkout session, persists a stripe_session item, returns { sessionId, url }. The URL is shareable with the customer. |
| GET | .../stripe/checkout | — | List recent checkout sessions for this account. |
| POST | .../stripe/charge | { paymentMethodId, amount, currency?, description? } | Charges a stored payment method via PaymentIntents. Requires owner approval (the corresponding MCP tool annotation triggers the Uranus approval gate). |
Stratos (crypto wallet)
| Method | Path | Body | Description |
|---|---|---|---|
| GET | .../stratos | — | Composite info: configured state + addresses + balances placeholder. |
| GET | .../stratos/balance/:network/:address | — | Live balance lookup via Stratos API. |
| POST | .../stratos/send | { network, toAddress, amount, currency } | Send crypto from the configured wallet. Requires owner approval. |
How OAC and Uranus call it
OAC dashboard flow
browser oac-api payments-svc
─────── ─────── ────────────
GET /api/oac/business/:id/payments/status
│ cookie
▼
canAccessBusiness(ctx, businessId)?
│ yes
▼
fetch payments.uranus.im/api/payments/by-business/:id/status
with Authorization: Bearer ${PAYMENTS_SVC_SECRET}
▼ isServiceAuthed? ✓
statusByBusiness(businessId)
◄──────────
◄──────────
(returns)
Uranus dashboard flow
browser manage worker payments-svc
─────── ───────────── ────────────
GET /api/agents/:id/payments-bridge/payments/status
│ cookie
▼
checkAgentAccess(email, agentId)?
│ yes
▼
fetch payments.uranus.im/api/payments/by-agent/:id/status
with Authorization: Bearer ${PAYMENTS_SVC_SECRET}
X-Owner-Email: <user-email>
▼ isServiceAuthed? ✓
statusByAgent(agentId)
resolveAgentAccount(agentId)
1) direct?
2) via linked business?
3) null
◄──────────
◄──────────
(returns)
Stripe webhook flow
Stripe payments-svc
────── ────────────
POST /webhooks/stripe/<accountId>
header: Stripe-Signature: t=...,v1=...
body: { event_id, type, data: { object } }
▼
lookup account.settings.stripe.webhookSecret
verifyStripeSig(rawBody, signature, secret)? ✓
▼
INSERT ... ON CONFLICT DO NOTHING into stripe_event_log
▼
handle event type:
checkout.session.completed → mark session paid + create payment row
issuing_transaction.created → create card-out payment row
issuing_authorization.request → approve/deny by limit
▼
200 OK
Data model
Defined in migrations/0001_payment_accounts.sql. Three tables, all live in oac-db D1 for now (can move to a dedicated payments-db later without changing the API).
payment_accounts
| Column | Type | Notes |
|---|---|---|
id | TEXT PK | UUID. The opaque account id returned by status / used in account-scoped routes. |
owner_type | TEXT | 'agent' | 'business'. CHECK enforced. |
owner_id | TEXT | Uranus agent id or OAC business id depending on owner_type. |
display_name | TEXT | Human label (e.g. "Agent payments", "Business payments"). |
settings | TEXT (JSON) | PaymentSettings shape — see below. |
created_at, updated_at | TEXT | ISO datetimes. |
UNIQUE constraint on (owner_type, owner_id) — one account per agent or business.
payment_items
The unified ledger. Every row is one record of one kind — payment / method / notification / stripe_session / issuing_card / wallet — keyed by account_id. Type-specific fields live in the data JSON.
| Column | Type | Notes |
|---|---|---|
id | TEXT PK | UUID. |
account_id | TEXT FK | → payment_accounts.id, ON DELETE CASCADE. |
type | TEXT | One of: payment, method, notification, stripe_session, issuing_card, wallet. |
title | TEXT | Display title. |
data | TEXT (JSON) | Type-specific fields (amount/currency/from/to/rail for payment, kind/last4/brand for method, etc.). |
status | TEXT | Logical status — active by default. |
source | TEXT | Provenance — service, stripe, stratos, webhook, etc. |
external_id | TEXT | Idempotency key from the source system (Stripe session id, tx hash, etc.). |
stripe_event_log
Webhook dedup — one row per event_id. Re-deliveries from Stripe are no-ops.
PaymentSettings JSON shape
{
defaultCurrency?: "USD",
requireApprovalAbove?: 100,
stripe?: {
secretKey?: "sk_live_...",
publishableKey?: "pk_live_...",
webhookSecret?: "whsec_...",
accountId?: "acct_..."
},
stratos?: {
apiBase?: "https://...",
apiKey?: "...",
walletNetwork?: "eip155:8453",
walletAddress?: "0x..."
},
x402FacilitatorUrl?: "https://x402.org/facilitator"
}
cURL examples
Replace $SECRET with the value of PAYMENTS_SVC_SECRET. These commands only work from a context that has the secret (i.e., a server, not a browser).
# Health (no auth)
curl https://payments.uranus.im/health
# Agent status (Uranus would call this server-side)
curl -H "Authorization: Bearer $SECRET" \
https://payments.uranus.im/api/payments/by-agent/agt_abc/status
# Connect an agent (creates account if missing)
curl -X POST -H "Authorization: Bearer $SECRET" \
-H "X-Owner-Email: owner@example.com" \
https://payments.uranus.im/api/payments/by-agent/agt_abc/connect
# Business status (OAC would call this server-side)
curl -H "Authorization: Bearer $SECRET" \
https://payments.uranus.im/api/payments/by-business/biz_xyz/status
# List transactions
curl -H "Authorization: Bearer $SECRET" \
"https://payments.uranus.im/api/payments/by-business/biz_xyz/transactions?limit=20"
# Manually log a payment
curl -X POST -H "Authorization: Bearer $SECRET" -H "Content-Type: application/json" \
-d '{"type":"bank","amount":100,"currency":"USD","description":"Test"}' \
https://payments.uranus.im/api/payments/by-business/biz_xyz/transactions
# Patch settings (paste Stripe creds)
curl -X PATCH -H "Authorization: Bearer $SECRET" -H "Content-Type: application/json" \
-d '{"stripe":{"secretKey":"sk_test_...","webhookSecret":"whsec_..."}}' \
https://payments.uranus.im/api/payments/by-business/biz_xyz/settings
# Create a Stripe checkout link
curl -X POST -H "Authorization: Bearer $SECRET" -H "Content-Type: application/json" \
-d '{"amount":25.00,"currency":"usd","description":"Order #42"}' \
https://payments.uranus.im/api/payments/by-business/biz_xyz/stripe/checkout
Source & deploy
- Repo:
/data/uranus/payments-svc/ - Migration:
wrangler d1 migrations apply oac-db --remote - Deploy:
wrangler deploy - Secrets:
SERVICE_SECRET(set on payments-svc) ≡PAYMENTS_SVC_SECRET(set on oac-api + cloudflare-agents-pages). Rotate by updating all three together. - Routes (wrangler.toml):
payments.uranus.imcustom domain.
Settings
/app#settings has three cards:
- Modules — count, link to the per-module toggle list. The toggle list patches
PATCH /api/oac/business/<sid>/moduleswhen you flip a switch; the sidebar updates immediately on success. - Account — current signed-in email, sign-out button.
- Businesses — every business this account owns, plus a button to start a new one. The currently-selected business is marked.
Persona avatar, name, and voice are not editable from Settings — they're set once by the wizard LLM during onboarding (stored in state.blueprint.persona). To change them, edit the blueprint on the Uranus side.
Control panel
/admin.html is a read-only cross-tenant view of every deployed business — a control surface for the platform owner, not per-user. Lives in the OAC tooling layer.
Auth gate
Two checks:
- Signed-in via the standard OAC OTP flow (otherwise “Sign in required”).
- Email is in the
OAC_ADMIN_EMAILSenv var on Uranus (comma-separated). Defaults tokwangwei@sol59.comif the env var isn't set, so the platform owner is admin out of the box.
What it shows
- Count strip — total deployed plus per-status counts (draft / requested / claimed / provisioned / abandoned).
- Status filters — clickable chips (All / Draft / Requested / Claimed / Provisioned / Abandoned), each with its count, combined with search.
- Search — client-side substring filter across name, email, archetype, status, persona name, tagline, session id, agent id.
- Table — checkbox, business name (click to inspect), owner email + persona name, archetype, status pill, agent id (first 12 chars), enabled-modules count, created date, and an “Inspect” link.
- Refresh button to re-fetch.
Inspect drawer
Clicking a business name (or its “Inspect” link) slides in a detail drawer with the full record — useful for looking into drafts before deleting them:
- Record — status, owner email, archetype, agent id (or “not provisioned”), session id, created/updated.
- Persona / Profile / Modules / Channels / Setup tasks — parsed from the blueprint JSON.
- Conversation — the full wizard chat history (user + agent turns).
- Footer: “Open in dashboard →” and a “Delete this business” button.
Delete & purge
- Select rows with the checkboxes (or select-all, which respects the active filter) to reveal the bulk action bar.
- Delete selected removes the chosen businesses after a confirmation dialog. Single-delete is also available from the inspect drawer.
- To purge junk, filter to Draft or Abandoned, select-all, delete.
- Deletes are OAC-layer only: they remove the
wizard_blueprintsrow plus anyoac_module_itemsfor the agent. For provisioned/claimed businesses the confirmation warns that the underlying agent runtime is not torn down — the DO is left orphaned. Drafts/abandoned have no agent, so they delete cleanly.
The “Open in dashboard” link routes to /app?biz=<sessionId>. The dashboard SPA only shows businesses owned by the signed-in user — true cross-account drill-in still needs an impersonation flow (see § Build status).
Endpoints
| Method | Path | Returns |
|---|---|---|
GET | /api/oac/admin/businesses | List + counts. Every wizard_blueprints row with derived modulesEnabled, personaName, tagline. Hard cap 500 rows. |
GET | /api/oac/admin/business/:id | Full detail — parsed blueprint + messages for the inspect drawer. |
POST | /api/oac/admin/businesses/delete | Body { ids: [...] } (max 200). Deletes the rows + their module items. Returns { deleted, moduleItemsClearedFor }. |
API reference
All OAC endpoints live under https://manage.uranus.im/api/oac/*. Authed endpoints require the oac_session cookie. CORS only allows the OAC origins listed in oac-auth.ts.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/oac/auth/request-otp | — | Generate + email a 6-digit OTP |
POST | /api/oac/auth/verify-otp | — | Verify code, set session cookie |
POST | /api/oac/auth/logout | cookie | Delete session row + clear cookie |
GET | /api/oac/me | cookie | Resolve cookie to user record |
GET | /api/oac/businesses | cookie | List businesses owned by the current user |
GET | /api/oac/business/:sid | owner | Full blueprint + status + agent id |
PATCH | /api/oac/business/:sid/modules | owner | Toggle modules on/off |
GET | /api/oac/business/:sid/stats | owner | Overview KPIs + activity |
GET | /api/oac/business/:sid/snapshot | owner | Cross-module KPI snapshot (same engine as the agent's oac_business_snapshot) |
GET | /api/oac/business/:sid/activity | owner | Append-only audit log (who/what/when) |
POST | /api/oac/business/:sid/chat | owner | Send a message to the agent |
POST | /api/oac/business/:sid/m/content/items/:id/send | owner | Blast a newsletter to a CRM segment (Cloudflare email) |
GET | /api/oac/business/:sid/m/:key/items | owner | List per-module items |
POST | /api/oac/business/:sid/m/:key/items | owner | Create an item |
PATCH | /api/oac/business/:sid/m/:key/items/:id | owner | Update an item |
DELETE | /api/oac/business/:sid/m/:key/items/:id | owner | Delete an item |
* | /api/oac/business/:sid/proxy/* | owner | Agent-data proxy (contacts/transactions/…) → Uranus agent |
GET | /api/oac/admin/businesses | admin | Cross-tenant business list (control panel) |
Agent & integration surface
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /mcp | agent Bearer | MCP JSON-RPC — per-business tools auto-derived from enabled modules |
POST | /api/oac/businesses/:id/mcp-keys | owner | Mint an agent MCP key |
GET | /api/oac/connectors | — | Connector catalog |
POST | /api/oac/business/:id/connections[/:cid/sync] | owner | Connect a provider / run a pull sync |
GET/POST | /api/oac/business/:id/google[/connect] | owner | Google Calendar connect / status / disconnect |
GET/POST | /api/oac/business/:id/analytics/{connect,accounts,property,report} | owner | GA4 connect, create property+stream, pull report |
GET/POST | /api/oac/business/:id/ads/{connect,customers,campaign,status,report} | owner | Google Ads connect, create/manage campaigns, metrics |
POST | /api/oac/businesses/:id/ingest-keys | owner | Mint a product SDK ingest key |
POST | /api/oac/ingest | ingest Bearer | Push product records into modules (SDK) |
POST | /api/internal/oac/provision | service | Source-agnostic provision from a Blueprint spec |
POST | /api/wizard/chat · /api/blueprint[/verify] | — | Wizard onboarding (one provisioning source; on Uranus) |
Principals: cookie (first-party human session) · owner (cookie scoped to a business they own) · agent Bearer (oacmcp_…, per-business) · ingest Bearer (oacingest_…, per-business, push-only) · service (OAC↔Uranus shared secret) · admin (allowlisted email).
Data model
Business data lives in OAC's own D1 (oac-db), keyed by OAC business id — independent of the Uranus agent runtime.
wizard_blueprints— one row per business. Key columns:id(session id),email,business_name,archetype,blueprint(full JSON),messages(chat history),status(draft / requested / claimed / provisioned / abandoned),agent_id(filled on claim),claim_token+claim_token_expires_at,ip_hash,user_agent,created_at,updated_at.oac_users— one row per signed-in account.id,email,display_name.oac_sessions— session cookies.id,user_id,expires_at,ip_hash,user_agent.oac_otps— in-flight OTPs.email,code_hash,expires_at,attempts. Deleted on success or expiry. Reused for both dashboard login and wizard blueprint email verification.oac_module_items— typed per-business per-module records (campaigns, leads, SKUs, transactions, …). Created by humans, the agent, connectors, or product SDK pushes — tagged withsource+external_id(idempotent upsert) +created_by.deleted_at= soft delete (deletes stamp it; lists/reports/relations filter it out, records stay recoverable).oac_audit_log— append-only record of every create/update/delete/send (actor, action, module, item, before/after, summary). Powers the dashboard activity feed.oac_businesses— the business record (id, owner_email, name, archetype, blueprint, modules,payment_settings, optionalagent_idlink).oac_mcp_keys/oac_ingest_keys— per-business agent (pull/tool) and product (push) credentials, hashed.oac_connections/oac_sync_runs— connector instances (encrypted creds + cursor, incl. the Google OAuth connections) and sync audit.
Uranus keeps only agent runtime state (conversation, memory, schedules, gateways, the DashboardAgent DO). The agent reaches business data through OAC's MCP tools.
Build status
Honest read on what's wired versus what's stubbed. Snapshot, will drift.
✓ Wired and working
- OTP login (email + 6-digit, 30-day cookie, attempt limiting).
- Dashboard shell, hash routing, sidebar (modules grouped by category), business switcher, mobile drawer.
- Chat with the agent (FAB → POST /chat → tool-call summaries render).
- Module on/off toggling; 21 modules / 7 categories with live reports.
- Full CRUD — schema-driven create/edit modals (per-type primary fields, brand-icon pills, conditional fields, soft delete), correct for multi-type modules.
- Per-module canvases (calendar, pipeline, cashflow, funnel, stock, lanes, people, order-flow, product grid, goals, content) with live-computed KPIs + List toggle.
- To-dos board — shared human↔agent kanban with drag-drop + live poll.
- Guidance layer on the expert modules (stateful checklists, tips, glossary, agent prompts).
- Integrations hub + first-party: Google Calendar, GA4 (create properties/streams + reports), Google Ads (create/manage campaigns), Stripe + Shopify connectors w/ 6-hourly cron, newsletter blasts via Cloudflare email.
- Audit log + merged activity feed; cross-module snapshot (REST + agent tool); optimization loop (goals + insights).
- Admin control panel — list, search, status filters, inspect drawer, bulk delete + purge.
~ Needs server-side credentials to activate
- Google Analytics / Ads / Calendar — code is live; needs the Analytics + AdWords scopes added to the OAuth consent screen, and a Google Ads developer token set as a Worker secret.
- Newsletter delivery — needs Email Routing on the from-domain +
OAC_EMAIL_FROM; Cloudflare delivers only to verified recipients.
✗ Not built yet
- Storefront generation/hosting runtime — the storefront module is the data model + management contract; the renderer that turns a surface spec into a live, hosted customer site (per-tenant custom domains via Cloudflare for SaaS) is the next major build. See
server/docs/storefront-architecture.md. - Approval-workflow queue — owner approval on spend/send actions is enforced via the Uranus gate + tool annotations, but there's no stored approval queue in OAC yet.
- Payments dual-path consolidation — REST forwards to payments-svc while the MCP tools still call a local handler.
- Admin cross-account impersonation & agent teardown on delete — unchanged from before.