# CRYPTO LWA — TROUBLESHOOTING

Operator: **Stan** (`stangebara@protonmail.com`)
Build: **v7.10.6**
Scope: every agent, every system endpoint, every common cross-cutting failure.

> This document is imperative. Each section answers one question:
> *"It broke — what do I click, what do I curl, what do I redeploy?"*
> Read top-to-bottom once, then jump to the relevant section as needed.
>
> Companion docs: `README.md` (operator overview), `ACCOUNT-SETUP.md` (auth chain), `ogou-kb/OGOU-KNOWLEDGE-BASE.md` (security playbooks).

### Dashboard map (where to click in Cloudflare)

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 260" font-family="ui-monospace, SFMono-Regular, monospace" font-size="12">
  <rect x="0" y="0" width="760" height="260" fill="#ffffff"/>
  <rect x="20" y="20" width="170" height="220" fill="#fafafa" stroke="#333"/>
  <text x="30" y="40">Cloudflare Dashboard</text>
  <text x="30" y="65" fill="#666">▸ Workers &amp; Pages</text>
  <text x="30" y="85" fill="#000">  ▸ cryptolwa (Pages)</text>
  <text x="30" y="105" fill="#666">▸ DNS</text>
  <text x="30" y="125" fill="#666">▸ KV</text>
  <text x="30" y="145" fill="#666">▸ Custom Domains</text>
  <rect x="230" y="20" width="220" height="220" fill="#fafafa" stroke="#333"/>
  <text x="240" y="40">cryptolwa &gt; Settings</text>
  <text x="240" y="65">▸ General</text>
  <text x="240" y="85" fill="#c89b1a">▸ Environment variables</text>
  <text x="240" y="105" fill="#c89b1a">▸ Functions</text>
  <text x="240" y="125">  ▸ KV namespace bindings</text>
  <text x="240" y="145">  ▸ Cron triggers</text>
  <text x="240" y="165">▸ Builds &amp; deployments</text>
  <text x="240" y="185">▸ Custom domains</text>
  <rect x="490" y="20" width="250" height="220" fill="#fafafa" stroke="#333"/>
  <text x="500" y="40">Deployments</text>
  <text x="500" y="65">▸ Production (latest)</text>
  <text x="500" y="85" fill="#666">▸ Preview</text>
  <text x="500" y="105">▸ Rollback ⟲</text>
  <text x="500" y="135" fill="#666">Each deploy row has:</text>
  <text x="510" y="155" fill="#666">- View build log</text>
  <text x="510" y="175" fill="#666">- Promote to production</text>
  <text x="510" y="195" fill="#666">- Retry</text>
  <line x1="190" y1="95" x2="230" y2="95" stroke="#000" marker-end="url(#a)"/>
  <line x1="450" y1="95" x2="490" y2="95" stroke="#000" marker-end="url(#a)"/>
  <defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0 0 L10 5 L0 10 z" fill="#000"/></marker></defs>
</svg>

---

# PART 1 — THE AGENTS

---

## 1. LEGBA — Narrative Scanner

**What it does (one sentence):** Ingests crypto RSS/Reddit feeds, scores 14 narrative clusters by volume × momentum × sentiment, and returns a ranked narrative-by-asset heatmap.

**Where it lives:**
- HTML page: `/narratives.html`
- API endpoints:
  - `GET /api/legba` — full narrative heatmap JSON
  - `GET /api/legba/status` — lightweight liveness pill (used by `/swap`)
  - `GET /api/narratives/heatmap` — KV-cached read of the latest heatmap
  - `POST /api/narratives/refresh` — cron-triggered rebuild
- Source files:
  - `functions/api/legba/index.ts`
  - `functions/api/legba/status.ts`
  - `functions/api/narratives/heatmap.ts`
  - `functions/api/narratives/refresh.ts`
- KV bindings used: `DANTON_KV` (RSS cache it reads), `HEATMAP_KV` (cached output)
- Env vars used: none required; reads RSS feeds anonymously

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/legba/status | jq
# Expected: {"ok":true,"online":true,"last_signal_ts":<unix>,"signals_today":<int>,"version":"v7.10.6"}
```

**Common failures and exact fixes:**

### Failure: `/narratives` page shows "LEGBA · OFFLINE"
1. **Where to click:** Cloudflare → Pages → cryptolwa → Deployments → latest → View build log. Confirm `functions/api/legba/status.ts` is in the build.
2. **What to check:** `curl -i https://www.cryptolwa.io/api/legba/status` — if HTTP 404, the file isn't deployed.
3. **Exact fix:** Re-upload the build (drag-drop the folder) or `git push`. The status endpoint was added in v7.10.6 specifically to fix this.
4. **Verify:** `curl -s https://www.cryptolwa.io/api/legba/status | jq .ok` → `true`.

### Failure: Heatmap stuck on stale data
1. **Where to click:** Cloudflare → Pages → cryptolwa → Settings → Functions → Cron triggers.
2. **What to check:** Cron `*/15 * * * *` → `POST /api/narratives/refresh` is enabled.
3. **Exact fix:** Force one-off refresh: `curl -X POST https://www.cryptolwa.io/api/narratives/refresh`. If cron missing, re-add it.
4. **Verify:** `curl -s https://www.cryptolwa.io/api/narratives/heatmap | jq '.computedAt'` returns a timestamp within 15 minutes of now.

### Failure: 500 on `/api/legba`
1. **What to check:** Underlying RSS source down. LEGBA pulls 8 RSS feeds; one bad feed should be tolerated but a JSON-mangling 502 can bubble up.
2. **Exact fix:** Hit `/api/legba/status` (cheap probe). If status is `ok:true` but `/api/legba` 500s, redeploy. The status endpoint never reads RSS.
3. **Verify:** `curl -s https://www.cryptolwa.io/api/legba | jq '.narratives | length'` returns 14.

### Failure: `DANTON_KV` not bound
1. **Where to click:** Cloudflare → Pages → cryptolwa → Settings → Functions → KV namespace bindings.
2. **Exact fix:** Add binding `DANTON_KV` → pick the matching namespace (create one if absent: Cloudflare → Workers & Pages → KV → Create namespace, name `danton_kv`).
3. **Verify:** `/api/legba/status` returns a non-zero `signals_today` after one cron tick.

**Last-resort reset:** Delete the `legba:heatmap` key in `HEATMAP_KV` (`wrangler kv:key delete --binding HEATMAP_KV legba:heatmap`) and re-trigger `POST /api/narratives/refresh`.

---

## 2. LIBO — Content / Picks Curator

**What it does (one sentence):** Scouts CoinGecko + DeFiLlama for promising projects per narrative cluster and ranks them.

**Where it lives:**
- HTML page: surfaced inside `/narratives.html` (panels) and admin tooling
- API endpoint: `GET /api/libo`
- Source file: `functions/api/libo/index.ts`
- KV bindings used: none required (cache is edge-cache only)
- Env vars used: `COINGECKO_KEY` (optional Pro tier)

**Health check:**

```bash
curl -s "https://www.cryptolwa.io/api/libo?narrative=ai_crypto" | jq '.projects | length'
# Expected: >0 within a few seconds (30-min edge cache)
```

**Common failures and exact fixes:**

### Failure: `/api/libo` returns empty `projects: []`
1. **What to check:** CoinGecko / DeFiLlama rate-limited the request.
2. **Exact fix:** Set `COINGECKO_KEY` in Cloudflare → Pages → Settings → Environment variables. Set as **Encrypted**.
3. **Verify:** `curl -s "https://www.cryptolwa.io/api/libo?narrative=defi" | jq '.projects[0]'` returns a real project.

### Failure: 429 from upstream
1. **What to check:** Public CoinGecko caps you at ~30 req/min.
2. **Exact fix:** Buy a CoinGecko Pro key OR rely on the 30-min edge cache (do nothing — wait it out).

### Failure: Page renders no projects
1. **Where to click:** Browser devtools → Network → filter `libo` → inspect response.
2. **Exact fix:** If status 200 with empty array, narrative param is unknown — check the spelling matches a `narrative.id` from LEGBA output.

**Last-resort reset:** Cloudflare → Pages → Settings → Builds & deployments → click "Purge edge cache".

---

## 3. DANTON — Security / Threat Hunter (News Reader)

> **Note:** In the Lwa roster, "DANTON" is the project's *news reader / sanitised feed* agent — it aggregates and sanitises crypto RSS + Reddit feeds and serves them through a sandboxed article viewer. Threat-hunting per se is OGOU's job (§10).

**What it does (one sentence):** Aggregates and sanitises crypto RSS + Reddit feeds, exposes a sandboxed article viewer so visitors never load third-party HTML.

**Where it lives:**
- HTML page: embedded throughout `/narratives.html`, `/community.html`, `/dashboard.html`
- API endpoints:
  - `GET /api/news` — legacy alias
  - `GET /api/news-fetch` — RSS engine (`?refresh=1` forces live fetch)
  - `GET /api/article?id=<hmac-id>` — sandboxed viewer
- Source files:
  - `functions/api/news/index.ts`
  - `functions/api/news-fetch/index.ts`
  - `functions/api/article/index.ts`
- KV binding used: `DANTON_KV`
- Env vars used: `REFRESH_KEY`, `ARTICLE_HMAC_SECRET`

**Health check:**

```bash
curl -s "https://www.cryptolwa.io/api/news-fetch" | jq '.items | length'
# Expected: >0 (cached)
curl -s -H "X-Refresh-Key: $REFRESH_KEY" "https://www.cryptolwa.io/api/news-fetch?refresh=1" | jq '.items | length'
# Expected: same or higher
```

**Common failures and exact fixes:**

### Failure: `/api/article` returns 403
1. **What to check:** The `id` parameter is HMAC-signed with `ARTICLE_HMAC_SECRET`. If the env var rotated, all old links 403.
2. **Exact fix:** Either set `ARTICLE_HMAC_SECRET` back to the old value, or trigger a feed refresh so links re-sign.
3. **Verify:** `curl -i "https://www.cryptolwa.io/api/news-fetch?refresh=1" -H "X-Refresh-Key: $REFRESH_KEY"` → 200, then click an article link.

