One Agent Company Docs
One Agent Company · Documentation

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 DashboardAgent Durable 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/list is 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 in tool-constants.ts) and remote MCP tools (declared by the server, e.g. OAC's oac_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/provision with 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 /app calls oac-api for everything; oac-api forwards 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_x402 connector.
  • Stratos wallet ops. On-chain signing + transfers run on Uranus (stratos_sign, stratos_transfer); the wallet state record (address, last balance) lives in OAC's payments.wallet item 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_payment over 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 UranusNow on OACAgent tool name
transactions tablepayments.payment item typeoac_payments_log_payment, list_transactions, check_balance
payment_methods tablepayments.method item typeoac_payments_list_methods
payment_notifications tablepayments.notification item typeoac_payments_log_notification
stripe_checkout_sessionspayments.stripe_sessionoac_payments_create_checkout
stripe_issuing_cardspayments.issuing_card(REST: /api/oac/business/:id/stripe/cards)
Stratos wallet statepayments.walletoac_payments_send_payment (requires approval)
POST /stripe-webhook on Uranus DOPOST /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 + payments module items, tagged with the synced department / project dimensions 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 + sales with the ref back 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 generic push/pushBatch for 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, source are 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

  1. Creates the business record in OAC (oac-db) — visible in the dashboard.
  2. Creates the Uranus agent (modules / gateways / payments configured from the spec).
  3. 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 / connectorPOST /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.

  1. Request OTPPOST /api/oac/auth/request-otp with { email }. The worker hashes a fresh 6-digit code into oac_otps(email, code_hash, expires_at, attempts) and emails it via Resend. TTL = 10 minutes.
  2. Verify OTPPOST /api/oac/auth/verify-otp with { email, code }. Compares hash, decrements attempt counter (max 5), deletes the row on success, creates a row in oac_sessions, and returns a session cookie.
  3. Cookieoac_session, HttpOnly, Secure, SameSite=None, Domain=manage.uranus.im, Path=/. Max-Age = 30 days.
  4. WhoamiGET /api/oac/me resolves the cookie to a user row from oac_users.
  5. LogoutPOST /api/oac/auth/logout deletes 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

HashView
/appOverview: persona banner, performance KPIs, merged activity feed (Uranus stats + OAC audit log), modules grid
/app#boardTo-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#settingsSettings landing: modules count, account, businesses
/app#settings/modulesPer-module toggle list
/app#settings/integrationsIntegrations 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, and source, so spend & revenue roll up by initiative/department/project.
  • Relations (ref/refs fields) — 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 one oac_related call.

📣 Marketing — get found & get attention

Campaigns
campaign. Planned pushes (promo, launch, blast) with spend, impressions, clicks, conversions, attributed revenue. The unit ROAS is measured on; ads/social/content link back to it.
Advertising
ad_set. Paid acquisition per network — budget, target CPA, ROAS, creative. The one place money flows out to buy attention; wired to the live Google Ads integration.
Social
post + account. Posts and per-platform engagement, plus connected profiles whose follower growth is tracked over time.
Content
draft + newsletter. Long-form (blogs/guides/scripts) and email newsletters blasted to a CRM segment via Cloudflare email. The compounding, no-pay-per-click channel.
SEO / GEO
keyword + page + citation. Target queries & intent, on-page optimization, and whether AI engines (ChatGPT/Perplexity) cite you — generative-engine optimization, tracked.

💰 Accounting — the books

Finance
transaction + invoice + budget. Income/expense P&L by dimension, AR invoices with overdue detection, and budgets so spend has a plan to compare against (variance).
Payments
payment, method, notification, stripe_session, issuing_card, wallet. The unified ledger across every rail (Stripe/card/crypto/x402/bank). The operational layer that executes charges + sends.
Procurement
po + supplier. Purchase orders plus a supplier directory (terms, lead times, ratings) so reorder & lead-time planning is real, not tribal knowledge.

🤝 Sales & Customer — win them, keep them

Sales
lead. The pipeline — stage, value, probability, win/loss, linked to a CRM contact and source campaign. Drag-stage board; win-rate & pipeline-value reports.
CRM
contact + interaction. The spine of the graph — almost every customer-facing record points back to a contact (deduped by email). Interaction log = the customer's full story.
Support
ticket + kb_article. Tickets with CSAT & resolution time, plus a knowledge base of canonical answers the agent draws from before improvising.
Scheduling
event + availability. Bookings with auto Google Meet links + invites, and working-hours rules that will feed the booking storefront.

🛒 Commerce — the storefront economy

Catalog
product, service, plan, menu_item. The price book everything commercial references — physical goods, bookable services, SaaS plans, food menu items.
Orders
order, queue_ticket, subscription. The demand side — orders flow pending→fulfilled, queue tickets for walk-ins, subscriptions for MRR. One module powers food ordering, e-commerce, POS & SaaS.
Storefront
surface. Customer-facing sites the agent builds (info, landing, ecommerce, POS, food-ordering, queue, booking, SaaS app) — brief → spec → published URL. The bridge from internal data to a live experience.

🏢 Workspace & Knowledge — running the company

People
member. Employees, contractors, and the agents themselves — roles, type, comp. Tasks/projects/deals assign to people; headcount cost feeds P&L.
Projects & Tasks
project + task. Operational throughput — and the shared human↔agent To-dos board, the most visible HITL collaboration surface.
Documents
document + contract. SOPs/policies/notes (durable agent instructions) and legal contracts (NDA/MSA/SOW) with lifecycle + value, linked to counterparty & deal.
Research
brief + competitor. Market awareness — intel briefs and structured competitor records (pricing/strengths) tracked over time.

🎯 Goals & Insights — what makes it self-driving

Goals
goal + insight. The keystone. Goals give the agent metric targets to steer toward; insights are its compounding memory — observation → recommendation → action taken → outcome — so each optimization pass builds on the last instead of repeating.

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 like oac_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).

