# Platform 2040 - Agent Guide

> **LLM-Friendly:** This guide is designed for AI agents and LLMs. Fetch this into your agent's context to discover and manage apps on the BrightWrapper Bubble server.

Platform 2040 is the private manager for 90+ aisloppy apps on the local server. It provides app discovery, semantic search, and infrastructure management (create, fork, rename, and delete apps).

**Base URL:** `https://platform2040.com`

**Web routes:**
- Landing page: `/`
- Private dashboard: `/dashboard`

## Authentication

All app inventory and management endpoints require authentication.

For machine-to-machine integration, Platform 2040 issues its own integration API keys — keys start with `p2040k_`.

```
X-API-Key: p2040k_your_key_here
```

Also accepted: `Authorization: Bearer p2040k_your_key_here`

Browser users authenticate on `platform2040.com` and then use Platform 2040's own routes. Browser code should use AuthReturn bearer tokens from `AuthReturn.init({ sessionScope: "local" })`; user-facing flows should stay on the app's own domain instead of redirecting people to raw AuthReturn app endpoints.

Integration key issuance is operator-only:
- Sign into Platform 2040 as an allowed operator through Platform 2040's own UI.
- Create keys with `POST /api/integration-keys`.
- List keys with `GET /api/integration-keys`.
- Revoke a key with `POST /api/integration-keys/<key_id>/revoke`.

Local-first rule:
- For browser and user-facing integrations, keep auth flows on `platform2040.com` and use Platform 2040's own endpoints such as `GET /api/auth/session` and `POST /api/integration-keys`.
- Do not send end users to `authreturn.com/api/apps/platform2040/...` as their primary interaction pattern.
- Direct AuthReturn login is only an operator automation fallback when you explicitly need a bearer JWT for headless scripting.

API key integrations are **locked by default** until explicitly enabled on the server via:

`/opt/secrets/platform2040.json`
```json
{"agent_integration_enabled": true}
```

When integration is locked, API key requests return `403`.

Protected management endpoints also enforce an operator allowlist. By default only:

`tremendous-agent@proton.me`

To change this allowlist, set `allowed_operator_emails` in `/opt/secrets/platform2040.json`.

**Auth-required endpoints:** `GET /api/projects`, `GET /api/server/apps`, `GET /api/server/apps/<name>/files`, `GET /api/server/apps/<name>/file`, `POST /api/search`, `POST /api/apps/validate`, `POST /api/apps/create`, `POST /api/apps/fork`, `POST /api/apps/rename`, `POST /api/apps/delete`, `GET /api/renames`, `GET /api/renames/<name>`, `POST /api/projects/rescan`, `GET /api/task/<task_id>`, `GET /api/infra/nginx-timeouts`, `POST /api/builds/record`, `GET /api/builds`, `POST /api/builder/run`, `GET /api/builder/task/<task_id>`, `POST /api/builder/cancel/<task_id>`, `GET /api/projects/<name>/loc-files`, `GET /api/app-farm/status`, `GET /api/app-farm/instances`, `GET /api/app-shell/configs`, `PUT /api/app-shell/configs/default`, `PUT /api/app-shell/configs/<host>`, `DELETE /api/app-shell/configs/<host>`

## Quick Start

### 1. Create An Integration Key

Preferred browser flow:

1. Sign into `https://platform2040.com`.
2. Create or rotate a key with `POST /api/integration-keys` from that authenticated session.

Headless operator fallback:

```bash
JWT=$(curl -s -X POST https://authreturn.com/api/apps/platform2040/login \
  -H "Content-Type: application/json" \
  -d '{"email":"tremendous-agent@proton.me","password":"YOUR_PASSWORD"}' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")

API_KEY=$(curl -s -X POST https://platform2040.com/api/integration-keys \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"label":"Hydraulic Butterfly","consumer_slug":"hydraulic-butterfly","replace_existing":true}' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['api_key'])")
```

If you already have a valid signed-in browser session, you do not need the direct AuthReturn login step above.

### 2. List All Apps

```bash
curl -s https://platform2040.com/api/projects \
  -H "X-API-Key: $API_KEY" | python3 -m json.tool | head -30
```