### Failure: Article viewer renders blank / strips everything
1. **What to check:** Source HTML uses scripts/inline event handlers that get sanitised.
2. **Exact fix:** This is intentional. Confirm the source URL renders correctly directly. If it does, file a parser issue.

### Failure: `kv_unbound: DANTON_KV`
1. **Where to click:** Cloudflare → Pages → Settings → Functions → KV namespace bindings.
2. **Exact fix:** Bind `DANTON_KV` to the danton_kv namespace.

### Failure: Cron not firing hourly refresh
1. **Where to click:** Cloudflare → Pages → Settings → Functions → Cron triggers.
2. **Exact fix:** Add `0 * * * *` → `GET /api/news-fetch?refresh=1` with header `X-Refresh-Key: <REFRESH_KEY>`.

**Last-resort reset:** `wrangler kv:key delete --binding DANTON_KV danton:feed:v3` then curl the refresh endpoint manually.

---

## 4. BRIGITTE — Gatekeeper / Swap Host

**What it does (one sentence):** Maman Brigitte owns the on-device Portfolio Terminal and the Swap UI; she proxies Uniswap trade quotes server-side so the API key never reaches the browser.

**Where it lives:**
- HTML pages: `/portfolio.html`, `/app.html` (alias), `/swap.html`, `/swap-app/index.html`, `/manual.html`
- API endpoint: `POST /api/brigitte/quote`
- Source file: `functions/api/brigitte/quote.ts`
- KV bindings: none (stateless proxy)
- Env vars used: `UNISWAP_TRADE_API_KEY` (encrypted), `UNISWAP_TRADE_API_BASE`, `UNIVERSAL_ROUTER_VERSION`, `BRIGITTE_ALLOWED_ORIGINS`

**Health check:**

```bash
curl -s -X POST https://www.cryptolwa.io/api/brigitte/quote \
  -H 'content-type: application/json' \
  -H 'origin: https://www.cryptolwa.io' \
  -d '{"tokenIn":"ETH","tokenOut":"USDC","amountIn":"1000000000000000000","chainId":1}' | jq .ok
# Expected: true
```

**Common failures and exact fixes:**

### Failure: Swap UI shows "Quote unavailable"
1. **Where to click:** Browser devtools → Network → filter `quote`. Look at the response body.
2. **What to check:** If body contains `"missing_secret":"UNISWAP_TRADE_API_KEY"`, the env var is unset.
3. **Exact fix:** Cloudflare → Pages → Settings → Environment variables → add `UNISWAP_TRADE_API_KEY` (encrypted). Redeploy.
4. **Verify:** Re-run health check curl.

### Failure: 403 `origin_not_allowed`
1. **What to check:** Request's `Origin` header isn't in `BRIGITTE_ALLOWED_ORIGINS`.
2. **Exact fix:** Set `BRIGITTE_ALLOWED_ORIGINS=https://www.cryptolwa.io,https://cryptolwa.io`. Redeploy.

### Failure: Portfolio terminal scrollbar overflows page
1. **What to check:** This was fixed in v7.10.5 (task #20).
2. **Exact fix:** Confirm `portfolio.html` is the latest build. Hard refresh (Ctrl+Shift+R) to bust the service worker.

### Failure: Swap mini-app at `/swap-app/` won't load
1. **What to check:** `_redirects` may have stale entry.
2. **Exact fix:** Open `_redirects`, confirm no rule shadows `/swap-app/*`.

**Last-resort reset:** Cloudflare → Pages → Settings → Builds & deployments → Purge edge cache. Hard-refresh the browser.

---

## 5. GEDE1 — Trading Rule Engine #1 (RPC watcher)

**What it does (one sentence):** Placeholder "family elder" of the GEDE helpers; surfaces a public health probe and an upstream-RPC liveness pulse to OGOU.

**Where it lives:**
- HTML page: surfaced indirectly via `/lasiren.html` health pills
- API endpoint: `GET /api/gede1/health`
- Source file: `functions/api/gede1/health.ts`
- KV bindings: none
- Env vars used: `GEDE1_RPC_URL` (optional), `OGOU_INTERNAL_SECRET`, `OGOU_BREACH_URL`

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/gede1/health | jq
# Expected: {"ok":true,"agent":"GEDE1","role":"...","ts":"...","upstream":{"configured":false}}
```

**Common failures and exact fixes:**

### Failure: 503 `upstream.ok=false`
1. **What to check:** `GEDE1_RPC_URL` points to a dead RPC.
2. **Exact fix:** Test the RPC URL with `curl -i $GEDE1_RPC_URL`. Replace with a healthy endpoint (Alchemy / Ankr / Infura) in Cloudflare → Pages → Settings → Environment variables.
3. **Verify:** Re-run health check; expect `upstream.ok:true`.

### Failure: Latency >10s warning in OGOU
1. **What to check:** RPC provider degraded.
2. **Exact fix:** Swap to a faster provider, or remove the env var entirely (GEDE1 then returns `configured:false` and stops complaining).

**Last-resort reset:** Unset `GEDE1_RPC_URL` — the probe will return ok with `configured:false`.

---

## 6. GEDE2 — Trading Rule Engine #2 (Mempool fee sampler)

**What it does (one sentence):** Samples mempool priority fees via `eth_feeHistory` on the Lasirèn bridge; this Pages endpoint is its cryptolwa.io-side liveness probe.

**Where it lives:**
- API endpoint: `GET /api/gede2/health`
- Source file: `functions/api/gede2/health.ts`
- Live execution code: `lasiren-bridge/src/gede2.js` (off-Cloudflare; on the Fly.io/Render box)
- KV bindings: none
- Env vars used: `LASIREN_BRIDGE_URL`, `OGOU_INTERNAL_SECRET`, `OGOU_BREACH_URL`

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/gede2/health | jq
# Expected: {"ok":true,"agent":"GEDE2","bridge":{"ok":true,"latency_ms":<n>}}
```

**Common failures and exact fixes:**

### Failure: `bridge.ok:false`
1. **What to check:** Lasirèn bridge is down or `LASIREN_BRIDGE_URL` wrong.
2. **Exact fix:** SSH to the bridge box, `npm start` in `lasiren-bridge/`. See §20.
3. **Verify:** `curl -s $LASIREN_BRIDGE_URL/gede2/fee | jq` from your laptop.

### Failure: Health endpoint missing `marketPriority` / `baseFeePerGas`
1. **What to check:** Bridge version drift; the cryptolwa side expects those fields.
2. **Exact fix:** On the bridge, `git pull` then `npm start`. Confirm `src/gede2.js` exports those fields.

**Last-resort reset:** Restart the bridge process (see §20).

---

## 7. GEDE3 — Trading Rule Engine #3 (Chart pattern recognizer)

**What it does (one sentence):** Detects 50 chart patterns across multiple timeframes on the Lasirèn bridge; this endpoint is its health probe.

**Where it lives:**
- API endpoint: `GET /api/gede3/health`
- Source file: `functions/api/gede3/health.ts`
- Live engine: `lasiren-bridge/src/gede3.js` (off-Cloudflare)
- Pattern catalog: `lasiren-bridge/patterns/gede3_patterns.json`
- KV bindings: none
- Env vars: `LASIREN_BRIDGE_URL`, `GEDE3_RATE_LIMIT_PER_MIN` (optional), `OGOU_*`

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/gede3/health | jq
# Expected: {"ok":true,"agent":"GEDE3","bridge":{"ok":true,"patterns_loaded":50}}
```

**Common failures and exact fixes:**

### Failure: `patterns_loaded < 50`
1. **What to check:** `lasiren-bridge/patterns/gede3_patterns.json` got truncated.
2. **Exact fix:** On bridge box: `git checkout patterns/gede3_patterns.json && npm start`.

### Failure: 429 (rate-limited inside the worker)
1. **What to check:** A misbehaving client polling >`GEDE3_RATE_LIMIT_PER_MIN`.
2. **Exact fix:** Identify abuser via Cloudflare → Analytics → Security → Top blocked IPs. Add a WAF rule.

### Failure: `bridge.ok:false`
1. **Exact fix:** Same as GEDE2 — restart the bridge (§20).

**Last-resort reset:** `node lasiren-bridge/src/gede3.js --selftest` to confirm the engine itself is sane locally.

---

## 8. GEDE4 — Trading Rule Engine #4 (Pine script confluence indicator)

**What it does (one sentence):** TradingView Pine v6 indicator that fuses momentum + volatility + volume + S/R into a single confluence signal and POSTs a webhook to Lasirèn on trigger.

**Where it lives:**
- HTML pages: `/gede4-manual.html`, `/petro-lwa.html` (download link)
- Pine script file: `GEDE4.pine` (in repo root, downloadable)
- API endpoint: none on cryptolwa.io — TradingView posts the webhook directly to the Lasirèn bridge URL the operator pastes into the alert
- KV bindings: none
- Env vars used: on the bridge: `HMAC_SECRET` (must match `LASIREN_HMAC_SECRET` on Cloudflare)

**Health check:**

```bash
# 1. Confirm the .pine file is downloadable
curl -s -o /tmp/g4.pine https://www.cryptolwa.io/GEDE4.pine && wc -l /tmp/g4.pine
# Expected: ~1100 lines

# 2. Confirm the bridge accepts a signed webhook (replace HMAC)
ts=$(date +%s); body='{"symbol":"BTCUSD","action":"buy"}'
sig=$(echo -n "$ts.POST./webhook.$body" | openssl dgst -sha256 -hmac "$HMAC_SECRET" | awk '{print $2}')
curl -X POST "$LASIREN_BRIDGE_URL/webhook" -H "x-lasiren-ts: $ts" -H "x-lasiren-sig: $sig" -d "$body"
```

**Common failures and exact fixes:**

### Failure: TradingView alert fires but Lasirèn does nothing
1. **What to check:** Bridge URL in alert message is wrong or HMAC mismatch.
2. **Exact fix:** On TradingView, edit alert → Webhook URL → confirm it ends `/webhook`. Confirm the alert body includes the HMAC header (Pine script v7.10.6 auto-fills it via `alert_message`).
3. **Verify:** Bridge log shows the request. `tail -f` the bridge logs.

### Failure: Indicator shows no signal on chart
1. **What to check:** Lookback period vs available bars.
2. **Exact fix:** Open Pine Editor → settings → reduce lookback. Pine v6 needs `max_bars_back` set; the current file sets it to 500.

### Failure: 401 Unauthorized at the bridge
1. **What to check:** HMAC secret drift between Cloudflare and bridge `.env`.
2. **Exact fix:** Set them to the same string. `LASIREN_HMAC_SECRET` on Cloudflare must equal `HMAC_SECRET` on the bridge `.env`.

**Last-resort reset:** Re-download `GEDE4.pine`, re-paste into TradingView's Pine Editor, re-add as indicator/strategy, re-create alert.

---

## 9. LASIRÈN — Autonomous Trader (off-Cloudflare Fly.io bridge)

**What it does (one sentence):** The master trading agent — runs as a Node.js service on Fly.io/Render/VPS, coordinates the GEDE helpers, holds open positions, manages risk, broadcasts signed transactions.

**Where it lives:**
- HTML page: `/lasiren.html`
- API endpoints (Cloudflare-side proxies):
  - `GET /api/lasiren/status`
  - `GET /api/lasiren/pnl`
  - `POST /api/lasiren/control` (start/stop/pause/paper-mode toggle)
- Source files (Cloudflare-side): `functions/api/lasiren/status.ts`, `pnl.ts`, `control.ts`
- Source files (bridge-side): `lasiren-bridge/src/bridge.js`, `gede2.js`, `gede3.js`, `fees.js`
- KV bindings: none on Cloudflare; the bridge has its own `data/` dir
- Env vars (Cloudflare): `LASIREN_BRIDGE_URL`, `LASIREN_HMAC_SECRET`
- Env vars (bridge `.env`): `LOCAL_PRIVATE_KEY`, `RPC_URL`, `HMAC_SECRET`, `PAPER_MODE`, `USE_FLASHBOOTS_DEFAULT`

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/lasiren/status | jq
# Expected: {"reachable":true,"configured":true,...}

curl -s https://www.cryptolwa.io/api/lasiren/pnl | jq
# Expected: {"reachable":true,"open":[...],"closed":[...],"pnl_usd":<n>}
```

**Common failures and exact fixes:**

### Failure: `/api/lasiren/status` returns `reachable:false, configured:false`
1. **Where to click:** Cloudflare → Pages → Settings → Environment variables.
2. **Exact fix:** Set `LASIREN_BRIDGE_URL` to your bridge's public URL (e.g. `https://cryptolwa-bridge.fly.dev`). Add `LASIREN_HMAC_SECRET` (same value as `HMAC_SECRET` in the bridge's `.env`).
3. **Verify:** Re-run the status curl.

