OpenClaw
OpenClaw Gateway · system architecture

OpenClaw Gateway

// shared ExoClaw bridge on Cloudflare Workers · BYOK Phase 1 → Codex OAuth Phase 2 → commercial Phase 3 · 50-trainee HICAM workshop

What it is

OpenClaw Gateway is a single Cloudflare Worker — organized-gateway — that lets 50 non-technical workshop attendees hit a shared OpenClaw instance running on claws-mac-mini, from any network, with no Docker, no Tailscale, no infrastructure knowledge. They drop a gateway URL and an API key into a .env, run setup.sh, and a working agent is in their project in under five minutes.

The Worker rate-limits per user (KV), logs every request to D1, and proxies to the OpenClaw on claws-mac-mini through a Tailscale bridge URL. Phase 1 ships BYOK OpenAI; Phase 2 swaps to Codex OAuth before the next event; Phase 3 turns the same Worker into a tiered commercial product. Three post-training upgrade paths are baked in from day one.

Event: "Problem Solved!: OpenClaw Workshop" at HICAM, 6201 Quinn Luke Trail, Austin TX 78724. $100 suggested / $20 minimum, token-gated entry. Goal: every attendee leaves with a working agent in their project.

Why a shared gateway

painwhy a local install failswhat the gateway does
Non-technical attendeesDocker / ClawBox install during a live event burns 30+ minutes per personOne URL + one API key in .env; nothing local to install
HICAM is on a different networkclaws-mac-mini lives on a Tailscale mesh; attendees can't reach it directlyExoClaw is the public-internet bridge; everything behind it stays Tailscale-private
50 concurrent traineesM4 Mini at 16 GB can't host 50 hot OpenClaw sessionsRate limit at 50 req/min/user flattens the peak; rate-limited bursts fit in ~8 GB
Anthropic OAuth is unfamiliarMost attendees have never touched claude auth loginPhase 1 uses BYOK OpenAI keys (familiar); Phase 2 upgrades to Codex OAuth before the next event
Post-training continuity"It worked at the event, what now?"Three upgrade paths baked in: stay on hosted, run ClawBox locally, or graduate to ClaudeFlare

Architecture

                OPENCLAW GATEWAY — FULL SYSTEM

  ┌─────────────────────────────────────────────────────────────────┐
  │  TRAINEE LAPTOP (any OS, any network)                           │
  │                                                                 │
  │  Organized Codebase boilerplate + .env:                         │
  │    OPENCLAW_GATEWAY_URL=https://organized-gateway.workers.dev   │
  │    OPENAI_API_KEY=sk-…   (BYOK)                                 │
  │    USER_ID=trainee-{firstname}                                  │
  └────────────────────────────┬────────────────────────────────────┘
                               │ HTTPS · POST /v1/chat/completions
                               │ X-User-ID + Authorization: Bearer sk-…
                               ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  Cloudflare Workers — organized-gateway                         │
  │  (the only public component)                                    │
  │                                                                 │
  │  1. Extract X-User-ID + Authorization                           │
  │  2. KV rate-limit check (50 req/min/user)                       │
  │  3. Log to D1 → table `requests`                                │
  │  4. Proxy to OPENCLAW_URL (Tailscale bridge)                    │
  │  5. Log response (status, latency, tokens)                      │
  │  6. Return to trainee                                           │
  └────────────────────────────┬────────────────────────────────────┘
                               │ Tailscale mesh (private)
                               ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  claws-mac-mini · 100.82.244.127                                │
  │                                                                 │
  │  ┌────────────────┐ ┌────────────────┐ ┌────────────────┐       │
  │  │ OpenClaw :18789│ │ Hermes :7700   │ │ NoClaw :11434  │       │
  │  │ Claude OAuth + │ │ memory + ctx   │ │ Gemma 3 27B    │       │
  │  │ OpenAI proxy   │ │ sessions       │ │ free inference │       │
  │  └────────────────┘ └────────────────┘ └────────────────┘       │
  └─────────────────────────────────────────────────────────────────┘

Request flow — Phase 1 vs Phase 2

PHASE 1 — BYOK OpenAI (workshop launch)

   trainee → ExoClaw → OpenClaw → OpenAI API
   (sk-… key passed through verbatim)
   cost: trainee pays OpenAI directly

──────────────────────────────────────────────────────────────────