### 3. Search Apps by Description

```bash
curl -s -X POST https://platform2040.com/api/search \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "authentication and login"}' | python3 -m json.tool
```

### 4. Create a New App

```bash
# Start creation (returns task_id)
TASK=$(curl -s -X POST https://platform2040.com/api/apps/create \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-new-app", "description": "A tool for tracking daily habits"}')

TASK_ID=$(echo $TASK | python3 -c "import sys,json; print(json.load(sys.stdin)['task_id'])")

# Poll until complete
while true; do
  STATUS=$(curl -s https://platform2040.com/api/task/$TASK_ID \
    -H "X-API-Key: $API_KEY")
  echo $STATUS | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'], d.get('logs',[''])[-1] if d.get('logs') else '')"
  echo $STATUS | python3 -c "import sys,json; sys.exit(0 if json.load(sys.stdin)['status'] in ('completed','failed') else 1)" && break
  sleep 3
done
```

### 5. Audit nginx timeout overrides

```bash
curl -s https://platform2040.com/api/infra/nginx-timeouts \
  -H "X-API-Key: $API_KEY" | python3 -m json.tool | head -80
```

## Cluster Standard: Long-Running Work

Use this as the default contract across services:

1. `POST /api/jobs/...` returns quickly with `202` and `task_id`
2. `GET /api/tasks/{task_id}` is the durable source of truth for status, logs, result, and error
3. `GET /api/tasks/{task_id}/events` is optional SSE for live observation only

Rules:
- Long-running compute must not sit behind ordinary sync request/response endpoints.
- Raised nginx timeout overrides on non-streaming routes are violations.
- Streaming routes may use longer idle timeouts, but they must actually stream data or heartbeats.
- SSE is transport, not durability. The task record is the durable state.

This guide's nginx audit endpoint helps identify existing violations in the fleet.

## API Reference

### GET /api/projects

List all apps with metadata. Merges cached LLM descriptions with live filesystem data.

**Auth:** Required

**Response:**
```json
{
  "projects": [
    {
      "name": "auth-return",
      "domain": "authreturn.com",
      "description": "Authentication service using AWS Cognito...",
      "last_modified": 1707422400.0,
      "type": "app"
    }
  ],
    "last_scan": "2025-01-15 10:30:00"
}
```

### GET /api/server/apps

List apps discovered on the current server. Use this for a live app picker when another app needs to browse local projects through Platform 2040 instead of reading `/home/ubuntu/apps` directly.

**Auth:** Required

**Response:**
```json
{
  "apps": [
    {
      "name": "tentacles",
      "domain": "tentacles.aisloppy.com",
      "url": "https://tentacles.aisloppy.com",
      "type": "app",
      "last_modified": 1707422400.0
    }
  ]
}
```

### GET /api/server/apps/<name>/files

Return indexed text files for one current-server app. Intended for code-exploration tools that need a safe file list over the network API.

**Auth:** Required

**Response:**
```json
{
  "app_name": "tentacles",
  "file_count": 42,
  "files": ["backend/server.py", "frontend/index.html"]
}
```

### GET /api/server/apps/<name>/file?path=<relative_path>

Read one text file from a current-server app.

**Auth:** Required

**Response:**
```json
{
  "app_name": "tentacles",
  "path": "backend/server.py",
  "content": "\"\"\"tentacles Backend..."
}
```

### GET /api/infra/nginx-timeouts

Inspect nginx site configs for timeout overrides and classify routes as standard, streaming, or suspicious.

**Auth:** Required

**Response:**
```json
{
  "summary": {
    "site_count": 104,
    "route_count": 233,
    "suspicious_timeout_override_count": 1,
    "streaming_timeout_override_count": 2,
    "streaming_route_count": 4
  },
  "sites": [
    {
      "config_path": "/etc/nginx/sites-available/toolong.aisloppy.com.conf",
      "server_names": ["toolong.aisloppy.com"],
      "locations": [
        {
          "match": "/",
          "classification": "suspicious_timeout_override",
          "recommendation": "Move long-running work behind an async job endpoint and keep request timeouts strict."
        }
      ]
    }
  ]
}
```

### POST /api/search

