# AccessAgent — Hosting Guide for AI Agents

> This document is for LLM agents. It contains everything you need to deploy, update, and manage static websites on AccessAgent. Follow the step-by-step process at the end.

---

## Quick Reference Index

| Need to... | Section | Key Details |
|------------|---------|-------------|
| **Understand the service** | 1 | Upload a zip, get a URL. No accounts, no wallets, no dependencies. |
| **Know the limits** | 2 | Bandwidth, rate limits, body size, abuse blocking |
| **Create a site** | 3 | `POST /api/sites` — multipart upload, server returns secret key |
| **Authenticate requests** | 4 | `Authorization: Bearer sk_...` header on all mutations |
| **Update a site** | 5 | `PUT /api/sites/:name` — re-upload zip with bearer token |
| **Manage individual files** | 6 | Upload, replace, delete, list files without re-zipping |
| **Delete a site** | 7 | `DELETE /api/sites/:name` — bearer token auth |
| **Upgrade to Pro** | 8 | `POST /api/sites/:name/activate` — pay monthly USDC for Pro tier |
| **Use a coupon** | 9 | Pass `coupon` field at creation for free or discounted activation |
| **Custom domains** | 10 | `POST /api/sites/:name/domain` — point your own domain |
| **Check site info** | 11 | `GET /api/sites/:name` — metadata, `POST .../analytics` for owner data |
| **Rotate your key** | 12 | `POST /api/sites/:name/rotate-key` — invalidate old key, get new one |
| **Know zip requirements** | 13 | 210MB max compressed, 40MB/200MB extracted, 100/2,000 files, must have index.html |
| **See the full flow** | 14 | Step-by-step from creation to live site |

### Key Technical Facts

| Fact | Value |
|------|-------|
| **API Base URL** | `https://accessagent.ai` |
| **Site URLs** | `https://{name}.accessagent.ai/` |
| **Custom domain URLs** | `https://yourcustomdomain.com/` (after DNS + domain assignment) |
| **Auth method** | `Authorization: Bearer sk_...` (secret key returned at creation) |
| **Create auth** | None required — server generates secret key |
| **Mutation auth** | Bearer token in `Authorization` header |
| **Site name format** | 3-30 chars, lowercase alphanumeric + hyphens, no start/end with hyphen |
| **Reserved names** | api, www, admin, static, assets, cdn, mail, ftp, localhost, s |
| **Max zip size** | 210MB compressed |
| **Max extracted size** | 40MB (free), 200MB (Pro) |
| **Max file count** | 100 (free), 2,000 (Pro) |
| **Max single file** | 5MB (free), 25MB (Pro) |
| **Required file** | `index.html` at root level |
| **Pro price** | Monthly (active billing catalog price) via USDC on Base or PayPal |
| **USDC contract** | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` (Base mainnet) |
| **Chain ID** | `8453` (Base mainnet) |
| **Create form fields** | `name`, `file` (the zip — field MUST be named "file"), `wallet` (optional), `coupon` (optional), `category` (optional), `description` (optional), `email` (optional) |
| **Valid categories** | game, portfolio, landing-page, docs, blog, app, tool, other |
| **Request format** | `multipart/form-data` for uploads, `application/json` for other operations |
| **Response format** | `{ success: true/false, data?: {...}, error?: { code, message } }` |
| **Rate limits (API)** | Create: 3/min, Update: 5/min, Delete: 10/min, Activate: 10/min, Domain: 5/min |
| **Rate limits (static)** | 120 req/sec per IP (burst 200) on all file serving routes |
| **Bandwidth (free)** | 10 GB/month per site |
| **Bandwidth (Pro)** | 100 GB/month per site |
| **Max request body** | 220 MB multipart (uploads), 1 MB JSON (other) |
| **Per-IP daily creates** | 5/day |
| **Content moderation** | AI-powered, blocks sexual/violence/illegal/malware/phishing/hate content |
| **Abuse auto-block** | 50+ rate-limit hits in 10 min → IP blocked for 30 min |

### API Cheatsheet

**1. Create a site (no auth needed)**
```
POST https://accessagent.ai/api/sites
Content-Type: multipart/form-data
Fields: name, file (zip), wallet (optional), coupon (optional), category (optional), description (optional), email (optional)
Success 201: { success: true, data: { secretKey: "sk_...", name, url, size, files, activated }, _warning: "Store this secret key safely." }
```

**2. Update a site (re-upload)**
```
PUT https://accessagent.ai/api/sites/{name}
Authorization: Bearer sk_...
Content-Type: multipart/form-data
Fields: file (zip)
Success 200: { success: true, data: { name, url, size, files } }
```

**3. Upload/replace a single file**
```
PUT https://accessagent.ai/api/sites/{name}/files/{path}
Authorization: Bearer sk_...
Content-Type: multipart/form-data
Fields: file
Success 200: { success: true, data: { name, path, size, siteSize, fileCount } }
```

**4. Upload multiple files**
```
POST https://accessagent.ai/api/sites/{name}/files
Authorization: Bearer sk_...
Content-Type: multipart/form-data
Fields: file fields named by path (e.g. "style.css", "assets/logo.png")
Success 200: { success: true, data: { name, uploaded, count, siteSize, fileCount } }
```

**5. Delete a file**
```
DELETE https://accessagent.ai/api/sites/{name}/files/{path}
Authorization: Bearer sk_...
Success 200: { success: true, data: { name, path, siteSize, fileCount } }
Cannot delete index.html.
```

**6. List files**
```
GET https://accessagent.ai/api/sites/{name}/files
Auth: None
Success 200: { success: true, data: { files: [{ path, size }, ...] } }
```

**7. Delete a site**
```
DELETE https://accessagent.ai/api/sites/{name}
Authorization: Bearer sk_...
Success 200: { success: true, data: { name, message } }
```

**8. Upgrade to Pro (monthly billing)**
```
POST https://accessagent.ai/api/sites/{name}/activate
— Without txHash (probe, no auth): returns 402 with payment info
— With txHash: Authorization: Bearer sk_..., Body: { "txHash": "0x..." }
Success 200: { success: true, data: { name, activated, tier, expiresAt, url } }
Payment needed 402: { success: false, payment: { treasury, amount, period, token, network } }
```

**9. Assign a custom domain**
```
POST https://accessagent.ai/api/sites/{name}/domain
Authorization: Bearer sk_...
Body: { "domain": "mycoolsite.com" }
Success 200: { success: true, data: { name, domain, message } }
```

**10. Remove a custom domain**
```
DELETE https://accessagent.ai/api/sites/{name}/domain
Authorization: Bearer sk_...
Success 200: { success: true, data: { name, message } }
```

**11. Get site metadata (public)**
```
GET https://accessagent.ai/api/sites/{name}
Auth: None
Success 200: { success: true, data: { name, activated, tier, category, description, ... } }
```

**12. Get analytics & usage (owner only)**
```
POST https://accessagent.ai/api/sites/{name}/analytics
Authorization: Bearer sk_...
Success 200: { success: true, data: { email, bandwidthUsedBytes, bandwidthLimitBytes, sizeBytes, analytics: { today, allTime } } }
```

**13. Update site metadata (owner only)**
```
PATCH https://accessagent.ai/api/sites/{name}
Authorization: Bearer sk_...
Body: { "category": "game", "description": "...", "email": "..." }
Include any combination of: category, description, email (at least one required)
Success 200: { success: true, data: { name, category, description, email } }
```

**14. Rotate secret key**
```
POST https://accessagent.ai/api/sites/{name}/rotate-key
Authorization: Bearer sk_...
Success 200: { success: true, data: { name, secretKey: "sk_new...", message: "New key active. Old key is now invalid." } }
```

**15. Billing status (owner only)**
```
GET https://accessagent.ai/api/sites/{name}/billing/status
Authorization: Bearer sk_...
Success 200: { success: true, data: { tier, billingStatus, nextPaymentDue, daysRemaining, renewalAmountFormatted, lastPaymentAt, ... } }
```

**16. Payment history (owner only)**
```
GET https://accessagent.ai/api/sites/{name}/billing/history
Authorization: Bearer sk_...
Success 200: { success: true, data: { payments: [{ provider, amountFormatted, paidAt, reference }], totalPaidFormatted, paymentCount } }
```

**17. Start PayPal subscription**
```
POST https://accessagent.ai/api/sites/{name}/billing/paypal/start
Authorization: Bearer sk_...
Success 200: { success: true, data: { subscriptionId, approvalUrl, status, amount, period } }
Requires verified email. Give approvalUrl to user. Pro activates after first PayPal payment.
```

**18. Verify email**
```
POST https://accessagent.ai/api/sites/{name}/verify-email
Authorization: Bearer sk_...
Body: { "code": "123456" }
Success 200: { success: true, data: { emailVerified: true } }
```

**19. Serve static files**
```
GET https://{name}.accessagent.ai/
GET https://{name}.accessagent.ai/path/to/file.js
No auth. Cached: HTML = no-cache, everything else = 1 year immutable.
```

**20. Health check**
```
GET https://accessagent.ai/health
Response: { status: "ok" }
```

---

## 1. What is AccessAgent?

AccessAgent is a static site hosting service designed for AI agents. Upload a zip file containing your website, get a live URL. No accounts, no wallets, no crypto libraries — just a simple secret key.

**Key properties:**
- **Zero-dependency auth**: Create a site → get a secret key (`sk_...`). Use it in the `Authorization` header. No ethers.js, no wallet signing, no npm installs.
- **Instant deploy**: Upload a zip → site is live in seconds at `https://{name}.accessagent.ai/`.
- **Update anytime**: Re-upload with the same secret key to replace all files.
- **Delete when done**: Remove the site with a simple authenticated request.
- **Pro tier**: Pay monthly (USDC on Base or PayPal subscription) for 200MB storage, 100GB/month bandwidth, and custom domains.
- **Custom domains**: Point your own domain with automatic SSL via Let's Encrypt (Pro tier).

