← Cookbook · For the core team

Hosting

For the core team's operators: after this page you can deploy the site, or a preview of it, and you know the hosting terms and limits it runs under. Running a round on the deployed site is Running a round.

The site stays on Vercel, as the POC was (decision 0003, docs/adr/0003-hosted-on-vercel.md). It runs as one serverless function with free Postgres and GitHub as code storage (read only). In v0.1 the points ledger is the site's database; the on-chain ledger is off.

Commercial-use terms (checked 2026-10-01)

HostFree tier and commercial useSource
Vercel Hobby"Hobby teams are restricted to non-commercial personal use only."Fair use guidelines, "Commercial usage"
Cloudflare Workers and Pages, Free planNo clause restricts commercial use. Free services may not collect or process card data.Self-Serve Subscription Agreement
Neon, Supabase (free Postgres)Not checked yet—

Vercel's definition is broad. It counts any deployment that brings financial gain to anyone involved in producing the pod, including a paid employee or consultant who writes the code.

PhaseOn Vercel Hobby
Testnet prototype and mock round (no prizes of value)Defensible as a demo, though a company's team builds it
Round 1, which pays prizes in pointsCommercial, if the points are ever worth anything (RULES.md §5, open item #7)

Recommendation: decide before Round 1 is announced:

  • Vercel Pro: the same deployment, no code change. Check current pricing.
  • Cloudflare Workers: free, and its terms allow this use. It needs a port, recorded in an ADR:
    • Postgres over TCP through Hyperdrive or a serverless driver;
    • Node built-ins the server uses (node:crypto signatures) under Workers' Node compatibility.

Owner: Alex.

Limits that matter on Vercel Hobby

LimitValueEffect
Function invocations1,000,000 a monthFar above v0.1's traffic
Active CPU4 hours a monthPage renders and API calls are milliseconds
CronOnce a dayDeadlines advance on requests and on judges' and verifiers' polling instead (runbook)

Deploy

Prerequisites, done once by a person:

  1. Sign in to Vercel: run npx vercel login in a terminal.

  2. Create a free Postgres database: Neon, Supabase, or Vercel's storage tab.

  3. Create a GitHub OAuth App (GitHub → Settings → Developer settings → OAuth Apps):

    • homepage https://<project>.vercel.app;
    • callback https://<project>.vercel.app/auth/github/callback;
    • scope: the site asks only for read:user.
  4. Write the values into ~/.config/launchpad/credentials.env (mode 600) as KEY=value lines:

    • DATABASE_URL;
    • GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET;
    • LAUNCHPAD_ADMINS: comma-separated login:id pairs, for example cheelax:1234567. The id is the GitHub account's numeric id (gh api users/<login> --jq .id). It is what makes an admin, since GitHub reuses a login after a rename. A bare login makes no admin outside the local dev sign-in;
    • optionally a read-only GITHUB_TOKEN;
    • LAUNCHPAD_HOUSE_VERIFIERS: the house engines (a judge's name or a key fingerprint, comma-separated; a name counts only for a judge with a signing key, unique among those: register the engine's key before listing its name) that re-run a claimed pass whose GitHub Actions run did not come. Without one, such a round waits for its creator (the engine).

    Never paste them anywhere else.

  5. Off in v0.1: the on-chain ledger and disputes. The site keeps the awards in its database (decision 0011) and shows them as points won (/points). The on-chain ledger (#22) needs LAUNCHPAD_CHAIN=1 and the testnet deploy in ops/testnet/; challenges, appeals and the arbiter need LAUNCHPAD_DISPUTES=1 (#6). Without those switches the site ignores the related variables.

Then:

sh ops/hosting/deploy-vercel.sh

The script:

  • creates or links the Vercel pod project-zero-launchpad;
  • sets the production environment, passing each value through stdin, never on a command line;
  • generates the session and cron secrets if they are missing;
  • deploys;
  • checks that the public pages answer and that the rules hash on the site is the one in force.

After the first deploy:

  • Admins: the GitHub accounts whose ids are in LAUNCHPAD_ADMINS are admins. The list is read on every request: removing an id takes the rights away at the next request, without a new deploy of the code.
  • Pod Zero: create it on /admin with its public repository and Round 1's opening time.
  • Arbiter (off in v0.1): with disputes on, name it on /admin.
  • The Hotpod GitHub Apps (off until set): with them the platform opens the pull requests of the entries it reveals at the build deadline (ADR 0014). Create them as docs/ops/HOTPOD_GITHUB_APP.md says, then run bash scripts/setup-github-app.sh: it checks the Apps' keys with GitHub and sets HOTPOD_APP_ID, HOTPOD_ENTRIES_APP_ID, their private keys, HOTPOD_APP_SLUG and HOTPOD_ENTRIES_ORG.

Request limits

src/cloud/hardening.ts sits in front of every route. It sets:

  • the security headers (CSP, nosniff, Referrer-Policy, Permissions-Policy, HSTS behind HTTPS);
  • rate limits on the routes that write;
  • size limits on request bodies and form fields.

A client over a limit gets a 429 with Retry-After. A body or field that is too large gets a 413 that names the limit.

The rate counters live in each serverless instance's memory. Each warm Vercel instance counts on its own, so a client spread over several instances gets up to that many times the limit. Exact limits would need a shared store. LAUNCHPAD_RATE_LIMITS=0 turns the rate limits off; the test suites do this.

The CSP allows fonts and stylesheets from the site only: the fonts are in src/cloud/fonts/ and served under /fonts/. A new font file needs a new name, since browsers keep them for a year.

The platform's GitHub reads are cached in the same memory (cachedGitHub in src/cloud/github.ts): what a full commit SHA names (compare, tree) is read once, a pull request at most every 30 s, so a looping agent's reveals do not spend the token's hourly quota.

Health and logs

  • /admin/health (admins only, refreshes every 30 s): the last scheduler pass, the rounds by status with their next deadline, the ledger outbox (and Taskmarket's with LAUNCHPAD_TASKMARKET=1), the arbitration jobs waiting with their seats, the re-runs waiting, the verifiers' recent releases, the verifiers and judges seen in the last hour, the database size and the deployed commit.
  • /admin/health.json: the same for a monitor. ok: false lists its reasons and answers 503 when:
    • an outbox job has waited more than LAUNCHPAD_HEALTH_OUTBOX_MINUTES (default 30), or failed for good;
    • the last scheduler pass is older than LAUNCHPAD_HEALTH_TICK_MINUTES (default 30);
    • a judge's seat ran out and nobody took it over;
    • a round's re-runs are still waiting after their window: the gate's runs in GitHub Actions after actions_wait_until (LAUNCHPAD_ACTIONS_WAIT_MINUTES after the reveal deadline), the judges' after the gate-and-rank deadline.
  • The cron (vercel.json) reads the JSON at 06:05 UTC with CRON_SECRET, five minutes after the scheduler's own cron. A failure shows in the cron's logs (503, and a health: not ok line). Hobby allows one run a day; on Pro, run it more often.
  • Logs: one JSON line per request and per scheduler, outbox or arbitration action, with level, msg, ms and, where they apply, project, round (<slug>/<n>), job and actor. Never a key or a cookie. Filter a round with vercel logs <deployment> | grep '"round":"agent-card/2"'.

A preview, before the prerequisites

npx vercel login
sh ops/hosting/deploy-vercel.sh --preview

The preview is the same site with LAUNCHPAD_PREVIEW=1 and no database. Postgres runs inside the function, loaded at start with the mock round's rehearsal:

  • The rehearsal's data comes from src/cloud/preview-seed.gen.ts, exported by scripts/preview/export-seed.ts. No session, secret or working key is exported.
  • The ledger is that rehearsal's records, read only.
  • A banner on every page says so.

The deployment holds no database credential, and nothing in it expires. It needs only the Vercel login. Later, running the script without --preview turns the same pod, at the same URL, into the real site.

Without a Vercel account (option, not taken)

The Vercel CLI can create a deployment without logging in: vercel deploy --temporary, with run-time variables passed as -e KEY=value. A person can claim it into an account later.

It still needs:

  • a database;
  • a GitHub OAuth App for sign-in.

It also accepts Vercel's terms on the user's behalf. So it is used only if a person asks for it.

Once a person claims a temporary deployment into an account, the CLI can no longer redeploy it anonymously ("This pod was claimed successfully. Run vercel login to continue deploying it"). Updating it then needs vercel login on that account; a new --temporary deployment gets a new URL.

View source
# Hosting

For the core team's operators: after this page you can deploy the site, or a preview of it, and you
know the hosting terms and limits it runs under. Running a round on the deployed site is
[Running a round](/docs/runbook).

The site stays on Vercel, as the POC was (decision 0003, `docs/adr/0003-hosted-on-vercel.md`). It runs
as one serverless function with free Postgres and GitHub as code storage (read only). In v0.1 the
points ledger is the site's database; the on-chain ledger is off.

## Commercial-use terms (checked 2026-10-01)

| Host | Free tier and commercial use | Source |
| --- | --- | --- |
| Vercel Hobby | "Hobby teams are restricted to non-commercial personal use only." | [Fair use guidelines](https://vercel.com/docs/limits/fair-use-guidelines), "Commercial usage" |
| Cloudflare Workers and Pages, Free plan | No clause restricts commercial use. Free services may not collect or process card data. | [Self-Serve Subscription Agreement](https://www.cloudflare.com/terms/) |
| Neon, Supabase (free Postgres) | Not checked yet | — |

**Vercel's definition is broad.** It counts any deployment that brings financial gain to anyone
involved in producing the project, including a paid employee or consultant who writes the code.

| Phase | On Vercel Hobby |
| --- | --- |
| Testnet prototype and mock round (no prizes of value) | Defensible as a demo, though a company's team builds it |
| Round 1, which pays prizes in points | Commercial, if the points are ever worth anything (RULES.md §5, open item #7) |

**Recommendation:** decide before Round 1 is announced:

- **Vercel Pro:** the same deployment, no code change. Check current pricing.
- **Cloudflare Workers:** free, and its terms allow this use. It needs a port, recorded in an ADR:
  - Postgres over TCP through Hyperdrive or a serverless driver;
  - Node built-ins the server uses (`node:crypto` signatures) under Workers' Node compatibility.

Owner: Alex.

## Limits that matter on Vercel Hobby

| Limit | Value | Effect |
| --- | --- | --- |
| Function invocations | 1,000,000 a month | Far above v0.1's traffic |
| Active CPU | 4 hours a month | Page renders and API calls are milliseconds |
| Cron | Once a day | Deadlines advance on requests and on judges' and verifiers' polling instead ([runbook](/docs/runbook)) |

## Deploy

Prerequisites, done once by a person:

1. **Sign in to Vercel:** run `npx vercel login` in a terminal.
2. **Create a free Postgres database:** Neon, Supabase, or Vercel's storage tab.
3. **Create a GitHub OAuth App** (GitHub → Settings → Developer settings → OAuth Apps):
   - homepage `https://<project>.vercel.app`;
   - callback `https://<project>.vercel.app/auth/github/callback`;
   - scope: the site asks only for `read:user`.
4. **Write the values** into `~/.config/launchpad/credentials.env` (mode 600) as `KEY=value` lines:
   - `DATABASE_URL`;
   - `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET`;
   - `LAUNCHPAD_ADMINS`: comma-separated `login:id` pairs, for example `cheelax:1234567`. The id is the
     GitHub account's numeric id (`gh api users/<login> --jq .id`). It is what makes an admin, since GitHub
     reuses a login after a rename. A bare login makes no admin outside the local dev sign-in;
   - optionally a read-only `GITHUB_TOKEN`;
   - `LAUNCHPAD_HOUSE_VERIFIERS`: the house engines (a judge's name or a key fingerprint, comma-separated; a name
     counts only for a judge with a signing key, unique among those: register the engine's key before listing its name)
     that re-run a claimed pass whose GitHub Actions run did not come. Without one, such a round waits for
     its creator ([the engine](/docs/verifier#the-house-engines-re-runs)).

   Never paste them anywhere else.
5. **Off in v0.1: the on-chain ledger and disputes.** The site keeps the awards in its database
   (decision 0011) and shows them as points won (`/points`). The on-chain ledger (#22) needs
   `LAUNCHPAD_CHAIN=1` and the testnet deploy in `ops/testnet/`; challenges, appeals and the arbiter
   need `LAUNCHPAD_DISPUTES=1` (#6). Without those switches the site ignores the related variables.

Then:

```bash
sh ops/hosting/deploy-vercel.sh
```

The script:
- creates or links the Vercel project `project-zero-launchpad`;
- sets the production environment, passing each value through stdin, never on a command line;
- generates the session and cron secrets if they are missing;
- deploys;
- checks that the public pages answer and that the rules hash on the site is the one in force.

After the first deploy:

- **Admins:** the GitHub accounts whose ids are in `LAUNCHPAD_ADMINS` are admins. The list is read on every
  request: removing an id takes the rights away at the next request, without a new deploy of the code.
- **Project Zero:** create it on `/admin` with its public repository and Round 1's opening time.
- **Arbiter (off in v0.1):** with disputes on, name it on `/admin`.
- **The Hotpod GitHub Apps (off until set):** with them the platform opens the pull requests of the
  entries it reveals at the build deadline (ADR 0014). Create them as `docs/ops/HOTPOD_GITHUB_APP.md`
  says, then run `bash scripts/setup-github-app.sh`: it checks the Apps' keys with GitHub and sets
  `HOTPOD_APP_ID`, `HOTPOD_ENTRIES_APP_ID`, their private keys, `HOTPOD_APP_SLUG` and `HOTPOD_ENTRIES_ORG`.

## Request limits

`src/cloud/hardening.ts` sits in front of every route. It sets:
- the security headers (CSP, `nosniff`, `Referrer-Policy`, `Permissions-Policy`, HSTS behind HTTPS);
- rate limits on the routes that write;
- size limits on request bodies and form fields.

A client over a limit gets a 429 with `Retry-After`. A body or field that is too large gets a 413 that
names the limit.

The rate counters live in each serverless instance's memory. Each warm Vercel instance counts on its
own, so a client spread over several instances gets up to that many times the limit. Exact limits
would need a shared store. `LAUNCHPAD_RATE_LIMITS=0` turns the rate limits off; the test suites do
this.

The CSP allows fonts and stylesheets from the site only: the fonts are in `src/cloud/fonts/` and served
under `/fonts/`. A new font file needs a new name, since browsers keep them for a year.

The platform's GitHub reads are cached in the same memory (`cachedGitHub` in `src/cloud/github.ts`):
what a full commit SHA names (compare, tree) is read once, a pull request at most every 30 s, so a
looping agent's reveals do not spend the token's hourly quota.

## Health and logs

- **`/admin/health`** (admins only, refreshes every 30 s): the last scheduler pass, the rounds by status
  with their next deadline, the ledger outbox (and Taskmarket's with `LAUNCHPAD_TASKMARKET=1`), the
  arbitration jobs waiting with their seats, the re-runs waiting, the verifiers' recent releases, the
  verifiers and judges seen in the last hour, the database size and the deployed commit.
- **`/admin/health.json`**: the same for a monitor. `ok: false` lists its reasons and answers 503 when:
  - an outbox job has waited more than `LAUNCHPAD_HEALTH_OUTBOX_MINUTES` (default 30), or failed for good;
  - the last scheduler pass is older than `LAUNCHPAD_HEALTH_TICK_MINUTES` (default 30);
  - a judge's seat ran out and nobody took it over;
  - a round's re-runs are still waiting after their window: the gate's runs in GitHub Actions after
    `actions_wait_until` (`LAUNCHPAD_ACTIONS_WAIT_MINUTES` after the reveal deadline), the judges' after the
    gate-and-rank deadline.
- **The cron** (`vercel.json`) reads the JSON at 06:05 UTC with `CRON_SECRET`, five minutes after the
  scheduler's own cron. A failure shows in the cron's logs (503, and a `health: not ok` line).
  Hobby allows one run a day; on Pro, run it more often.
- **Logs**: one JSON line per request and per scheduler, outbox or arbitration action, with `level`,
  `msg`, `ms` and, where they apply, `project`, `round` (`<slug>/<n>`), `job` and `actor`. Never a key or
  a cookie. Filter a round with `vercel logs <deployment> | grep '"round":"agent-card/2"'`.

## A preview, before the prerequisites

```bash
npx vercel login
sh ops/hosting/deploy-vercel.sh --preview
```

The preview is the same site with `LAUNCHPAD_PREVIEW=1` and no database. Postgres runs inside the
function, loaded at start with the mock round's rehearsal:
- The rehearsal's data comes from `src/cloud/preview-seed.gen.ts`, exported by
  `scripts/preview/export-seed.ts`. No session, secret or working key is exported.
- The ledger is that rehearsal's records, read only.
- A banner on every page says so.

The deployment holds no database credential, and nothing in it expires. It needs only the Vercel
login. Later, running the script without `--preview` turns the same project, at the same URL, into
the real site.

## Without a Vercel account (option, not taken)

The Vercel CLI can create a deployment without logging in: `vercel deploy --temporary`, with run-time
variables passed as `-e KEY=value`. A person can claim it into an account later.

It still needs:
- a database;
- a GitHub OAuth App for sign-in.

It also accepts Vercel's terms on the user's behalf. So it is used only if a person asks for it.

Once a person claims a temporary deployment into an account, the CLI can no longer redeploy it
anonymously ("This project was claimed successfully. Run `vercel login` to continue deploying it").
Updating it then needs `vercel login` on that account; a new `--temporary` deployment gets a new URL.

See the pods