Semantic similarity search across app descriptions using embeddings.

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `query` | yes | Natural language search query |

**Response:**
```json
{
  "query": "authentication",
  "results": [
    {
      "name": "auth-return",
      "domain": "authreturn.com",
      "description": "Authentication service...",
      "similarity": 0.8734
    }
  ]
}
```

Results are sorted by similarity (highest first). Filter by `similarity > 0.3` for relevant matches.

### POST /api/apps/validate

Check if an app name is valid and available.

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | Proposed app name (lowercase, hyphens ok) |

**Response (valid):**
```json
{"valid": true, "normalized": "my-app", "domain": "my-app.aisloppy.com"}
```

**Response (invalid):**
```json
{"valid": false, "error": "App \"my-app\" already exists"}
```

### POST /api/apps/create

Create a new app with full infrastructure (Linux user, systemd service, nginx, SSL, favicon). Returns a task ID for async polling.

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | App name (lowercase, hyphens allowed, 2-30 chars) |
| `description` | yes | What the app does (used for favicon generation) |

**Response:** `202 Accepted`
```json
{
  "task_id": "uuid",
  "status": "pending",
  "poll_url": "/api/task/uuid"
}
```

**What gets created:**
- Linux user `app_<name>`
- Code directory `/home/ubuntu/apps/<name>/`
- Data directory `/home/app_<name>/data/`
- Python venv with Flask + gunicorn
- Systemd service on next available port
- Nginx config with SSL (via certbot)
- AuthReturn app registration (app is ready for auth immediately)
- AI-generated favicon
- Git repo initialized

### POST /api/apps/fork

Provision a fresh target app, then copy an existing source app's workspace into it and retarget the obvious app identifiers.

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `source_name` | yes | Existing app to copy from |
| `target_name` | yes | New app slug to provision |
| `description` | no | Provisioning description for the new target app; defaults to `Fork of <source_name>` plus cached description when available |

**Response:** `202 Accepted`
```json
{
  "task_id": "uuid",
  "status": "pending",
  "poll_url": "/api/task/uuid"
}
```

**Completed task result:**
```json
{
  "source_name": "overview",
  "name": "overview-agent",
  "domain": "overview-agent.aisloppy.com",
  "port": 9274,
  "service_active": true,
  "http_status": 200,
  "warnings": [
    "Runtime data directories are not copied when forking."
  ]
}
```

**Important fork semantics:**
- Fresh infra is created for the target app first (user, service, nginx, SSL, AuthReturn registration, default app-shell contract).
- Source code is copied into the new workspace and obvious identifiers are retargeted (`name`, domain, paths, app user).
- Runtime data directories are **not** copied.
- Host-specific app-shell overrides are **not** cloned.
- App-specific secrets are copied when `/opt/secrets/<source>.json` exists, but secret values may need manual rotation afterward.

### POST /api/apps/rename

Rename an existing app, updating all 12+ sources of truth (directory, user, service, nginx, SSL, registry, code references).

Rename preserves application routes by proxying the new domain to the renamed service, not only its `/api` routes. App slugs may contain hyphens; generated Unix users and `/home/app_<name>/data/` paths normalize them to underscores. JSON manifests such as `platform2040.json` are retargeted along with source files. The task is not complete until `service_active` is `true`; verify the new public health endpoint after polling the task to completion.

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `old_name` | yes | Current app name |
| `new_name` | yes | New app name |

**Response:** `202 Accepted` (same task pattern as create)

**Completed task result:**
```json
{
  "old_name": "old-app",
  "new_name": "new-app",
  "domain": "new-app.aisloppy.com",
  "service_active": true
}
```

### POST /api/apps/delete

Delete an existing app and its local infrastructure resources.

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | Existing app name |

**Response:** `202 Accepted` (same task pattern as create)

### GET /api/renames

List all historical app renames, newest first.

**Auth:** Required

**Response:**
```json
{
  "renames": [
    {
      "old_name": "quest-viz",
      "new_name": "camelot",
      "timestamp": 1707422400,
      "date": "2025-02-08 22:40:00"
    }
  ]
}
```

### GET /api/renames/\<name\>