### Tiers

| | Free | Pro (monthly billing) |
|---|---|---|
| **Storage** | 40MB | 200MB |
| **Bandwidth** | 10GB/month | 100GB/month |
| **Max files per site** | 100 | 2,000 |
| **Max single file** | 5MB | 25MB |
| **Custom domains** | No | Yes |
| **Price** | $0 | $7/month (USDC on Base or PayPal) |

Pro is billed monthly and reverts to free-tier limits if the paid period ends. Renew anytime — renewal extends from the current expiry date (you don't lose unused time).

---

## 2. Limits & Protections

AccessAgent enforces limits to keep the service reliable for everyone. Hitting a limit returns a clear error — plan for these in your integration.

### Bandwidth Limits

Each site has a monthly bandwidth allowance based on its tier:

| Tier | Monthly Bandwidth | What Happens When Exceeded |
|------|-------------------|---------------------------|
| **Free** | 10 GB | All requests to the site return a **503** HTML page: "Bandwidth Limit Reached" |
| **Pro** | 100 GB | Same — 503 page until the next calendar month |

- Bandwidth resets on the 1st of each month (UTC).
- Bandwidth is tracked per site, not per IP. Every byte served from the site counts (HTML, JS, CSS, images, etc.).
- The 503 page is a small static HTML page — it does not count toward bandwidth.
- **Check bandwidth usage**: `POST https://accessagent.ai/api/sites/{name}/analytics` (bearer auth required) — includes `bandwidthUsedBytes`, `bandwidthLimitBytes`, `bandwidthUsedFormatted`, and visitor analytics.

### Rate Limits

**API endpoints** — sliding window per IP:

| Operation | Limit |
|-----------|-------|
| Create site | 3/min |
| Create site (daily) | 5/day per IP |
| Update site (zip or files) | 5/min |
| Delete site | 10/min |
| Activate | 10/min |
| Domain management | 5/min |

When exceeded → **429 Too Many Requests** with `{ error: { code: "RATE_LIMIT_EXCEEDED" } }`.

**Static file serving** — token bucket per IP:
- 120 requests/second sustained, burst up to 200
- Applies to all file serving: subdomains (`{name}.accessagent.ai`), custom domains, and path routes (`/s/{name}/`)
- When exceeded → **429** plain text

### Abuse Auto-Blocking

If an IP accumulates **50 or more rate-limit violations** (429 responses) within a 10-minute window, it is automatically blocked for **30 minutes**. During a block, all requests from that IP return **403 Forbidden**.

This is application-level only — there is no permanent ban. After the 30 minutes, the IP is unblocked automatically.

### Content Moderation

All uploaded content is automatically scanned by AI before going live. The following content is **prohibited** and will be rejected:

| Category | Examples |
|----------|----------|
| **Phishing** | Fake login pages, brand impersonation, credential harvesting |
| **Malware** | Crypto miners, obfuscated malicious JS, keyloggers, drive-by downloads |
| **Sexual** | Explicit sexual content, pornography |
| **Illegal** | Drug sales, weapons trafficking, child exploitation |
| **Violence** | Extreme realistic gore (cartoon/game violence is fine) |
| **Hate** | Hate speech, harassment, discrimination |

If content is flagged, the upload is rejected with a `CONTENT_POLICY_VIOLATION` error that lists the specific files and reasons:

```json
{
  "success": false,
  "error": {
    "code": "CONTENT_POLICY_VIOLATION",
    "message": "Content violates our acceptable use policy",
    "violations": [
      { "file": "index.html", "category": "phishing", "description": "Fake Google login page" }
    ]
  }
}
```

This applies to all upload operations: site creation, site updates, single file uploads, and batch file uploads.

### Request Body Size Limits

| Content Type | Max Size | Response If Exceeded |
|--------------|----------|---------------------|
| `multipart/form-data` (uploads) | 220 MB | **413** `{ error: { code: "BODY_TOO_LARGE" } }` |
| All other (JSON, etc.) | 1 MB | **413** `{ error: { code: "BODY_TOO_LARGE" } }` |

### Security Headers

All responses include these headers:

| Header | Value |
|--------|-------|
| `X-Content-Type-Options` | `nosniff` |
| `X-Frame-Options` | `SAMEORIGIN` |
| `X-XSS-Protection` | `1; mode=block` |
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |
| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` |

### Error Codes Summary (Limits)

| Code | Status | Meaning |
|------|--------|---------|
| `RATE_LIMIT_EXCEEDED` | 429 | API rate limit hit — wait and retry |
| `DAILY_LIMIT_EXCEEDED` | 429 | Daily IP creation limit reached |
| `CONTENT_POLICY_VIOLATION` | 400 | Uploaded content violates acceptable use policy |
| `BODY_TOO_LARGE` | 413 | Request body exceeds size limit |
| (plain text) | 429 | Static file rate limit hit |
| (plain text) | 403 | IP temporarily blocked (abuse) |
| (HTML page) | 503 | Site bandwidth exceeded for the month |

### What To Do When You Hit a Limit

| Error | Action | Retryable? |
|-------|--------|------------|
| **429 API rate limit** | Wait 60 seconds, then retry. Do NOT retry in a tight loop — that triggers auto-blocking. | Yes, after waiting |
| **429 Daily create limit** | Stop creating sites. The limit resets on a rolling 24-hour window. You can still update/delete existing sites. | Yes, after ~24h |
| **429 Static file rate limit** | Your script is fetching files too fast (>120/sec). Add a delay between requests. | Yes, slow down |
| **403 IP blocked** | Your IP was auto-blocked for 30 minutes due to excessive 429s. Wait it out. Do NOT keep retrying. | Yes, after 30 min |
| **413 Body too large** | Reduce your zip/file size. Free tier: 40MB extracted (100 files, 5MB/file), Pro: 200MB (2,000 files, 25MB/file). | No — fix the payload |
| **503 Bandwidth exceeded** | The site has used all its monthly bandwidth. It resets on the 1st of the month (UTC). Upgrade to Pro for 100GB/month. | No — wait for reset or upgrade |
| **400 Content policy** | Moderation flagged your content. Check the `violations` array in the response for which files were flagged and why. Fix the content and re-upload. | No — fix the content |
| **400 Invalid name** | Name must be 3-30 chars, lowercase alphanumeric + hyphens, no leading/trailing hyphens, no consecutive hyphens. Reserved names: api, www, admin, static, assets, cdn, mail, ftp, localhost, s. | No — choose a different name |

**Key principle:** If you get a 429, always wait before retrying. Never retry in a loop — 50 rate-limit violations in 10 minutes will get your IP blocked for 30 minutes.

---

## 3. Create a Site

### Endpoint: POST https://accessagent.ai/api/sites

Upload a zip file to create a new hosted site. **No authentication required** — the server generates a secret key and returns it in the response.

**Content-Type**: `multipart/form-data`

**Form fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Site name (3-30 chars, lowercase alphanumeric + hyphens) |
| `file` | File | Yes | Zip file (max 210MB compressed, must contain index.html at root) |
| `wallet` | string | No | Ethereum address (0x + 40 hex chars) — stored as optional metadata |
| `coupon` | string | No | Coupon code for free or discounted activation |
| `category` | string | No | Site type: game, portfolio, landing-page, docs, blog, app, tool, other |
| `description` | string | No | 10-500 chars describing the site |
| `email` | string | No | Contact email (required before Pro activation) |

**Success Response (201):**

```json
{
  "success": true,
  "data": {
    "secretKey": "sk_a3f8e7c1d9b4f0e2a6c8d3b5f1e7a9c0d4b6f2e8a0c5d7b3f9e1a4c6d8b0f2",
    "name": "my-game",
    "url": "https://my-game.accessagent.ai/",
    "size": "2.4MB",
    "files": 42,
    "activated": false,
    "category": "game",
    "description": "A retro platformer",
    "email": null,
    "emailPending": false
  },
  "_warning": "Store this secret key safely. It cannot be retrieved later."
}
```

**CRITICAL: Save the `secretKey` from the response!** This is the ONLY time it is returned. If you lose it, you lose the ability to update or delete the site. There is no recovery mechanism.

### Key Storage

Save the secret key to a file so it persists across sessions:

```
.accessagent/
└── key.txt    ← contains the sk_... secret key
```

Add `.accessagent/` to `.gitignore` to prevent accidentally committing the key.

```typescript
import { mkdirSync, writeFileSync, existsSync, readFileSync, appendFileSync } from "fs";

// After creating a site, save the key:
mkdirSync(".accessagent", { recursive: true });
writeFileSync(".accessagent/key.txt", secretKey);

// Add to .gitignore
const gitignore = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
if (!gitignore.includes(".accessagent")) {
  appendFileSync(".gitignore", "\n# AccessAgent secret key — never commit\n.accessagent/\n");
}
```

### Remind the User

After creating a site, always tell the user:
- Their secret key is saved at `.accessagent/key.txt`
- They should **back up this file** (copy it somewhere safe outside the project)
- If they lose the secret key, they **cannot update or delete their site** — there is no recovery
- Never commit this file to git (it's auto-added to `.gitignore`)

**Error Codes:**

| Code | Status | Meaning |
|------|--------|---------|
| `NAME_TAKEN` | 409 | Site name already exists |
| `INVALID_NAME` | 400 | Name doesn't match format rules |
| `ZIP_TOO_LARGE` | 400 | Exceeds 210MB compressed |
| `ZIP_INVALID` | 400 | Not a valid zip file |
| `EXTRACTION_FAILED` | 400 | Zip contains disallowed files or exceeds limits |
| `NO_INDEX_HTML` | 400 | No index.html at root level |
| `INVALID_COUPON` | 400 | Coupon code is invalid, expired, or exhausted |
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests (3/min for create) |
| `DAILY_LIMIT_EXCEEDED` | 429 | Daily IP creation limit reached (5/day) |
| `CONTENT_POLICY_VIOLATION` | 400 | Content flagged by AI moderation |

### Full Create Example

```typescript
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from "fs";

const name = "my-game";
const zipFile = readFileSync("./site.zip");

const form = new FormData();
form.append("name", name);
form.append("file", new Blob([zipFile], { type: "application/zip" }), "site.zip");
// Optional: form.append("email", "dev@example.com");
// Optional: form.append("category", "game");

const response = await fetch("https://accessagent.ai/api/sites", {
  method: "POST",
  body: form,
});

const result = await response.json();
if (result.success) {
  console.log("Site live at:", result.data.url);

  // IMPORTANT: Save the secret key
  mkdirSync(".accessagent", { recursive: true });
  writeFileSync(".accessagent/key.txt", result.data.secretKey);
  console.log("Secret key saved to .accessagent/key.txt");
} else {
  console.error("Error:", result.error.code, result.error.message);
}
```

---

## 4. Authentication

All mutation operations (update, delete, file management, domains, activation, analytics) require the secret key in the `Authorization` header.

### How It Works

1. You create a site → server returns `secretKey: "sk_..."` (67 characters: `sk_` + 64 hex chars)
2. For all subsequent requests, include: `Authorization: Bearer sk_...`
3. That's it. No signing, no timestamps, no crypto libraries.

### Example

```typescript
// Load key from file
import { readFileSync } from "fs";
const secretKey = readFileSync(".accessagent/key.txt", "utf-8").trim();

// Use in any mutation request
const res = await fetch("https://accessagent.ai/api/sites/my-game", {
  method: "DELETE",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
});
```

### Auth Error Codes

| Code | Status | Meaning |
|------|--------|---------|
| `UNAUTHORIZED` | 401 | Missing or malformed `Authorization` header. Must be `Bearer sk_...` |
| `INVALID_KEY` | 401 | Secret key doesn't match this site |
| `SITE_NOT_FOUND` | 404 | Site doesn't exist |

### Security Notes

- The secret key is a 256-bit random value — equivalent to a strong password
- It's stored as a bcrypt hash on the server — even a database breach doesn't expose keys
- Keys are cached (SHA-256 fast path) to avoid bcrypt overhead on every request
- If you suspect a key is compromised, rotate it immediately (see Section 12)

---

## 5. Update a Site

### Endpoint: PUT https://accessagent.ai/api/sites/{name}

Re-upload a zip to replace all files. Old files are completely removed and replaced with the new zip contents.

**Headers:** `Authorization: Bearer sk_...`
**Content-Type**: `multipart/form-data`

**Form fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file` | File | Yes | Zip file (max 210MB compressed) |
| `category` | string | No | Update site category |
| `description` | string | No | Update site description |
| `email` | string | No | Update contact email |

**Success Response (200):**

```json
{
  "success": true,
  "data": {
    "name": "my-game",
    "url": "https://my-game.accessagent.ai/",
    "size": "3.1MB",
    "files": 55
  }
}
```

**Example:**

```typescript
const secretKey = readFileSync(".accessagent/key.txt", "utf-8").trim();
const zipFile = readFileSync("./site.zip");

const form = new FormData();
form.append("file", new Blob([zipFile], { type: "application/zip" }), "site.zip");

const res = await fetch("https://accessagent.ai/api/sites/my-game", {
  method: "PUT",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
  body: form,
});
const data = await res.json();
console.log("Updated:", data.data.url);
```

---

## 6. File Management

Manage individual files without re-zipping and re-uploading the entire site. Ideal for incremental updates — change one CSS file, add a new image, or remove an unused asset.

All file operations share the update rate limit (5/min per IP).

### Upload/Replace a Single File

**Endpoint**: PUT https://accessagent.ai/api/sites/{name}/files/{path}

```typescript
const secretKey = readFileSync(".accessagent/key.txt", "utf-8").trim();

const form = new FormData();
form.append("file", new Blob(["body { color: blue; }"], { type: "text/css" }), "style.css");

const res = await fetch("https://accessagent.ai/api/sites/my-game/files/style.css", {
  method: "PUT",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
  body: form,
});
// { success: true, data: { name, path: "style.css", size: "20B", siteSize: "2.4MB", fileCount: 43 } }
```

Nested paths work too: `PUT /api/sites/my-game/files/assets/sprites/hero.png`

### Upload Multiple Files

**Endpoint**: POST https://accessagent.ai/api/sites/{name}/files

Each non-auth form field is treated as a file. The **field name** is the destination path, and the **field value** is the file content.

```typescript
const form = new FormData();
form.append("style.css", new Blob(["body { color: blue; }"]));
form.append("assets/logo.png", new Blob([logoPngBuffer]));

const res = await fetch("https://accessagent.ai/api/sites/my-game/files", {
  method: "POST",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
  body: form,
});
// { success: true, data: { name, uploaded: ["style.css", "assets/logo.png"], count: 2, siteSize: "2.5MB", fileCount: 44 } }
```

### Delete a File

**Endpoint**: DELETE https://accessagent.ai/api/sites/{name}/files/{path}

```typescript
await fetch("https://accessagent.ai/api/sites/my-game/files/old-script.js", {
  method: "DELETE",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
});
// { success: true, data: { name, path: "old-script.js", siteSize: "2.3MB", fileCount: 42 } }
```

**Note**: `index.html` cannot be deleted — every site must have it.

### List Files

**Endpoint**: GET https://accessagent.ai/api/sites/{name}/files

No authentication required.

```typescript
const res = await fetch("https://accessagent.ai/api/sites/my-game/files");
// { success: true, data: { files: [{ path: "index.html", size: 1234 }, { path: "style.css", size: 567 }, ...] } }
```

### File Management Error Codes

| Code | Status | Meaning |
|------|--------|---------|
| `FILE_TOO_LARGE` | 400 | Single file exceeds limit (5MB free, 25MB Pro) |
| `TOO_MANY_FILES` | 400 | Would exceed file limit (100 free, 2,000 Pro) |
| `INVALID_EXTENSION` | 400 | File extension not in allowlist |
| `INVALID_PATH` | 400 | Empty path, null bytes, or too deep |
| `PATH_TRAVERSAL` | 400 | `../` or path escape detected |
| `CANNOT_DELETE_INDEX` | 400 | Cannot delete index.html |
| `FILE_NOT_FOUND` | 404 | File doesn't exist (delete only) |
| `UNAUTHORIZED` | 401 | Missing bearer token |
| `INVALID_KEY` | 401 | Wrong secret key |

---

## 7. Delete a Site

### Endpoint: DELETE https://accessagent.ai/api/sites/{name}

Remove a site and all its files. Requires the site's secret key.

```typescript
const secretKey = readFileSync(".accessagent/key.txt", "utf-8").trim();

await fetch("https://accessagent.ai/api/sites/my-game", {
  method: "DELETE",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
});
// { success: true, data: { name: "my-game", message: "Site deleted" } }
```

---

## 8. Upgrade to Pro (monthly billing)

Pro tier gives you 200MB storage, 100GB/month bandwidth, and custom domains. It is billed monthly via USDC on Base or PayPal subscription.

### Prerequisites

**A verified email is required before Pro activation.** Set an email via create or PATCH, then verify it:

1. Set email: include `email` field when creating, or use `PATCH /api/sites/{name}` with `{ "email": "dev@example.com" }`
2. A verification code is sent to that email
3. Verify: `POST https://accessagent.ai/api/sites/{name}/verify-email` with `{ "code": "123456" }` (bearer auth required)

### How Pro Activation Works

The user pays directly from their own wallet (MetaMask, Coinbase Wallet, etc.) and gives you the transaction hash. You do NOT need to handle private keys or send transactions yourself. Any wallet can pay — the bearer token proves site ownership, not the payment sender.

**Step-by-step flow:**

1. **Probe for payment info**: `POST https://accessagent.ai/api/sites/{name}/activate` with empty body `{}` → returns 402 with treasury address and amount (no auth needed)
2. **Give the user payment instructions** (see template below)
3. **User sends the monthly USDC amount** from any wallet to the treasury address on Base
4. **User gives you the transaction hash** (starts with `0x`, 66 characters)
5. **Confirm with the server**: `POST https://accessagent.ai/api/sites/{name}/activate` with `Authorization: Bearer sk_...` and body `{ "txHash": "0x..." }`
6. **Done**: Site is on Pro tier for 1 month (renewable monthly)

**Renewal**: If you renew before expiry, the new month is added from the current expiry date (you don't lose unused time). If expired, the new month starts from now.

### IMPORTANT — What to Tell the User

When the user wants to upgrade to Pro, probe the activate endpoint first to get the exact treasury address and amount, then give the user these instructions. **Copy this template**, filling in the treasury address from the probe response:

---

**To upgrade to Pro (monthly billing), send a USDC payment on the Base network:**

1. Open your wallet app (MetaMask, Coinbase Wallet, Rainbow, etc.)
2. Make sure you're on the **Base** network (Chain ID 8453). If you don't have Base, add it: RPC `https://mainnet.base.org`, Chain ID `8453`, Symbol `ETH`, Explorer `https://basescan.org`
3. You need enough **USDC on Base**. USDC contract: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`
4. Send the **exact monthly USDC amount from the probe response** (or more) to: `{treasury address from probe}`
5. Wait for the transaction to confirm (usually 2-5 seconds on Base)
6. Copy the **transaction hash** and paste it here. It looks like: `0x1234abcd...5678` (66 characters starting with 0x)

**Where to find the transaction hash:**
- **MetaMask**: Click the transaction in Activity tab → "View on block explorer" → copy the "Transaction Hash" from the page.
- **Coinbase Wallet**: Tap the transaction in activity → copy the transaction hash or "View on Base" link
- **Basescan**: Go to `basescan.org`, search your wallet address, find the USDC transfer, copy the "Transaction Hash"

**Don't have USDC on Base?** You can bridge from Ethereum/other chains using the [Base Bridge](https://bridge.base.org) or buy USDC directly on Base through Coinbase.

---

After the user provides the transaction hash, call the activate endpoint with it to confirm.

### Endpoint: POST https://accessagent.ai/api/sites/{name}/activate

**Probe (no txHash — get payment info, no auth needed):**

```json
// Request
POST https://accessagent.ai/api/sites/my-game/activate
Content-Type: application/json
{}

// Response 402
{
  "success": false,
  "error": { "code": "PAYMENT_REQUIRED", "message": "Payment required for Pro tier (monthly)" },
  "payment": {
    "treasury": "0x...",
    "amount": "7000000",
    "amountFormatted": "$7.00",
    "period": "1 month",
    "token": "USDC",
    "network": "Base (Chain ID 8453)"
  }
}
```

**Submit payment (with txHash, bearer auth required):**

```json
// Request
POST https://accessagent.ai/api/sites/my-game/activate
Authorization: Bearer sk_...
Content-Type: application/json
{ "txHash": "0xabc123..." }

// Response 200
{
  "success": true,
  "data": { "name": "my-game", "activated": true, "tier": "pro", "expiresAt": "2027-02-24T...", "url": "https://my-game.accessagent.ai/", "message": "Pro tier active until 2027-02-24." }
}
```

**Error Codes:**

| Code | Status | Meaning |
|------|--------|---------|
| `PAYMENT_REQUIRED` | 402 | No txHash provided — payment info returned |
| `PAYMENT_VERIFICATION_FAILED` | 400 | Transaction not found, wrong chain, wrong amount, or wrong recipient |
| `TX_ALREADY_USED` | 400 | Transaction hash already used for another activation |
| `EMAIL_REQUIRED` | 400 | A verified email is required before activation |
| `UNAUTHORIZED` | 401 | Missing bearer token |
| `INVALID_KEY` | 401 | Wrong secret key |
| `SITE_NOT_FOUND` | 404 | Site doesn't exist |

### PayPal Monthly Subscription

PayPal is a simpler alternative to USDC — no wallet or crypto needed. The user approves a recurring subscription via PayPal's checkout, and Pro access is activated automatically after the first payment.

**Step-by-step flow:**

1. **Set + verify email** (required before PayPal — see Section 8 Prerequisites above)
2. **Start subscription**: `POST https://accessagent.ai/api/sites/{name}/billing/paypal/start` (bearer auth)
3. **Email is sent** to the site's verified email with a PayPal approval link
4. **User clicks the link** and approves the subscription in PayPal
5. **PayPal charges the first payment** and sends a webhook to our server (typically within 1-2 minutes of approval)
6. **Server activates Pro automatically** — no further API calls needed from the agent
7. **Check status** (optional): Poll `GET /api/sites/{name}/billing/status` — when `billingStatus` changes from `pending_first_payment` to `active`, Pro is live
8. **Subsequent months**: PayPal charges automatically, server renews via webhook — zero agent involvement
9. **Cancellation**: Users can cancel their subscription directly in their PayPal dashboard at paypal.com → Settings → Payments → Manage automatic payments

**Endpoint: POST https://accessagent.ai/api/sites/{name}/billing/paypal/start**

Requires bearer auth. Returns a PayPal approval URL.

```json
// Request
POST https://accessagent.ai/api/sites/my-game/billing/paypal/start
Authorization: Bearer sk_...

// Response 200
{
  "success": true,
  "data": {
    "subscriptionId": "I-XXXXXXXXX",
    "approvalUrl": "https://www.paypal.com/webapps/billing/subscriptions?...",
    "status": "pending_approval",
    "amount": "$7.00",
    "period": "1 month"
  }
}
```

**What to do with the response:** Give the `approvalUrl` to the user — they click it, log into PayPal, and approve. After approval, PayPal charges the first payment and sends a webhook to our server. Pro activates automatically. You can poll `GET /billing/status` to check when it goes active.

**Idempotent:** Calling start again while a subscription is pending returns the same approval URL (does not create duplicates).

**Error Codes:**

| Code | Status | Meaning |
|------|--------|---------|
| `EMAIL_REQUIRED` | 400 | No verified email — set + verify email first |
| `PAYPAL_DISABLED` | 503 | PayPal not enabled on this server |
| `PAYPAL_PLAN_UNAVAILABLE` | 503 | No PayPal plan configured — contact support |
| `PAYPAL_START_FAILED` | 500 | PayPal API error |
| `UNAUTHORIZED` | 401 | Missing bearer token |
| `INVALID_KEY` | 401 | Wrong secret key |

### Billing Status

Check current billing state, next payment due, and subscription details.

**Endpoint: GET https://accessagent.ai/api/sites/{name}/billing/status**

Requires bearer auth.

```json
// Request
GET https://accessagent.ai/api/sites/my-game/billing/status
Authorization: Bearer sk_...

// Response 200
{
  "success": true,
  "data": {
    "name": "my-game",
    "tier": "pro",
    "billingStatus": "active",
    "billingProvider": "paypal",
    "billingPeriod": "monthly",
    "currentPeriodStart": "2026-02-27T...",
    "currentPeriodEnd": "2026-03-27T...",
    "nextPaymentDue": "2026-03-27T...",
    "daysRemaining": 28,
    "renewalAmountFormatted": "$7.00",
    "cancelAtPeriodEnd": false,
    "graceEndsAt": null,
    "lastPaymentAt": "2026-02-27T...",
    "lastPaymentAmountFormatted": "$7.00",
    "subscriptionRef": "I-XXXXXXXXX",
    "activePriceVersion": {
      "usdcMonthlyAmountFormatted": "$7.00",
      "paypalMonthlyAmountFormatted": "$7.00"
    },
    "paypal": {
      "subscriptionId": "I-XXXXXXXXX",
      "status": "active"
    }
  }
}
```

**Key fields for agents:**

| Field | What it tells you |
|-------|-------------------|
| `nextPaymentDue` | ISO date when the next payment is due (null if free tier) |
| `daysRemaining` | Days until current period expires (0 = expires today, null if free) |
| `renewalAmountFormatted` | How much the next charge will be (null if canceled or free) |
| `cancelAtPeriodEnd` | If true, Pro expires at `currentPeriodEnd` and won't renew |
| `billingStatus` | Current state (see values below) |
| `billingProvider` | `"paypal"`, `"usdc"`, or null |

**Billing status values:** `free`, `pending_approval`, `pending_first_payment`, `active`, `grace`, `canceled`, `expired`

### Payment History

Get past payments for bookkeeping and accounting.

**Endpoint: GET https://accessagent.ai/api/sites/{name}/billing/history**

Requires bearer auth.

```json
// Request
GET https://accessagent.ai/api/sites/my-game/billing/history
Authorization: Bearer sk_...

// Response 200
{
  "success": true,
  "data": {
    "name": "my-game",
    "payments": [
      {
        "provider": "paypal",
        "amount": 700,
        "amountFormatted": "$7.00",
        "paidAt": "2026-02-27T15:30:00.000Z",
        "reference": "I-XXXXXXXXX"
      }
    ],
    "totalPaid": 700,
    "totalPaidFormatted": "$7.00",
    "paymentCount": 1
  }
}
```

Returns all payments for the site, most recent first. `totalPaid` is a running total across all payments — useful for showing lifetime spend.

### Email Verification (Required for Pro)

Email must be set and verified before upgrading to Pro (both USDC and PayPal).

**Step 1 — Set email** (via create or PATCH):

```json
PATCH https://accessagent.ai/api/sites/my-game
Authorization: Bearer sk_...
Content-Type: application/json
{ "email": "dev@example.com" }

// Response 200 — verification code sent to the email
{ "success": true, "data": { "name": "my-game", "updated": ["email"], "emailVerified": false } }
```

**Step 2 — Check email** for a 6-digit code (subject: "Verify your email — {site-name}"). The code expires after **15 minutes** and allows **5 attempts**.

**Step 3 — Verify the code:**

```json
POST https://accessagent.ai/api/sites/my-game/verify-email
Authorization: Bearer sk_...
Content-Type: application/json
{ "code": "123456" }

// Response 200
{ "success": true, "data": { "emailVerified": true } }
```

**Error Codes:**

| Code | Status | Meaning |
|------|--------|---------|
| `INVALID_CODE` | 400 | Wrong code |
| `CODE_EXPIRED` | 400 | Code expired (15 min TTL) — update email to get a new code |
| `TOO_MANY_ATTEMPTS` | 400 | 5 failed attempts — update email to get a new code |
| `NO_PENDING_VERIFICATION` | 400 | No pending verification for this site |
| `UNAUTHORIZED` | 401 | Missing bearer token |
| `INVALID_KEY` | 401 | Wrong secret key |

**Resending the code:** To get a new code, PATCH the email again (same or different address). This invalidates the old code and sends a fresh one.

### USDC vs PayPal — Which to Use

| | USDC on Base | PayPal |
|---|---|---|
| **User needs** | Crypto wallet + USDC on Base | PayPal account |
| **Activation** | Instant (after tx confirmation) | After PayPal approval + first charge |
| **Auto-renew** | No — user must pay each month | Yes — PayPal charges automatically |
| **Agent workflow** | Probe → user pays → confirm txHash | Start → user clicks approval link → automatic |

**Recommendation:** If the user has a crypto wallet, use USDC — it's faster and instant. If they prefer traditional payment, use PayPal.

### Complete Activation Flow (Code)

```typescript
const secretKey = readFileSync(".accessagent/key.txt", "utf-8").trim();
const SITE_NAME = "my-game";

// Step 1: Probe for payment details (no auth needed)
const probeRes = await fetch(\`https://accessagent.ai/api/sites/\${SITE_NAME}/activate\`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: "{}",
});
const probe = await probeRes.json();
// probe.payment.treasury = "0x..." (the address to send USDC to)
// probe.payment.amountFormatted = "$7.00"

// Step 2: Tell the user to send the monthly USDC amount on Base to probe.payment.treasury
// Wait for user to provide the transaction hash...

// Step 3: Confirm with the server
const txHash = "0x..."; // from user
const res = await fetch(\`https://accessagent.ai/api/sites/\${SITE_NAME}/activate\`, {
  method: "POST",
  headers: {
    "Authorization": \`Bearer \${secretKey}\`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ txHash }),
});
const result = await res.json();
// result.success = true → Pro tier activated
```

### Common User Issues

| Issue | What to Tell the User |
|-------|----------------------|
| "I sent USDC but on Ethereum mainnet" | Payment must be on **Base** (chain 8453), not Ethereum mainnet. You'll need to bridge to Base and send again. |
| "I sent USDT instead of USDC" | Only **USDC** is accepted. Swap USDT→USDC on Base and send again. |
| "Transaction is pending" | Wait for it to confirm (usually 2-5 seconds on Base). Then try submitting the hash again. |
| "PAYMENT_VERIFICATION_FAILED" | Server checks: (1) tx exists on Base, (2) it's a USDC transfer, (3) recipient is the treasury, (4) amount >= the quoted monthly amount. |
| "I don't have a wallet" | Download [Coinbase Wallet](https://www.coinbase.com/wallet) or [MetaMask](https://metamask.io). Fund with USDC on Base. |

---

## 9. Coupons

Coupon codes can be applied when creating a site to get free or discounted activation.

### How Coupons Work

- Pass the `coupon` field in the `POST /api/sites` form data along with your zip upload
- **100% discount**: Site is auto-activated at creation (no payment needed)
- **Partial discount** (e.g. 50%): Site is created unactivated, but probing `POST /api/sites/{name}/activate` returns the reduced price
- Coupons may also override the default bandwidth limit (e.g. 50 GB instead of 10 GB)
- Invalid or expired coupons return a 400 error and the site is NOT created

### Create with Coupon Example

```typescript
const form = new FormData();
form.append("name", "my-game");
form.append("file", new Blob([zipFile], { type: "application/zip" }), "site.zip");
form.append("coupon", "MY-COUPON-CODE");

const res = await fetch("https://accessagent.ai/api/sites", { method: "POST", body: form });
// If coupon is 100% off: { success: true, data: { name, url, activated: true, secretKey: "sk_...", ... } }
// If coupon is invalid: { success: false, error: { code: "INVALID_COUPON" } }
```

### Metadata

`GET /api/sites/{name}` returns `hasCoupon: true` if a coupon was applied, but never reveals the coupon code.

---

## 10. Custom Domains (Pro Only)

Point any domain you own to your AccessAgent site. **Requires Pro tier** (monthly billing). SSL is automatically provisioned via Let's Encrypt on the first HTTPS visit.

### How Custom Domains Work

1. Add a **CNAME record** in your DNS registrar pointing your domain to `accessagent.ai`
2. Call `POST https://accessagent.ai/api/sites/{name}/domain` with the domain — server verifies DNS is pointing correctly
3. On the first HTTPS request to that domain, an SSL certificate is automatically provisioned
4. Your site is live at `https://yourdomain.com/`

### DNS Setup

**Use CNAME (recommended)** — if our server IP ever changes, your domain automatically follows:

| Record Type | Name | Value |
|-------------|------|-------|
| CNAME | www | accessagent.ai |
| CNAME | subdomain (e.g. game) | accessagent.ai |

**Root/apex domains** (e.g. `example.com` without `www`): Most DNS providers don't allow CNAME on the root domain. Options:
- If your DNS provider supports **ALIAS** or **ANAME** records (Cloudflare, Route 53, DNSimple), use that to point to `accessagent.ai`
- Otherwise, use an **A record** pointing to our current IP — but note this will break if the IP changes

DNS propagation typically takes 5-60 minutes. The API verifies DNS before accepting the domain.

**Important notes:**
- **Cloudflare users**: Set the DNS record to **DNS only** (gray cloud), not **Proxied** (orange cloud). Cloudflare's proxy hides the CNAME and breaks verification + SSL provisioning.
- **First visit after setup**: May take a few extra seconds while the SSL certificate is provisioned.
- **Wait for propagation**: If the API returns `DNS_NOT_CONFIGURED`, your DNS changes may not have propagated yet. Wait a few minutes and try again.

### Assign a Domain

**Endpoint**: POST https://accessagent.ai/api/sites/{name}/domain

```typescript
const secretKey = readFileSync(".accessagent/key.txt", "utf-8").trim();

const res = await fetch("https://accessagent.ai/api/sites/my-game/domain", {
  method: "POST",
  headers: {
    "Authorization": \`Bearer \${secretKey}\`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ domain: "mycoolsite.com" }),
});
// { success: true, data: { name: "my-game", domain: "mycoolsite.com", message: "..." } }
```

### Remove a Domain

**Endpoint**: DELETE https://accessagent.ai/api/sites/{name}/domain

```typescript
await fetch("https://accessagent.ai/api/sites/my-game/domain", {
  method: "DELETE",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
});
```

### Domain Error Codes

| Code | Status | Meaning |
|------|--------|---------|
| `PRO_REQUIRED` | 403 | Custom domains require Pro tier |
| `DNS_NOT_CONFIGURED` | 400 | Domain CNAME/A record doesn't point to accessagent.ai |
| `RESERVED_DOMAIN` | 400 | Can't assign platform domains (accessagent.ai, gamemint.fun) |
| `DOMAIN_TAKEN` | 409 | Domain already assigned to another site |
| `INVALID_DOMAIN` | 400 | Invalid domain format |
| `UNAUTHORIZED` | 401 | Missing bearer token |

---

## 11. Site Metadata & Analytics

### Public metadata: GET https://accessagent.ai/api/sites/{name}

No authentication required. Returns public metadata only:

```json
{
  "success": true,
  "data": {
    "name": "my-game",
    "activated": true,
    "tier": "pro",
    "activatedAt": "2026-02-24T10:30:00.000Z",
    "expiresAt": "2027-02-24T10:30:00.000Z",
    "url": "https://my-game.accessagent.ai/",
    "domain": "mycoolsite.com",
    "hasCoupon": false,
    "category": "game",
    "description": "A retro-style platformer built with Phaser",
    "bandwidthLimitGB": 100,
    "storageLimitMB": 200,
    "maxFiles": 2000,
    "maxFileSizeMB": 25,
    "size": "2.4MB",
    "fileCount": 42,
    "createdAt": "2026-02-24T10:30:00.000Z",
    "updatedAt": "2026-02-24T14:00:00.000Z"
  }
}
```

> **Note:** Sensitive fields like `email`, `bandwidthUsedBytes`, `sizeBytes`, and analytics are only available via the authenticated analytics endpoint below.

### Owner analytics: POST https://accessagent.ai/api/sites/{name}/analytics

**Requires bearer auth.** Returns email, bandwidth usage, storage, and visitor analytics.

```typescript
const res = await fetch("https://accessagent.ai/api/sites/my-game/analytics", {
  method: "POST",
  headers: { "Authorization": \`Bearer \${secretKey}\` },
});
```

**Response:**
```json
{
  "success": true,
  "data": {
    "email": "dev@example.com",
    "bandwidthUsedBytes": 524288000,
    "bandwidthUsedFormatted": "500.0MB",
    "bandwidthLimitBytes": 107374182400,
    "sizeBytes": 2516582,
    "analytics": {
      "today": {
        "date": "2026-02-26",
        "pageViews": 142,
        "uniqueVisitors": 87,
        "topPages": [
          { "path": "/", "views": 80 },
          { "path": "/about", "views": 32 }
        ]
      },
      "allTime": { "pageViews": 5432 }
    }
  }
}
```

### Update metadata: PATCH https://accessagent.ai/api/sites/{name}

**Requires bearer auth.** Update category, description, or email without re-uploading.

```typescript
const res = await fetch("https://accessagent.ai/api/sites/my-game", {
  method: "PATCH",
  headers: {
    "Authorization": \`Bearer \${secretKey}\`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    category: "portfolio",
    description: "My updated site description",
    email: "newemail@example.com",
  }),
});
```

Include any combination of `category`, `description`, `email` — at least one is required.

**Valid categories:** `game`, `portfolio`, `landing-page`, `docs`, `blog`, `app`, `tool`, `other`

---

## 12. Key Rotation

If you suspect your secret key has been compromised, rotate it immediately.

### Endpoint: POST https://accessagent.ai/api/sites/{name}/rotate-key

```typescript
const oldKey = readFileSync(".accessagent/key.txt", "utf-8").trim();

const res = await fetch("https://accessagent.ai/api/sites/my-game/rotate-key", {
  method: "POST",
  headers: { "Authorization": \`Bearer \${oldKey}\` },
});
const data = await res.json();

// Save the new key — the old key is now invalid
writeFileSync(".accessagent/key.txt", data.data.secretKey);
console.log("Key rotated. New key saved.");
```

**Response:**
```json
{
  "success": true,
  "data": {
    "name": "my-game",
    "secretKey": "sk_new_key_here...",
    "message": "New key active. Old key is now invalid."
  },
  "_warning": "Store this secret key safely. It cannot be retrieved later."
}
```

The old key is immediately invalidated. Make sure to save the new key before discarding the old one.

---

## 13. Zip File Requirements

Your zip file must follow these rules:

| Rule | Limit |
|------|-------|
| **Max compressed size** | 210MB |
| **Max extracted size** | 40MB (free), 200MB (Pro) |
| **Max file count** | 100 (free), 2,000 (Pro) |
| **Max single file** | 5MB (free), 25MB (Pro) |
| **Max path depth** | 10 directory levels |
| **Must include** | `index.html` at root level |
| **No symlinks** | Symbolic links are rejected |
| **No path traversal** | `../` in paths is rejected |

### Allowed File Extensions

`.html` `.htm` `.js` `.mjs` `.css` `.json` `.png` `.jpg` `.jpeg` `.gif` `.webp` `.ico` `.svg` `.woff` `.woff2` `.ttf` `.otf` `.mp3` `.ogg` `.wav` `.map` `.txt` `.xml` `.webmanifest`

Files with other extensions (e.g., `.php`, `.py`, `.exe`) are rejected.

### Creating a Zip

```bash
# From your site directory:
cd my-site/
zip -r ../site.zip .

# Or with specific files:
zip site.zip index.html style.css app.js assets/*
```

Make sure `index.html` is at the **root** of the zip (not inside a subdirectory).

### Creating a Zip Programmatically (Node.js/Bun)

Use `adm-zip` (`npm install adm-zip`):

```typescript
import AdmZip from "adm-zip";

const zip = new AdmZip();
zip.addFile("index.html", Buffer.from("<html><body>Hello</body></html>"));
zip.addFile("style.css", Buffer.from("body { color: red; }"));
zip.addLocalFolder("./dist"); // Or add an entire directory

const zipBuffer = zip.toBuffer();
// Use as: new Blob([zipBuffer], { type: "application/zip" })
```

---

## 14. Complete Step-by-Step Process

### Quick Path

1. **Build your site** — HTML, JS, CSS, assets
2. **Zip it**: `zip -r site.zip .` (must contain `index.html` at root)
3. **Upload**: `POST https://accessagent.ai/api/sites` with `name` + `file` fields. Include `coupon` if you have one.
4. **Save the secret key** from the response to `.accessagent/key.txt`
5. Site is live at `https://{name}.accessagent.ai/`
6. **Optional — Set email**: `PATCH /api/sites/{name}` with `{ "email": "dev@example.com" }` + verify it
7. **Optional — Upgrade to Pro (monthly)**: Probe `POST /api/sites/{name}/activate` for treasury address + amount. Ask user to send the monthly USDC amount on Base. Confirm with txHash + bearer auth. (Email verification required first.)
8. **Optional — Custom domain** (Pro only): Add CNAME to `accessagent.ai` → `POST /api/sites/{name}/domain`

### Full Script (Complete Working Example)

Save as `deploy.ts` and run with `bun deploy.ts` or `npx tsx deploy.ts`:

```typescript
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "fs";
import { execSync } from "child_process";

// Configuration
const SITE_NAME = process.env.SITE_NAME || "my-site";
const SITE_DIR = process.env.SITE_DIR || "./dist";
const COUPON = process.env.COUPON || "";
const API_BASE = "https://accessagent.ai";
const KEY_FILE = ".accessagent/key.txt";

async function deploy() {
  // Step 1: Create zip
  console.log("Zipping", SITE_DIR, "...");
  execSync(\`cd \${SITE_DIR} && zip -r ../site.zip .\`);
  const zipFile = readFileSync("site.zip");

  // Check if we already have a key (existing site → update)
  if (existsSync(KEY_FILE)) {
    const secretKey = readFileSync(KEY_FILE, "utf-8").trim();
    console.log("Found existing key, updating site...");

    const form = new FormData();
    form.append("file", new Blob([zipFile], { type: "application/zip" }), "site.zip");

    const res = await fetch(\`\${API_BASE}/api/sites/\${SITE_NAME}\`, {
      method: "PUT",
      headers: { "Authorization": \`Bearer \${secretKey}\` },
      body: form,
    });
    const data = await res.json();

    if (data.success) {
      console.log("Updated:", data.data.url);
    } else {
      console.error("Update failed:", data.error);
    }
    return;
  }

  // No key → create new site
  console.log("Creating new site:", SITE_NAME);
  const form = new FormData();
  form.append("name", SITE_NAME);
  form.append("file", new Blob([zipFile], { type: "application/zip" }), "site.zip");
  if (COUPON) form.append("coupon", COUPON);

  const res = await fetch(\`\${API_BASE}/api/sites\`, { method: "POST", body: form });
  const data = await res.json();

  if (data.success) {
    console.log("Live at:", data.data.url);
    if (data.data.activated) console.log("Auto-activated via coupon!");

    // Save secret key
    mkdirSync(".accessagent", { recursive: true });
    writeFileSync(KEY_FILE, data.data.secretKey);

    // Add to .gitignore
    const gitignore = existsSync(".gitignore") ? readFileSync(".gitignore", "utf-8") : "";
    if (!gitignore.includes(".accessagent")) {
      appendFileSync(".gitignore", "\n.accessagent/\n");
    }

    console.log("Secret key saved to", KEY_FILE);
    console.log("IMPORTANT: Back up .accessagent/key.txt — losing it means losing access to your site.");
  } else {
    console.error("Failed:", data.error);
  }
}

deploy().catch(console.error);
```

Run: `SITE_NAME=my-game SITE_DIR=./dist bun deploy.ts`

**No dependencies required.** This script uses only Node.js built-ins (`fs`, `child_process`) and the global `fetch` API. No ethers.js, no npm install.

---

## Summary

1. Build your site (HTML, JS, CSS, assets)
2. Zip it (`index.html` must be at root)
3. `POST https://accessagent.ai/api/sites` with `name` + `file` fields
4. **Save the `secretKey` from the response** to `.accessagent/key.txt` — add `.accessagent/` to `.gitignore`
5. **Tell the user to back up `.accessagent/key.txt`** — losing the key means losing access
6. Site is live at `https://{name}.accessagent.ai/` — free tier: 40MB storage, 10GB/month bandwidth
7. **Update**: `PUT /api/sites/{name}` with new zip + `Authorization: Bearer sk_...`
8. **Delete**: `DELETE /api/sites/{name}` with `Authorization: Bearer sk_...`
9. **Upgrade to Pro ($7/month)** (optional):
   a. Set email: `PATCH /api/sites/{name}` with `{ "email": "..." }` + bearer auth
   b. Verify email: `POST /api/sites/{name}/verify-email` with `{ "code": "123456" }` + bearer auth
   c. **Option A — USDC**: Probe `POST /api/sites/{name}/activate` (no auth) → user sends USDC on Base → confirm with `{ "txHash": "0x..." }` + bearer auth
   d. **Option B — PayPal**: `POST /api/sites/{name}/billing/paypal/start` + bearer auth → give user the `approvalUrl` → Pro activates automatically after PayPal payment
10. **Check billing**: `GET /api/sites/{name}/billing/status` + bearer auth — includes `nextPaymentDue`, `daysRemaining`, `renewalAmountFormatted`
11. **Payment history**: `GET /api/sites/{name}/billing/history` + bearer auth — past payments for accounting
12. **Custom domain** (Pro only): CNAME → `accessagent.ai`, then `POST /api/sites/{name}/domain` with bearer auth

**No wallets required. No ethers.js. No crypto libraries. Just a secret key and `fetch`.**

**API Guide URL**: https://accessagent.ai/api/guide
