Files
api-company/MASTER_BUILD_PROMPT.md
T
0xWheatyz 40631fee7c chore: bootstrap repo scaffolding
Initial scaffolding committed by bootstrap script. See ROADMAP.md
for phase plan and SESSION_LOG.md (api-company only) for context.
2026-05-18 17:18:47 -04:00

28 KiB
Raw Blame History

MASTER BUILD PROMPT — Recursive API Company Bootstrap

For Claude Code | Talos/Kubernetes + RapidAPI + Free Public Data


CONTEXT & CONSTRAINTS

You are building a production API business from scratch for a developer who:

  • Runs a multi-site Talos Kubernetes cluster (homelab + VPS tunnel for static IP)
  • Will list APIs on RapidAPI marketplace at age 18 (target launch date ~30 days out)
  • Target: $100/mo net profit with <40 hrs/mo maintenance
  • Has zero hosting budget (existing infra only)
  • PayPal account for RapidAPI payouts goes live on 18th birthday

Stack:

  • Kubernetes with Nginx or Traefik ingress already running
  • VPS acts as a WireGuard/tunnel endpoint with static IP
  • Node.js or Python (use whichever fits the task better)
  • Gitea at gitea.leeworks.dev for source control — NOT GitHub
  • Gitea Actions for CI (same YAML syntax as GitHub Actions, runs on Gitea runners)
  • Flux CD handles ALL Kubernetes deployments — never run helm upgrade manually
  • Helm charts committed to git; Flux watches and reconciles automatically
  • OpenAPI spec (YAML) is the single source of truth for all documentation

PRIME DIRECTIVE

This is a recursive build loop. You will never stop unless explicitly told to.

The loop is:

RESEARCH → PLAN → SPEC → BUILD → TEST → COMMIT → (Flux deploys) → DOCUMENT → RESEARCH → ...

When you exhaust your context window on building, switch to research. When you exhaust research, return to building. You never idle. You never ask "what should I do next?" — you decide and do it.


PHASE 0 — ORIENTATION (run once at start, then never again)

Before touching any code, do all of the following:

  1. Audit existing cluster state

    • kubectl get nodes — document node count, roles, resource totals
    • kubectl get namespaces — identify existing workloads
    • kubectl get ingress -A — understand current ingress setup
    • kubectl top nodes — check available headroom
    • flux get kustomizations — understand what Flux is already managing
    • flux get helmreleases -A — list Helm releases under Flux control
    • Save findings to ./docs/cluster-audit.md
  2. Check existing tooling

    • node --version
    • python3 --version
    • helm version
    • flux version
    • kubectl config current-context
    • Confirm git remote points to gitea.leeworks.dev
    • Save to ./docs/tooling.md
  3. Create project root and push to Gitea

    mkdir -p api-company/{apis,helm,flux,docs,scripts,research,monitoring,openapi}
    cd api-company
    git init
    git remote add origin https://gitea.leeworks.dev/{your-username}/api-company.git
    git push -u origin main
    
  4. Bootstrap Flux on this repo (if not already watching it)

    flux create source git api-company \
      --url=https://gitea.leeworks.dev/{your-username}/api-company \
      --branch=main \
      --interval=1m \
      --secret-ref=gitea-credentials
    
    flux create kustomization api-company \
      --source=GitRepository/api-company \
      --path="./flux" \
      --prune=true \
      --interval=5m
    

    All Flux manifests live in ./flux/. Flux watches this path and reconciles HelmRelease, Kustomization, and ConfigMap changes automatically on every commit.

  5. Internalize the RapidAPI constraints

    • 25% platform fee + ~2% PayPal fee = keep ~73.5 cents per dollar charged
    • Payout only via PayPal (18+ required)
    • Free tier required on every API
    • Recommended 4 tiers: Free / Basic / Pro / Ultra
    • Every plan needs a "Requests" object
    • Rate limits set per plan, enforced by RapidAPI gateway
    • OpenAPI spec upload uses RapidAPI Platform API (Bearer token auth)

REPOSITORY STRUCTURE