Check if an app name was renamed. Searches both old and new names. Use this when an app seems to be missing — it may have been renamed.

**Auth:** Required

**Response (found):**
```json
{
  "found": true,
  "old_name": "quest-viz",
  "new_name": "camelot",
  "timestamp": 1707422400,
  "date": "2025-02-08 22:40:00"
}
```

**Response (not found):**
```json
{"found": false}
```

### POST /api/projects/rescan

Trigger a full rescan of all apps — regenerates LLM descriptions and embeddings. Expensive operation (calls LLM for each app).

**Auth:** Required

**Response:** `202 Accepted` (same task pattern)

### GET /api/task/\<task_id\>

Poll an async task's status.

**Auth:** Required

**Status codes:**
- `202` — Task still running
- `200` — Task completed or failed

**Response:**
```json
{
  "status": "running",
  "progress": 45,
  "logs": ["[10:30:01] Processing 5/12: auth-return"],
  "result": null,
  "error": null
}
```

Status values: `pending` → `running` → `completed` | `failed`

### GET /api/favicons/\<domain\>

Serve a cached favicon for an app domain. If the domain has no cached favicon yet or no site favicon is available, the endpoint returns a default icon immediately and refreshes the cache in the background. Cache entries are refreshed at most once every 24 hours.

**Auth:** Not required

**Response:** Image binary (PNG, ICO, WebP, SVG, or default fallback with correct Content-Type)

### GET /api/builds

List recent Claude Code builds, newest first (max 50). Automatically refreshes status of running builds by probing BrightWrapper.

**Auth:** Required

**Response:**
```json
{
  "builds": [
    {
      "task_id": "uuid",
      "project_name": "my-app",
      "prompt": "A tool for tracking daily habits",
      "status": "completed",
      "started_at": 1707422400.0,
      "completed_at": 1707422800.0,
      "error": null
    }
  ]
}
```

Status values: `running`, `completed`, `failed`

### POST /api/builds/record

Manually record a build that was started externally (e.g., via direct BrightWrapper API call).

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `task_id` | yes | BrightWrapper task ID |
| `project_name` | yes | App name |
| `prompt` | no | Build description/prompt |

**Response:** `201 Created`
```json
{"status": "recorded", "task_id": "uuid"}
```

### POST /api/builder/run

Start a Claude Code build session for an app. Sends an enriched prompt to BrightWrapper's claude-code endpoint.

**Auth:** Required

**Body:**
| Field | Required | Description |
|-------|----------|-------------|
| `project_name` | yes | App name (must have systemd service) |
| `prompt` | yes | What to build |
| `model` | no | Optional model pin. Omit it to let the downstream builder choose the current default. |
| `max_budget_usd` | no | Max spend (default: `3.00`) |

**Response:** Proxied from BrightWrapper (includes `task_id`)

### GET /api/builder/task/\<task_id\>

Poll a Claude Code build task for status and logs. Proxied from BrightWrapper.

**Auth:** Required

**Response:** Proxied from BrightWrapper (includes `status`, `logs`/`output`)

### GET /api/version

**Auth:** Not required

**Response:** `{"version": "2.11.0"}`

### GET /api/config

Return non-secret frontend config.

**Auth:** Not required

**Response:**
```json
{
  "auth_return_app_id":"<authreturn-app-id>",
  "architecture_page_id":"<diagram-page-id>"
}
```

### GET /api/app-shell/config

Return resolved cross-app shell config for a host. This endpoint is public so edge/runtime shell injection can fetch config without app-level credentials.

**Auth:** Not required
**CORS:** `Access-Control-Allow-Origin: *`

**Query parameters:**
- `host` (optional): Hostname to resolve. If omitted, server infers from request host headers.

**Response:**
```json
{
  "host": "sora-test.aisloppy.com",
  "config": {
    "enabled": true,
    "brand_label": "Aisloppy",
    "brand_subtitle": "",
    "brand_mark": "",
    "brand_href": "https://platform2040.com/dashboard",
    "links": [
      {"label": "Landing", "href": "https://platform2040.com/"},
      {
        "label": "Docs",
        "children": [
          {"label": "Agent Guide", "href": "https://platform2040.com/agent-guide.md"}
        ]
      },
      {"label": "Dashboard", "href": "https://platform2040.com/dashboard"}
    ],
    "hide_selectors": [],
    "mount_selector": "",
    "right_widget": "",
    "header_variant": "",
    "auth_policy": "app"
  },
  "meta": {
    "source": "default",
    "updated_at": 1770000000.0,
    "schema_version": 6
  }
}
```