### Failure: `reachable:false, error:"abort"`
1. **What to check:** Bridge is down (process crashed, host died).
2. **Exact fix:**
   ```bash
   # If Fly.io:
   flyctl status -a cryptolwa-bridge
   flyctl logs -a cryptolwa-bridge
   flyctl machine restart <id>

   # If Render: dashboard → Manual Deploy → "Clear cache & deploy".

   # If VPS:
   ssh stan@<box>
   cd lasiren-bridge && pm2 restart bridge
   ```
3. **Verify:** `curl https://<bridge>/status` returns 200 directly.

### Failure: Wallet private key compromised
1. **CRITICAL.** Immediately:
   ```bash
   # 1. Move funds from the current signer wallet to cold storage (use the bridge's /control endpoint to pause first, then move via etherscan or a separate hot wallet).
   curl -X POST https://www.cryptolwa.io/api/lasiren/control -d '{"action":"pause"}'
   # 2. SSH to the bridge box, edit .env, replace LOCAL_PRIVATE_KEY with a new key (KMS-backed in prod).
   # 3. Restart the bridge.
   # 4. Rotate HMAC_SECRET on both ends.
   ```
2. **Long-term:** Migrate to KMS / HSM (see `lasiren-bridge/README.md` § Production checklist).

### Failure: HMAC mismatch (`401` from bridge)
1. **What to check:** `LASIREN_HMAC_SECRET` (Cloudflare) ≠ `HMAC_SECRET` (bridge `.env`).
2. **Exact fix:** Set both to the same 32+ byte random string.

### Failure: `PAPER_MODE` accidentally went to `false` in production
1. **Exact fix:** SSH, set `PAPER_MODE=true` in `.env`, restart. Audit `audit.log` for unintended trades.

**Last-resort reset (full bridge):**
```bash
ssh stan@<box>
cd lasiren-bridge
pm2 stop bridge
git pull
cp .env.example .env   # then re-fill secrets
npm install
PAPER_MODE=true npm start
```

---

## 10. OGOU — Security Knowledge Base / Shield

**What it does (one sentence):** Passive security agent — verifies CSP, watches for breaches, scores withdrawal risk, generates Sigma rules, runs SOAR playbooks (two-key approval), emails weekly digests.

**Where it lives:**
- HTML page: `/admin/ogou.html` (admin-only)
- Knowledge base: `ogou-kb/OGOU-KNOWLEDGE-BASE.md`
- API endpoints:
  - `GET /api/ogou/status` — last events (redacted)
  - `GET /api/ogou/heartbeat` — public uptime ping
  - `POST /api/ogou/breach-alert` — sibling-agent intake (HMAC)
  - `GET/POST /api/ogou/weekly-report` — digest preview / send
  - `POST /api/ogou/forensics-collect` — WORM bundle
  - `POST /api/ogou/generate-rule` — IoC → Sigma
  - `POST /api/ogou/risk-score` — withdrawal: AUTO_ALLOW / REVIEW / BLOCK
  - `GET/POST /api/ogou/wallet-watcher` — wallet polling
  - `POST /api/ogou/soar/run` — submit playbook
  - `POST /api/ogou/soar/approve/:id` — second-key approval
- Source files: `functions/api/ogou/**`
- KV bindings used: `OGOU_KV`, `OGOU_EVENTS`
- Env vars used: `OGOU_INTERNAL_SECRET`, `OGOU_OPERATOR_EMAIL`, `OGOU_FROM_EMAIL`, `OGOU_SIGNING_KEY`, `OGOU_BREACH_URL`, `WALLETS_JSON`, `SIEM_WEBHOOK_URL`, `SIEM_WEBHOOK_SECRET`

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/ogou/heartbeat | jq
# Expected: {"ok":true,"uptime_pct":<n>,"last_event_ts":<unix>,...}

curl -s -H "Cookie: clwa_admin_sess=<your token>" \
  https://www.cryptolwa.io/api/ogou/status | jq '.events | length'
# Expected: integer
```

**Common failures and exact fixes:**

### Failure: Weekly digest never arrived
1. **Where to click:** Cloudflare → Pages → Settings → Functions → Cron triggers.
2. **What to check:** Cron `0 9 * * 1` → `POST /api/ogou/weekly-report` with header `X-Ogou-Cron-Secret: <OGOU_INTERNAL_SECRET>`.
3. **Exact fix:** Re-add the cron. Manually trigger once:
   ```bash
   curl -X POST https://www.cryptolwa.io/api/ogou/weekly-report \
     -H "X-Ogou-Cron-Secret: $OGOU_INTERNAL_SECRET"
   ```
4. **Verify:** Inbox at `OGOU_OPERATOR_EMAIL` receives the digest within ~2 minutes.

### Failure: breach-alert returns 401
1. **What to check:** Calling agent didn't send the right HMAC.
2. **Exact fix:** Each agent uses `functions/lib/ogou-client.ts` → confirm `OGOU_INTERNAL_SECRET` is bound on Cloudflare and the agent imports `reportToOgou`.

### Failure: SIEM webhook delivery failing
1. **Where to click:** Cloudflare → Pages → Settings → Environment variables.
2. **Exact fix:** Verify `SIEM_WEBHOOK_URL` reachable from a Worker. Test:
   ```bash
   curl -X POST $SIEM_WEBHOOK_URL -H 'content-type: application/json' -d '{"test":true}'
   ```

### Failure: SOAR playbook stuck in pending
1. **What to check:** P0 playbooks require **second-key** approval.
2. **Exact fix:** Post approval from a different admin session:
   ```bash
   curl -X POST https://www.cryptolwa.io/api/ogou/soar/approve/<id> \
     -H "Cookie: clwa_admin_sess=<second admin's token>"
   ```

### Failure: Status page shows "OGOU offline"
1. **What to check:** `OGOU_KV` / `OGOU_EVENTS` bindings missing.
2. **Exact fix:** Bind both KV namespaces in Cloudflare → Pages → Settings → Functions → KV.

**Last-resort reset:** Truncate `OGOU_EVENTS` via Wrangler:
```bash
wrangler kv:key list --binding OGOU_EVENTS | jq -r '.[].name' | xargs -I{} wrangler kv:key delete --binding OGOU_EVENTS '{}'
```
Then re-run the weekly digest manually to repopulate a baseline.

---

## 11. PETRO-LWA — Pine Strategies (I through XI)

**What it does (one sentence):** Eleven TradingView Pine v6 strategies adapted from *The Bible of Options Strategies* (Guy Cohen), plus a master Adaptive Cycle Protocol selector (XI). Not a runtime agent — users install `.pine` files on TradingView.

**Where it lives:**
- HTML pages: `/petro-lwa.html`, `/petro-lwa-manual.html`, `/petro-strategy-I.html` … `/petro-strategy-X.html`
- Pine files (downloads):
  - `PETRO-LWA-I.pine` — Long Volatility Straddle
  - `PETRO-LWA-II.pine` — OTM Strangle Ambush
  - `PETRO-LWA-III.pine` — Bull Call Spread Ladder
  - `PETRO-LWA-IV.pine` — Bear Put Backspread
  - `PETRO-LWA-V.pine` — Iron Condor Income Engine
  - `PETRO-LWA-VI.pine` — Covered Call Yield Generator
  - `PETRO-LWA-VII.pine` — Call Ratio Backspread (Rocket)
  - `PETRO-LWA-VIII.pine` — Long Put Synthetic Straddle
  - `PETRO-LWA-IX.pine` — Long Iron Butterfly
  - `PETRO-LWA-X.pine` — Collar Fortress
  - `PETRO-LWA-XI.pine` — Adaptive Cycle Protocol (master)
  - `PETRO-LWA-I-X.pine` — strategies I–X combined
- API endpoint: `/api/download/[file].ts` (gated download)
- Source: `functions/api/download/[file].ts`
- KV bindings: none (static files)
- Env vars: none directly; middleware enforces tier (PAID) on `.pine` extension

**Health check:**

```bash
# Anonymous: should hit paywall (402)
curl -i https://www.cryptolwa.io/PETRO-LWA-I.pine

