An independent, searchable archive of ads running across ChatGPT — built on Cloudflare's full stack.
- Overview
- Tech Stack
- Repository Layout
- Local Development
- API Reference
- 🚀 Deploying to Cloudflare (Step-by-Step)
- Environment Variables
- CI / CD with GitHub Actions
- Regenerating Data
- Useful Scripts
- Disclaimer
The ChatGPT Ads Library lets you browse and search every ad running inside ChatGPT — including ad creative, headline copy, advertiser profiles, publish dates, and impression counts. It is fully indexed for SEO, AEO (Answer Engine Optimization), and GEO (Generative Engine Optimization).
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React 18 + Vite + TypeScript + Tailwind CSS | Website / UI |
| API | Cloudflare Workers + Hono | Search, filtering, sorting |
| Database | Cloudflare D1 (SQLite at the edge) | Ads + advertiser metadata |
| Image storage | Cloudflare R2 | Ad screenshots and logos (optional) |
| CDN / Cache | Cloudflare global network | Low-latency delivery worldwide |
| Hosting | Cloudflare Pages | Static frontend with SPA routing |
| Domain / DNS | Cloudflare | chatgpt-ads-library.com |
chatgpt-ads-library/
├── api/ # Cloudflare Worker (Hono + D1)
│ ├── src/
│ │ ├── index.ts # Worker entry point
│ │ ├── routes.ts # API route handlers
│ │ ├── db.ts # D1 query helpers
│ │ └── types.ts # Shared types
│ ├── schema.sql # D1 table schema
│ ├── seed.sql # Full schema + data seed (~3,157 ads)
│ └── wrangler.toml # Worker / D1 / R2 config ← edit this
├── frontend/ # React + Vite SPA
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── pages/ # Route-level page components
│ │ ├── lib/ # API client, schema helpers, utils
│ │ └── hooks/ # Custom React hooks
│ ├── public/
│ │ ├── _redirects # SPA fallback for Cloudflare Pages
│ │ ├── robots.txt # Crawl rules + AI bot directives
│ │ ├── sitemap.xml # Full URL sitemap
│ │ └── llms.txt # GEO content for AI engines
│ ├── .env.example # Environment variable template
│ └── vite.config.ts # Dev proxy: /api → localhost:8787
├── data/
│ ├── ads.json # Normalized ad records (3,157)
│ └── advertisers.json # Derived advertiser records (1,359)
├── scripts/
│ ├── normalize.mjs # Raw JSON → clean data files
│ ├── generate-seed.mjs # data/ → api/seed.sql
│ └── generate-sitemap.mjs # Generates public/sitemap.xml
└── package.json # Root workspace scripts
Prerequisites: Node.js ≥ 18 (.nvmrc pins 22). A Cloudflare account is only needed for D1/Worker commands — the local dev server uses a local D1 SQLite file automatically.
# 1. Clone the repo
git clone https://github.com/your-username/ChatGPT-Ads-Library.git
cd ChatGPT-Ads-Library
# 2. Install all workspace dependencies (api + frontend)
npm install
# 3. Seed the local D1 database (creates tables + inserts all data)
npm run seed:local
# 4. Run the API worker and frontend together
npm run dev| Service | URL |
|---|---|
| Frontend (Vite) | http://localhost:5173 |
| API Worker (Wrangler) | http://localhost:8787 |
The Vite dev server proxies all /api/* requests to the local Worker on port 8787, so the frontend connects to the API automatically — no .env file needed for local development.
All endpoints return JSON and are read-only.
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check |
GET |
/api/stats |
Totals, date range, top advertisers |
GET |
/api/ads |
List / search / filter all ads |
GET |
/api/ads/:id |
Single ad detail |
GET |
/api/advertisers |
List / search advertisers |
GET |
/api/advertisers/:slug |
Advertiser detail + their ads |
| Parameter | Type | Default | Description |
|---|---|---|---|
q |
string |
— | Full-text search across advertiser, domain, copy, description |
advertiser |
string |
— | Filter by advertiser slug |
sort |
string |
date_desc |
date_desc, date_asc, impressions_desc, impressions_asc |
page |
number |
1 |
Page number |
limit |
number |
24 |
Page size (max 100) |
min_impressions |
number |
— | Minimum impression count |
max_impressions |
number |
— | Maximum impression count |
date_from |
string |
— | Start date YYYY-MM-DD |
date_to |
string |
— | End date YYYY-MM-DD |
Example:
GET /api/ads?q=ezoic&sort=impressions_desc&min_impressions=50&page=1&limit=24
| Parameter | Type | Default | Description |
|---|---|---|---|
q |
string |
— | Search by name or domain |
sort |
string |
ad_count_desc |
ad_count_desc, impressions_desc, name_asc, name_desc |
page |
number |
1 |
Page number |
limit |
number |
60 |
Page size (max 5000) |
This project is built entirely for Cloudflare's free and low-cost tiers — no external servers needed.
GitHub repo
│
├─► Cloudflare Workers ──► D1 Database (API)
│ └──► R2 Bucket (Images, optional)
│
└─► Cloudflare Pages (Frontend)
└──► Custom Domain (DNS on Cloudflare)
- Cloudflare account — sign up free
- Node.js 18+ installed locally
- Wrangler CLI — installed as a dev dependency, use
npx wrangleror install globally:npm install -g wrangler
npx wrangler loginThis opens a browser window. Log in with your Cloudflare account and authorize Wrangler. Your credentials are stored locally in ~/.wrangler/config.
Verify it worked:
npx wrangler whoamicd api
npx wrangler d1 create chatgpt-ads-libraryYou'll see output like:
✅ Successfully created DB 'chatgpt-ads-library'
[[d1_databases]]
binding = "DB"
database_name = "chatgpt-ads-library"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ← copy this
Open api/wrangler.toml and paste the real database_id:
[[d1_databases]]
binding = "DB"
database_name = "chatgpt-ads-library"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # ← replace placeholderImportant
Without the correct database_id, the Worker cannot connect to D1 and all API calls will fail.
This imports the schema and all ~3,157 ad records into your live D1 database.
# From the api/ directory
npx wrangler d1 execute chatgpt-ads-library --remote --file=./seed.sqlOr from the root using the workspace script:
npm run seed:remoteNote
The seed.sql file is ~3.4 MB. It contains both the CREATE TABLE statements and all INSERT data — you only need to run this once. If you run it again, it uses INSERT OR REPLACE so no duplicates are created.
Verify the data landed:
npx wrangler d1 execute chatgpt-ads-library --remote --command="SELECT COUNT(*) FROM ads"cd api
npm run deployThis runs wrangler deploy and publishes the Worker to Cloudflare's global edge network. You'll see output like:
✅ Deployed chatgpt-ads-library-api
https://chatgpt-ads-library-api.<your-subdomain>.workers.dev
Save that Worker URL — you'll need it in Step 5 if the API and frontend are on different origins.
Test the live API:
curl https://chatgpt-ads-library-api.<your-subdomain>.workers.dev/api/health
curl https://chatgpt-ads-library-api.<your-subdomain>.workers.dev/api/statsYou have two options: via the Cloudflare dashboard (recommended for first-time) or via Wrangler CLI.
This is the recommended approach. Every push to main will auto-deploy.
-
Click Create application → Pages → Connect to Git
-
Authorize GitHub and select your ChatGPT-Ads-Library repository
-
Configure the build settings:
Setting Value Framework preset None(orVite)Build command npm run build -w frontendBuild output directory frontend/distRoot directory /(leave as repo root)Node.js version 18or22 -
Add the environment variable (only if API is on a different domain from Pages):
Variable Value VITE_API_URLhttps://chatgpt-ads-library-api.<your-subdomain>.workers.dev[!TIP] If you later add a custom domain and route
/api/*to your Worker (see Step 6), you can removeVITE_API_URLentirely and the frontend will call/apion the same origin. -
Click Save and Deploy. Cloudflare builds and deploys — usually in under 60 seconds.
Your site will be live at https://<your-project>.pages.dev.
# Build the frontend
cd frontend
npm run build
# Deploy the dist/ folder to Pages
npx wrangler pages deploy dist --project-name=chatgpt-ads-libraryOn the first run you'll be prompted to create a new Pages project. Subsequent runs update the existing project.
If your domain isn't already on Cloudflare:
- Go to Cloudflare Dashboard → Add a site
- Enter your domain (e.g.
chatgpt-ads-library.com) - Follow the steps to update your registrar's nameservers to Cloudflare's
- In your Pages project → Custom domains → Set up a custom domain
- Enter
chatgpt-ads-library.com(and optionallywww.chatgpt-ads-library.com) - Cloudflare automatically creates the DNS records and provisions a free SSL certificate
Instead of a separate workers.dev subdomain for the API, you can serve everything from one domain using a Worker Route. This lets the frontend call /api without setting VITE_API_URL.
- Go to Workers & Pages → your Worker → Triggers → Add route
- Add the route:
chatgpt-ads-library.com/api/* - Select Zone:
chatgpt-ads-library.com - Remove
VITE_API_URLfrom your Pages environment variables (or set it to empty) - Redeploy Pages to pick up the change
Now https://chatgpt-ads-library.com/api/* hits the Worker, and everything else serves from Pages.
The seed data references ad screenshots hosted on the original source's Supabase storage, so the site works without R2 from day one. Set up R2 only if you want full image independence.
npx wrangler r2 bucket create chatgpt-ads-library-imagesUpdate api/wrangler.toml with the exact bucket name:
[[r2_buckets]]
binding = "ADS_IMAGES"
bucket_name = "chatgpt-ads-library-images"Download each image URL from data/ads.json (media_url and advertiser_logo fields) and upload them to R2:
# Example: upload a single file
npx wrangler r2 object put chatgpt-ads-library-images/ads/abc123.webp --file=./abc123.webpFor bulk migration, write a small script that:
- Reads
data/ads.json - Downloads each
media_url - Uploads it to R2 with a consistent key
- Updates the URL in the data file
Then re-seed:
npm run seed:generate # regenerate seed.sql from updated data/
npm run seed:remote # push to D1The ADS_IMAGES binding is already wired into the Worker as c.env.ADS_IMAGES.
Copy frontend/.env.example to frontend/.env for local overrides.
| Variable | Required | Default | Description |
|---|---|---|---|
VITE_API_URL |
No | "" (same origin) |
Full URL of the API Worker if on a different domain |
VITE_SITE_URL |
No | https://chatgpt-ads-library.com |
Canonical site URL used in SEO tags and JSON-LD |
Set these under Settings → Environment variables:
| Variable | Production value |
|---|---|
VITE_API_URL |
https://chatgpt-ads-library-api.<subdomain>.workers.dev (omit if using Worker route) |
VITE_SITE_URL |
https://your-domain.com |
Note
Variables prefixed with VITE_ are embedded into the JavaScript bundle at build time. They are not secret — do not put API keys here.
If you prefer GitHub Actions over Cloudflare's built-in Git integration, add this workflow:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-api:
name: Deploy API Worker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Deploy Worker
run: npm run deploy -w api
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
deploy-frontend:
name: Deploy Frontend to Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build frontend
run: npm run build -w frontend
env:
VITE_SITE_URL: https://chatgpt-ads-library.com
- name: Publish to Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy frontend/dist --project-name=chatgpt-ads-libraryRequired GitHub secrets:
| Secret | Where to get it |
|---|---|
CLOUDFLARE_API_TOKEN |
Cloudflare Dashboard → Profile → API Tokens → Create Token → Edit Cloudflare Workers template |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare Dashboard → right sidebar on any page |
If you update the raw source JSON (supaintent-*.json):
# 1. Normalize raw data → data/ads.json + data/advertisers.json
node scripts/normalize.mjs
# 2. Generate the SQL seed file
npm run seed:generate # → api/seed.sql
# 3. Regenerate the sitemap
npm run sitemap:generate # → frontend/public/sitemap.xml
# 4. Push to local or remote D1
npm run seed:local # local dev
npm run seed:remote # productionRun these from the repo root unless noted.
| Script | Description |
|---|---|
npm run dev |
Run API worker + frontend concurrently |
npm run build |
Build both worker and frontend |
npm run typecheck |
TypeScript check across all workspaces |
npm run seed:local |
Seed local D1 database |
npm run seed:remote |
Seed remote D1 database |
npm run seed:generate |
Regenerate api/seed.sql from data/ |
npm run sitemap:generate |
Regenerate frontend/public/sitemap.xml |
npm run deploy (api/) |
Deploy Worker to Cloudflare |
This is an independent project and is not affiliated with or endorsed by OpenAI. Ad data may be incomplete or change over time.