Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76abe394a1 | |||
| db42d81879 | |||
| c7bc7d3a50 | |||
| 137cdb3b01 | |||
| 74ddce364f | |||
| c83ccad4df | |||
| b4bd508170 | |||
| ad679ddc97 | |||
| 8518878358 | |||
| be9f80419b | |||
| 9370d2c898 | |||
| 411ee92106 |
@@ -0,0 +1,58 @@
|
||||
# Validate and publish OpenAPI specs to RapidAPI when apis/*/openapi.yaml changes on main.
|
||||
# Requires secrets (configured once RapidAPI listings are live):
|
||||
# RAPIDAPI_PLATFORM_KEY, RAPIDAPI_ZIP_API_ID, RAPIDAPI_ZIP_VERSION_ID,
|
||||
# RAPIDAPI_HOLIDAYS_API_ID, RAPIDAPI_HOLIDAYS_VERSION_ID,
|
||||
# RAPIDAPI_AQI_API_ID, RAPIDAPI_AQI_VERSION_ID
|
||||
#
|
||||
# If RAPIDAPI_PLATFORM_KEY is not set the publish step exits 0 with a skip message.
|
||||
|
||||
name: Validate and Publish OpenAPI Specs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apis/*/openapi.yaml'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Lint and publish specs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout (with history for diff)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Redocly CLI
|
||||
run: npm install -g @redocly/cli@latest
|
||||
|
||||
- name: Lint OpenAPI specs
|
||||
run: |
|
||||
echo "Linting all OpenAPI specs..."
|
||||
npx @redocly/cli lint apis/*/openapi.yaml
|
||||
|
||||
- name: Detect changed specs
|
||||
id: changed
|
||||
run: |
|
||||
changed=$(git diff --name-only HEAD~1 HEAD | grep 'openapi\.yaml' || true)
|
||||
echo "Changed specs: ${changed:-none}"
|
||||
echo "files=${changed}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish specs to RapidAPI
|
||||
env:
|
||||
RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_PLATFORM_KEY }}
|
||||
RAPIDAPI_ZIP_API_ID: ${{ secrets.RAPIDAPI_ZIP_API_ID }}
|
||||
RAPIDAPI_ZIP_VERSION_ID: ${{ secrets.RAPIDAPI_ZIP_VERSION_ID }}
|
||||
RAPIDAPI_HOLIDAYS_API_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_API_ID }}
|
||||
RAPIDAPI_HOLIDAYS_VERSION_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_VERSION_ID }}
|
||||
RAPIDAPI_AQI_API_ID: ${{ secrets.RAPIDAPI_AQI_API_ID }}
|
||||
RAPIDAPI_AQI_VERSION_ID: ${{ secrets.RAPIDAPI_AQI_VERSION_ID }}
|
||||
run: node scripts/publish-openapi.js
|
||||
@@ -15,6 +15,7 @@ Meta-repo for the `leeworks-apis` recursive build loop. This repo holds cross-cu
|
||||
- `MASTER_BUILD_PROMPT.md` — the prime directive driving the build loop
|
||||
- `ROADMAP.md` — cross-repo milestones (Phase 0–6)
|
||||
- `STATUS.md` — living revenue + infra scorecard
|
||||
- `docs/pre-launch-checklist.md` — go-live gate checklist (run the day before public launch)
|
||||
- `SESSION_LOG.md` — append-only log of agent work sessions
|
||||
- `research/RESEARCH_LOG.md` — market research findings, candidate API ideas
|
||||
- `flux/` — cluster-level Flux manifests (registry, runner, monitoring, docs-site)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Company Status
|
||||
|
||||
_Last updated: 2026-05-28 (agent cycle — cycle-2 triage)_
|
||||
_Last updated: 2026-05-30 (sprint planning cycle)_
|
||||
|
||||
## APIs
|
||||
| API | Spec | Code | Deployed | Listed on RapidAPI | Paying Users | MRR |
|
||||
@@ -21,7 +21,37 @@ Legend: [x]=done, [~]=in-progress, [ ]=not started
|
||||
- **API service manifests:** `flux/zip-enrichment/`, `flux/holidays/`, `flux/air-quality/` scaffolded (PR #48, closes #46)
|
||||
- **Image automation:** `flux/image-automation/` — ImageRepository + ImagePolicy + ImageUpdateAutomation for all three APIs
|
||||
|
||||
## Completed This Cycle (2026-05-28 — Cycle-2 Triage)
|
||||
## Completed This Cycle (2026-05-30 — Sprint Cycle)
|
||||
- **#108** — STATUS.md updated with 2026-05-30 sprint plan and critical path.
|
||||
- **#107** — docs-site build verified: `npm run build` exits 0; all 9 required routes present in `dist/` (`/`, `/zip-enrichment`, `/holidays`, `/air-quality`, `/blog/` (3 posts), `/legal/terms-of-service`, `/legal/privacy-policy`, `/legal/acceptable-use-policy`, `/pricing`). Unblocks #30.
|
||||
- **PR #105 MERGED** — `scripts/publish-openapi.js` + `.gitea/workflows/publish-openapi.yaml` (closes #100).
|
||||
- **PR #103 MERGED** — `docs-site/src/pages/pricing.astro` + nav link (closes #101).
|
||||
- **PR #104 MERGED** — `research/RESEARCH_LOG.md` first session: VIN Decoder next candidate (closes #102).
|
||||
- **New issues triaged** — #100–#108 (sprint planning cycle issues reviewed).
|
||||
- **`0xWheatyz/api-company`** — still does not exist; fork sync skipped (blocker #47).
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
## Critical Path (operator must complete in order)
|
||||
1. **Create `0xWheatyz/api-company` repo** (#47) — **highest priority, unblocks everything**
|
||||
2. **Add api-company Flux source to Talos** (#90) — needs Talos PR, reference manifests at `flux/api-company-source/`
|
||||
3. **Create `gitea-leeworks-agents-token` secret** in `flux-system` (#76)
|
||||
4. **Create `gitea-runner-token` secret** in `gitea-runner` (#77)
|
||||
5. **Enable Gitea packages + DNS for `registry.leeworks.dev`** (#4)
|
||||
6. **All other secrets** (#70, #73, #74, #79, #80, #81, #83) follow in order
|
||||
|
||||
## New Issues This Sprint (2026-05-29, #100–#108)
|
||||
- **#100** — `scripts/publish-openapi.js` + CI workflow — **DONE (PR #105 merged)**
|
||||
- **#101** — Pricing page at `docs.leeworks.dev/pricing/` — **DONE (PR #103 merged)**
|
||||
- **#102** — Seed `research/RESEARCH_LOG.md` — **DONE (PR #104 merged)**
|
||||
- **#106** — Add `grafana.leeworks.dev` as 7th subdomain to DNS task (#33) — tracker only; docs already updated (#67)
|
||||
- **#107** — docs-site build verification — **DONE (build passes, all 9 routes confirmed)**
|
||||
- **#108** — STATUS.md update — **DONE (this update)**
|
||||
|
||||
## Backlog
|
||||
- **28 open issues total**; all remaining blocked on operator Phase 0 actions above.
|
||||
- **Agent work that can proceed without operator:** all completed this cycle.
|
||||
|
||||
## Completed Previous Cycle (2026-05-28 — Cycle-2 Triage)
|
||||
- **#83** — Triaged: `GITEA_TOKEN` Actions secrets needed in `zip-enrichment`, `holidays`, `air-quality` repos. Manual operator task; reuse token from #74 if it has `write:package` scope. Step-by-step instructions posted.
|
||||
- **No open PRs** — queue empty.
|
||||
- **`kustomize build flux/` = PASS** — no regressions.
|
||||
|
||||
@@ -31,6 +31,7 @@ const { title, description = "leeworks.dev API documentation" } = Astro.props;
|
||||
<a href="/zip-enrichment">ZIP Enrichment</a>
|
||||
<a href="/holidays">Holidays</a>
|
||||
<a href="/air-quality">Air Quality</a>
|
||||
<a href="/pricing">Pricing</a>
|
||||
<a href="/blog">Blog</a>
|
||||
<a href="https://rapidapi.com/leeworks" target="_blank" rel="noopener">RapidAPI</a>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="API Pricing" description="API Pricing — leeworks.dev — compare plans for ZIP Enrichment, Holidays, and Air Quality APIs">
|
||||
<style>
|
||||
.pricing-hero { padding: 4rem 2rem 2rem; text-align: center; }
|
||||
.pricing-hero h1 { font-size: 2.5rem; font-weight: 800; background: linear-gradient(135deg, #90cdf4, #667eea); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 1rem; }
|
||||
.pricing-hero p { font-size: 1.15rem; color: #a0aec0; max-width: 600px; margin: 0 auto 2rem; }
|
||||
.pricing-wrap { padding: 1rem 2rem 3rem; max-width: 1100px; margin: 0 auto; overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; background: #1a1d27; border-radius: 12px; overflow: hidden; min-width: 640px; }
|
||||
caption { caption-side: top; text-align: left; font-size: 0.875rem; color: #718096; padding: 0.75rem 1rem; font-style: italic; }
|
||||
thead tr { background: #2d3748; }
|
||||
th, td { padding: 0.9rem 1.25rem; text-align: left; border-bottom: 1px solid #2d3748; }
|
||||
th { font-weight: 700; color: #90cdf4; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
td { color: #e2e8f0; font-size: 0.95rem; }
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: #232738; }
|
||||
.tier-name { font-weight: 600; color: #fff; }
|
||||
.tier-price { color: #9ae6b4; font-weight: 600; }
|
||||
.free-price { color: #68d391; }
|
||||
.cta-row { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin: 2.5rem 0 1rem; }
|
||||
.cta-btn { display: inline-block; background: #667eea; color: #fff; padding: 0.65rem 1.5rem; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 0.95rem; }
|
||||
.cta-btn:hover { background: #5a67d8; }
|
||||
.disclaimer { text-align: center; color: #718096; font-size: 0.85rem; margin-top: 2rem; padding: 0 2rem; }
|
||||
</style>
|
||||
|
||||
<div class="pricing-hero">
|
||||
<h1>API Pricing</h1>
|
||||
<p>Simple, transparent pricing for all three APIs. Start free — upgrade when you need more.</p>
|
||||
</div>
|
||||
|
||||
<div class="pricing-wrap">
|
||||
<table>
|
||||
<caption>All plans are monthly. Requests reset at the start of each billing cycle.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Tier</th>
|
||||
<th scope="col">Price / mo</th>
|
||||
<th scope="col">ZIP Enrichment</th>
|
||||
<th scope="col">Holidays</th>
|
||||
<th scope="col">Air Quality</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tier-name">Free</td>
|
||||
<td class="tier-price free-price">$0</td>
|
||||
<td>100 req/mo · 5 req/min</td>
|
||||
<td>100 req/mo · 5 req/min</td>
|
||||
<td>100 req/mo · 5 req/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tier-name">Basic</td>
|
||||
<td class="tier-price">$9</td>
|
||||
<td>5,000 req/mo · 60 req/min</td>
|
||||
<td>5,000 req/mo · 60 req/min</td>
|
||||
<td>5,000 req/mo · 60 req/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tier-name">Pro</td>
|
||||
<td class="tier-price">$19</td>
|
||||
<td>25,000 req/mo · 200 req/min</td>
|
||||
<td>20,000 req/mo · 200 req/min</td>
|
||||
<td>20,000 req/mo · 200 req/min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tier-name">Ultra</td>
|
||||
<td class="tier-price">$49</td>
|
||||
<td>100,000 req/mo · 500 req/min</td>
|
||||
<td>100,000 req/mo · 500 req/min</td>
|
||||
<td>100,000 req/mo · 500 req/min</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="cta-row">
|
||||
<a href="#" class="cta-btn">ZIP Enrichment on RapidAPI</a>
|
||||
<a href="#" class="cta-btn">Holidays on RapidAPI</a>
|
||||
<a href="#" class="cta-btn">Air Quality on RapidAPI</a>
|
||||
</div>
|
||||
|
||||
<p class="disclaimer">Subscriptions managed via RapidAPI marketplace. Prices shown in USD. Rate limits apply per API key.</p>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,359 @@
|
||||
# Launch Announcement Copy
|
||||
|
||||
Ready-to-copy marketing text for leeworks.dev API launch day.
|
||||
|
||||
**Status key:**
|
||||
- **DRAFT** — copy is written and ready; needs final RapidAPI URLs inserted once issue #44 is complete
|
||||
- **READY** — all placeholders filled; copy-paste ready to publish
|
||||
|
||||
All sections are currently **DRAFT** pending RapidAPI listing URLs from issue #44.
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Reference
|
||||
|
||||
When issue #44 is complete, replace these placeholders throughout this document:
|
||||
|
||||
| Placeholder | Replace with |
|
||||
|---|---|
|
||||
| `[RAPIDAPI_ZIP_URL]` | RapidAPI listing URL for ZIP Code Enrichment API |
|
||||
| `[RAPIDAPI_HOLIDAYS_URL]` | RapidAPI listing URL for Holidays API |
|
||||
| `[RAPIDAPI_AQI_URL]` | RapidAPI listing URL for Air Quality Index API |
|
||||
| `[RAPIDAPI_PROFILE_URL]` | Your RapidAPI provider profile URL |
|
||||
|
||||
---
|
||||
|
||||
## 1. Hacker News — Show HN Post [DRAFT]
|
||||
|
||||
**Title:**
|
||||
```
|
||||
Show HN: I built 3 free-data APIs on Kubernetes — ZIP enrichment, public holidays, air quality
|
||||
```
|
||||
|
||||
**Body (paste into the "text" field):**
|
||||
```
|
||||
Three small APIs I've been building over the past few months, deployed via Flux GitOps on a self-hosted Kubernetes cluster.
|
||||
|
||||
**What they do:**
|
||||
|
||||
1. ZIP Code Enrichment API — turn any US ZIP code into city, state, county, timezone, area codes, and coordinates. Backed by USPS/Census public data, refreshed monthly. [RAPIDAPI_ZIP_URL]
|
||||
|
||||
2. Public Holidays API — query official public holidays for any country and year. 90+ countries, ISO 3166 codes. Backed by Nager.Date / public government calendars. [RAPIDAPI_HOLIDAYS_URL]
|
||||
|
||||
3. Air Quality Index API — current and historical AQI by city or coordinates. PM2.5, PM10, O3, NO2, SO2, CO. Backed by OpenAQ public dataset. [RAPIDAPI_AQI_URL]
|
||||
|
||||
**Tech stack:** Fastify (Node.js), SQLite (data cache), Flux GitOps on Talos Linux, cert-manager + ingress-nginx, Prometheus + Grafana for metrics, Gatus for status page.
|
||||
|
||||
**Business model:** Free tier (100 req/mo) + paid tiers ($9/$19/$49/mo) on RapidAPI. All three APIs use only public-domain data sources with no redistribution restrictions, so operating costs are cluster hosting only.
|
||||
|
||||
**Why I built this:** I wanted to learn GitOps/Kubernetes end-to-end, build something that generates real revenue, and ship entirely on open data. The stack is overkill for 3 simple APIs — but that's the point.
|
||||
|
||||
Code is private (it's a product), but happy to answer questions about the architecture.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Reddit Posts [DRAFT]
|
||||
|
||||
### r/webdev
|
||||
|
||||
**Title:**
|
||||
```
|
||||
I built a ZIP code enrichment API on public Census data — city, state, county, timezone from a single lookup
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
Been working on a simple utility API for the past few months. ZIP Code Enrichment takes any US ZIP code and returns:
|
||||
|
||||
- City name + state (abbreviation and full name)
|
||||
- County + FIPS code
|
||||
- Timezone (IANA name + UTC offset)
|
||||
- Area codes
|
||||
- Latitude/longitude centroid
|
||||
- ZIP type (standard, PO Box, military, unique)
|
||||
|
||||
**The data source** is a monthly-refreshed dataset from USPS/Census Bureau — entirely public domain, no scraping.
|
||||
|
||||
**Code example:**
|
||||
|
||||
```javascript
|
||||
const response = await fetch('https://zip.leeworks.dev/v1/lookup?zip=10001', {
|
||||
headers: { 'X-RapidAPI-Proxy-Secret': process.env.RAPIDAPI_KEY }
|
||||
});
|
||||
const data = await response.json();
|
||||
// { zip: "10001", city: "New York", state: "NY", county: "New York County",
|
||||
// timezone: "America/New_York", lat: 40.7484, lon: -73.9967, ... }
|
||||
```
|
||||
|
||||
Free tier is 100 requests/month. Paid plans start at $9/mo for 10,000 req/mo.
|
||||
|
||||
RapidAPI listing: [RAPIDAPI_ZIP_URL]
|
||||
|
||||
Happy to answer any questions about the stack (Fastify + SQLite + Kubernetes/Flux).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### r/SideProject
|
||||
|
||||
**Title:**
|
||||
```
|
||||
Launched 3 data APIs on RapidAPI — ZIP enrichment, public holidays, air quality. $0 → targeting $100/mo MRR
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
Finally shipped the thing I've been building on weekends for the past few months.
|
||||
|
||||
**What I built:**
|
||||
Three utility APIs on RapidAPI, all backed by free public-domain data:
|
||||
|
||||
1. **ZIP Code Enrichment** — city/state/county/timezone from a ZIP code ([RAPIDAPI_ZIP_URL])
|
||||
2. **Public Holidays** — official holidays for 90+ countries ([RAPIDAPI_HOLIDAYS_URL])
|
||||
3. **Air Quality Index** — current + historical AQI by city or coordinates ([RAPIDAPI_AQI_URL])
|
||||
|
||||
**Stack:** Fastify + SQLite + Kubernetes (Talos Linux) + Flux GitOps + Prometheus/Grafana
|
||||
|
||||
**Business model:**
|
||||
- Free tier: 100 req/month (marketing + trial)
|
||||
- Basic: $9/mo — 10,000 req/mo
|
||||
- Pro: $19/mo — 50,000 req/mo
|
||||
- Ultra: $49/mo — 250,000 req/mo
|
||||
|
||||
**Data cost: $0.** All three APIs use US government / OpenAQ public datasets with no licensing fees.
|
||||
|
||||
**Revenue so far:** $0 (launched today). Target: $100/mo net within 90 days, which is roughly 12 Basic subscribers across all three APIs.
|
||||
|
||||
The whole build — from first commit to Kubernetes deployment — is documented in a private research log. Happy to share architecture details.
|
||||
|
||||
What would you do differently for the pricing?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### r/learnprogramming
|
||||
|
||||
**Title:**
|
||||
```
|
||||
I used free US government data to build a ZIP code API — here's how the data pipeline works
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
A walkthrough of the data layer behind the ZIP Code Enrichment API I just launched.
|
||||
|
||||
**The problem:** ZIP codes change. Cities merge. New ZIPs are added. Any ZIP lookup service needs to stay fresh.
|
||||
|
||||
**The solution:** A monthly seed script that:
|
||||
1. Downloads the latest US ZIP code dataset from USPS/Census Bureau (public domain)
|
||||
2. Parses and normalizes ~43,000 records
|
||||
3. Inserts into SQLite with upsert logic (new ZIPs added, old ones retired)
|
||||
4. Runs automatically via a Kubernetes CronJob on the 1st of each month
|
||||
|
||||
**The API itself** is a Fastify (Node.js) server that queries SQLite. Cold query: ~5ms. The whole thing runs in a 128MB container.
|
||||
|
||||
**Code snippet** (the seed script core logic):
|
||||
|
||||
```javascript
|
||||
// Fetch and parse Census ZIP dataset
|
||||
const stream = await fetch(CENSUS_ZIP_URL);
|
||||
const records = await parseCSV(stream.body);
|
||||
|
||||
// Upsert into SQLite
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO zips (zip, city, state, county, lat, lon, timezone, type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(zip) DO UPDATE SET
|
||||
city=excluded.city, state=excluded.state,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
`);
|
||||
|
||||
for (const record of records) {
|
||||
stmt.run([record.zip, record.city, record.state, record.county,
|
||||
record.lat, record.lon, record.timezone, record.type]);
|
||||
}
|
||||
```
|
||||
|
||||
The API is live on RapidAPI with a free tier: [RAPIDAPI_ZIP_URL]
|
||||
|
||||
Happy to answer questions about SQLite performance, the seed pipeline, or the Kubernetes/Flux deployment.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Indie Hackers Milestone Post [DRAFT]
|
||||
|
||||
**Title:**
|
||||
```
|
||||
Launched 3 data APIs on RapidAPI: $0 MRR, targeting $100/mo in 90 days
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
### What I built
|
||||
|
||||
Three utility APIs backed entirely by free public-domain data:
|
||||
|
||||
- **ZIP Code Enrichment** — city/state/county/timezone/coordinates from any US ZIP ([RAPIDAPI_ZIP_URL])
|
||||
- **Public Holidays** — official holidays for 90+ countries + year ([RAPIDAPI_HOLIDAYS_URL])
|
||||
- **Air Quality Index** — current + historical AQI by city or coordinates ([RAPIDAPI_AQI_URL])
|
||||
|
||||
### Revenue: $0 → targeting $100/mo
|
||||
|
||||
The $100/mo target is ~12 Basic subscribers ($9/mo) across all three APIs after RapidAPI's 25% cut. Stretch: 4 Pro subscribers ($19/mo each).
|
||||
|
||||
### Stack
|
||||
|
||||
- **API servers:** Fastify (Node.js) + SQLite for data cache
|
||||
- **Infrastructure:** Talos Linux Kubernetes cluster (self-hosted, single-node)
|
||||
- **GitOps:** Flux CD — everything is declared in YAML, zero manual kubectl
|
||||
- **Observability:** Prometheus + Grafana, Gatus status page at status.leeworks.dev
|
||||
- **Data sources:** USPS/Census (ZIP), Nager.Date (Holidays), OpenAQ (AQI) — all public domain, $0 licensing cost
|
||||
|
||||
### What I learned
|
||||
|
||||
1. **GitOps is excellent for solo projects.** Flux means my cluster is always in sync with git. I've done zero manual deploys.
|
||||
2. **SQLite is underrated for read-heavy APIs.** Sub-5ms query times for cached lookups, zero infrastructure overhead vs PostgreSQL.
|
||||
3. **Public-domain data has a moat.** Anyone can build this, but most people don't bother. The data is stable, legal, and free forever.
|
||||
4. **Kubernetes is overkill — and that's fine.** I did it to learn. I now know Talos, Flux, cert-manager, ingress-nginx, Prometheus, Grafana from first principles.
|
||||
|
||||
### What's next
|
||||
|
||||
- Monitor first 30 days for subscriber growth
|
||||
- Build VIN Decoder as API #4 (NHTSA vPIC data, also public domain)
|
||||
- Add batch endpoints to ZIP and Holidays
|
||||
|
||||
Would love feedback on pricing — is $9/mo entry too high or too low for a utility API with a free tier?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Product Hunt Listing [DRAFT]
|
||||
|
||||
### Tagline (60 chars max)
|
||||
```
|
||||
3 utility APIs on public data — ZIP, Holidays, Air Quality
|
||||
```
|
||||
*(58 characters ✓)*
|
||||
|
||||
### Description (260 chars max)
|
||||
```
|
||||
Look up ZIP codes, public holidays for 90+ countries, and air quality index data — all via clean REST APIs backed by free government datasets. Free tier included. No API keys to generate — available on RapidAPI.
|
||||
```
|
||||
*(211 characters ✓)*
|
||||
|
||||
### First Comment (Maker Note)
|
||||
```
|
||||
Hey Product Hunt! 👋
|
||||
|
||||
I'm the developer behind leeworks.dev — three utility APIs I've been building over the past few months:
|
||||
|
||||
**ZIP Code Enrichment** [RAPIDAPI_ZIP_URL]
|
||||
Turn any US ZIP code into city, state, county, timezone, area codes, and GPS coordinates. 43,000+ ZIP codes, refreshed monthly from USPS/Census Bureau public data.
|
||||
|
||||
**Public Holidays API** [RAPIDAPI_HOLIDAYS_URL]
|
||||
Query official public holidays for any country and year. 90+ countries, ISO 3166 codes, backed by government calendar data. Great for payroll software, scheduling tools, and calendar apps.
|
||||
|
||||
**Air Quality Index API** [RAPIDAPI_AQI_URL]
|
||||
Current and historical AQI readings by city or coordinates. PM2.5, PM10, O3, NO2, SO2, CO — backed by the OpenAQ public dataset covering thousands of monitoring stations worldwide.
|
||||
|
||||
**What makes these different:**
|
||||
- All data is 100% public domain — no licensing fees, no terms restrictions
|
||||
- Free tier (100 req/mo) to try before you buy
|
||||
- Paid plans start at $9/mo for 10,000 requests/month
|
||||
- Running on Kubernetes with Prometheus monitoring and a public status page at status.leeworks.dev
|
||||
|
||||
Happy to answer questions about the data sources, the tech stack (Fastify + SQLite + Flux GitOps), or the pricing model. Thanks for checking it out!
|
||||
```
|
||||
|
||||
### Gallery / Screenshot URL Placeholders
|
||||
```
|
||||
1. docs-site homepage: https://docs.leeworks.dev (screenshot)
|
||||
2. Grafana dashboard: https://grafana.leeworks.dev (screenshot)
|
||||
3. status.leeworks.dev (screenshot)
|
||||
4. Example API response (ZIP lookup): code screenshot
|
||||
5. RapidAPI listing page: [RAPIDAPI_PROFILE_URL] (screenshot)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Email Waitlist Message [DRAFT]
|
||||
|
||||
**Subject line:**
|
||||
```
|
||||
leeworks.dev APIs are live — here's your free tier access
|
||||
```
|
||||
|
||||
**Body:**
|
||||
```
|
||||
Hi there,
|
||||
|
||||
The three APIs I've been building are now live on RapidAPI. Here's what's available and how to get started:
|
||||
|
||||
---
|
||||
|
||||
**ZIP Code Enrichment API**
|
||||
Turn any US ZIP code into city, state, county, timezone, area codes, and GPS coordinates — in a single API call.
|
||||
→ [RAPIDAPI_ZIP_URL]
|
||||
|
||||
**Public Holidays API**
|
||||
Query official public holidays for any country and year. 90+ countries, ISO 3166 codes.
|
||||
→ [RAPIDAPI_HOLIDAYS_URL]
|
||||
|
||||
**Air Quality Index API**
|
||||
Current and historical AQI by city or coordinates. PM2.5, PM10, O3, NO2, SO2, CO.
|
||||
→ [RAPIDAPI_AQI_URL]
|
||||
|
||||
---
|
||||
|
||||
**How to try for free:**
|
||||
1. Click any link above
|
||||
2. Subscribe to the **Free tier** (100 requests/month, no credit card needed)
|
||||
3. Copy your RapidAPI key from the dashboard
|
||||
4. Make your first request — full docs at https://docs.leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
**Quick start (ZIP enrichment):**
|
||||
|
||||
```bash
|
||||
curl "https://zip.leeworks.dev/v1/lookup?zip=90210" \
|
||||
-H "X-RapidAPI-Key: YOUR_KEY" \
|
||||
-H "X-RapidAPI-Host: zip-enrichment.p.rapidapi.com"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Want more than 100 requests/month?**
|
||||
Paid plans start at $9/mo for 10,000 requests. See the full pricing table on each API's RapidAPI listing page.
|
||||
|
||||
Questions? Reply to this email or open an issue at https://docs.leeworks.dev/support.
|
||||
|
||||
Thanks for your interest,
|
||||
Wyatt
|
||||
leeworks.dev
|
||||
|
||||
---
|
||||
|
||||
*You're receiving this because you signed up for early access. To unsubscribe, reply with "unsubscribe".*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Publishing
|
||||
|
||||
Before changing any section from DRAFT to READY:
|
||||
|
||||
- [ ] Issue #44 complete — RapidAPI listing URLs obtained
|
||||
- [ ] Replace all `[RAPIDAPI_ZIP_URL]` placeholders
|
||||
- [ ] Replace all `[RAPIDAPI_HOLIDAYS_URL]` placeholders
|
||||
- [ ] Replace all `[RAPIDAPI_AQI_URL]` placeholders
|
||||
- [ ] Replace all `[RAPIDAPI_PROFILE_URL]` placeholders
|
||||
- [ ] Verify code examples work against live endpoints
|
||||
- [ ] Confirm free tier limit is accurate (currently documented as 100 req/mo)
|
||||
- [ ] Confirm all three APIs pass pre-launch-checklist.md
|
||||
- [ ] Product Hunt gallery screenshots captured
|
||||
- [ ] Email list exported from whatever signup form was used
|
||||
|
||||
Once all items above are checked, update the Status key at the top of this document from DRAFT to READY for each section.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Pre-Launch Checklist
|
||||
|
||||
Use this checklist as the final go-live gate — run through every item the day before flipping the public switch. All items must be ✅ before announcing public availability.
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- [ ] All Flux components `READY=True` (`flux get all`)
|
||||
- [ ] All three API pods Running and READY
|
||||
- [ ] `zip-enrichment` pod `READY=1/1` (`kubectl get pods -n zip-enrichment`)
|
||||
- [ ] `holidays` pod `READY=1/1` (`kubectl get pods -n holidays`)
|
||||
- [ ] `air-quality` pod `READY=1/1` (`kubectl get pods -n air-quality`)
|
||||
- [ ] `docs-site` pod Running and READY (`kubectl get pods -n docs-site`)
|
||||
- [ ] Prometheus scraping all three API services (check Prometheus Targets UI)
|
||||
- [ ] Grafana dashboard accessible at `grafana.leeworks.dev`
|
||||
|
||||
---
|
||||
|
||||
## DNS & TLS
|
||||
|
||||
- [ ] `zip.leeworks.dev` → cluster ingress IP (`dig zip.leeworks.dev +short` + `curl -I https://zip.leeworks.dev`)
|
||||
- [ ] `holidays.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `aqi.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `docs.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `status.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `registry.leeworks.dev` → cluster ingress IP
|
||||
- [ ] `grafana.leeworks.dev` → cluster ingress IP
|
||||
- [ ] TLS certificates issued for all 7 subdomains (`kubectl get certificates -A`)
|
||||
|
||||
---
|
||||
|
||||
## Functional Verification
|
||||
|
||||
- [ ] `GET /health` returns HTTP 200 on **zip-enrichment** (`curl https://zip.leeworks.dev/health`)
|
||||
- [ ] `GET /health` returns HTTP 200 on **holidays** (`curl https://holidays.leeworks.dev/health`)
|
||||
- [ ] `GET /health` returns HTTP 200 on **air-quality** (`curl https://aqi.leeworks.dev/health`)
|
||||
- [ ] `GET /zip/{zip}` returns correct data for a sample ZIP code (e.g. `curl https://zip.leeworks.dev/zip/10001`)
|
||||
- [ ] `GET /holidays/{year}` returns correct data (e.g. `curl https://holidays.leeworks.dev/holidays/2026`)
|
||||
- [ ] `GET /aqi/{city}` returns correct data (e.g. `curl https://aqi.leeworks.dev/aqi/New%20York`)
|
||||
- [ ] Request **without** `X-RapidAPI-Proxy-Secret` returns HTTP 403 on all three APIs
|
||||
- [ ] `docs.leeworks.dev/pricing` loads correctly
|
||||
- [ ] `status.leeworks.dev` shows all three APIs as **UP**
|
||||
|
||||
---
|
||||
|
||||
## Legal & Monetisation
|
||||
|
||||
- [ ] `docs/legal/terms-of-service.md` committed and reachable at `docs.leeworks.dev/legal/terms-of-service`
|
||||
- [ ] `docs/legal/privacy-policy.md` committed and reachable at `docs.leeworks.dev/legal/privacy-policy`
|
||||
- [ ] `docs/legal/acceptable-use-policy.md` committed and reachable at `docs.leeworks.dev/legal/acceptable-use-policy`
|
||||
- [ ] All three APIs listed on RapidAPI with **Free + 3 paid tiers** (leeworks-agents/api-company#44)
|
||||
- [ ] PayPal linked to RapidAPI (leeworks-agents/api-company#19)
|
||||
- [ ] `rapidapi-proxy-secret` updated with **real** RapidAPI values in all 3 namespaces (leeworks-agents/api-company#81)
|
||||
|
||||
---
|
||||
|
||||
## Post-Launch
|
||||
|
||||
- [ ] Alert channel confirmed — Slack webhook active (leeworks-agents/api-company#73)
|
||||
- [ ] Grafana alert rules firing correctly on synthetic test downtime
|
||||
- [ ] First STATUS.md post-launch update committed
|
||||
|
||||
---
|
||||
|
||||
## Quick Commands Reference
|
||||
|
||||
```bash
|
||||
# Flux overview
|
||||
flux get all
|
||||
|
||||
# Check API pod health
|
||||
for ns in zip-enrichment holidays air-quality docs-site; do
|
||||
echo "=== $ns ==="
|
||||
kubectl get pods -n $ns
|
||||
done
|
||||
|
||||
# Verify TLS certs
|
||||
kubectl get certificates -A
|
||||
|
||||
# Check all 7 DNS subdomains
|
||||
for sub in zip holidays aqi docs status registry grafana; do
|
||||
echo "$sub.leeworks.dev -> $(dig $sub.leeworks.dev +short)"
|
||||
done
|
||||
|
||||
# Smoke test health endpoints
|
||||
for svc in "zip.leeworks.dev/health" "holidays.leeworks.dev/health" "aqi.leeworks.dev/health"; do
|
||||
echo "$svc: $(curl -s -o /dev/null -w '%{http_code}' https://$svc)"
|
||||
done
|
||||
|
||||
# Verify 403 on missing proxy secret
|
||||
curl -s -o /dev/null -w "%{http_code}" https://zip.leeworks.dev/zip/10001
|
||||
# Expected: 403
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_References: ROADMAP.md §Phase 6; leeworks-agents/api-company#33, #44, #19, #81, #73, #7, #8, #18_
|
||||
@@ -4,3 +4,4 @@ resources:
|
||||
- namespace.yaml
|
||||
- externalsecret.yaml
|
||||
- helmrelease.yaml
|
||||
- servicemonitor.yaml
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: air-quality
|
||||
namespace: air-quality
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: air-quality
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
@@ -4,3 +4,4 @@ resources:
|
||||
- namespace.yaml
|
||||
- externalsecret.yaml
|
||||
- helmrelease.yaml
|
||||
- servicemonitor.yaml
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: holidays
|
||||
namespace: holidays
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: holidays
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
@@ -4,3 +4,4 @@ resources:
|
||||
- namespace.yaml
|
||||
- externalsecret.yaml
|
||||
- helmrelease.yaml
|
||||
- servicemonitor.yaml
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: zip-enrichment
|
||||
namespace: zip-enrichment
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: zip-enrichment
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* publish-openapi.js
|
||||
*
|
||||
* Uploads each API's openapi.yaml to the RapidAPI Platform API.
|
||||
* Environment variables required per API (skips silently if not set):
|
||||
* RAPIDAPI_KEY — RapidAPI Platform API bearer token
|
||||
* RAPIDAPI_ZIP_API_ID — API ID for ZIP Enrichment on RapidAPI
|
||||
* RAPIDAPI_ZIP_VERSION_ID — Version ID for ZIP Enrichment
|
||||
* RAPIDAPI_HOLIDAYS_API_ID
|
||||
* RAPIDAPI_HOLIDAYS_VERSION_ID
|
||||
* RAPIDAPI_AQI_API_ID
|
||||
* RAPIDAPI_AQI_VERSION_ID
|
||||
*
|
||||
* Usage: node scripts/publish-openapi.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || process.env.RAPIDAPI_PLATFORM_KEY;
|
||||
|
||||
if (!RAPIDAPI_KEY) {
|
||||
console.log('⚠️ RAPIDAPI_KEY / RAPIDAPI_PLATFORM_KEY not set — skipping all spec uploads.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const APIS = [
|
||||
{
|
||||
name: 'zip-enrichment',
|
||||
specPath: path.join(__dirname, '..', 'apis', 'zip-enrichment', 'openapi.yaml'),
|
||||
apiId: process.env.RAPIDAPI_ZIP_API_ID,
|
||||
versionId: process.env.RAPIDAPI_ZIP_VERSION_ID,
|
||||
},
|
||||
{
|
||||
name: 'holidays',
|
||||
specPath: path.join(__dirname, '..', 'apis', 'holidays', 'openapi.yaml'),
|
||||
apiId: process.env.RAPIDAPI_HOLIDAYS_API_ID,
|
||||
versionId: process.env.RAPIDAPI_HOLIDAYS_VERSION_ID,
|
||||
},
|
||||
{
|
||||
name: 'air-quality',
|
||||
specPath: path.join(__dirname, '..', 'apis', 'air-quality', 'openapi.yaml'),
|
||||
apiId: process.env.RAPIDAPI_AQI_API_ID,
|
||||
versionId: process.env.RAPIDAPI_AQI_VERSION_ID,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Build a multipart/form-data body from a file buffer.
|
||||
* Returns { body: Buffer, boundary: string }
|
||||
*/
|
||||
function buildMultipart(fieldName, filename, fileBuffer, contentType = 'application/yaml') {
|
||||
const boundary = '----FormBoundary' + Math.random().toString(36).slice(2);
|
||||
const CRLF = '\r\n';
|
||||
const parts = [
|
||||
Buffer.from(
|
||||
`--${boundary}${CRLF}` +
|
||||
`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"${CRLF}` +
|
||||
`Content-Type: ${contentType}${CRLF}${CRLF}`
|
||||
),
|
||||
fileBuffer,
|
||||
Buffer.from(`${CRLF}--${boundary}--${CRLF}`),
|
||||
];
|
||||
return { body: Buffer.concat(parts), boundary };
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a spec file to RapidAPI Platform API.
|
||||
* Returns a promise that resolves with the response status code.
|
||||
*/
|
||||
function uploadSpec(api) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!api.apiId || !api.versionId) {
|
||||
console.log(`⏭️ Skipping ${api.name}: API ID or Version ID not configured.`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(api.specPath)) {
|
||||
console.log(`⏭️ Skipping ${api.name}: spec file not found at ${api.specPath}`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
const fileBuffer = fs.readFileSync(api.specPath);
|
||||
const { body, boundary } = buildMultipart('spec', 'openapi.yaml', fileBuffer);
|
||||
|
||||
const options = {
|
||||
hostname: 'platformapi1.p.rapidapi.com',
|
||||
path: `/v1/apis/${api.apiId}/versions/${api.versionId}`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${RAPIDAPI_KEY}`,
|
||||
'X-RapidAPI-Key': RAPIDAPI_KEY,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': body.length,
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
console.log(`✓ Published ${api.name} spec to RapidAPI (HTTP ${res.statusCode})`);
|
||||
resolve(res.statusCode);
|
||||
} else {
|
||||
reject(new Error(`Failed to publish ${api.name}: HTTP ${res.statusCode} — ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let hasError = false;
|
||||
for (const api of APIS) {
|
||||
try {
|
||||
await uploadSpec(api);
|
||||
} catch (err) {
|
||||
console.error(`✗ ${err.message}`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user