api-company/
├── apis/
│   ├── zip-enrichment/
│   │   ├── src/                  ← application code
│   │   ├── scripts/              ← data seeding scripts
│   │   ├── Dockerfile
│   │   └── openapi.yaml          ← OpenAPI spec (source of truth)
│   ├── holidays/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   └── openapi.yaml
│   └── air-quality/
│       ├── src/
│       ├── Dockerfile
│       └── openapi.yaml
├── helm/
│   ├── zip-enrichment/           ← Helm chart
│   ├── holidays/
│   ├── air-quality/
│   └── docs-site/
├── flux/
│   ├── sources/
│   │   └── gitea-api-company.yaml
│   ├── zip-enrichment/
│   │   ├── helmrelease.yaml      ← Flux watches this, deploys automatically
│   │   └── namespace.yaml
│   ├── holidays/
│   │   ├── helmrelease.yaml
│   │   └── namespace.yaml
│   ├── air-quality/
│   │   ├── helmrelease.yaml
│   │   └── namespace.yaml
│   ├── docs-site/
│   │   ├── helmrelease.yaml
│   │   └── namespace.yaml
│   └── monitoring/
│       ├── helmrelease.yaml
│       └── namespace.yaml
├── docs-site/                    ← Astro + Redoc site
├── monitoring/                   ← Prometheus, Grafana, Gatus configs
├── docs/                         ← Internal docs
├── research/
├── scripts/
│   └── publish-openapi.js        ← RapidAPI spec upload script
├── .gitea/
│   └── workflows/
│       ├── build-and-deploy.yaml
│       ├── publish-openapi.yaml
│       └── rebuild-docs.yaml
├── SESSION_LOG.md
└── STATUS.md

THE OPENAPI-FIRST WORKFLOW

The OpenAPI spec is written once and drives everything downstream:

apis/{name}/openapi.yaml
     │
     ├──▶ Gitea Actions validates spec on every push (redocly lint)
     │
     ├──▶ Gitea Actions uploads to RapidAPI Platform API
     │         (updates listing, endpoint docs, example responses automatically)
     │
     ├──▶ Redoc container at docs.leeworks.dev/{name} renders it live
     │         (same spec, beautiful searchable docs site, self-hosted)
     │
     └──▶ Postman collection auto-generated from spec (for testing)

OpenAPI spec template

Create ./apis/{api-name}/openapi.yaml. Example for ZIP enrichment:

openapi: 3.0.3
info:
  title: ZIP Code Enrichment
  # Do NOT write "API" in the title — RapidAPI appends it automatically
  description: |
    Enrich any US ZIP code with Census demographic data, geographic metadata,
    and USPS-verified information. Sourced from the US Census Bureau ACS and
    HUD-USPS crosswalk files. Updated monthly.

    ## Data Sources
    - US Census Bureau ACS 5-year estimates
    - HUD-USPS ZIP crosswalk files
    - IANA timezone boundary data

    ## Authentication
    All requests must come through the RapidAPI gateway. Direct access is blocked.
  version: "1.0.0"
  x-rapidapi-category: "Data"
  x-rapidapi-subcategory: "Geodata"

servers:
  - url: https://zip.leeworks.dev

paths:
  /lookup:
    get:
      summary: Enrich a single ZIP code
      description: Returns full demographic and geographic data for a US ZIP code.
      parameters:
        - name: zip
          in: query
          required: true
          schema:
            type: string
            pattern: '^\d{5}$'
            example: "01085"
      responses:
        "200":
          description: Successful enrichment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZipResult'
              example:
                zip: "01085"
                city: "Westfield"
                state: "MA"
                county: "Hampden"
                lat: 42.1501
                lon: -72.7495
                timezone: "America/New_York"
                population: 41301
                median_income: 58200
                median_age: 40.1
                unemployment_rate: 4.2
                metro_area: "Springfield, MA"
                housing_units: 17842
                congressional_district: "MA-01"
                data_vintage: "ACS 2023 5-year"
        "400":
          description: Invalid ZIP code format
        "404":
          description: ZIP code not found

  /batch:
    post:
      summary: Enrich up to 100 ZIP codes in one request
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                zips:
                  type: array
                  items:
                    type: string
                  maxItems: 100
                  example: ["01085", "10001", "90210"]
      responses:
        "200":
          description: Array of enriched ZIP results

  /radius:
    get:
      summary: Find all ZIP codes within N miles of a given ZIP
      parameters:
        - name: zip
          in: query
          required: true
          schema:
            type: string
        - name: miles
          in: query
          required: true
          schema:
            type: number
            minimum: 1
            maximum: 100
      responses:
        "200":
          description: Array of nearby ZIP codes

  /health:
    get:
      summary: Health check and data freshness
      responses:
        "200":
          description: Service status