CategoryAggregates (leaves)Live reports
Accountingfinance, payments, procurement, sales, inventory, ordersP&L · AR · AP (currency-aware)
Marketingcampaigns, advertising, social, content, seoSpend & ROAS · Reach & Funnel
Salessales, crm, catalog, orders, payments, schedulingPipeline by stage
Operationsinventory, procurement, scheduling, projects, ordersInventory on hand · Task throughput
Customercrm, support, orders, payments, schedulingLTV · Support & CSAT · MRR
Commercestorefront, catalog, orders, payments, inventoryOrders & revenue · Queue throughput
Workspacepeople, projects, documents, goalsHeadcount & 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's periodField, default occurredAt).
  • Measures: sum / count / avg / min / max on 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:

  • TraversalGET /api/oac/business/:id/m/:mod/items/:itemId/related walks 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 ref field that points to another (e.g. payments.amount grouped by leads.campaign for ROAS attribution).

Endpoints

MethodPathPurpose
GET/api/oac/categoriesCategory catalog
GET/api/oac/relationsFull inter-module relation graph
GET/api/oac/business/:id/category/:catCategory 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/relatedLinked 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):

IntegrationPowersWhat it does in-portal
Google Calendar & MeetSchedulingOAuth connect; online bookings auto-mint a Meet link + calendar invite to the contact.
Google Analytics (GA4)Storefront, Marketing, ContentCreate 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 AdsAdvertisingCreate & 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, OrdersCheckout links, card charges, virtual cards, crypto sends — via payments-svc.
Newsletters (Cloudflare email)ContentBlast 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.

ConnectorAuthFeeds
Stripesecret keycharges→payments · customers→crm · subscriptions→orders
Shopifyadmin tokenproducts→catalog · customers→crm · orders→orders
Squareaccess tokencatalog→catalog · customers→crm · payments→payments
WooCommercekey + secretproducts→catalog · customers→crm · orders→orders
HubSpotprivate-app tokencontacts→crm · deals→sales
MailchimpAPI keymembers→crm · campaigns→campaigns
Klaviyoprivate keyprofiles→crm · campaigns→campaigns
Zendeskemail + tokentickets→support · users→crm
Meta Adslong-lived tokencampaigns+insights→advertising
QuickBooks · Xero · SalesforceOAuth2invoices/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).

MethodPathPurpose
GET/api/oac/connectorsConnector catalog (id, category, status, which modules it feeds)
POST/api/oac/business/:id/connectionsConnect a provider (creds encrypted)
POST/api/oac/business/:id/connections/:cid/syncRun 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.

MethodPathPurpose
POST/api/oac/business/:id/ingest-keysOwner mints an ingest key (shown once)
POST/api/oac/ingestPush 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 · generic push / pushBatch.
  • Linked captures (idempotent by email): captureLead (CRM contact + Sales lead) and captureOrder (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

URLhttps://payments.uranus.im
Source/data/uranus/payments-svc/src/index.ts
Dataoac-db D1 — own tables: payment_accounts, payment_items, stripe_event_log
AuthShared 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:

PrincipalHowAllowed 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

MethodPathAuthReturns
GET/healthnone{ ok: true, service: "payments-svc" }

Stripe webhook (public)

MethodPathAuthDescription
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.

MethodPathDescription
GET/api/payments/by-agent/:agentId/statusReturns { connected, provisioned, accountId, ... }. connected=false if the agent has no account (neither direct nor via linked business).
POST/api/payments/by-agent/:agentId/connectOne-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/provisionAlias 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

MethodPathDescription
GET/api/payments/by-business/:businessId/statusReturns the business's account state.
POST/api/payments/by-business/:businessId/connectCreates the business's payment account if not present. Idempotent.
POST/api/payments/by-business/:businessId/provisionAlias 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/businessIdaccountId.

Provisioning & settings
MethodPathBodyReturns
GET.../statusFull 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)
MethodPathBodyDescription
GET.../transactionsLists 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)
MethodPathBodyDescription
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)
MethodPathBodyDescription
GET.../notificationsParsed 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
MethodPathBodyDescription
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/checkoutList 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)
MethodPathBodyDescription
GET.../stratosComposite info: configured state + addresses + balances placeholder.
GET.../stratos/balance/:network/:addressLive 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