### GET /api/app-shell/configs

Return the full app-shell configuration document (default + host overrides).

**Auth:** Required

### PUT /api/app-shell/configs/default

Patch default shell configuration.

**Auth:** Required

**Body keys (optional):** `enabled`, `brand_label`, `brand_subtitle`, `brand_mark`, `brand_href`, `links`, `hide_selectors`, `mount_selector`, `right_widget`, `header_variant`, `auth_policy`
`right_widget` values: `""`, `"bw_auth"`, `"graphtutor_auth"`, `"agent_manager"`, `"domain_gen"`, `"authreturn_basic"`
`header_variant`: empty string or lowercase slug matching `^[a-z0-9][a-z0-9_-]{0,63}$` (no per-app enum whitelist)
`auth_policy`: `app`, `authreturn`, or `trusted_network`. Host `finance` should use `trusted_network`; public hosts normally use `authreturn` or app-owned policy.

### PUT /api/app-shell/configs/\<host\>

Patch host-specific shell overrides.

**Auth:** Required

**Body keys (optional):** `enabled`, `brand_label`, `brand_subtitle`, `brand_mark`, `brand_href`, `links`, `hide_selectors`, `mount_selector`, `right_widget`, `header_variant`
`right_widget` values: `""`, `"bw_auth"`, `"graphtutor_auth"`, `"agent_manager"`, `"domain_gen"`, `"authreturn_basic"`
`header_variant`: empty string or lowercase slug matching `^[a-z0-9][a-z0-9_-]{0,63}$` (no per-app enum whitelist)

### DELETE /api/app-shell/configs/\<host\>

Delete host-specific shell override.

**Auth:** Required

### App Shell Event Protocol (v1)

The injected runtime (`https://platform2040.com/static/infra-inject.js`) publishes global shell events via `window` `CustomEvent`s:

- `infra-app-shell-ready`
- `infra-auth-change`
- `infra-nav-change`
- `infra-shell-layout-change`

Each payload includes:

- `protocol_version`
- `emitted_at`
- `host`

Adapter API exposed by runtime:

- `window.InfraAppShellEvents.emitAuthChange(detail)`
- `window.InfraAppShellEvents.emitNavChange(detail)`
- `window.InfraAppShellEvents.emitShellLayoutChange(detail)`

Decision record and payload contract:

- `docs/app-shell-migration-contract.md`

Recommended integration pattern for app pages:

- Include an explicit, agent-visible top slot: `<header data-platform-shell data-shell-version="1" data-brand-label="My App" data-brand-href="/"><div data-platform-shell-fallback>…</div></header>` near the top of `<body>`. Local brand attributes win over host defaults, which is essential when tunnel-only apps share one hostname on different ports.
- Import the shared implementation explicitly in the app source: `<script defer src="https://platform2040.com/app-shell.js"></script>`. Never inject it invisibly through nginx.
- Declare the dependency in `platform2040.json` as `"shared_shell":{"provider":"platform2040","version":1,"mount":"[data-platform-shell]"}`.
- Include Platform 2040's hosted runtime (`https://platform2040.com/static/infra-inject.js`) so the page gets the shared event/runtime contract.
- Keep exactly one visible top header; do not stack a second fixed global header over an app-owned header.
- Treat `GET /api/app-shell/config` as the source of truth for `brand_label`, `brand_href`, `brand_mark`, `links`, and `header_variant`.
- Render the header locally inside the app repo so it matches the app's own branding and layout. If multiple pages share the same header, factor it into app-owned assets rather than duplicating inline markup.
- `https://platform2040.com/static/app-header.js` has been retired; do not use it for new app work.
- Use Browser Driver for screenshot-based visual verification when the header layout is nontrivial; confirm logo presence, alignment, auth controls, mobile behavior, and no duplicate top header.

