# Cloudflare production deploy (lvlltd.com)

## Architecture

| Layer | Product | Role |
|-------|---------|------|
| **Primary edge** | Cloudflare Pages `lvlltd-skill-marketplace` | Static site + Functions for `/api/*` |
| **Custom domains** | `lvlltd.com`, `www.lvlltd.com` | Bound on Pages project |
| **Optional** | `brain.lvlltd.com` | Not wired yet — use `/brain/` until DNS; see [DNS-BRAIN.md](./DNS-BRAIN.md) |
| **Sister** | `music.lvlltd.com`, `swarm.lvlltd.com` | Separate workers/sites; paths in [AGENT-FLEET-PATHS.md](./AGENT-FLEET-PATHS.md) |
| **Optional Worker** | `lvlltd-agent-marketplace` / `lvlltd-marketplace-prod` | Full Hono swarm API (`/health`, agents, etc.) |
| **Secondary** | Vercel `lvlltd-agent-marketplace` | Fallback / API CJS mirror |

### Agent path pitfalls

| Wrong | Right |
|-------|-------|
| `swarm.lvlltd.com/api/health` | `swarm.lvlltd.com/health` |
| Full `music.lvlltd.com/catalog.json` every session | `lvlltd.com/api/music-catalog?fields=meta` then `?limit=50` |
| `brain.lvlltd.com` (ENOTFOUND) | `lvlltd.com/brain/` |

## Deploy (recommended)

```bash
cd lvlltd-agent-marketplace
npm run build          # catalog + sealed packs → public/sealed
npm run cf:deploy      # wrangler pages deploy public + post smoke
# full gate:
npm run release        # ci:check → build → cf:deploy → smoke
```

Or:

```bash
node scripts/deploy-cloudflare.mjs
```

### KV (agent social)

Pages project `lvlltd-skill-marketplace` binds namespace **`lvlltd-kv`** (`8315cc0f6f634340814d34a3500dd97b`) as **`KV`**.

- `functions/api/agents.js` reads `env.KV` (also accepts `AGENTS_KV` / `LVL_KV`).
- Verify: `curl -s "https://lvlltd.com/api/agents?action=list&limit=1" | jq .storage` → **`kv`**.
- Documented map: `wrangler.pages.toml`.

### Local API + static

```bash
# Prefer pages:dev (Functions + public + KV).
# Worker DO config lives in wrangler.worker.jsonc so it does not break Pages local.
npm run pages:dev
# → http://127.0.0.1:8788  (API + static + local KV)
# Static-only: npm run dev → http://localhost:3000
```

### x402 unlock (manual)

See [X402-UNLOCK.md](./X402-UNLOCK.md) for seller-agent / generational-agent Base USDC path.

## Pages Functions (live API)

| Path | File |
|------|------|
| `/api/health` | `functions/api/health.js` |
| `/api/pay` | `functions/api/pay.js` |
| `/api/packs` | `functions/api/packs.js` |
| `/api/buy` | `functions/api/buy.js` |
| `/api/escrow` | `functions/api/escrow.js` |

## Environment variables (Pages → Settings → Environment variables)

| Name | Value |
|------|--------|
| `PAYMENT_RECEIVER_BASE` | **`0xa00876513bAA433ce2B58A5341Fd06d2b6f9A6ED`** (canonical treasury — must match all pay_to fields; never 0xabEB…) |
| `X402_LIVE` | `1` |
| `X402_NETWORK` | `base` |
| `X402_ALLOW_DEMO` | `0` in production (set `1` only for emergency demos) |
| `BASE_RPC_URL` | optional override (default mainnet.base.org) |
| `OPS_SECRET` | **Required for /ops, /dashboard, /profit, /api/metrics, /api/auditor, /api/revenue, /api/zone** (encrypted secret) |
| `CLOUDFLARE_API_TOKEN` | **Zone API** — DNS Edit + Pages Edit for `/api/zone` (brain CNAME, etc.). See [ZONE-API.md](./ZONE-API.md) |

Set via dashboard (**Pages → lvlltd-skill-marketplace → Settings → Environment variables → Production**) or:

```bash
# Interactive prompt — do not paste the secret into chat logs
npx wrangler pages secret put OPS_SECRET --project-name=lvlltd-skill-marketplace

# Confirm it is listed (names only; values stay encrypted)
npx wrangler pages secret list --project-name=lvlltd-skill-marketplace

# Redeploy so Functions pick up the new secret binding
npm run cf:deploy
```

**Probe without revealing the secret:**

```bash
# Before bind: 503 + error ops_auth_not_configured
# After bind (no auth header): 401
curl -sS https://lvlltd.com/api/metrics | head -c 200

# After bind + local secret for smoke:
#   set OPS_SECRET=...   (PowerShell: $env:OPS_SECRET = '...')
npm run smoke
```

Unauthenticated browsers hit Basic Auth challenge on HTML ops pages (`WWW-Authenticate`).

## Sealed packs (operational packages after purchase)

Marketplace unlock is the **ownership record** (x402 payment + ERC-7857 iNFT). The **operational package** is the sealed pack payload returned in `POST /api/pay` as `sealed_pack.files`.

- Built to `goods/sealed/` and mirrored to `public/sealed/` for CDN (`npm run build:sealed`)
- Catalog coverage: every `catalog.json` skill id gets a pack (skill-details + synthetic from catalog / `~/.grok/skills`)
- Unlock response embeds full `sealed_pack.files` after payment verify (code + docs + optional `ops/*`)
- Multi-agent spine packs ship `ops/topology.json`, `ops/role-cards.json`, `ops/tool-profiles.json`, board/handoff schemas
- UI: after unlock, browser downloads unlock JSON; **Download sealed pack** re-downloads
- Install CLI: `node scripts/install-unlocked-pack.mjs unlock.json ./skills`
- List: `https://lvlltd.com/api/packs` · metadata: `https://lvlltd.com/api/packs?skill=<id>`

## Smoke after deploy

```bash
curl -s https://lvlltd.com/api/health | jq .
curl -s -o /dev/null -w "%{http_code}\n" "https://lvlltd.com/api/pay?skill=grant-writing"   # 402
curl -s https://lvlltd.com/sealed/manifest.json | jq "{pack_count, ops_pack_count, premium_count}"
curl -s "https://lvlltd.com/api/packs?skill=agent-role-card-generator" | jq "{ok, operational_package, files}"
curl -s https://lvlltd.com/sealed/agent-role-card-generator/ops/topology.json | head
curl -s https://lvlltd.com/catalog.json | jq .version
```


## DNS notes

Custom domains are already on the Pages project. If API still returns HTML:

1. Confirm Functions deployed with latest upload
2. Purge Cloudflare cache for `/api/*`
3. Ensure no Page Rule routes `/api/*` to static-only

## Worker (optional full API)

```bash
npm run cf:deploy:worker
```

Uses `wrangler.jsonc` production env. Prefer Pages for `lvlltd.com` SPA + paywall unless you need Durable Objects swarm endpoints.

