A web aggregator for U.S. high-power rocketry (HPR) motor availability across multiple vendors.
Live at motor.fusionspace.co.
The U.S. hobby motor shortage makes finding a specific impulse / diameter / propellant motor difficult — flyers often check each vendor manually. This project scrapes a curated set of vendors on a schedule, normalizes their listings against a canonical motor catalog (ThrustCurve.org), and presents a single searchable view of stock status by motor — and when a motor is sold out everywhere, points you to comparable in-stock substitutes you could fly instead.
- One searchable view of live stock and pricing for AeroTech, Cesaroni, and Loki across U.S. vendors, each listing matched to a canonical ThrustCurve motor. The full catalog ships to the browser, so search, filtering, and sorting are instant — no round-trip per click.
- Whole-catalog coverage — every AeroTech/Cesaroni/Loki D+ motor is browsable, including ones no tracked vendor stocks: search a real motor and you always land on an honest page ("not sold by any tracked vendor — here's the closest in-stock swap") instead of an empty result.
- Filter and sort by impulse class, diameter, total-impulse range, certification level (L1–L3), manufacturer, vendor, propellant, reload case, and in-stock-only; sort by class, impulse, thrust, diameter, or cheapest in-stock price. Every filter lives in the URL, so any view is shareable.
- Per-motor detail pages (
/motor/<mfr>/<designation>) — full specs, every vendor's price and stock side by side, an availability history (how often the motor's been buyable since the scrape cadence became reliable, with a per-vendor in-stock/out timeline), similar in-stock motors, and a link to the ThrustCurve thrust curve; each carriesProduct/Offerstructured data and a generated social card. - In-stock substitutes — when a motor is sold out at every vendor, surfaces comparable in-stock motors you could fly instead: same diameter and impulse class, total impulse within ±15% and average thrust within ±35%, ranked by closest fit.
- Compare side by side — line up the full specs of up to four motors (class, diameter, total impulse, average thrust, burn time, specific impulse, propellant, case/type) and overlay their thrust curves in one view, so you can pick between candidates — e.g. two reloads that fit the same rocket. The selection lives in the URL, so a comparison is shareable.
- My Rockets — save a rocket by its motor-mount diameter (the only required field), optionally pinning a cert level, impulse class, reload case, and/or total-impulse band; each shows how many in-stock motors fit it, and one tap opens a "fly it" loadout — the in-stock motors that fit (cheapest first), one tap to add them all to a Plan order, and the closest buyable swaps when nothing that fits is in stock.
- Plan your order — star the motors you want, then get the cheapest way to buy them all across vendors: set a quantity per motor and an estimated shipping/HAZMAT cost per order, and it minimizes motor cost + shipping × number of shipments (and is pack-aware — where a motor is sold only in a multipack, it buys whole packs and prices per motor). When a motor on your list is sold out everywhere, it surfaces in-stock swaps you can add in one tap to keep the order buyable. The plan is shareable as a link (re-priced live for whoever opens it) and exportable as plain text.
- Restock email alerts — get notified when a specific motor, or anything that fits a saved rocket, comes back in stock — or appears in stock for the first time (watch a "phantom" motor no vendor stocks and hear the moment one lists it) (setup).
- History-powered buying signals — a catalog badge flags motors that are in stock now but rarely are (grab it) vs. often out (the scarcity verdict only fires once a motor has been tracked for several days, and never on discontinued stock); each in-stock listing's price gets a marker vs. its own tracked history (
↓ lowest tracked/↓ price dropped/↑ above its low). Cadence-sensitive stats are clipped to the reliable-scrape epoch; price min/max isn't, so it uses full history with a noise guard. - Restock & last-in-stock history plus best-price-across-vendors (compared per motor, so multipacks — some vendors sell small motors in 2/3/12-packs — are priced by the unit with the pack total noted), derived from successive hourly snapshots.
- Scrape-health monitoring — catches silent breakages (carry-forward, sustained staleness, below-baseline count / in-stock / match-rate anomalies, a block served as a successful HTTP 200, and a registered vendor contributing zero listings) and opens a single auto-closing tracking issue (details).
- Free public JSON API — the whole dataset (every motor + per-vendor stock and pricing) as static, CORS-open, no-key, no-rate-limit JSON, refreshed hourly (docs · on the site).
- Plus a ★ watchlist, dark mode, and an on-page explainer of exactly how every figure is derived. Interactive behavior is covered by a headless-browser (Playwright) end-to-end suite in CI.
A free, read-only JSON API of everything on the site — served as static files on a CDN, so there's no key, no rate limit, and no cost, and it's Access-Control-Allow-Origin: * (call it from a browser). Refreshed on the hourly deploy.
Base URL: https://motor.fusionspace.co/api/v1/
| Endpoint | What it is |
|---|---|
meta.json |
schema version, generated_at, counts, manufacturer list |
motors.json |
every matched motor we have a listing for |
in-stock.json |
same shape, only motors in stock somewhere |
vendors.json |
the vendors we track + per-vendor counts |
motors/{mfr}/{designation}.json |
a single motor (slugs mirror the /motor URL) |
openapi.json |
OpenAPI 3.1 spec |
# Cheapest in-stock H motors right now
curl -s https://motor.fusionspace.co/api/v1/in-stock.json \
| jq '.motors[] | select(.impulse_class=="H")
| {designation, from: .cheapest_in_stock.unit_price_cents, vendor: .cheapest_in_stock.vendor}'Full field reference, schema, and terms: docs/api.md.
A personal hobby project. Not affiliated with any vendor or manufacturer listed. Stock and price data are best-effort, often stale, and not authoritative — always confirm on the vendor's own site before purchasing. If you operate a listed vendor and would like the scraper changed or removed, please open an issue and we'll comply.
Released under the MIT License — fork it, modify it, deploy your own copy, no attribution required.
Today: AeroTech across ten vendors (including manufacturer-direct from AeroTech itself); Cesaroni (CTI) on Chris' Rocket Supplies, Wildman Rocketry, Performance Hobbies, Moto-Joe Rocketry, and Animal Motor Works; and Loki Research direct from the manufacturer and via Performance Hobbies.
| Vendor | State | Platform | Motors |
|---|---|---|---|
| Chris' Rocket Supplies | GA | Custom (Schema.org JSON-LD) | AeroTech, Cesaroni |
| BuyRocketMotors | TX | Shopify | AeroTech |
| Wildman Rocketry | IL | Shopify | AeroTech, Cesaroni |
| Animal Motor Works | AZ | VirtueMart | AeroTech, Cesaroni |
| Sirius Rocketry | WI | Zen Cart | AeroTech |
| Loki Research | MO | Custom (ASP store) | Loki |
| Performance Hobbies | VA | Custom (ASP.NET store) | AeroTech, Cesaroni, Loki |
| AeroTech (direct) | UT | Shopify | AeroTech (manufacturer-direct) |
| Moto-Joe Rocketry | — | OpenCart | AeroTech, Cesaroni |
| Balsa Machining Service | NV | Custom (single-page ASP) | AeroTech |
| New Century Rocketry | SC | Shopify | AeroTech |
| eRockets | OH | BigCommerce | AeroTech (low/mid-power) |
AeroTech currently backorders nearly everything rather than holding stock, and their store doesn't expose real inventory — so AeroTech-direct listings are shown as special-order with a fulfillment lead-time (e.g. "special order · ~16–20 weeks"), parsed live from AeroTech's own published lead-time banner, rather than as "in stock."
eRockets' host blocks the GitHub Actions data-center IPs outright (403 on every request; it works fine from residential IPs). Its listings are live anyway: the block trips the scraper's egress fail-over, and the free Cloudflare Worker relay fetches the pages from a clean IP. Nothing else engages that path today — it stays dormant unless a vendor blocks us.
The architecture isn't motor-specific — see Adding a vendor and Extending beyond motors if you want to grow it.
You need Python 3.12+ and Node 20+.
# Backend (scraper + catalog + CLI)
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
# Frontend (Next.js UI)
cd ../frontend
npm install
npm run dev # http://localhost:3000 — uses snapshot.example.json out of the boxTo run a real scrape against live vendors:
cd backend
hpr catalog refresh # download AeroTech, Cesaroni, and Loki motors from ThrustCurve
hpr scrape run all # ~5-10 min across all 12 vendors
hpr snapshot export # writes data/snapshot.jsonAfter that, npm run dev will pick up snapshot.json automatically (preferred over the example seed).
┌───────────────────┐ cron ┌──────────────────┐
│ vendor websites │ ──────────▶ │ backend scrapers │
└───────────────────┘ └────────┬─────────┘
│ Listings
▼
┌──────────────────┐
│ SQLite (hpr.db) │
└────────┬─────────┘
│ snapshot export
▼
┌──────────────────┐
│ snapshot.json │
└────────┬─────────┘
│ read at build/request
▼
┌──────────────────┐
│ Next.js frontend │
└──────────────────┘
Key pieces:
- backend/hpr_finder/normalize.py — turn vendor product titles into canonical AeroTech designations (e.g.
D13-10W↔D13W). - backend/hpr_finder/db.py —
find_motor_idruns a fallback chain of designation transforms to match listings against the ThrustCurve catalog. Currently 99%+ match rate. - backend/hpr_finder/scrapers/ — one file per vendor; each implements the
Scraperprotocol and returns a list ofListing. Discovery strategies vary (Shopifyvendor=AEROTECHfilter, VirtueMart category sweep, Zen Cart manufacturer-page pagination). - backend/hpr_finder/http.py —
PoliteAsyncClientenforces per-host concurrency cap + minimum-interval pacing +Retry-Afterhonoring. - frontend/lib/snapshot.ts — the only contract between backend and frontend. If
data/snapshot.jsonexists it wins; otherwise the loader falls back to the trackeddata/snapshot.example.json. Apredev/prebuildscript (frontend/scripts/copy-snapshot.mjs) copies both files intofrontend/data/because Next 16 + Turbopack refuses to bundle files from outside the project root.
- Add a scraper at
backend/hpr_finder/scrapers/<slug>.pyimplementingScraper.scrape(). Look at csrocketry.py (Schema.org JSON-LD) or sirius.py (Zen Cart manufacturer-page paginator) as templates. - Register it in backend/hpr_finder/scrapers/init.py.
- Verify the vendor's robots.txt permits crawling and set conservative
max_concurrent_per_host/min_start_interval_son the scraper class. - Run
hpr scrape run <slug>and check the match rate; tunenormalize.pyif their title format trips the regex.
The data model is generic: a Listing is {vendor, designation, sku, status, price, url}. To support kits, electronics, recovery, etc., add a product_type to Listing / Motor, generalize the catalog table, and broaden extract_designation. The scraping infrastructure (polite client, scheduling, snapshot contract, normalize fallback chain) doesn't change.
Free hosting works since the architecture is essentially a static site fed by a periodic snapshot.
- Frontend: Cloudflare Pages (static export) — deploy on
git pushvia GitHub Actions. - Scrapes: GitHub Actions cron, e.g. every 6h: run
hpr scrape run all && hpr snapshot export, uploadsnapshot.jsonas a build artifact, trigger a frontend deploy.
A test workflow lives at .github/workflows/test.yml. A deploy workflow is left as an exercise — pick the host you prefer.
For OpenGraph / Twitter share cards, the site origin defaults to the production domain (https://motor.fusionspace.co). If you deploy your own copy, set NEXT_PUBLIC_SITE_URL to your deployed origin so card image URLs resolve to your domain instead.
This project scrapes vendors without prior permission. The bar we hold ourselves to:
- Identify ourselves. The
User-Agentincludes the project URL and a contact channel (GitHub Issues). - Honor
robots.txt. Each vendor'srobots.txtis reviewed before the vendor is added; those that disallow generic crawlers are excluded (e.g. Apogee Components, Miller Motor Works — see the vendor report). This is a manual check at onboarding, not a runtime fetch. - Rate limit. Per-host concurrency cap (typically 2-4) and minimum interval between request starts (0.25-1s) — see each scraper's class attributes.
- Honor
Retry-After. On 429/503 we sleep the full duration before continuing. - No evasion. No rotating User-Agents, no residential proxies, no captcha solvers. If a vendor blocks us, we stop.
- Take down on request. Open an issue and we comply, no questions asked.
backend/ Python scraper, normalizer, SQLite store, CLI
frontend/ Next.js UI (App Router, TypeScript, Tailwind)
data/ Runtime data
snapshot.example.json tracked, frozen reference for `npm run dev`
snapshot.json gitignored, generated by `hpr snapshot export`
thrustcurve_aerotech.json tracked, refreshed via `hpr catalog refresh`
docs/ Research notes and the original vendor directory
.github/workflows/ CI (pytest, vitest, next build, Playwright e2e) + hourly scrape
Issues and PRs are welcome — especially vendor parser fixes and new vendors. See CONTRIBUTING.md for setup, the checks to run before a PR (they mirror CI), how to add a vendor, and how the golden-fixture scraper tests work. Optional email-alert config is documented in frontend/.env.example and docs/email-alerts.md.
MIT.