40631fee7c
Initial scaffolding committed by bootstrap script. See ROADMAP.md for phase plan and SESSION_LOG.md (api-company only) for context.
959 lines
28 KiB
Markdown
959 lines
28 KiB
Markdown
# 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**
|
||
```bash
|
||
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)
|
||
```bash
|
||
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:
|
||
|
||
```yaml
|
||
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`:
|
||
|
||
```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`:
|
||
|
||
```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`
|
||
|
||
```javascript
|
||
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`:
|
||
|
||
```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`
|
||
|
||
```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:
|
||
```bash
|
||
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:
|
||
```javascript
|
||
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:
|
||
```bash
|
||
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`:
|
||
|
||
```markdown
|
||
## 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`:
|
||
```yaml
|
||
# 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)
|
||
```yaml
|
||
# 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
|
||
```
|
||
|
||
### Legal (prepare before 18th birthday)
|
||
`./docs/legal/terms-of-service.md`, `privacy-policy.md`, `acceptable-use-policy.md`
|
||
|
||
---
|
||
|
||
## PHASE 7 — LAUNCH SEQUENCE (18th birthday)
|
||
|
||
```bash
|
||
# 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:**
|
||
```bash
|
||
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`:**
|
||
```markdown
|
||
## 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`
|
||
|
||
```markdown
|
||
# 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.*
|