ColumnTypeNotes
idTEXT PKUUID. The opaque account id returned by status / used in account-scoped routes.
owner_typeTEXT'agent' | 'business'. CHECK enforced.
owner_idTEXTUranus agent id or OAC business id depending on owner_type.
display_nameTEXTHuman label (e.g. "Agent payments", "Business payments").
settingsTEXT (JSON)PaymentSettings shape — see below.
created_at, updated_atTEXTISO 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.

ColumnTypeNotes
idTEXT PKUUID.
account_idTEXT FKpayment_accounts.id, ON DELETE CASCADE.
typeTEXTOne of: payment, method, notification, stripe_session, issuing_card, wallet.
titleTEXTDisplay title.
dataTEXT (JSON)Type-specific fields (amount/currency/from/to/rail for payment, kind/last4/brand for method, etc.).
statusTEXTLogical status — active by default.
sourceTEXTProvenance — service, stripe, stratos, webhook, etc.
external_idTEXTIdempotency 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.im custom 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>/modules when 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:

  1. Signed-in via the standard OAC OTP flow (otherwise “Sign in required”).
  2. Email is in the OAC_ADMIN_EMAILS env var on Uranus (comma-separated). Defaults to kwangwei@sol59.com if 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_blueprints row plus any oac_module_items for 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

MethodPathReturns
GET/api/oac/admin/businessesList + counts. Every wizard_blueprints row with derived modulesEnabled, personaName, tagline. Hard cap 500 rows.
GET/api/oac/admin/business/:idFull detail — parsed blueprint + messages for the inspect drawer.
POST/api/oac/admin/businesses/deleteBody { 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.

MethodPathAuthPurpose
POST/api/oac/auth/request-otpGenerate + email a 6-digit OTP
POST/api/oac/auth/verify-otpVerify code, set session cookie
POST/api/oac/auth/logoutcookieDelete session row + clear cookie
GET/api/oac/mecookieResolve cookie to user record
GET/api/oac/businessescookieList businesses owned by the current user
GET/api/oac/business/:sidownerFull blueprint + status + agent id
PATCH/api/oac/business/:sid/modulesownerToggle modules on/off
GET/api/oac/business/:sid/statsownerOverview KPIs + activity
GET/api/oac/business/:sid/snapshotownerCross-module KPI snapshot (same engine as the agent's oac_business_snapshot)
GET/api/oac/business/:sid/activityownerAppend-only audit log (who/what/when)
POST/api/oac/business/:sid/chatownerSend a message to the agent
POST/api/oac/business/:sid/m/content/items/:id/sendownerBlast a newsletter to a CRM segment (Cloudflare email)
GET/api/oac/business/:sid/m/:key/itemsownerList per-module items
POST/api/oac/business/:sid/m/:key/itemsownerCreate an item
PATCH/api/oac/business/:sid/m/:key/items/:idownerUpdate an item
DELETE/api/oac/business/:sid/m/:key/items/:idownerDelete an item
*/api/oac/business/:sid/proxy/*ownerAgent-data proxy (contacts/transactions/…) → Uranus agent
GET/api/oac/admin/businessesadminCross-tenant business list (control panel)

Agent & integration surface

MethodPathAuthPurpose
POST/mcpagent BearerMCP JSON-RPC — per-business tools auto-derived from enabled modules
POST/api/oac/businesses/:id/mcp-keysownerMint an agent MCP key
GET/api/oac/connectorsConnector catalog
POST/api/oac/business/:id/connections[/:cid/sync]ownerConnect a provider / run a pull sync
GET/POST/api/oac/business/:id/google[/connect]ownerGoogle Calendar connect / status / disconnect
GET/POST/api/oac/business/:id/analytics/{connect,accounts,property,report}ownerGA4 connect, create property+stream, pull report
GET/POST/api/oac/business/:id/ads/{connect,customers,campaign,status,report}ownerGoogle Ads connect, create/manage campaigns, metrics
POST/api/oac/businesses/:id/ingest-keysownerMint a product SDK ingest key
POST/api/oac/ingestingest BearerPush product records into modules (SDK)
POST/api/internal/oac/provisionserviceSource-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 with source + 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, optional agent_id link).
  • 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.