// shared ExoClaw bridge on Cloudflare Workers · BYOK Phase 1 → Codex OAuth Phase 2 → commercial Phase 3 · 50-trainee HICAM workshop
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.
| pain | why a local install fails | what the gateway does |
|---|---|---|
| Non-technical attendees | Docker / ClawBox install during a live event burns 30+ minutes per person | One URL + one API key in .env; nothing local to install |
| HICAM is on a different network | claws-mac-mini lives on a Tailscale mesh; attendees can't reach it directly | ExoClaw is the public-internet bridge; everything behind it stays Tailscale-private |
| 50 concurrent trainees | M4 Mini at 16 GB can't host 50 hot OpenClaw sessions | Rate limit at 50 req/min/user flattens the peak; rate-limited bursts fit in ~8 GB |
| Anthropic OAuth is unfamiliar | Most attendees have never touched claude auth login | Phase 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 |
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 │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
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
organized-gatewayOne 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
| rule | value | why |
|---|---|---|
| req/min/user | 50 | Protects claws-mac-mini from runaway loops with 50 concurrent attendees |
| window | 60 s rolling, KV TTL 120 s | Simple per-minute counter key; auto-evicts |
| over-limit response | HTTP 429 + JSON error | Clear message — attendee knows it'll reset shortly |
| burst allowance | none (hard 50) | One person can't monopolize the M4 Mini |
| anonymous fallback | still rate-limited under 'anonymous' bucket | Incentivizes setting X-User-ID properly |
-- 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.
─── 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
.env. Paste OPENCLAW_GATEWAY_URL (shared, provided), OPENAI_API_KEY (their own sk-), and USER_ID (their first name).bash setup.sh — confirms gateway connection, prints "Agent ready."| scenario | memory load | decision |
|---|---|---|
| 5 concurrent attendees | ~4 GB | Fine on M4 Mini 16 GB |
| 20 concurrent | ~10 GB | Tight — rate limit helps |
| 50 concurrent (peak) | ~20 GB+ | Provision Hetzner CX41 ($18/mo, temp) |
| 50 attendees rate-limited | ~8 GB burst | Rate 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
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
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
}
claude auth login.bash setup.sh --auth codex stores the token in KV under their USER_ID..env. Repo is cleaner; token lives in the Worker, not in the project.The same Worker, three tiers, Stripe-driven. Attendees who want continued access pay monthly; ExoClaw enforces tier limits and meters usage.
| tier | price | scope | infra cost / customer | margin |
|---|---|---|---|---|
| Tier 1 | $99 / mo | 1 project · 10k req/mo · shared infra | ~$15 (shared 16 GB) | ~85% |
| Tier 2 | $299 / mo | 3 projects · 50k req/mo · priority slots | ~$25 | ~91% |
| Tier 3 | $699 / mo | Unlimited · custom SLA · dedicated node | ~$85 (Hetzner) | ~88% |
checkout.session.completed writes tier:{user_id} = tier1|tier2|tier3 to KV.monthly_usage table); returns HTTP 402 at limit.| customers | compute | cost |
|---|---|---|
| 1–5 | claws-mac-mini M4 (16 GB) | existing |
| 6–15 | Hetzner AX41 (64 GB) | $55/mo |
| 16–30 | Hetzner AX52 (128 GB) | $85/mo |
| 30+ | Multi-node OpenClaw cluster via Tailscale | varies |
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.
| path | who | what changes in .env | cost |
|---|---|---|---|
| A — Shared | Non-technical execs who just want it to work | nothing — same URL, just pay Stripe | $99–$699/mo |
| B — ClawBox | Tech-curious devs who want local control + speed | OPENCLAW_GATEWAY_URL=http://localhost:18789 | free (self-run) + $20/mo Codex |
| C — ClaudeFlare | Builders who want full sovereignty | OPENCLAW_GATEWAY_URL=https://my-exoclaw.workers.dev | DIY infra |
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.
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.
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.
| layer | tech | purpose |
|---|---|---|
| Worker runtime | Cloudflare Workers | ExoClaw bridge — public proxy to Tailscale |
| HTTP framework | Hono | Tiny, CF-native router inside the Worker |
| Rate limit | Cloudflare KV | Per-user minute counters |
| Usage log | Cloudflare D1 (SQLite) | requests table + user_summary view |
| Deploy | Wrangler CLI | Deploy, secrets, D1 migrate |
| Types | @cloudflare/workers-types | KV, D1, Env types |
| Validation | Zod | Request schema validation |
| Monorepo | pnpm workspaces | Organized AI standard |
| Boilerplate | Organized Codebase | CLAUDE.md · .claude/ · PLANNING/ · Boris/Ralphy/GSD |
| Local agent | OpenClaw :18789 | Claude CLI OAuth on claws-mac-mini |
| Memory | Hermes :7700 | Agent context + session memory |
| Free inference | NoClaw :11434 | Gemma 3 27B on claw for non-billable work |
# 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)
# 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
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"}
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"
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'"
--commit-dirty=true. Wrangler refuses to deploy from a dirty git tree without it. During event setup you'll be hot-fixing — pin the flag in your script.ANTHROPIC_API_KEY in Worker secrets. Phase 1 = trainee's BYOK in Authorization header. Phase 2 = trainee's Codex token in KV. Jordan's key is never exposed to the gateway.USER_ID (firstname-lastname) before the event; bake it into their .env.example. Otherwise everything bunches in the 'anonymous' KV bucket and rate limiting collapses.SELECT count(*) FROM requests after smoke-test, every time.tailscale up --auto-update or use a Cloudflare Tunnel as the bridge instead.