PHASE 2 — Codex OAuth (before the next event)

   trainee → ExoClaw → OpenClaw → Anthropic Claude
   (trainee's Codex OAuth token resolved from KV)
   cost: trainee's $20/mo Codex plan
   no API key in headers — token lives in the Worker

ExoClaw Worker — organized-gateway

One Hono app. Routes /v1/* through the rate limiter + logger + Tailscale proxy.

// apps/organized-gateway/src/index.ts
import { Hono } from 'hono'

const app = new Hono<{ Bindings: Env }>()

async function logRequest(env: Env, e: Entry) {
  await env.DB.prepare(
    `INSERT INTO requests (user_id, endpoint, status, latency_ms,
     tokens_est, ip_hash, created_at)
     VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`,
  ).bind(e.user_id, e.endpoint, e.status, e.latency_ms,
         e.tokens_est ?? 0, e.ip_hash).run()
}

async function checkRateLimit(env: Env, userId: string): Promise<boolean> {
  const minute = Math.floor(Date.now() / 60000)
  const key = `rate:${userId}:${minute}`
  const current = parseInt(await env.GATEWAY_KV.get(key) ?? '0')
  if (current >= 50) return false
  await env.GATEWAY_KV.put(key, String(current + 1), { expirationTtl: 120 })
  return true
}

app.all('/v1/*', async (c) => {
  const start = Date.now()
  const userId = c.req.header('X-User-ID') ?? 'anonymous'
  const authHeader = c.req.header('Authorization') ?? ''
  const ipHash = await hashIp(c.req.header('CF-Connecting-IP') ?? '')
  const endpoint = new URL(c.req.url).pathname

  if (!(await checkRateLimit(c.env, userId))) {
    await logRequest(c.env, { user_id: userId, endpoint, status: 429,
                              latency_ms: 0, ip_hash: ipHash })
    return c.json({ error: 'Rate limit exceeded. 50 requests/minute.' }, 429)
  }

  const upstream = await fetch(`${c.env.OPENCLAW_URL}${endpoint}`, {
    method: c.req.method,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': authHeader,             // pass-through BYOK
      'X-Forwarded-User': userId,
    },
    body: c.req.method !== 'GET' ? await c.req.text() : undefined,
  })

  const body = await upstream.text()
  let tokens = 0
  try { tokens = JSON.parse(body)?.usage?.total_tokens ?? 0 } catch {}

  await logRequest(c.env, {
    user_id: userId, endpoint, status: upstream.status,
    latency_ms: Date.now() - start, tokens_est: tokens, ip_hash: ipHash,
  })

  return new Response(body, {
    status: upstream.status,
    headers: { 'Content-Type': 'application/json' },
  })
})

app.get('/health', (c) => c.json({ status: 'ok', gateway: 'organized-gateway' }))

export default app

Rate limiting

rulevaluewhy
req/min/user50Protects claws-mac-mini from runaway loops with 50 concurrent attendees
window60 s rolling, KV TTL 120 sSimple per-minute counter key; auto-evicts
over-limit responseHTTP 429 + JSON errorClear message — attendee knows it'll reset shortly
burst allowancenone (hard 50)One person can't monopolize the M4 Mini
anonymous fallbackstill rate-limited under 'anonymous' bucketIncentivizes setting X-User-ID properly

D1 schema — usage telemetry

-- migrations/0001_init.sql
CREATE TABLE IF NOT EXISTS requests (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id      TEXT    NOT NULL,
  endpoint     TEXT    NOT NULL,
  status       INTEGER NOT NULL,
  latency_ms   INTEGER NOT NULL,
  tokens_est   INTEGER DEFAULT 0,
  ip_hash      TEXT,
  created_at   TEXT    DEFAULT (datetime('now'))
);

CREATE INDEX idx_user_id ON requests(user_id);
CREATE INDEX idx_created ON requests(created_at);

-- Live monitoring view
CREATE VIEW user_summary AS
SELECT
  user_id,
  COUNT(*)        AS total_requests,
  SUM(tokens_est) AS total_tokens,
  AVG(latency_ms) AS avg_latency,
  MAX(created_at) AS last_seen
FROM requests
GROUP BY user_id;

The requests table is the source of truth for who used the gateway, how often, and at what cost. After the workshop it powers the upgrade-path conversation: every attendee's activity is queryable.

Phase 1 — HICAM Workshop · BYOK OpenAI

Two-week prep timeline

─── BEFORE EVENT ───────────────────────────────────────────────────

Week -2  Deploy ExoClaw to CF
         ↳ wrangler deploy + D1 migrate + KV namespace create
         ↳ Set OPENCLAW_URL secret → Tailscale bridge to claws-mac-mini

Week -1  Capacity check
         ↳ Decide: stay on M4 Mini, OR provision Hetzner CX41 ($18/mo)
         ↳ Test 10 concurrent sessions, watch memory_pressure

Week -1  Build onboarding ZIP
         ↳ Organized Codebase boilerplate + .env.example
         ↳ .claude/agents/project-agent.json pre-wired
         ↳ README: "3 steps to your first agent"
         ↳ QR code for the ZIP + gateway URL

─── DAY OF EVENT ───────────────────────────────────────────────────

Step 1   Trainee scans QR → downloads ZIP
Step 2   Unzip, rename .env.example → .env
Step 3   Paste OPENAI_API_KEY + USER_ID (firstname)
Step 4   bash setup.sh → "Agent ready."
Step 5   First agent task: describe their business → get a workflow

─── AFTER EVENT ────────────────────────────────────────────────────

Day +1   Review D1 — identify most-active attendees
Day +3   Send upgrade-path email (paths A / B / C)
Day +7   Spin down temp compute if used

3-step trainee onboarding

  1. Unpack boilerplate. Download ZIP from QR code, unzip, open in Cursor or VS Code.
  2. Fill in .env. Paste OPENCLAW_GATEWAY_URL (shared, provided), OPENAI_API_KEY (their own sk-), and USER_ID (their first name).
  3. Run setup. bash setup.sh — confirms gateway connection, prints "Agent ready."

Capacity planning

scenariomemory loaddecision
5 concurrent attendees~4 GBFine on M4 Mini 16 GB
20 concurrent~10 GBTight — rate limit helps
50 concurrent (peak)~20 GB+Provision Hetzner CX41 ($18/mo, temp)
50 attendees rate-limited~8 GB burstRate limit flattens the peak — likely fine on M4 Mini

Success criteria for Phase 1:
• Zero setup failures — every attendee finishes the 3 steps in < 5 minutes with no support
• 50 active USER_IDs logged in D1 by end of day
• No gateway downtime during the workshop
• ≥ 10 attendees ask about post-training access within 48 hours

Phase 2 — Codex OAuth

Before the next event, swap BYOK OpenAI for Codex OAuth. Attendees use their own $20/mo Anthropic plan; no API keys live in .env anymore.

PHASE 1 (BYOK OpenAI)                  PHASE 2 (Codex OAuth)
──────────────────────────             ─────────────────────────────
Header: Authorization: Bearer sk-…   Header: (none — OAuth)
Model:  OpenAI GPT-4o                  Model:  Claude (Codex plan)
Cost:   trainee → OpenAI               Cost:   trainee → Anthropic $20/mo
Auth:   API key string                 Auth:   OAuth token (oauth_…)
OpenClaw: routes key to OpenAI         OpenClaw: native Claude integration
Worker:  pass-through Authorization    Worker:  resolve token from KV by USER_ID

The Worker diff for Phase 2

const upstreamHeaders: Record<string, string> = {
  'Content-Type': 'application/json',
  'X-Forwarded-User': userId,
}

if (c.env.AUTH_MODE === 'codex') {
  // Phase 2: lookup trainee's OAuth token in KV (stored at onboarding)
  const codexToken = await c.env.GATEWAY_KV.get(`oauth:${userId}`)
  if (!codexToken) return c.json({ error: 'No Codex session.' }, 401)
  upstreamHeaders['X-Codex-Token'] = codexToken
} else {
  // Phase 1: pass-through BYOK key
  upstreamHeaders['Authorization'] = authHeader
}

Phase 2 onboarding

  1. Visit Anthropic Codex. Sign up for the $20/mo plan; mint an OAuth token via claude auth login.
  2. Paste OAuth in setup. bash setup.sh --auth codex stores the token in KV under their USER_ID.
  3. No API key in .env. Repo is cleaner; token lives in the Worker, not in the project.

Phase 3 — Commercial product

The same Worker, three tiers, Stripe-driven. Attendees who want continued access pay monthly; ExoClaw enforces tier limits and meters usage.

tierpricescopeinfra cost / customermargin
Tier 1$99 / mo1 project · 10k req/mo · shared infra~$15 (shared 16 GB)~85%
Tier 2$299 / mo3 projects · 50k req/mo · priority slots~$25~91%
Tier 3$699 / moUnlimited · custom SLA · dedicated node~$85 (Hetzner)~88%

Stripe wiring

  1. Customer picks tier via Stripe Checkout link.
  2. Stripe webhook → CF Worker; on checkout.session.completed writes tier:{user_id} = tier1|tier2|tier3 to KV.
  3. ExoClaw reads tier on every request; enforces monthly counter in D1 (monthly_usage table); returns HTTP 402 at limit.
  4. Cron Worker resets monthly counters on the 1st of each month.

Compute scaling by customer count

customerscomputecost
1–5claws-mac-mini M4 (16 GB)existing
6–15Hetzner AX41 (64 GB)$55/mo
16–30Hetzner AX52 (128 GB)$85/mo
30+Multi-node OpenClaw cluster via Tailscalevaries

Post-training upgrade paths

Every attendee leaves with a working agent. After the event, the same .env is the contract — the gateway URL is the only thing that changes per path.

pathwhowhat changes in .envcost
A — SharedNon-technical execs who just want it to worknothing — same URL, just pay Stripe$99–$699/mo
B — ClawBoxTech-curious devs who want local control + speedOPENCLAW_GATEWAY_URL=http://localhost:18789free (self-run) + $20/mo Codex
C — ClaudeFlareBuilders who want full sovereigntyOPENCLAW_GATEWAY_URL=https://my-exoclaw.workers.devDIY infra

Path A — Shared infrastructure (hosted)

Lowest friction. Nothing changes in their project. They click a Stripe link, their USER_ID gets a tier in KV, access continues. Jordan maintains claws-mac-mini (or scales to Hetzner), keeps OpenClaw updated, handles downtime. Customers get an SLA by tier.

Path B — Local ClawBox

For tech-curious attendees. Download the Tauri v2 desktop app from clawbox-wiki.pages.dev; it spins up a local OpenClaw at localhost:18789. Connect Codex OAuth. Update .env to point at localhost. Same boilerplate, same agent, now running locally — faster, offline-capable, no dependency on Jordan's infra.

Path C — Full ClaudeFlare stack

For builders. Fork organized-gateway as their own ExoClaw on their own CF account; run OpenClaw on their Mac (via ClawBox), a VPS, or a Tailscale mesh; optionally extend into ephemeral Workers, auto-doc generation, and the Agent Client Protocol for editor-driven invocation. Zero dependency on Jordan.

Stack

layertechpurpose
Worker runtimeCloudflare WorkersExoClaw bridge — public proxy to Tailscale
HTTP frameworkHonoTiny, CF-native router inside the Worker
Rate limitCloudflare KVPer-user minute counters
Usage logCloudflare D1 (SQLite)requests table + user_summary view
DeployWrangler CLIDeploy, secrets, D1 migrate
Types@cloudflare/workers-typesKV, D1, Env types
ValidationZodRequest schema validation
Monorepopnpm workspacesOrganized AI standard
BoilerplateOrganized CodebaseCLAUDE.md · .claude/ · PLANNING/ · Boris/Ralphy/GSD
Local agentOpenClaw :18789Claude CLI OAuth on claws-mac-mini
MemoryHermes :7700Agent context + session memory
Free inferenceNoClaw :11434Gemma 3 27B on claw for non-billable work

Deploy runbook

1. Bootstrap (once)

# Provision KV + D1 + run migrations
CLOUDFLARE_ACCOUNT_ID=691fe25d377abac03627d6a88d3eeac9 \
  bash scripts/bootstrap.sh

# Creates:
#   KV namespace: organized-gateway-kv
#   D1 database:  organized-gateway-db   (+ runs migrations/0001_init.sql)

2. Configure secrets

# Tailscale bridge URL (or CF Tunnel) → claws-mac-mini
echo "https://your-tailscale-bridge" | \
  wrangler secret put OPENCLAW_URL --name organized-gateway

# Phase 1 = "openai", Phase 2 = "codex"
echo "openai" | \
  wrangler secret put AUTH_MODE --name organized-gateway

3. Deploy

CLOUDFLARE_ACCOUNT_ID=691fe25d377abac03627d6a88d3eeac9 \
  wrangler deploy \
    --name organized-gateway \
    --config apps/organized-gateway/wrangler.toml \
    --commit-dirty=true

# Confirm live
curl https://organized-gateway.691fe25d.workers.dev/health
# → {"status":"ok","gateway":"organized-gateway"}

4. Smoke-test the trainee flow

curl -X POST https://organized-gateway.691fe25d.workers.dev/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "X-User-ID: test-jordan" \
  -H "Authorization: Bearer sk-your-openai-key" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}'

# Confirm the row landed in D1
wrangler d1 execute organized-gateway-db \
  --command "SELECT * FROM requests ORDER BY id DESC LIMIT 5"

5. Live monitor during the event

watch -n5 "wrangler d1 execute organized-gateway-db \
  --command 'SELECT user_id, count(*) FROM requests \
             WHERE created_at > datetime(\"now\",\"-5 minutes\") \
             GROUP BY user_id ORDER BY 2 DESC'"

Pitfalls