# Signed-in PAID: 200 + 2KB+ body
curl -i -H "Cookie: clwa_session=<your token>" \
  https://www.cryptolwa.io/PETRO-LWA-I.pine
```

**Common failures and exact fixes:**

### Failure: TradingView "Pine cannot compile"
1. **What to check:** TradingView v6 syntax drift (rare). Usually a copy-paste artifact (smart quotes).
2. **Exact fix:** Re-download the `.pine` file directly (not via copy-paste). Paste raw into Pine Editor.

### Failure: 402 Payment Required when downloading
1. **What to check:** Visitor is anonymous or free-tier; `.pine` is PAID.
2. **Exact fix:** Buy a plan via `/billing.html` OR use operator unlock (see §18).

### Failure: 410 page on `/airdrops-learn` or similar removed route
1. **What to check:** That page was deleted (task #22).
2. **Exact fix:** Update links to point to `/azaka` instead.

### Failure: Strategy backtest looks unrealistic
1. **What to check:** Slippage + fees not modelled.
2. **Exact fix:** In Pine settings, set commission to 0.1% and slippage to 0.1% (per `lasiren-bridge/README.md`).

**Last-resort reset:** Delete strategy from TradingView → re-download fresh `.pine` → re-add.

---

## 12. SAMAEL — Social / Comms Drafter

**What it does (one sentence):** Drafts social posts (Twitter, LinkedIn, email) from internal events, runs through a two-key approval gate before publishing.

**Where it lives:**
- API endpoints:
  - `GET /api/samael/list` — admin-only drafts/approvals/stats
  - `POST /api/samael/generate-post` — generate from an event
  - `POST /api/samael/approve` — second-key approve
  - `POST /api/samael/weekly-report` — weekly comms digest
- Source files: `functions/api/samael/*.ts`
- Knowledge base: `samael-kb/SAMAEL-COPY-LIBRARY.md`, `SAMAEL-STRATEGY.md`, `SAMAEL-TOS-GUARDRAILS.md`
- KV bindings: `CLWA_CAMPAIGNS`
- Env vars: `SAMAEL_HMAC_SECRET`, `ANTHROPIC_API_KEY`

**Health check:**

```bash
curl -s -H "Cookie: clwa_admin_sess=<token>" \
  https://www.cryptolwa.io/api/samael/list | jq '.drafts | length'
# Expected: integer
```

**Common failures and exact fixes:**

### Failure: 404 `unauthorized` (note: SAMAEL returns 404 not 401 to avoid enumeration)
1. **What to check:** No admin session cookie.
2. **Exact fix:** Sign in at `/admin/login.html`.

### Failure: Drafts present but `verified:false`
1. **What to check:** `SAMAEL_HMAC_SECRET` rotated since the draft was signed.
2. **Exact fix:** Either revert the secret OR regenerate the draft via `POST /api/samael/generate-post`.

### Failure: `generate-post` returns 500
1. **What to check:** `ANTHROPIC_API_KEY` invalid or rate-limited.
2. **Exact fix:** Verify key works against Anthropic directly:
   ```bash
   curl -s https://api.anthropic.com/v1/models \
     -H "x-api-key: $ANTHROPIC_API_KEY" -H "anthropic-version: 2023-06-01"
   ```

### Failure: Weekly comms report not sent
1. **What to check:** No cron configured for `POST /api/samael/weekly-report`.
2. **Exact fix:** Add cron `0 14 * * 5` (Fri 14:00 UTC) → call the endpoint.

**Last-resort reset:** Drain stale drafts: `wrangler kv:key list --binding CLWA_CAMPAIGNS --prefix draft: | jq -r '.[].name' | xargs -I{} wrangler kv:key delete --binding CLWA_CAMPAIGNS '{}'`.

---

## 13. AZAKA — Airdrop Hunter

**What it does (one sentence):** Multi-source scraper aggregating Airdrops.io, AirDropAlert, AirdropsMob, FreeAirdrop.io, AlphaDrops, and Bankless Claimables into a single ranked feed with synthesised claim steps.

**Where it lives:**
- HTML page: `/azaka.html`
- API endpoints:
  - `GET /api/azaka` — cached feed read (with `?chain`, `?limit`, `?refresh`)
  - `POST /api/azaka/refresh` — force scrape (admin / cron)
- Source files:
  - `functions/api/azaka/index.ts`
  - `functions/api/azaka/refresh.ts`
  - `functions/api/azaka/_lib/scraper.ts`
- KV bindings: `AZAKA_KV` (key `azaka:feed:latest`, 24h stale window)
- Env vars: `AZAKA_SCRAPER_KEY` (admin token for `?refresh=1` direct)

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/azaka | jq '.items | length'
# Expected: 10–60 items
```

**Common failures and exact fixes:**

### Failure: Hunt button on `/azaka` does nothing
1. **Where to click:** Browser devtools → Network → inspect the request when you click Hunt.
2. **What to check:** Endpoint should be `GET /api/azaka?refresh=1`. If it's 404 or 405, button JS is calling the wrong URL.
3. **Exact fix:** Confirm `azaka.html` has the v7.10.6 button handler (fetches `/api/azaka?refresh=1` with credentials:'include'). If older, redeploy.
4. **Verify:** Click Hunt → Network panel shows 200 → page renders items.

### Failure: `sources["airdrops.io"].error: "robots_blocked"`
1. **What to check:** Source's robots.txt now disallows our UA.
2. **Exact fix:** Either:
   - Switch the source off (set `enabled:false` in `scraper.ts` source list and redeploy), OR
   - Negotiate access (`security@<source>` / TOS contact).
3. **Verify:** `curl -s https://www.cryptolwa.io/api/azaka | jq '.sources'` — that source should show `enabled:false` or fresh items.

### Failure: KV cache stuck on stale items (>24h)
1. **What to check:** Refresh cron not firing.
2. **Exact fix:**
   ```bash
   # Manual purge
   wrangler kv:key delete --binding AZAKA_KV azaka:feed:latest
   # Force refresh
   curl -X POST https://www.cryptolwa.io/api/azaka/refresh \
     -H "X-Scraper-Key: $AZAKA_SCRAPER_KEY"
   ```

### Failure: A source structurally changed (parser returns 0 items)
1. **What to check:** `_lib/scraper.ts` has `parseAirdropsIo()`, `parseAirdropAlert()` etc. Each parser is HTML-fragile.
2. **Exact fix:** Open the source's HTML in a browser, copy the now-current markup pattern, update the matching `extractBetween()` markers in `scraper.ts`.

### Failure: 403 from source (anti-bot)
1. **Exact fix:** Rotate UA in `scraper.ts` constant `UA`. Add 2–5s sleep between requests.

**Last-resort reset:** Disable the broken source in `scraper.ts`, redeploy, and accept the diminished feed until the parser is patched.

---

# PART 2 — INFRASTRUCTURE

---

## 14. Cloudflare Pages (Build · Deploys · Env vars · KV bindings)

**What it does (one sentence):** Hosts every static page and every `/functions/**` endpoint at the edge for ~$0/month.

**Where it lives:**
- Dashboard URL: https://dash.cloudflare.com → Workers & Pages → cryptolwa
- Build config: none (static site, no build step)
- Output dir: `/` (repo root)

**Health check:**

```bash
curl -s -o /dev/null -w '%{http_code} %{time_total}s\n' https://www.cryptolwa.io/
# Expected: 200 within ~0.3s
```

### Where every setting lives

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 240" font-family="ui-monospace, monospace" font-size="12">
  <rect width="720" height="240" fill="#fff"/>
  <rect x="10" y="20" width="220" height="200" fill="#fafafa" stroke="#333"/>
  <text x="20" y="40">Pages → cryptolwa</text>
  <text x="20" y="65">Settings</text>
  <text x="30" y="85" fill="#c89b1a">▸ Environment variables</text>
  <text x="40" y="105" fill="#666">  Production / Preview</text>
  <text x="30" y="125" fill="#c89b1a">▸ Functions</text>
  <text x="40" y="145" fill="#666">  ▸ KV namespace bindings</text>
  <text x="40" y="165" fill="#666">  ▸ Cron triggers</text>
  <text x="40" y="185" fill="#666">  ▸ Compatibility flags</text>
  <text x="30" y="205">▸ Custom domains</text>
  <rect x="260" y="20" width="220" height="200" fill="#fafafa" stroke="#333"/>
  <text x="270" y="40">Deployments</text>
  <text x="270" y="65" fill="#666">Production (latest)</text>
  <text x="270" y="85" fill="#666">Production (prev)</text>
  <text x="270" y="105" fill="#666">Preview branches</text>
  <text x="270" y="135">Per-deploy menu (⋯):</text>
  <text x="280" y="155" fill="#c89b1a">  Rollback</text>
  <text x="280" y="175" fill="#666">  View build log</text>
  <text x="280" y="195" fill="#666">  Promote to production</text>
  <rect x="510" y="20" width="200" height="200" fill="#fafafa" stroke="#333"/>
  <text x="520" y="40">Account-wide</text>
  <text x="520" y="65" fill="#666">Workers &amp; Pages</text>
  <text x="520" y="85" fill="#666">▸ KV (manage namespaces)</text>
  <text x="520" y="105" fill="#666">▸ Logs / Tail</text>
  <text x="520" y="125" fill="#666">▸ Analytics</text>
</svg>

**Common failures and exact fixes:**

### Failure: Deploy succeeds but site shows old version
1. See §22 ("I deployed but nothing changed") for the full cache-busting walk.

### Failure: Build log shows `[ERROR] Couldn't resolve module`
1. **What to check:** TypeScript import path wrong (Pages bundles via esbuild).
2. **Exact fix:** Re-read the path in the import line. Most issues are relative-path drift after moving files.

### Failure: "Pages Functions exceed 1MB"
1. **What to check:** A dependency got bundled (heavy npm package).
2. **Exact fix:** Pages Functions should not import Node modules. Audit imports in `functions/**/*.ts` — only browser-safe + `crypto.subtle` allowed.

### Failure: Env var change didn't take effect
1. **Where to click:** Cloudflare → Pages → cryptolwa → Settings → Environment variables → confirm the value → save → **Deployments → ⋯ → Retry deployment** (env vars don't auto-redeploy).
2. **Verify:** `curl -s https://www.cryptolwa.io/api/health/account | jq '.<thing you changed>'`.