components:
  schemas:
    ZipResult:
      type: object
      properties:
        zip:
          type: string
        city:
          type: string
        state:
          type: string
        county:
          type: string
        lat:
          type: number
        lon:
          type: number
        timezone:
          type: string
        population:
          type: integer
        median_income:
          type: number
        median_age:
          type: number
        unemployment_rate:
          type: number
        metro_area:
          type: string
        housing_units:
          type: integer
        congressional_district:
          type: string
        data_vintage:
          type: string

GITEA ACTIONS CI PIPELINES

Pipeline 1 — Build image, update Flux manifest

.gitea/workflows/build-and-deploy.yaml:

name: Build and Deploy

on:
  push:
    branches: [main]
    paths:
      - 'apis/**'
      - 'helm/**'

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      apis: ${{ steps.filter.outputs.changes }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            zip-enrichment:
              - 'apis/zip-enrichment/**'
            holidays:
              - 'apis/holidays/**'
            air-quality:
              - 'apis/air-quality/**'

  build:
    needs: detect-changes
    runs-on: ubuntu-latest
    strategy:
      matrix:
        api: ${{ fromJSON(needs.detect-changes.outputs.apis) }}
    steps:
      - uses: actions/checkout@v4

      - name: Build and push Docker image
        run: |
          IMAGE="registry.leeworks.dev/${{ matrix.api }}:${{ gitea.sha }}"
          docker build -t $IMAGE ./apis/${{ matrix.api }}
          docker push $IMAGE

      - name: Update Flux HelmRelease image tag
        run: |
          sed -i "s|tag:.*|tag: ${{ gitea.sha }}|" \
            flux/${{ matrix.api }}/helmrelease.yaml
          git config user.email "ci@leeworks.dev"
          git config user.name "Gitea Actions"
          git add flux/${{ matrix.api }}/helmrelease.yaml
          git commit -m "chore: update ${{ matrix.api }} to ${{ gitea.sha }}"
          git push
          # Flux detects commit → reconciles → rolling update → done
          # No helm upgrade needed. Ever.

Pipeline 2 — Validate and publish OpenAPI spec to RapidAPI

.gitea/workflows/publish-openapi.yaml:

name: Publish OpenAPI to RapidAPI

on:
  push:
    branches: [main]
    paths:
      - 'apis/*/openapi.yaml'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Validate all OpenAPI specs
        run: npx @redocly/cli lint apis/*/openapi.yaml
        # Pipeline fails here if spec is invalid. Never publish broken docs.

      - name: Detect changed specs
        id: changed
        run: |
          CHANGED=$(git diff --name-only HEAD~1 HEAD | grep 'openapi.yaml' || true)
          echo "CHANGED_FILES=$CHANGED" >> $GITHUB_ENV

      - name: Publish to RapidAPI
        env:
          RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_PLATFORM_KEY }}
          ZIP_API_ID: ${{ secrets.RAPIDAPI_ZIP_API_ID }}
          ZIP_VERSION_ID: ${{ secrets.RAPIDAPI_ZIP_VERSION_ID }}
          HOLIDAYS_API_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_API_ID }}
          HOLIDAYS_VERSION_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_VERSION_ID }}
          AQI_API_ID: ${{ secrets.RAPIDAPI_AQI_API_ID }}
          AQI_VERSION_ID: ${{ secrets.RAPIDAPI_AQI_VERSION_ID }}
        run: node scripts/publish-openapi.js

./scripts/publish-openapi.js

const fs = require('fs');
const path = require('path');
const FormData = require('form-data');
const fetch = require('node-fetch');

const API_MAP = {
  'zip-enrichment': {
    apiId: process.env.ZIP_API_ID,
    versionId: process.env.ZIP_VERSION_ID,
  },
  'holidays': {
    apiId: process.env.HOLIDAYS_API_ID,
    versionId: process.env.HOLIDAYS_VERSION_ID,
  },
  'air-quality': {
    apiId: process.env.AQI_API_ID,
    versionId: process.env.AQI_VERSION_ID,
  },
};

async function publishSpec(apiName) {
  const { apiId, versionId } = API_MAP[apiName];
  if (!apiId || !versionId) {
    console.log(`Skipping ${apiName} — RapidAPI ID not configured yet`);
    return;
  }

  const specPath = path.join('apis', apiName, 'openapi.yaml');
  const specContent = fs.readFileSync(specPath, 'utf8');

  const form = new FormData();
  form.append('file', specContent, {
    filename: 'openapi.yaml',
    contentType: 'application/yaml',
  });

  const url = `https://platformapi1.p.rapidapi.com/v1/apis/${apiId}/versions/${versionId}`;
  const response = await fetch(url, {
    method: 'PUT',
    headers: {
      'x-rapidapi-host': 'platformapi1.rapidapi-x.rapidapi.com',
      'x-rapidapi-key': process.env.RAPIDAPI_KEY,
      ...form.getHeaders(),
    },
    body: form,
  });

  if (!response.ok) {
    throw new Error(`Failed to publish ${apiName}: ${response.status} ${await response.text()}`);
  }
  console.log(`✓ Published ${apiName} spec to RapidAPI`);
}

const changedFiles = (process.env.CHANGED_FILES || '').split('\n').filter(Boolean);
const apisToPublish = Object.keys(API_MAP).filter(api =>
  changedFiles.length === 0 || changedFiles.some(f => f.includes(api))
);

Promise.all(apisToPublish.map(publishSpec))
  .then(() => console.log('Done'))
  .catch(err => { console.error(err); process.exit(1); });

Pipeline 3 — Rebuild docs site when specs change

.gitea/workflows/rebuild-docs.yaml:

name: Rebuild Docs Site

on:
  push:
    branches: [main]
    paths:
      - 'apis/*/openapi.yaml'
      - 'docs-site/**'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Copy specs into docs site
        run: |
          mkdir -p docs-site/public/specs
          cp apis/zip-enrichment/openapi.yaml docs-site/public/specs/
          cp apis/holidays/openapi.yaml docs-site/public/specs/
          cp apis/air-quality/openapi.yaml docs-site/public/specs/

      - name: Build Astro site
        run: |
          cd docs-site && npm ci && npm run build

      - name: Push docs image and update Flux
        run: |
          docker build -t registry.leeworks.dev/docs-site:${{ gitea.sha }} ./docs-site
          docker push registry.leeworks.dev/docs-site:${{ gitea.sha }}
          sed -i "s|tag:.*|tag: ${{ gitea.sha }}|" flux/docs-site/helmrelease.yaml
          git config user.email "ci@leeworks.dev"
          git config user.name "Gitea Actions"
          git add flux/docs-site/helmrelease.yaml
          git commit -m "chore: update docs-site to ${{ gitea.sha }}"
          git push

FLUX CD MANIFEST TEMPLATES

./flux/zip-enrichment/helmrelease.yaml

apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: zip-enrichment
  namespace: zip-enrichment
spec:
  interval: 5m
  chart:
    spec:
      chart: ./helm/zip-enrichment
      sourceRef:
        kind: GitRepository
        name: api-company
        namespace: flux-system
  values:
    replicaCount: 2
    image:
      repository: registry.leeworks.dev/zip-enrichment
      tag: latest    # Gitea Actions updates this on every push
    resources:
      requests:
        memory: "128Mi"
        cpu: "50m"
      limits:
        memory: "256Mi"
        cpu: "200m"
    ingress:
      enabled: true
      host: zip.leeworks.dev
    env:
      RAPIDAPI_PROXY_SECRET:
        valueFrom:
          secretKeyRef:
            name: rapidapi-secrets
            key: proxy-secret

Repeat for holidays (namespace: holidays) and air-quality (namespace: air-quality).


PHASE 1 — FIRST API: US ZIP CODE ENRICHMENT

Why this one first

  • Data source: US Census Bureau API (free, no key for basic use)
  • Secondary: HUD-USPS crosswalk files (free download)
  • High demand: shipping, marketing, real estate, fintech
  • Low maintenance: Census updates ~annually
  • Proven market: competitors charge $49$299/mo

Step 1.1 — Write the OpenAPI spec first

Before any application code, create ./apis/zip-enrichment/openapi.yaml using the template above. Validate locally:

npx @redocly/cli lint apis/zip-enrichment/openapi.yaml

Commit and push. The spec is now the contract — implement to match it.

Step 1.2 — Data acquisition & caching

Build ./apis/zip-enrichment/scripts/seed-data.js:

Pull and cache:

  • Census Bureau ACS 5-year: https://api.census.gov/data/2023/acs/acs5 (population, median income, median age, housing units, unemployment rate)
  • HUD-USPS crosswalk: ZIP to county, metro area, census tract
  • Timezone data from precomputed IANA/Census boundary CSV

Store in SQLite at ./apis/zip-enrichment/data/zip.db, indexed on ZIP for <5ms lookups. Monthly CronJob re-seeds from source.

Failure mode: Census API down → return stale data with X-Data-Freshness header. Never return a 500 to a paying customer.

Step 1.3 — API server

./apis/zip-enrichment/src/server.js — Node.js + Fastify

Endpoints must match openapi.yaml paths exactly:

  • GET /lookup?zip=01085
  • POST /batch — body { zips: [...] }, max 100
  • GET /radius?zip=01085&miles=25
  • GET /health

RapidAPI proxy secret verification on every request:

const PROXY_SECRET = process.env.RAPIDAPI_PROXY_SECRET;
fastify.addHook('preHandler', (req, reply, done) => {
  if (req.headers['x-rapidapi-proxy-secret'] !== PROXY_SECRET) {
    reply.code(403).send({ error: 'Direct access not permitted' });
    return;
  }
  done();
});

Step 1.4 — Deploy via Flux

Create Helm chart at ./helm/zip-enrichment/ and Flux manifest at ./flux/zip-enrichment/helmrelease.yaml. Commit and push. Flux reconciles within 5 minutes. Monitor:

flux get helmreleases -n zip-enrichment --watch

Use a PersistentVolumeClaim for the SQLite file so it survives pod restarts.

Step 1.5 — Pricing tiers

Plan Price Requests/mo Overage
Free $0 100 Not available
Basic $9/mo 5,000 $0.003/req
Pro $19/mo 25,000 $0.002/req
Ultra $49/mo 100,000 $0.001/req

To net $100/mo: need ~$136/mo gross = 15 Basic OR 7 Pro OR 3 Ultra subscribers.


PHASE 2 — SECOND API: PUBLIC HOLIDAYS & BUSINESS DAYS

Data sources (free)

  • Nager.Date: https://date.nager.at/api/v3/PublicHolidays/{year}/{countryCode}
  • US Federal Reserve bank holidays (scrape federalreserve.gov)
  • NYSE trading days (public calendar)

Endpoints

GET /holidays?country=US&year=2025
GET /is-holiday?country=US&date=2025-12-25
GET /business-days?country=US&start=2025-01-01&end=2025-01-31
GET /next-business-day?country=US&date=2025-12-24
GET /trading-days?exchange=NYSE&year=2025  ← Pro+ only

Deployment

Same Gitea Actions + Flux pattern. Namespace: holidays. Data is tiny — rebuild from upstream on pod start, no PVC needed.

Pricing

| Free | Basic $5/mo | Pro $15/mo | Ultra $29/mo (unlimited) |


PHASE 3 — THIRD API: AIR QUALITY + POLLEN

Data sources (free)

  • OpenAQ: https://api.openaq.org/v3/
  • EPA AirNow: https://www.airnowapi.org/
  • Open-Meteo: https://air-quality-api.open-meteo.com/

Endpoints

GET /aqi?lat=42.11&lon=-72.59
GET /aqi?zip=01085
GET /forecast?lat=42.11&lon=-72.59&days=3
GET /pollen?lat=42.11&lon=-72.59
GET /history?lat=42.11&lon=-72.59&start=2025-01-01&end=2025-01-07

RECURSIVE RESEARCH PROTOCOL

When finishing a build phase OR hitting a context limit, run this block:

SEARCH 1: "RapidAPI most subscribed APIs {current_year}"
SEARCH 2: "developers complaining about {category} API {current_year} reddit"
           (rotate: geocoding, business data, weather, finance, legal)
SEARCH 3: "free public dataset API {niche} no existing wrapper"
SEARCH 4: site:rapidapi.com {category} (check subscriber counts)
SEARCH 5: "is there an API for" site:reddit.com

Append findings to ./research/RESEARCH_LOG.md:

## Research Session {date} {time}

### Findings
- [finding]: [URL] [demand: high/medium/low]

### New API Candidates
1. {name} — {description} — {data source} — {evidence}

### Decision
Building next: {name} because {reason}

PHASE 4 — MONITORING & OBSERVABILITY

Deploy via Flux HelmRelease once 2+ APIs are live:

  • Prometheus + Grafana (community Helm charts)
  • Instrument with prom-client (Node) or prometheus-client (Python)
  • Metrics per API: api_requests_total, api_response_duration_seconds, api_data_freshness_seconds, api_active_subscribers

Deploy Gatus for public status page at status.leeworks.dev:

# monitoring/gatus/config.yaml
endpoints:
  - name: ZIP Enrichment
    url: https://zip.leeworks.dev/health
    interval: 30s
    conditions:
      - "[STATUS] == 200"
      - "[RESPONSE_TIME] < 500"
  - name: Holidays API
    url: https://holidays.leeworks.dev/health
    interval: 30s
    conditions:
      - "[STATUS] == 200"

Gatus is self-hosted and Kubernetes-native — fits the existing stack. Customers trust APIs with a public status page. This is not optional.


PHASE 5 — DOCUMENTATION SITE

Stack: Astro + Redoc, self-hosted at docs.leeworks.dev
Deployed via: Flux HelmRelease (same as everything else)

Redoc renders each openapi.yaml as a polished, searchable reference page. Gitea Actions copies specs into the docs site at build time (Pipeline 3 above). Docs always match the live API because they come from the same file.

docs.leeworks.dev/                  ← company landing page
docs.leeworks.dev/zip-enrichment    ← Redoc renders openapi.yaml
docs.leeworks.dev/holidays
docs.leeworks.dev/air-quality
docs.leeworks.dev/blog/             ← SEO content (not optional)
docs.leeworks.dev/pricing/          ← cross-API comparison

Blog posts for SEO — write one per API at minimum:

  • "How to get Census demographic data by ZIP code in Node.js"
  • "Checking if a date is a business day in 30+ countries with one API call"
  • "Building an air quality widget for your React app using free EPA data"

Each post has a working code example using YOUR API and links to the RapidAPI listing. This is how developers find you on Google before they find you on RapidAPI.


PHASE 6 — COMPANY INFRASTRUCTURE

Gitea organization at gitea.leeworks.dev

  • Create org: leeworks-apis (or chosen company name)
  • Repos: one per API + helm-charts + docs-site + flux-config
  • All CI: Gitea Actions (.gitea/workflows/*.yaml)
  • All deploys: Flux watching gitea.leeworks.dev

Gitea Actions runner (deploy in cluster if not running)

# flux/gitea-runner/helmrelease.yaml
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: gitea-runner
  namespace: gitea-runner
spec:
  chart:
    spec:
      chart: gitea-act-runner
      sourceRef:
        kind: HelmRepository
        name: gitea
  values:
    config:
      runner:
        labels:
          - "ubuntu-latest:docker://node:20"

DNS (all routed through VPS tunnel)

zip.leeworks.dev        → ZIP enrichment API
holidays.leeworks.dev   → Holidays API
aqi.leeworks.dev        → Air quality API
docs.leeworks.dev       → Documentation + Redoc
status.leeworks.dev     → Gatus status page
gitea.leeworks.dev      → Already running ✓
registry.leeworks.dev   → Container registry

./docs/legal/terms-of-service.md, privacy-policy.md, acceptable-use-policy.md


PHASE 7 — LAUNCH SEQUENCE (18th birthday)

# 1. Verify Flux is healthy
flux get helmreleases -A
# All should show READY=True RECONCILING=False

# 2. Smoke test each API
curl https://zip.leeworks.dev/health
curl https://holidays.leeworks.dev/health
curl https://aqi.leeworks.dev/health

# 3. Verify OpenAPI specs are showing on RapidAPI
# (check each listing's Endpoints tab — must match your openapi.yaml)

# 4. Link PayPal to RapidAPI (manual)
# rapidapi.com → avatar → Personal Payouts → Link PayPal

# 5. Enable paid tiers on all RapidAPI listings (manual)
# Monetize tab → enable Basic / Pro / Ultra plans

# 6. Post announcements
# - Hacker News: "Show HN: I built 3 APIs on free Census/EPA data"
# - r/webdev, r/SideProject, r/learnprogramming
# - Indie Hackers
# - Product Hunt (schedule in advance)

# 7. Email waitlist from 30-day build window

RECURSIVE LOOP RULES — READ EVERY SESSION

  1. Read SESSION_LOG.md and STATUS.md first. Always. No exceptions.

  2. Read ./research/RESEARCH_LOG.md to understand decisions already made.

  3. Check cluster state before writing any deployment code:

    flux get helmreleases -A
    kubectl get deployments -A
    
  4. Never run helm upgrade manually. Commit to git, let Flux reconcile. Manual helm commands cause drift and break the GitOps workflow.

  5. OpenAPI spec before code. Write and validate the spec entry for any new endpoint before implementing the handler. Docs drift is a trust killer.

  6. Prioritize:

    • Fix broken things first (flux get all -A for errors)
    • Deploy nearly-finished things second
    • Build new things third
    • Research when blocked or between phases
  7. End every session by appending to SESSION_LOG.md:

    ## Session {timestamp}
    ### Completed
    - ...
    ### In progress
    - ...
    ### Next session starts with
    - ...
    
  8. The goal is $100/mo net. If it doesn't move toward revenue, it's optional.

  9. When truly stuck: run the research protocol. There is always another API.


STATUS TRACKER — ./STATUS.md

# Company Status

## APIs
| API            | Spec | Code | Deployed | Listed on RapidAPI | Paying Users | MRR |
|----------------|------|------|----------|--------------------|--------------|-----|
| ZIP Enrichment | [ ]  | [ ]  | [ ]      | [ ]                | 0            | $0  |
| Holidays       | [ ]  | [ ]  | [ ]      | [ ]                | 0            | $0  |
| Air Quality    | [ ]  | [ ]  | [ ]      | [ ]                | 0            | $0  |

## Infrastructure
- Cluster nodes:
- Flux healthy: Y/N
- Gitea Actions runner: UP/DOWN
- VPS tunnel: UP/DOWN
- Site 3 expansion: IN PROGRESS / DONE

## Revenue
- Gross MRR: $0
- Net MRR (after ~26.5% fees): $0
- Target: $100/mo net
- Gap: $100

## Next action
{one sentence — exactly what to do first next session}

This prompt is intentionally never complete. More APIs, more research, more revenue. The loop ends when you say it ends — not before.