I Built an AI Company with OpenClaw + Vercel + Supabase - Two Weeks Later, They Run It Themselves

Friday, February 6, 2026 AI

Scraped Article

6 AI agents, 1 VPS, 1 Supabase database — going from "agents can talk" to "agents run the website autonomously" took me two weeks. This article covers exactly what's missing in between, how to fix it, and an architecture you can take home and use. Starting Point: You Have OpenClaw. Now What? If you've been playing with AI agents recently, chances are you already have OpenClaw set up. It solves a big problem: letting Claude use tools, browse the web, operate files, and run scheduled tasks. You can assign cron jobs to agents — daily tweets, hourly intel scans, periodic research reports. That's where I started too. My project is called VoxYZ Agent World — 6 AI agents autonomously operating a website from inside a pixel-art office. The tech stack is simple: OpenClaw (on VPS): The agents' "brain" — runs roundtable discussions, cron jobs, deep research Next.js + Vercel: Website frontend + API layer Supabase: Single source of truth for all state (proposals, missions, events, memories) Six roles, each with a job: Minion makes decisions, Sage analyzes strategy, Scout gathers intel, Quill writes content, Xalt manages social media, Observer does quality checks. OpenClaw's cron jobs get them to "show up for work" every day. Roundtable lets them discuss, vote, and reach consensus. But that's just "can talk," not "can operate." Everything the agents produce — drafted tweets, analysis reports, content pieces — stays in OpenClaw's output layer. Nothing turns it into actual execution, and nothing tells the system "done" after execution completes. Between "agents can produce output" and "agents can run things end-to-end," there's a full execute → feedback → re-trigger loop missing. That's what this article is about. What a Closed Loop Looks Like Let's define "closed loop" first, so we don't build the wrong thing. A truly unattended agent system needs this cycle running: Agent proposes an idea (Proposal) ↓ Auto-approval check (Auto-Approve) ↓ Create mission + steps (Mission + Steps) ↓ Worker claims and executes (Worker) ↓ Emit event (Event) ↓ Trigger new reactions (Trigger / Reaction) ↓ Back to step one Sounds straightforward? In practice, I hit three pitfalls — each one made the system "look like it's running, but actually spinning in place." Pitfall 1: Two Places Fighting Over Work My VPS had OpenClaw workers claiming and executing tasks. At the same time, Vercel had a heartbeat cron running mission-worker, also trying to claim the same tasks. Both querying the same table, grabbing the same step, executing independently. No coordination, pure race condition. Occasionally a step would get tagged with conflicting statuses by both sides. Fix: Cut one. VPS is the sole executor. Vercel only runs the lightweight control plane (evaluate triggers, process reaction queue, clean up stuck tasks). The change was minimal — remove the runMissionWorker call from the heartbeat route: // Heartbeat now does only 4 things const triggerResult = await evaluateTriggers(sb, 4_000); const reactionResult = await processReactionQueue(sb, 3_000); const learningResult = await promoteInsights(sb); const staleResult = await recoverStaleSteps(sb); Bonus: saved the cost of Vercel Pro. Heartbeat doesn't need Vercel's cron anymore — one line of crontab on VPS does the job: */5 * * * * curl -s -H "Authorization: Bearer $KEY" https://yoursite.com/api/ops/heartbeat Pitfall 2: Triggered But Nobody Picked It Up I wrote 4 triggers: auto-analyze when a tweet goes viral, auto-diagnose when a mission fails, auto-review when content gets published, auto-promote when an insight matures. During testing I noticed: the trigger correctly detected the condition and created a proposal. But the proposal sat forever at pending — never became a mission, never generated executable steps. The reason: triggers were directly inserting into the ops_mission_proposals table, but the normal approval flow is: insert proposal → evaluate auto-approve → if approved, create mission + steps. Triggers skipped the last two steps. Fix: Extract a shared function createProposalAndMaybeAutoApprove. Every path that creates a proposal — API, triggers, reactions — must call this one function. // proposal-service.ts — the single entry point for all proposal creation export async function createProposalAndMaybeAutoApprove( sb: SupabaseClient, input: ProposalServiceInput, // includes source: 'api' | 'trigger' | 'reaction' ): Promise<ProposalServiceResult> { // 1. Check daily limit // 2. Check Cap Gates (explained below) // 3. Insert proposal // 4. Emit event // 5. Evaluate auto-approve // 6. If approved → create mission + steps // 7. Return result } After the change, triggers just return a proposal template. The evaluator calls the service: // trigger-evaluator.ts if (outcome.fired && outcome.proposal) { await createProposalAndMaybeAutoApprove(sb, { ...outcome.proposal, source: 'trigger', }); } One function to rule them all. Any future ch