### App Shell Acceptance Gate

Run migration gate checks (contract + host checks):

```bash
python3 scripts/app_shell_acceptance_gate.py --hosts "pictonode.aisloppy.com wordlewithatimer.com"
```

Machine-readable output:

```bash
python3 scripts/app_shell_acceptance_gate.py --hosts "pictonode.aisloppy.com" --json
```

### GET /api/auth/session

Return server-verified browser session state from a Bearer token.

**Auth:** Optional

**Response (authorized session):**
```json
{
  "authenticated": true,
  "authorized": true,
  "user": {"id": "user_id", "email": "operator@example.com"}
}
```

**Response (no session or invalid token):**
```json
{"authenticated": false, "authorized": false, "user": null}
```

### GET /api/app-farm/status

Return App Farm reachability/version from Platform 2040.

**Auth:** Required

### GET /api/app-farm/instances

Return the signed-in operator's App Farm instances through Platform 2040.

**Auth:** Required

## Async Task Pattern

All mutating operations (create, rename, rescan) are async:

1. **POST** the request → get `202` with `task_id`
2. **Poll** `GET /api/task/<task_id>` every 2-3 seconds
3. **Check status:** `202` = still running, `200` = done
4. **Read result:** `status: "completed"` has `result`, `status: "failed"` has `error`

## App Installation Workflow

For deploying code from a laptop to the server:

1. **Create the app** via `POST /api/apps/create` (sets up all infrastructure)
2. **rsync your code** to the server:
   ```bash
   rsync -avz ./my-app/ user@server:/home/ubuntu/apps/my-app/
   ```
3. **Restart the service:**
   ```bash
   ssh user@server "sudo systemctl restart my-app"
   ```

The create step handles user creation, ports, nginx, SSL — you just need to deploy your code.

## Integration Pattern

```python
import json
import requests
import time

API_KEY = "p2040k_your_key"
BASE = "https://platform2040.com"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# Search for apps
resp = requests.post(f"{BASE}/api/search", json={"query": "image generation"}, headers=HEADERS)
for app in resp.json()["results"][:5]:
    print(f"{app['name']}: {app['similarity']:.0%} - {app['description']}")

# Create a new app
resp = requests.post(f"{BASE}/api/apps/create", json={
    "name": "my-tool",
    "description": "A productivity tool for managing daily tasks",
}, headers=HEADERS)
task_id = resp.json()["task_id"]

# Poll until done
while True:
    task = requests.get(f"{BASE}/api/task/{task_id}", headers=HEADERS).json()
    if task["status"] == "completed":
        print(f"App created! Domain: {task['result']['domain']}")
        break
    if task["status"] == "failed":
        raise RuntimeError(task["error"])
    time.sleep(3)
```

## DNS Management

DNS for app domains is managed via the shared Namecheap API toolset at `/home/ubuntu/apps/namecheap/`.

### List All DNS Records

```bash
HOME=/opt/secrets python3 /home/ubuntu/apps/namecheap/list_dns_records.py
```

Shows all A records for aisloppy.com with their target IPs.

### Check Where a Domain Points

```bash
python3 /home/ubuntu/apps/namecheap/check_dns_ready.py my-app.aisloppy.com
```

Resolves the domain and checks if it points to the expected IP. Useful before requesting SSL certs.

### Add a DNS Record

```bash
HOME=/opt/secrets python3 /home/ubuntu/apps/namecheap/add-dns-record-namecheap.py my-app
# Adds: my-app.aisloppy.com → 100.50.104.176 (default IP)

# Custom domain/IP:
HOME=/opt/secrets python3 /home/ubuntu/apps/namecheap/add-dns-record-namecheap.py my-app aisloppy.com 1.2.3.4
```

Preserves all existing records and adds/updates the specified subdomain. Has safety checks to never lose `@` or `www` records.

### Update All Records to New IP

```bash
HOME=/opt/secrets python3 /home/ubuntu/apps/namecheap/update_dns_to_new_ip.py --old-ip 1.2.3.4 --new-ip 5.6.7.8
```

Bulk-updates all A records pointing to the old IP. Used during server migrations.

### Credentials