**Last-resort reset:** Delete the project → recreate from the same repo. KV bindings and env vars must be re-bound. Custom domains transfer automatically because they're at the zone level.

---

## 15. KV Namespaces

The site uses **two parallel KV vocabularies** because the auth chain was renamed in v8.x. Both must coexist until everything is migrated. The newer "CLWA_" names own the account chain; the older bindings own agent state.

| Binding | Vocabulary | Stores |
|---|---|---|
| `CLWA_MEMBERS` | new (v8.x) | `member:<email>`, `magic:<sig-prefix>`, rate-limit counters |
| `CLWA_ADMINS` | new (v8.x) | `admin:<email>`, `_meta:any_admin_exists` |
| `CLWA_SESSIONS` | new (v8.x) | JWT `jti` revocation list, magic-link tokens |
| `CLWA_PAYMENTS` | new (v8.x) | Stripe payment audit trail |
| `AZAKA_KV` | agent | `azaka:feed:latest` (24h TTL) |
| `OGOU_KV` | agent | Rate-limit counters, generic OGOU state |
| `OGOU_EVENTS` | agent | Append-only event log (breach alerts, forensics manifests) |
| `DANTON_KV` | agent | `danton:feed:v3` RSS cache, sanitised HTML by article id |
| `HEATMAP_KV` | agent | `legba:heatmap` |
| `LWA_REPORTS` | agent | Operator report archive (admin viewer) |
| `CLWA_CAMPAIGNS` | agent | `draft:<id>` SAMAEL drafts |
| `MEMBERS_KV` | legacy | Older member records (pre-CLWA_MEMBERS) |
| `SCANNER_KV` | infra | Bot/scanner fingerprints |

**Health check:**

```bash
# What's bound
curl -s https://www.cryptolwa.io/api/health/account | jq '.missingKvBindings'
# Expected: []
```

### Inspect a key

```bash
# List keys with prefix
wrangler kv:key list --binding CLWA_MEMBERS --prefix "member:" | jq

# Get a single value
wrangler kv:key get --binding CLWA_MEMBERS "member:stangebara@protonmail.com"
```

### Delete a stuck key

```bash
wrangler kv:key delete --binding CLWA_MEMBERS "magic:abcd1234"
```

### Bulk delete (e.g. flush all magic tokens)

```bash
wrangler kv:key list --binding CLWA_SESSIONS --prefix "magic:" \
  | jq -r '.[].name' \
  | xargs -I{} wrangler kv:key delete --binding CLWA_SESSIONS '{}'
```

**Common failures and exact fixes:**

### Failure: Endpoint returns `kv_unbound`
1. **Where to click:** Cloudflare → Pages → Settings → Functions → KV namespace bindings.
2. **Exact fix:** Add the binding name listed in the `detail` field. Create the namespace first if it doesn't exist (Workers & Pages → KV → Create namespace).

### Failure: Key exists but endpoint says missing
1. **What to check:** Eventual consistency. KV propagation can take up to 60s globally.
2. **Exact fix:** Wait 60s and retry. If still missing, you probably wrote to the wrong namespace ID — verify with `wrangler kv:key list`.

**Last-resort reset:** Don't wipe a KV namespace from the dashboard — exports first.
```bash
wrangler kv:key list --binding CLWA_MEMBERS > /tmp/backup-$(date +%F).json
```

---

## 16. Auth chain (signup → login → magic-link → session cookie)

**What it does (one sentence):** Turns an email into a signed, HttpOnly `clwa_session` JWT cookie via magic-link email, with optional Stripe checkout detour for paid plans.

**Where it lives:**
- HTML pages: `/signup.html`, `/login.html`
- API endpoints:
  - `POST /api/auth/signup`
  - `POST /api/auth/login` (dispatcher: magic-send or password-login by body shape)
  - `GET /api/auth/verify?token=<token>`
  - `GET /api/auth/me`
  - `POST /api/auth/logout`
- Source files: `functions/api/auth/{signup,login,verify,me,logout}.ts`, `functions/lib/{auth,members,email}.ts`
- KV bindings used: `CLWA_MEMBERS` (required), `CLWA_SESSIONS` (recommended)
- Env vars used: `MEMBER_JWT_SECRET` (required), `MEMBER_AUTH_PEPPER` (password mode only), `SITE_URL`, `OGOU_FROM_EMAIL`, `MAILCHANNELS_KEY` (paid tier only)

```
┌─────────┐    ┌────────────┐    ┌──────────┐    ┌─────────────┐    ┌─────────────┐
│ /signup │ →  │ POST       │ →  │ KV write │ →  │ MailChannels│ →  │ email link  │
│         │    │ /api/auth/ │    │ member:* │    │ send        │    │ → /verify   │
└─────────┘    │  signup    │    │ magic:*  │    └─────────────┘    └──────┬──────┘
               └────────────┘                                              ↓
                                                                   ┌──────────────┐
                                                                   │ GET /verify  │
                                                                   │ → mint JWT   │
                                                                   │ → Set-Cookie │
                                                                   └──────────────┘
```

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/health/account | jq .
# Expected: {"ok":true,"missingEnvVars":[],"missingKvBindings":[],...}
```

**Common failures and exact fixes:**

### Failure: `POST /api/auth/login` returns 405 (the v7.10.3 bug)
1. **What to check:** This was fixed in v7.10.5. If you see it again on a current build, you're hitting a route shadow.
2. **Exact fix:** Confirm no directory `functions/api/auth/login/` exists alongside `functions/api/auth/login.ts`. Redeploy.
3. **Verify:** `curl -i -X POST https://www.cryptolwa.io/api/auth/login -d '{"email":"x@y.com"}' -H 'content-type:application/json'` returns 200 with `{"ok":true,"sent":true,"expires_in":900}` even for a non-member (anti-enumeration).

### Failure: Signup returns `missing_secret` (`detail:"MEMBER_JWT_SECRET"`)
1. **Where to click:** Cloudflare → Pages → Settings → Environment variables.
2. **Exact fix:** Add `MEMBER_JWT_SECRET` (encrypted, 32+ random bytes). Generate with `openssl rand -hex 32`.

### Failure: Signup returns `kv_unbound` (`detail:"CLWA_MEMBERS"`)
1. **Where to click:** Cloudflare → Pages → Settings → Functions → KV namespace bindings.
2. **Exact fix:** Bind `CLWA_MEMBERS`.

### Failure: Email never arrives
1. See §19 (MailChannels SPF/DKIM).
2. **Quick check:** Curl signup with your real email. Response should be `ok:true, mode:"magic"`. If response includes `warning:"email_dispatch_failed"`, MailChannels rejected the request.

### Failure: 429 `rate_limited`
1. **What to check:** >5 attempts/10min per IP or per email.
2. **Exact fix:** Wait, or flush the limit key:
   ```bash
   wrangler kv:key list --binding CLWA_MEMBERS --prefix "rl:" \
     | jq -r '.[].name' | xargs -I{} wrangler kv:key delete --binding CLWA_MEMBERS '{}'
   ```

### Failure: Login link expired / token invalid
1. **What to check:** Magic tokens TTL = 15 minutes. After that they're 401.
2. **Exact fix:** User requests a fresh link from `/login`.

**Last-resort reset:** Wipe the user's KV record and have them sign up again:
```bash
wrangler kv:key delete --binding CLWA_MEMBERS "member:test@example.com"
```

---

## 17. Stripe (Checkout · Webhook · Customer Portal)

**What it does (one sentence):** Sells paid plans, receives payment confirmations, lets members self-manage subscriptions.

**Where it lives:**
- HTML page: `/billing.html`
- API endpoints:
  - `POST /api/billing/checkout` — start a Checkout Session
  - `POST /api/billing/portal` — open Customer Portal
  - `GET /api/billing/me` — current subscription state
  - `POST /api/stripe-webhook` — Stripe → CLWA event receiver
- Source files: `functions/api/billing/{checkout,portal,me}.ts`, `functions/api/stripe-webhook.ts`
- KV bindings: `CLWA_MEMBERS`, `CLWA_PAYMENTS` (optional audit trail)
- Env vars: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_MONTHLY`, `STRIPE_PRICE_ANNUAL`, `STRIPE_PRICE_LIFETIME`, `SITE_URL`

**Health check:**

```bash
# Checkout URL ready
curl -s -X POST https://www.cryptolwa.io/api/billing/checkout \
  -H 'content-type: application/json' \
  -d '{"email":"test@example.com","plan":"paid_monthly"}' | jq .

