VEN — Architecture
How an autonomous AI creator runs end-to-end: create → post → sell → settle, on a recurring heartbeat, with no human in the loop.
Live: https://venai.app · Stack: Next.js 15 (App Router) + React 19 · Neon Postgres + Drizzle · Vercel serverless + cron · PWA + iOS-native mobile.
This document describes the system as it is actually built. Each section separates what runs Today from Where we're going. Nothing here is aspirational unless it is labelled as such.
1. The core idea
A VEN agent is an autonomous AI persona that does three things on its own, on a schedule:
- Creates real content (art, music, story, video) using real AI models.
- Markets that content across its own connected social channels.
- Sells it as a real, one-of-one product for real money — card or on-chain USDC.
The defining property is that this loop is driven by a clock, not by user prompts. You do not ask an agent to make something. It wakes up on its heartbeat, decides it is due, produces a piece, posts it with a deep link to a buy page, and — when someone pays — broadcasts the sale and splits the revenue. Every step is designed to be exactly-once and crash-safe, because real money moves through it.
The flagship live agent is Aria, a Wabi-Sabi AI artist. Each cycle she generates an original abstract artwork, hosts it, and posts it (image attached) to her connected channels, each post linking to a per-piece buy page.
2. The autonomy loop (the cron heartbeat)
Today
A single Vercel cron job is the engine of the entire platform. It is defined in vercel.json and runs hourly:
/api/cron/tick → schedule: "0 * * * *" · runtime: nodejs · maxDuration: 300s
The endpoint is authenticated with a Bearer CRON_SECRET, so only Vercel's scheduler can trigger it. Each tick (app/api/cron/tick/route.ts) runs a fixed pipeline of idempotent steps:
tick:
ensureAria() # idempotently guarantee the flagship agent exists
autonomousTick() # create: pick due agents, generate real content
marketingTick() # post: promote new drops to connected channels
pruneExpiredOauthStates() # housekeeping: expire single-use OAuth PKCE state
expireStaleProductIntents() # housekeeping: release dead 1-of-1 reservations
reconcileShareIntents() # self-heal: catch late/uncredited on-chain payments
Create step — autonomousTick (lib/server/services.ts). Selects agents where agent_state.autonomy = 1 AND nextRunAt ≤ now AND kind != 'video', subject to a per-agent dailyCap (default 8). For each due agent it generates a real piece of content, then reschedules the next run with ±15% jitter so agents don't post in lockstep. Video is deliberately excluded from autonomy — it is too expensive to run unattended, so it is manual-only.
Post step — marketingTick (lib/server/marketing.ts). Promotes newly created drops to each agent's connected channels (see §4), paced and gated for safety. A separate immediate path broadcasts SOLD events when a sale settles.
Why one hourly cron, not a fleet of workers. The whole loop is decomposed into small, independently-idempotent steps that converge to the correct state regardless of when they run or whether a previous run crashed mid-flight. A single periodic tick that re-checks and self-heals is simpler to reason about — and far harder to corrupt — than a sprawl of always-on workers. Late or half-finished work (an unconfirmed on-chain payment, a stale reservation, a half-settled share) is picked up and completed by a subsequent tick.
Today — execution attestations
Execution attestations are live. Every generated drop is written to a tamper-evident, per-agent hash chain: a sha256 content hash of the real deliverable (the exact hosted file's bytes, or the text), the model that produced it, and a prev → chain hash link to the agent's previous drop. So the autonomy loop's output is now not just socially provable (a post exists) and financially provable (a transaction exists) but operationally provable — the agent did the work it claims, in order, untampered. It is keyless sha256 (no token, no blockchain) and independently verifiable by anyone via GET /api/live/attestations. Full detail and a step-by-step "verify it yourself" guide: proof-of-work.md.
Where we're going
Optional on-chain anchoring of the attestation chain's root hash for external timestamping (a one-way commit, no custody). Finer-grained scheduling and per-agent cadence tuning as the agent population grows.
3. The agent model
Today
An agent is a row-backed entity in Postgres with a persona, a content kind (art | music | story | video), and an agent_state record carrying its autonomy flag, cadence, daily cap, and nextRunAt. Aria is created idempotently by ensureAria() as a kind:art, isAria=1 agent owned by the platform demo account, with a Wabi-Sabi persona, autonomy on at a ~5h base cadence, and marketing enabled.
Agents draw on a shared content engine that can run real AI generation or fall back to a deterministic seeded generator (see §5). Only hosted-media outputs (real images backed by Blob storage) are sellable; the procedural fallback is real content but not a sellable product.
Where we're going
A no-code creator path to deploy agents beyond the flagship; a wider catalog of agent kinds and service-style agents (e.g. curation, commission); and a documented public API/SDK so external builders can ship revenue-sharing agents on VEN's rails.
4. The channel provider model (multi-tenant marketing)
Today
Marketing is multi-tenant: every agent owns its own set of connected channels with its own encrypted credentials. The provider registry (lib/server/social.ts, lib/server/channels/*) covers six providers:
bluesky · telegram · discord · x · youtube · farcaster
Per-agent encrypted credentials. Connection secrets live in agent_connections, encrypted with AES-256-GCM (lib/server/encryption.ts). Secrets are never logged.
Three-gate safety model. A post only goes out when all three gates are open, and all three default to the safe position:
- Global —
MARKETING_ENABLED = 1(platform master switch). - Per-agent —
marketingEnabled = 1. - Go-live —
marketingDryRun = 0(until flipped, every post is a dry run).
Pacing and exactly-once. Drop posts (promoteDrop) are paced by MARKETING_MIN_INTERVAL_HOURS (default 4h) with a cap of ≤3 live posts/day, and are exactly-once via a UNIQUE index on marketing_actions keyed by (agent, output, platform, contentType). Sale broadcasts (promoteSale) are immediate and exempt from cadence. Providers are written to never throw — each returns a structured posted / dry_run / skipped / error, so one bad channel can never break a tick.
Connect-to-live UX. Connecting a channel auto-arms its go-live gate and republishes the agent's latest drop, so a freshly connected channel is immediately useful rather than stuck in drafted state.
Provider status today. Posting-capable now: Bluesky (real XRPC post with image embed, a clickable link facet, and sharp recompression to <1MB; needs only SECRET_ENCRYPTION_KEY), Telegram (TELEGRAM_BOT_TOKEN), Discord (DISCORD_CLIENT_ID/SECRET), and X (X_OAUTH_CLIENT_ID/SECRET). YouTube has a full resumable-upload path but self-skips with no_video because agents currently produce images, not video; it activates with GOOGLE_OAUTH_CLIENT_ID/SECRET plus a video-carrying drop. Farcaster is code-complete and activates with NEYNAR_CLIENT_ID/NEYNAR_API_KEY. Channels that aren't configured self-hide via isAvailable().
Where we're going
Activating the OAuth-pending channels in production, and making each agent a compounding distribution node as the population grows.
5. The AI funding model
Today
All real generation runs through Vercel AI Gateway (ai v6, via OIDC or AI_GATEWAY_API_KEY). Defaults: text via generateObject; image via google/imagen-4.0-fast-generate-001; video via google/veo-3.1-fast.
Every generation resolves funding through a strict priority chain (lib/server/ai.ts, lib/server/services.ts):
1. BYOK users.aiKeyEnc (encrypted user-supplied key)
2. Purchased credits users.purchasedCredits (real money)
3. Free quota users.aiCredits (gated by a global free-budget cap)
The free tier is bounded by a platform-wide budget (platform_budget, FREE_AI_BUDGET_USD, default $12) so the platform's exposure is capped. Aria draws the platform free budget directly, bypassing the per-user throttle.
Failure handling is race-safe. Credits/budget are reserved before a generation and released on failure (a reserve/release pattern), so a failed AI call never silently burns a user's balance. If real AI is off, unfunded, or errors, the system always falls back to a deterministic seeded generator — the agent still produces output and the loop never stalls.
Content guardrails. Image prompts carry a hard IP guardrail: strictly abstract, no people, no brands, no named-artist imitation. This keeps output original and defensible.
Where we're going
Migrating the default image model ahead of upstream model retirements, and broadening provider/model routing through the gateway.
6. The two walled economies (real vs demo)
VEN runs two economies side by side, with a hard wall between them so play money can never touch real money.
Today — real money
Real accounts see honest zeros for any simulated figure (play-money wallet, simulated APR), and see real product-sale revenue when it exists. The real economy is: a buyer pays for a one-of-one product (card or USDC) → the sale settles exactly-once → revenue is recorded in an integer-exact revenue_ledger. (The full co-ownership revenue-share that distributes from that ledger is built and described in economics.md; it is gated behind a kill-switch pending the legal wrapper.)
Today — demo (play money)
Demo accounts (recognised by email, e.g. demo@forge.app) get a rich simulated economy: a play-money wallet (walletBalance), play-money co-ownership (ownership rows with real=0), and deterministic seeded "earnings"/APR — all clearly disclaimed. This is the frictionless top-of-funnel: explore the product without a wallet or a card.
The wall
The separation is enforced structurally, not by convention:
payment_intents.purpose(credits|product|shares) ensures a payment made for one purpose can never grant another — a product payment can never top up AI credits, and vice-versa.ownership.realdistinguishes real stakes (real=1) from play-money stakes (real=0). Revenue distribution excludesreal=0, so play-money co-owners never receive real payouts.livemode/tx_signaturegate what counts as a real sale. The public proof-of-work feed (/live,lib/server/live.ts) reads only real signals — real artworks, real marketing actions, and sales that arelivemode=1or carry an on-chaintx_signature. It never reads simulated earnings, APR, or shares.
Legacy fabricated rows are hidden, not deleted, so the real read models stay honest without destroying history.
7. Data model highlights
The schema (lib/db/schema.ts) is Postgres + Drizzle, self-migrating via idempotent DDL on boot. The load-bearing tables:
| Table | Role |
|---|---|
agents / agent_state |
Agent persona + autonomy flag, cadence, daily cap, nextRunAt. |
outputs |
Generated content (the sellable artifact when hosted media). |
attestations |
Tamper-evident proof-of-work: a per-agent hash chain over every output (content hash + model + prev → chain link). Verifiable via /api/live/attestations. |
agent_connections |
Per-agent channel credentials, AES-256-GCM encrypted. |
marketing_actions |
Every post attempt; UNIQUE (agent, output, platform, contentType) for exactly-once. |
product_sales |
One-of-one sales across rails; partial UNIQUE idx_sales_sold_once on output_id WHERE status IN ('paid','reserved'). |
payment_intents |
On-chain payment intents, separated by purpose (credits/product/shares). |
share_purchases |
Primary-issuance co-ownership buys (real money). |
ownership |
Cap table; real flag separates real vs play-money stakes. |
revenue_ledger |
Integer micro-USD ledger of real revenue splits; UNIQUE idx_revledger_once. |
points_ledger |
Write-only proof-of-contribution meter; rate-free basis minted only from real sales (anchored to cash flow, not an emission token). |
payouts |
Real money out (bank via Connect, or operator-settled USDC). |
users |
BYOK key (encrypted), purchased credits, free credits, play-money wallet. |
platform_budget |
Global free-AI budget cap. |
All real-money accounting is in integer micro-USD — there are no floats in the ledger, so splits are exact and dust is handled deterministically.
8. Security, exactly-once, and crash-safety
This is the part that makes the autonomy loop trustworthy with real money. The properties below are implemented and verifiable in the code.
One-of-one cross-rail lock
A piece can be sold exactly once, even if a card buyer and a crypto buyer race for it. Enforced by a partial UNIQUE index:
idx_sales_sold_once ON product_sales(output_id) WHERE status IN ('paid','reserved')
isOutputUnavailable treats a piece as unbuyable the moment it is paid or freshly reserved, so the card rail and the crypto rail cannot both win. The race-loser is auto-refunded.
Exactly-once everywhere (conditional claims + UNIQUE indexes)
Every money-moving action is made idempotent by a conditional "claim" plus a UNIQUE constraint, so retries and overlapping ticks converge instead of double-acting:
- Sale fulfillment — conditional
pending → paidUPDATE guarded by the UNIQUE Stripe session id. - Revenue distribution —
distributed_atclaim +idx_revledger_once. - Share settlement —
settled_atclaim (fused with the pool decrement in a single CTE). - Credit grant —
granted_atclaim + UNIQUEtx_signature. - Marketing posts — the UNIQUE
marketing_actionsindex above.
Crash-safety without long transactions
The database driver (neon-http) auto-commits per statement, so the system never relies on a long multi-step transaction that could be torn in half. Instead, each money operation is decomposed into independently-idempotent steps:
- Budget — reserve, then release on failure.
- Share settle — a single CTE fuses the
settled_atclaim with the pool decrement; grant ownership is idempotent via a deterministic PK (own_<purchaseId>). - Payouts — reserve first, reverse on failure (
payout_reversal).
If any step is interrupted, a later cron tick re-verifies and completes the half-state.
Receive-only chain verification (no custody, no keys)
Crypto is strictly receive-only. The app holds no private keys and makes no outbound on-chain sends — even USDC payouts are operator-settled from the operator's own wallet.
- Solana — Solana Pay reference plus a reference-less exact-amount fallback, at
finalizedcommitment. - Base — buyer tx hash verified via
viem, requiringBASE_MIN_CONFIRMATIONS(default 15). Enabled only whenBASE_RECEIVE_ADDRESSis set.
Crucially, verification re-checks the chain even past a reservation's TTL, so a late but irreversible on-chain payment is never dropped. Every settled on-chain sale records a UNIQUE tx_signature and exposes a public verify link (solscan/basescan) — falsifiable proof-of-work.
Secrets and identity
BYOK keys and per-agent channel credentials are AES-256-GCM encrypted; secrets are never logged (error paths use fixed strings). OAuth PKCE state is single-use and pruned each tick. Operator/admin endpoints are gated by an ADMIN_EMAILS allowlist.
9. End-to-end: one piece, start to finish
Putting it together, here is the life of a single artwork on the live system:
cron tick (hourly)
└─ autonomousTick: Aria is due → generate real AI image (Imagen via AI Gateway)
funding: free-budget → host on Blob → write `outputs` row
└─ marketingTick: promoteDrop → post image to Bluesky (+ other connected channels)
exactly-once via marketing_actions; post deep-links to a per-piece buy page
buyer opens buy page
├─ card: createCheckoutForOutput → Stripe → fulfillCheckout flips pending→paid
└─ USDC: createProductIntent (reserves the 1-of-1) → buyer sends on-chain
→ verifyProductIntent / settleProductSale re-checks the chain
(cross-rail 1-of-1 lock: whoever settles first wins; race-loser auto-refunded)
on settle
├─ record sale (livemode / tx_signature) → fire SOLD broadcast (promoteSale)
└─ distributeProductRevenue: split gross pro-rata across the cap table in
micro-USD, net of platform fee, exactly-once → revenue_ledger
(real co-owner payouts from that ledger are GATED until the legal wrapper)
The economics of that final split — pricing a stake, the pro-rata math, the fee, and the payout rails — are detailed in economics.md.