`/opt/secrets/namecheap.json` — contains `api_user`, `api_key`, and `api_ip` (whitelisted IP for API access). The scripts read `~/namecheap.json`, so run them with `HOME=/opt/secrets`.

---

## App Farm Infrastructure

Platform 2040 manages 90+ apps running on shared servers. This section documents cross-cutting patterns that apply when integrating multiple apps together.

### Secrets

Each app stores its API keys and credentials in `/opt/secrets/<app>.json`. Files are ACL-protected so only the app's Linux user can read them.

```bash
# Example: reading a BrightWrapper API key
cat /opt/secrets/brightwrapper.json
# {"api_key": "bw_..."}
```

**API keys are per-app.** Each shared service (BrightWrapper, AuthReturn, Camelot, etc.) issues separate keys per consumer app. A BrightWrapper key won't work on Camelot.

### Shared API Key Handling

Multiple apps on the same server may share a single API key for a given service. **Read the key from disk on every API call** — never cache it in a module-level variable or at import time.

```python
# CORRECT — reads fresh each call
def _brightwrapper_key():
    with open('/opt/secrets/brightwrapper.json') as f:
        return json.load(f)['api_key']

resp = requests.post(url, headers={'X-API-Key': _brightwrapper_key()}, ...)

# WRONG — stale after key regeneration
BRIGHTWRAPPER_KEY = json.load(open('/opt/secrets/brightwrapper.json'))['api_key']
```

**Why:** Regenerating a key instantly revokes all previous keys for that user+app. If any session regenerates the shared key (e.g. while debugging a 401), every running process that cached the old key in memory will start getting 401 Unauthorized until restarted. Reading from disk means all services pick up the new key automatically.

### Service Architecture

Each app runs as a systemd service on a dedicated port, behind nginx with SSL.

| Component | Convention |
|-----------|-----------|
| Linux user | `app_<name>` |
| Code directory | `/home/ubuntu/apps/<name>/` |
| Data directory | `/home/app_<name>/data/` |
| Port | Defined in `/etc/systemd/system/<name>.service` |
| Secrets | `/opt/secrets/<name>.json` (ACL-protected) |

**Ports are assigned, not chosen.** The port is set in the systemd service file. Apps must read it from the `PORT` environment variable — never hardcode or use a fallback default.

### Running Scripts as App User

Scripts that produce data (imports, migrations, backfills) must run as the app user, not `ubuntu`. Otherwise files end up owned by `ubuntu` and the service can't write to them.

```bash
# CORRECT
sudo -u app_my_app python3 backend/import_data.py

# WRONG — data owned by ubuntu, service can't use it
python3 backend/import_data.py
```

### Shared Services Mesh

Apps communicate with shared services via their public APIs. Each service publishes an agent guide at `/agent-guide.md`:

| Service | URL | Purpose |
|---------|-----|---------|
| BrightWrapper | brightwrapper.com/agent-guide.md | LLM chat, images, structured output, frontend components |
| AuthReturn | authreturn.com/agent-guide.md | Authentication, login/signup |
| Platform 2040 | platform2040.com/agent-guide.md | App management, discovery |
| Camelot | camelot.aisloppy.com/agent-guide.md | Quest/goal tracking |
| Diagram Notes | diagram-notes.aisloppy.com/agent-guide.md | Annotated Mermaid diagrams |
| FaviconGen | favicon-gen.aisloppy.com/agent-guide.md | AI favicon generation |
| Browser Driver | browser-driver.aisloppy.com/agent-guide.md | Browser automation, screenshots |
| Synaptic | synaptic.aisloppy.com/agent-guide.md | Cross-app dependency tracking |
| Option Table | option-table.aisloppy.com/agent-guide.md | Weighted decision matrices |

When integrating a service, fetch its agent guide and follow the instructions there. Treat it like a third-party SaaS — use the public API, not the source code.

---

## Error Responses

All errors return JSON:

```json
{"error": "Description of what went wrong"}
```

| Status | Meaning |
|--------|---------|
| 400 | Invalid request (bad name, missing fields) |
| 401 | Missing or invalid API key |
| 404 | Task or resource not found |
| 500 | Server error |