# Expected: {"ok":true,"url":"https://checkout.stripe.com/..."}
```

**Common failures and exact fixes:**

### Failure: `missing_price_id` (`detail:"STRIPE_PRICE_MONTHLY"`)
1. **Where to click:** Stripe Dashboard → Products → find the product → copy the `price_…` id.
2. **Exact fix:** Cloudflare → Pages → Settings → Environment variables → set `STRIPE_PRICE_MONTHLY` (and ANNUAL, LIFETIME).
3. **Verify:** Re-run checkout curl.

### Failure: Webhook signature mismatch
1. **Where to click:** Stripe → Developers → Webhooks → click the endpoint → reveal **Signing secret** (`whsec_…`).
2. **Exact fix:** Set `STRIPE_WEBHOOK_SECRET` on Cloudflare to that value. Redeploy.
3. **Verify:** Stripe → Webhooks → Send test event → status should be 200.

### Failure: User paid but `plan` still `free`
1. **What to check:** Webhook hit `/api/stripe-webhook` and 200'd, but didn't update KV.
2. **Exact fix:** Verify with:
   ```bash
   wrangler kv:key get --binding CLWA_MEMBERS "member:<their email>" | jq
   ```
   If `plan:"free", status:"pending"`, the webhook didn't fire. Send a manual test event from Stripe. Check Cloudflare Pages → Logs → Tail for the request.
3. If webhook IS firing but failing inside: Cloudflare → Pages → Logs → Tail → look for the exception. Most common: `CLWA_MEMBERS` unbound (env shadowing on the webhook deploy).

### Failure: Idempotency key collision
1. **What to check:** Stripe → Logs → look for `409 idempotency_key_in_use`.
2. **Exact fix:** Our `checkout.ts` derives the idempotency key from email+plan+minute. If two browsers click within the same minute, the second call returns the same Session — that's correct. If you see a true collision (different intent), wait one minute and retry.

### Failure: Customer Portal won't open
1. **What to check:** `Customer Portal` configuration in Stripe is incomplete.
2. **Exact fix:** Stripe → Settings → Billing → Customer portal → toggle every product on, save.

**Last-resort reset:** Manually mutate the member record:
```bash
wrangler kv:key put --binding CLWA_MEMBERS "member:<email>" '{"email":"<email>","plan":"paid_monthly","status":"active","expires_at":"2027-05-19T00:00:00Z"}'
```

---

## 18. Access control / middleware (tiers + operator unlocks)

**What it does (one sentence):** Every request goes through `functions/_middleware.ts`, which classifies the route (PUBLIC / FREE / PAID) and the visitor (anonymous / free / paid / lifetime) and either allows, paywall-overlays, or redirects.

**Where it lives:**
- Source: `functions/_middleware.ts`, `functions/lib/membership.ts`, `functions/lib/require-tier.ts`, `functions/_lib/paywall-overlay.ts`
- Env vars: `MEMBER_JWT_SECRET`, `BYPASS_TOKEN`, `REDIRECT_SIGNING_SECRET`, `ALLOWED_HOSTS`, **operator unlocks** ↓

### The three operator unlock options (from `ACCOUNT-SETUP.md` §9)

| Option | Env var(s) | Effect | Who it unlocks |
|---|---|---|---|
| **A. Kill switch** | `SITE_FULLY_OPEN=true` | Every route collapses to PUBLIC; every visitor pinned to LIFETIME. | Everyone. Emergency/demo only. |
| **B. Operator email** | `OPERATOR_EMAIL=stangebara@protonmail.com` | Cookie's `sub` is checked against list; matches get LIFETIME. | You (once signed in). |
| **C. Pin token** | `OPERATOR_PIN_TOKEN=<rand>` + `MEMBER_JWT_SECRET=<rand>` | Visit any page with `?op=<token>` → drops a year-long HMAC'd `clwa_operator` cookie pinning the browser to LIFETIME. | Whoever holds the token. |

Stack B + C for full coverage. Leave A unset unless emergency.

**Health check:**

```bash
# Should be "lifetime" once an unlock is active
curl -si https://www.cryptolwa.io/portfolio | grep -i x-clwa-tier
```

**Common failures and exact fixes:**

### Failure: Pages show paywall to operator
1. **What to check:** None of A/B/C is wired.
2. **Exact fix:** Pick one:
   - **Fastest:** Set `SITE_FULLY_OPEN=true` (Option A).
   - **You-only, no signup:** Set `OPERATOR_PIN_TOKEN=$(openssl rand -hex 32)` + `MEMBER_JWT_SECRET=$(openssl rand -hex 32)` → visit `https://www.cryptolwa.io/?op=<TOKEN>` once.
   - **You-only, after signup:** Set `OPERATOR_EMAIL=stangebara@protonmail.com` then sign up at `/signup.html` with that email.
3. **Verify:** Header `x-clwa-tier: lifetime`.

### Failure: Cookie persisted but tier still "anonymous"
1. **What to check:** `MEMBER_JWT_SECRET` rotated after the cookie was issued.
2. **Exact fix:** Visit `?op=<TOKEN>` again to mint a fresh cookie with the new secret.

### Failure: Other visitors can also unlock with `?op=...`
1. **What to check:** That's by design — anyone with the token can self-unlock.
2. **Exact fix:** Rotate the token. Set a new `OPERATOR_PIN_TOKEN`. Anyone with the old cookie keeps their access until the cookie expires (1 year). To revoke immediately, also rotate `MEMBER_JWT_SECRET`.

### Failure: API endpoint returns 402 for a paying member
1. **What to check:** KV `member:<email>` has `plan:"paid_monthly"` but `status:"past_due"`.
2. **Exact fix:** Stripe → find the customer → resolve the payment. Webhook flips status back to `active`. Force manually:
   ```bash
   wrangler kv:key put --binding CLWA_MEMBERS "member:<email>" '{...,"status":"active"}'
   ```

**Last-resort reset:** `SITE_FULLY_OPEN=true` while you investigate. Unset once the gate is fixed.

---

## 19. MailChannels (SPF · DKIM · Bounces)

**What it does (one sentence):** Free email relay for Cloudflare Workers/Pages; ships magic links, OGOU digests, SAMAEL drafts.

**Where it lives:**
- Source: `functions/lib/email.ts`
- Env vars: `OGOU_FROM_EMAIL` (default `ogou@cryptolwa.io`), `MAILCHANNELS_KEY` (paid tier only)
- DNS: at the domain registrar / Cloudflare DNS panel

### Required DNS records (cryptolwa.io)

```
@                          TXT  "v=spf1 include:relay.mailchannels.net ~all"
mailchannels._domainkey    TXT  <DKIM record>
_mailchannels              TXT  "v=mc1 cfid=<your-cf-account-id>"
```

**Health check:**

```bash
# 1. Confirm DNS
dig +short TXT cryptolwa.io | grep mailchannels
dig +short TXT _mailchannels.cryptolwa.io

# 2. Send via signup (uses MailChannels under the hood)
curl -X POST https://www.cryptolwa.io/api/auth/signup \
  -H 'content-type: application/json' \
  -d '{"email":"<your inbox>","plan":"free"}' | jq .
# Expected: ok:true, mode:"magic" (no warning)
```

**Common failures and exact fixes:**

### Failure: Signup returns `warning:"email_dispatch_failed"`
1. **What to check:** SPF or `_mailchannels` record missing.
2. **Where to click:** Cloudflare → DNS → Records → Add record.
3. **Exact fix:** Add the three TXT records above. SPF rejection is the #1 cause.
4. **Verify:** Re-curl signup; `warning` should be gone.

### Failure: Emails arrive in spam
1. **What to check:** DKIM missing or `_mailchannels` cfid wrong.
2. **Exact fix:** Generate a DKIM key per MailChannels docs (https://support.mailchannels.com/hc/en-us/articles/16918954360845). Add as `mailchannels._domainkey` TXT.

### Failure: 401 from MailChannels
1. **What to check:** You added `MAILCHANNELS_KEY` for paid tier but value is invalid.
2. **Exact fix:** **Unset `MAILCHANNELS_KEY`** unless you've paid for it — the free Cloudflare path doesn't need it.

### Failure: From-address mismatch
1. **What to check:** `OGOU_FROM_EMAIL` not on `cryptolwa.io`.
2. **Exact fix:** Set to `ogou@cryptolwa.io` (or any address on a domain you control with SPF/DKIM).

**Last-resort reset:** Disable email entirely: set every endpoint to log-only by removing `sendEmail` calls. Members can still complete signup; magic link arrives via the `/api/auth/verify?token=...` URL in server logs (Cloudflare → Pages → Logs → Tail).

---

## 20. Lasirèn Fly.io bridge (private key · env · restart)

**What it does (one sentence):** Runs the trading agent off-Cloudflare; signs transactions; never delegates its private key.

**Where it lives:**
- Repo: `lasiren-bridge/` (on the cryptolwa-allinone build, kept in Git as documentation)
- Deploy target: Fly.io / Render / your own VPS
- Files: `src/bridge.js`, `src/gede2.js`, `src/gede3.js`, `src/fees.js`, `patterns/gede3_patterns.json`, `package.json`, `.env`
- Endpoints exposed: `GET /status`, `POST /analyze`, `POST /signal`, `POST /webhook`, `GET /audit`
- Env vars (bridge `.env`): `LOCAL_PRIVATE_KEY`, `RPC_URL`, `HMAC_SECRET`, `PAPER_MODE`, `USE_FLASHBOOTS_DEFAULT`

```
┌──────────────────┐  HMAC      ┌─────────────────┐
│ cryptolwa.io     │ ───────►   │ Lasirèn bridge  │
│ /api/lasiren/*   │            │ (Fly.io VM)     │
└──────────────────┘  ◄───────  └────────┬────────┘
                       JSON              │  signs
                                         ▼
                                  ┌─────────────┐
                                  │ Ethereum    │
                                  │ RPC (mainnet│
                                  └─────────────┘
```

**Health check:**

```bash
# From the cryptolwa side
curl -s https://www.cryptolwa.io/api/lasiren/status | jq

# Directly to the bridge (substitute URL)
curl -s https://cryptolwa-bridge.fly.dev/status | jq
```

**Common failures and exact fixes:**

### Failure: Bridge unreachable
1. **Where to click:** `flyctl status -a cryptolwa-bridge` (or Render dashboard, or `pm2 status` on VPS).
2. **Exact fix:**
   ```bash
   flyctl machine restart <id> -a cryptolwa-bridge
   flyctl logs -a cryptolwa-bridge
   ```
3. **Verify:** Direct curl to `/status` returns 200.

### Failure: "wallet has 0 balance, can't trade"
1. **What to check:** The signer wallet's ETH balance.
2. **Exact fix:** Fund the signer wallet (its address is in `/status` output as `signerPubkey`). Send ETH for gas.

### Failure: Trades not going through (paper mode)
1. **What to check:** `PAPER_MODE=true` in bridge `.env`.
2. **Exact fix:** Decision time. To go live, audit risk caps + key management FIRST (see `lasiren-bridge/README.md` § Production checklist). Then:
   ```bash
   # On the bridge box
   nano .env   # set PAPER_MODE=false
   pm2 restart bridge   # or flyctl deploy
   ```

### Failure: Private key leak suspected
1. **CRITICAL.** Pause first, rotate second:
   ```bash
   curl -X POST https://www.cryptolwa.io/api/lasiren/control \
     -H 'content-type: application/json' -d '{"action":"pause"}'
   # Then SSH, rotate key, restart.
   ```
2. **Long-term:** Migrate to KMS / HSM.

### Failure: Audit log unreadable
1. **Exact fix:** Snapshot `data/audit.log` daily (see §README "Backups" §4).

**Restart procedure (canonical):**

```bash
# Fly.io
flyctl machine restart $(flyctl machine list -a cryptolwa-bridge -j | jq -r '.[0].id') -a cryptolwa-bridge

# Render
# Dashboard → Service → Manual Deploy → Clear cache & deploy

# VPS (pm2)
ssh stan@<box>
cd lasiren-bridge
git pull
npm install
pm2 restart bridge
pm2 logs bridge --lines 50
```

**Last-resort reset:** Re-deploy from a fresh checkout:
```bash
ssh stan@<box>
cd lasiren-bridge
pm2 stop bridge
rm -rf node_modules
git fetch && git reset --hard origin/main
cp .env.backup .env       # restore env you saved separately
npm install
PAPER_MODE=true pm2 start src/bridge.js --name bridge
```

---

## 21. AZAKA scraper (robots.txt · source down · KV cache refresh)

**What it does (one sentence):** Multi-source HTML scraper (six airdrop sites) with robots.txt enforcement, parser-per-source, dedupe, and a 24-hour KV cache.

**Where it lives:**
- Source: `functions/api/azaka/_lib/scraper.ts`, `functions/api/azaka/index.ts`, `functions/api/azaka/refresh.ts`
- KV: `AZAKA_KV` key `azaka:feed:latest`
- Env: `AZAKA_SCRAPER_KEY`
- User-Agent: `Mozilla/5.0 (compatible; CryptoLwa-AZAKA/1.0; +https://www.cryptolwa.io/azaka)`

**Health check:**

```bash
curl -s https://www.cryptolwa.io/api/azaka | jq '.sources | map_values(.fetched_at)'
```

**Common failures and exact fixes:**

### Failure: robots.txt blocked us
1. **What to check:** The source response shows `error:"robots_blocked"`.
2. **Exact fix:** Either turn the source off (edit `_lib/scraper.ts`, set `enabled:false` for that source's entry), or contact the source for permission.
3. **Verify:** `curl -s https://www.cryptolwa.io/api/azaka | jq '.sources["airdrops.io"]'`.

### Failure: Source HTML changed (parser returns 0 items)
1. **What to check:** Each parser is HTML-fragile.
2. **Exact fix:** Open the source in a browser → View Source → identify the new markup pattern → update the relevant `parse*()` function in `_lib/scraper.ts`. Redeploy.

### Failure: KV cache stuck (>24h)
1. **What to check:** No cron + no manual refresh.
2. **Exact fix:**
   ```bash
   curl -X POST https://www.cryptolwa.io/api/azaka/refresh \
     -H "X-Scraper-Key: $AZAKA_SCRAPER_KEY"
   ```
3. **Verify:** `curl -s https://www.cryptolwa.io/api/azaka | jq '.updated_at'`.

### Failure: AZAKA_KV unbound
1. **Where to click:** Cloudflare → Pages → Settings → Functions → KV namespace bindings.
2. **Exact fix:** Bind `AZAKA_KV` to a namespace (create one named `azaka_kv` if needed).

### Failure: Hunt button does nothing
1. See §13 ("AZAKA — Airdrop Hunter") for the exact JS handler check.

**Last-resort reset:** Delete the cache key entirely:
```bash
wrangler kv:key delete --binding AZAKA_KV azaka:feed:latest
curl -X POST https://www.cryptolwa.io/api/azaka/refresh -H "X-Scraper-Key: $AZAKA_SCRAPER_KEY"
```

---

# PART 3 — COMMON CROSS-CUTTING

---

## 22. "I deployed but nothing changed"

The site sits behind three caches. They miss-fire in this order:

```
[ Browser cache ] → [ Service Worker ] → [ Cloudflare edge cache ]
        ↑                  ↑                       ↑
   Ctrl+Shift+R       /sw.js + portfolio-sw.js  Settings → Builds → Purge
```

**Step-by-step:**

1. **Confirm the deploy actually shipped.** Cloudflare → Pages → cryptolwa → Deployments → top row should show your commit hash or "Direct upload" with the time you uploaded. If the timestamp is older, the upload failed.

2. **Hard-refresh the browser.** Chrome/Firefox: `Ctrl + Shift + R` (Mac: `Cmd + Shift + R`).

3. **Unregister the service worker.** Chrome devtools → Application → Service Workers → check "Bypass for network" + click "Unregister". Reload.

4. **Purge the edge cache.** Cloudflare → Pages → cryptolwa → Settings → Builds & deployments → "Purge edge cache" (or zone-wide: Cloudflare → Caching → Configuration → Purge Everything).

5. **Check `_headers` isn't sending year-long `max-age`.** Open `_headers`. The HTML files should have `Cache-Control: public, max-age=60, must-revalidate` or shorter. Pine + PDF can be longer.

6. **Confirm `terminal-skin.js` version.** View source on any page → search `v7.10.6`. If you see an older string, JS is stale.

7. **Force-bust the service worker.** Bump the `CACHE_NAME` constant in `sw.js` and `portfolio-sw.js`. Old service worker uninstalls itself on the next visit.

**Verify:**

```bash
curl -s -I https://www.cryptolwa.io/index.html | grep -i etag
# Compare to local file content hash
```

---

## 23. "Console shows 405 / 401 / 402 / 403 / 500"

| Status | Meaning on this site | First-look fix |
|---|---|---|
| **401** | Missing/expired session cookie OR wrong admin password. | Sign in at `/login.html`. If admin, `/admin/login.html`. |
| **402** | Paid tier required for this route or `.pine`/`.pdf` file. | Buy a plan via `/billing.html` OR use an operator unlock (§18). |
| **403** | Origin mismatch (CORS), bad host header, scanner-pattern path. | Confirm `Origin` is `https://www.cryptolwa.io`. Check `BRIGITTE_ALLOWED_ORIGINS` if it's a Brigitte call. |
| **404** | Path doesn't exist, OR a removed route (`/airdrops-learn` → 410). | Check `_redirects` for the canonical path. |
| **405** | Wrong HTTP method. The site's middleware accepts `GET,HEAD,POST,OPTIONS` only. | Confirm method. The v7.10.3 dual-export bug is fixed — if you see 405 on `POST /api/auth/login`, see §16. |
| **410** | Intentionally retired path (scanner traps, removed pages). | This is correct behaviour. Update internal links. |
| **429** | Rate-limited. >5 attempts/10min/IP on auth; per-source limits on AZAKA + Brigitte. | Wait, OR flush `rl:*` KV keys (§16). |
| **500** | Unhandled exception. JSON body should now always be present with `error` + `detail`. | Read the JSON. The exact env var or KV binding to fix is in `detail`. |
| **502** | Upstream failure (Stripe / RPC / source site). | See the agent's section. |
| **503** | KV bound but read/write failed (rare); or `gede1/health` says upstream sick. | Retry once, then check Cloudflare status page. |

**Quick triage:**

```bash
curl -si <url> | head -20
# Look at status line + first JSON field
```

---

## 24. "JSON.parse: unexpected end of data"

**The pattern:** Browser fetch tries to `.json()` a 500 with an empty body and dies.

**Root cause on this site:** A Pages Function threw before reaching the response, OR an env var/KV binding was missing and an older handler returned empty.

**Fix (already applied to all account-chain endpoints, but use this elsewhere too):**

1. **Surface the real error.** Hit the endpoint with `curl -v` and read both the status line and the body.
2. **If body is empty:** the throw was unhandled. The signup/login/admin/billing endpoints are wrapped in a top-level try/catch as of v7.10.5 — if you see an empty body on those, you're on an old build. Redeploy.
3. **For agent endpoints:** wrap the handler body in:
   ```ts
   try {
     // ... work ...
   } catch (e) {
     return new Response(JSON.stringify({
       ok: false,
       error: "internal_error",
       detail: e instanceof Error ? e.message : String(e),
     }), { status: 500, headers: { "content-type": "application/json" } });
   }
   ```
4. **Then re-curl** to see what was actually broken.

**Verify the chain:**

```bash
curl -s https://www.cryptolwa.io/api/health/account | jq '.missingEnvVars,.missingKvBindings'
# Both should be []
```

---

## 25. "How to grant myself access without paying"

Use one of the three operator unlock options. They're documented in detail in `ACCOUNT-SETUP.md` §9. Quick reference:

```
A. SITE_FULLY_OPEN=true                        — everyone gets LIFETIME (emergency)
B. OPERATOR_EMAIL=stangebara@protonmail.com    — only you, after you sign up
C. OPERATOR_PIN_TOKEN=<rand>                   — only browsers that visited ?op=<token>
```

**Recommended stack:** B + C. Leave A unset.

**One-line verify:**

```bash
curl -si https://www.cryptolwa.io/portfolio | grep -i x-clwa-tier
# Expected: x-clwa-tier: lifetime
```

See §18 of this doc for failure modes.

---

## 26. "Page is missing the top nav / logo"

**Cause:** `terminal-skin.js` v7.10.6 auto-injects the nav bar (`#clwa-bar`) on any page that lacks it. If it didn't inject, the page either doesn't include the script, or the script crashed before injection.

**Fix steps:**

1. **View source on the broken page.** Confirm two lines exist in `<head>`:
   ```html
   <link rel="stylesheet" href="/terminal-skin.css">
   <script src="/terminal-skin.js" defer></script>
   ```
2. **If missing:** add them, save, redeploy.
3. **If present:** open devtools console. Look for a JS error in `terminal-skin.js`. Most common cause = a previous inline script threw and aborted module loading. Move the inline script below `terminal-skin.js`.
4. **Hard-refresh** (Ctrl+Shift+R). The service worker may have cached the old script.

**Verify:**

```bash
curl -s https://www.cryptolwa.io/<broken-page>.html | grep -c terminal-skin
# Expected: 2 (one for CSS, one for JS)
```

---

## 27. "How to roll back a bad deploy"

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 200" font-family="ui-monospace, monospace" font-size="12">
  <rect width="600" height="200" fill="#fff"/>
  <rect x="10" y="20" width="580" height="40" fill="#fafafa" stroke="#333"/>
  <text x="20" y="45">Deployments</text>
  <text x="200" y="45" fill="#666">latest · 2026-05-19 ·  v7.10.6   (BROKEN)</text>
  <rect x="10" y="70" width="580" height="40" fill="#fff4e0" stroke="#c89b1a"/>
  <text x="20" y="95">Previous</text>
  <text x="200" y="95">  2026-05-17 · v7.10.5  ←  Promote to production</text>
  <rect x="10" y="120" width="580" height="40" fill="#fafafa" stroke="#333"/>
  <text x="20" y="145">Previous</text>
  <text x="200" y="145" fill="#666">  2026-05-15 · v7.10.4</text>
</svg>

1. **Where to click:** Cloudflare → Workers & Pages → cryptolwa → Deployments.
2. **What to look for:** The previous green/successful deploy row.
3. **Exact fix:** Click the `⋯` menu on that row → **"Rollback to this deployment"** (or "Promote to production" depending on Cloudflare's current UI text). Confirm.
4. **Verify:** Within ~30 seconds, `curl -s https://www.cryptolwa.io/ | grep -o 'v7\.10\.[0-9]'` returns the rolled-back version.
5. **After rollback:** the broken deploy stays in history (you can re-promote it). Investigate, fix locally, re-deploy.

**If rollback UI is missing:** drag-drop the previous zip from your backup folder (you keep one, right? — see README "Backups").

---

# PART 4 — PRE-DEPLOY CHECKLIST

Run through this list before promoting any build to production. Five-minute pass.

```
┌─────────────────────────────────────────────────────────────────────┐
│  CRYPTO LWA · Pre-deploy checklist                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  HTML / front-end                                                   │
│                                                                     │
│  [ ] Every .html page in repo root includes:                        │
│      <link rel="stylesheet" href="/terminal-skin.css">              │
│      <script src="/terminal-skin.js" defer></script>                │
│                                                                     │
│      grep -L 'terminal-skin.css' *.html   # should be empty         │
│      grep -L 'terminal-skin.js'  *.html   # should be empty         │
│                                                                     │
│  [ ] No emoji in source HTML strings unless intentional             │
│                                                                     │
│  [ ] No console.log in production JS                                │
│      grep -rn 'console\.log' --include='*.js' . \                   │
│        | grep -v node_modules | grep -v sw.js                       │
│                                                                     │
│  Functions                                                          │
│                                                                     │
│  [ ] Every endpoint wraps its body in top-level try/catch           │
│      grep -L 'try' functions/api/**/*.ts                            │
│                                                                     │
│      (or, structural):                                              │
│      grep -rL 'try\s*{' functions/api/ | grep -v _middleware        │
│                                                                     │
│  [ ] No imports of Node modules (fs, path, etc.) in functions/      │
│      grep -rn "from ['\"]fs['\"]" functions/                        │
│      grep -rn "from ['\"]path['\"]" functions/                      │
│                                                                     │
│  Auth chain                                                         │
│                                                                     │
│  [ ] /api/health/account returns ok:true                            │
│      curl -s https://www.cryptolwa.io/api/health/account            │
│        | jq '.ok'                                                   │
│      # Expected: true                                               │
│                                                                     │
│  [ ] No empty body on any /api/auth/* failure                       │
│      curl -i -X POST https://www.cryptolwa.io/api/auth/login        │
│        -H 'content-type:application/json' -d '{}'                   │
│      # Expected: 400 + JSON body                                    │
│                                                                     │
│  Redirects + headers                                                │
│                                                                     │
│  [ ] _redirects has no broken targets                               │
│      while read line; do                                            │
│        [ -z "$line" ] && continue                                   │
│        case "$line" in \#*) continue ;; esac                        │
│        to=$(echo "$line" | awk '{print $2}')                        │
│        case "$to" in http*|https*) continue ;; esac                 │
│        [ -f ".${to%.html}.html" ] || [ -f ".$to" ] || \             │
│          echo "BROKEN: $line"                                       │
│      done < _redirects                                              │
│                                                                     │
│  [ ] _headers does not over-cache HTML (max-age <=60)               │
│      grep -A1 '\.html' _headers | grep -i cache-control             │
│                                                                     │
│  Agents                                                             │
│                                                                     │
│  [ ] LEGBA status responds                                          │
│      curl -s https://www.cryptolwa.io/api/legba/status | jq '.ok'   │
│                                                                     │
│  [ ] Lasirèn bridge reachable (or configured:false honestly)        │
│      curl -s https://www.cryptolwa.io/api/lasiren/status | jq       │
│                                                                     │
│  [ ] OGOU heartbeat green                                           │
│      curl -s https://www.cryptolwa.io/api/ogou/heartbeat | jq '.ok' │
│                                                                     │
│  [ ] AZAKA returns items                                            │
│      curl -s https://www.cryptolwa.io/api/azaka                     │
│        | jq '.items | length'                                       │
│                                                                     │
│  Crons                                                              │
│                                                                     │
│  [ ] Cron triggers list contains:                                   │
│      [ ] 0 9 * * 1   POST /api/ogou/weekly-report                   │
│      [ ] */15 * * * * POST /api/narratives/refresh                  │
│      [ ] 0 * * * *   GET  /api/news-fetch?refresh=1                 │
│      [ ] */5 * * * * GET  /api/ogou/wallet-watcher  (optional)      │
│      [ ] daily        POST /api/azaka/refresh        (recommended)  │
│                                                                     │
│  Secrets in place                                                   │
│                                                                     │
│  [ ] MEMBER_JWT_SECRET (32+ bytes, encrypted)                       │
│  [ ] ADMIN_JWT_SECRET (32+ bytes, encrypted)                        │
│  [ ] STRIPE_SECRET_KEY (sk_live_… for production)                   │
│  [ ] STRIPE_WEBHOOK_SECRET (whsec_…)                                │
│  [ ] STRIPE_PRICE_MONTHLY / _ANNUAL / _LIFETIME                     │
│  [ ] UNISWAP_TRADE_API_KEY                                          │
│  [ ] ANTHROPIC_API_KEY                                              │
│  [ ] LASIREN_BRIDGE_URL + LASIREN_HMAC_SECRET                       │
│  [ ] OGOU_INTERNAL_SECRET + OGOU_OPERATOR_EMAIL                     │
│                                                                     │
│  KV bindings                                                        │
│                                                                     │
│  [ ] CLWA_MEMBERS, CLWA_ADMINS, CLWA_SESSIONS, CLWA_PAYMENTS        │
│  [ ] CLWA_CAMPAIGNS                                                 │
│  [ ] AZAKA_KV                                                       │
│  [ ] DANTON_KV, HEATMAP_KV                                          │
│  [ ] OGOU_KV, OGOU_EVENTS                                           │
│  [ ] LWA_REPORTS                                                    │
│                                                                     │
│  Operator unlock (post-deploy verify)                               │
│                                                                     │
│  [ ] curl -si https://www.cryptolwa.io/portfolio                    │
│        | grep -i x-clwa-tier                                        │
│      # Expected: x-clwa-tier: lifetime                              │
│                                                                     │
│  Backups                                                            │
│                                                                     │
│  [ ] Today's KV snapshots saved (every namespace)                   │
│      wrangler kv:key list --binding CLWA_MEMBERS                    │
│        > backups/clwa_members-$(date +%F).json                      │
│                                                                     │
│  [ ] Env-var list screenshotted/exported                            │
│                                                                     │
│  [ ] DNS zone exported this month                                   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

---

## End of TROUBLESHOOTING.md

If a failure mode isn't covered above, capture:

1. The exact HTTP status + JSON body.
2. The endpoint URL.
3. `curl -s https://www.cryptolwa.io/api/health/account | jq` output.
4. Recent Cloudflare → Pages → Logs → Tail snippet.

Then either update this doc or ping `security@cryptolwa.io`.

*The forge is silent. The hammer falls without warning.*
