Use the free NBME Score Calculator →
NBMEcalc is a free USMLE Step score predictor for combining NBME, UWSA, Free 120, AMBOSS, and CMS results. It returns an independent planning estimate with a 95% confidence interval instead of presenting a single score as certain.
- Free NBME Score Calculator — combine multiple practice exams in one Step score forecast.
- NBME Score Conversion Calculator and Chart — review current CCSSA and CBSSA form families.
- Free 120 Step 2 Score Conversion — compare current, 2021, and 2019 Free 120 versions.
- CMS Form Score Conversion — use subject-form results to investigate weak rotations.
NBMEcalc is an independent educational tool and is not affiliated with or endorsed by NBME, FSMB, or USMLE.
- Framework: Next.js 15 (App Router) + TypeScript
- Styling: Tailwind CSS + custom design tokens (mint #34D399 brand)
- Components: Custom shadcn/ui-style primitives + Radix UI
- Fonts: Plus Jakarta Sans (display + body), JetBrains Mono (numbers)
- Hosting: Cloudflare Pages (edge runtime,
@cloudflare/next-on-pages) - Database: Cloudflare D1 (Drizzle ORM)
- PDF: Cloudflare Browser Rendering Worker, with an edge-safe fallback PDF
- Payments: Stripe Checkout
- Email: Postal HTTP API for magic-link email delivery
- Tests: Vitest (pure-logic units in
lib/**)
# 1. Install dependencies
npm install
# 2. Run dev server
npm run dev
# 3. Open browser
http://localhost:3000nbmecalc/
├── app/
│ ├── layout.tsx # Root layout with fonts + metadata
│ ├── page.tsx # Homepage (composes all sections)
│ └── globals.css # Tailwind + design tokens
├── components/
│ ├── ui/ # shadcn primitives (Button, Input, Accordion, Badge)
│ ├── logo.tsx # Bell-curve SVG logo (Logo + LogoMark)
│ ├── iphone-mockup.tsx # iPhone 17 Pro CSS mockup
│ └── sections/
│ ├── nav.tsx # 1. Top navigation
│ ├── hero.tsx # 2. Hero with iPhone mockup
│ ├── logo-wall.tsx # 3. Trust school list
│ ├── value-props.tsx # 4. 3-column value cards
│ ├── calculator.tsx # 5. Live predictor (CORE)
│ ├── reviews.tsx # 6. Reddit quotes
│ ├── stats.tsx # 7. Stats with count-up
│ ├── how-it-works.tsx# 8. 3-step explainer
│ ├── comparison.tsx # 9. Predictor comparison table
│ ├── resource-hub.tsx# 10. Featured guides
│ ├── reviewers.tsx # 11. Physician reviewers
│ ├── faq.tsx # 12. FAQ accordion
│ ├── blog-grid.tsx # 13. 6 blog cards
│ ├── footer.tsx # 14. Footer + legal
│ └── cookie-banner.tsx # First-visit disclaimer
├── lib/
│ ├── utils.ts # cn() helper
│ ├── data.ts # NBME conversion logic + types
│ └── email.ts # Postal HTTP email delivery
├── workers/
│ └── pdf-renderer/ # Cloudflare Browser Rendering PDF Worker
├── public/
│ └── placeholders/ # AI image placeholders + prompts
├── tailwind.config.ts # Mint color scale + animations
└── package.json
| Token | Value |
|---|---|
| Brand color | #34D399 (mint-500) |
| Font | Plus Jakarta Sans |
| Radius | rounded-full (buttons), rounded-3xl (cards) |
| CTA | Black button, white text |
All real-photo locations use <Image src="/images/[name].jpg" /> pointing at AI-generated files. The pictures are generated by the npm run gen:images script which calls the Evolink.ai Z Image Turbo API.
# 1. Get an API key at: https://evolink.ai/dashboard/keys
# 2. Copy the template and fill in your key
cp .env.example .env.local
# Then open .env.local and set EVOLINK_API_KEY=sk-xxxxxnpm run gen:images # only missing ones
npm run gen:images:force # regenerate everything
# Or one specific image:
node scripts/gen-images.mjs reviewer-1
node scripts/gen-images.mjs --force blog-cover-nbmeOutput: /public/images/*.jpg (already wired to all components).
Prompts live in /public/placeholders/*.prompt.txt — edit them to tweak style, then re-run with --force.
| Component | File | Aspect | Prompt file |
|---|---|---|---|
| Reviewers (×3) | reviewer-1/2/3.jpg |
1:1 | reviewer-1/2/3.prompt.txt |
| Resource Hub (×3) | blog-cover-nbme/ci/cram.jpg |
4:3 | blog-cover-*.prompt.txt |
| Blog Grid (×6) | blog-cram/most-tested/...jpg |
16:9 | blog-*.prompt.txt |
- Free: Single prediction, 95% CI, basic subject preview
- Single Report — $14.99: Full PDF + 14-day plan + complete subject map
- Lifetime — $34.99 one-time: Unlimited re-runs + Step 1/2/3 tracking + live timeline
- Founding Lifetime — $19.99 one-time: Current founding-stage price with no automatic member cap or deadline
All sales are final. No refunds (digital product, delivered immediately).
The app degrades gracefully without D1 (next dev: predictions still compute, just aren't persisted), so you can do most UI work without touching Cloudflare. Wire up D1 when you need to test the funnel or the rate limiter end-to-end.
# 1. Apply the committed migrations against a local SQLite shadow DB.
npx wrangler d1 migrations apply nbmecalc-prod --local
# 2. Optionally peek at the schema.
npx wrangler d1 execute nbmecalc-prod --local --command "SELECT name FROM sqlite_schema WHERE type='table';"wrangler keeps the local SQLite under .wrangler/state/v3/d1/ — it's git-ignored. To exercise the real edge runtime + D1 binding locally, use npm run preview instead of npm run dev.
# 1. Create the database. Wrangler prints a database_id.
npx wrangler d1 create nbmecalc-prod
# 2. Paste the returned id into wrangler.toml (replaces REPLACE_ME_AFTER_FIRST_D1_CREATE).
# 3. Apply migrations to the remote DB.
npx wrangler d1 migrations apply nbmecalc-prod --remote# 1. Edit lib/db/schema.ts.
# 2. Generate a migration. Pick a descriptive --name.
npx drizzle-kit generate --name add_users_table
# 3. Apply locally (and later remote).
npx wrangler d1 migrations apply nbmecalc-prod --localMigrations are committed to lib/db/migrations/ so production deploys don't need drizzle-kit on the server — they just wrangler d1 migrations apply --remote.
Migration 0006_lifetime.sql creates the durable Lifetime entitlement table.
The Founding offer is controlled by one deployment setting rather than a
counter or timer:
# 1. Create two one-time Stripe Prices and configure them in Pages.
STRIPE_PRICE_LIFETIME_FOUNDING=price_... # $19.99
STRIPE_PRICE_LIFETIME_REGULAR=price_... # $34.99
NEXT_PUBLIC_LIFETIME_FOUNDING_OFFER_ENABLED=true
# 2. Apply the entitlement migration and deploy.
npx wrangler d1 migrations apply nbmecalc-prod --remote
npm run deployTo end the Founding offer for future purchases, set
NEXT_PUBLIC_LIFETIME_FOUNDING_OFFER_ENABLED=false and redeploy. Checkout then
selects and verifies the $34.99 Stripe Price server-side. Existing Lifetime
entitlements are never changed by this switch.
For the GitHub Actions deployment, create or update the repository variable at
Settings > Secrets and variables > Actions > Variables. If the variable is
absent, the workflow intentionally defaults to true.
The buyer count remains available internally without appearing on the page:
npx wrangler d1 execute nbmecalc-prod --remote --command "SELECT COUNT(*) AS founding_members FROM lifetime_entitlements WHERE status = 'active' AND promotion_applied = 1;"The Stripe webhook endpoint must receive checkout.session.completed,
checkout.session.async_payment_succeeded, charge.refunded, and
charge.dispute.created.
charge.refunded is retained as an entitlement-safety event: it revokes access
if an operator or payment network records a full refund despite the no-refund
policy. It does not advertise or create a customer refund right.
Premium report downloads use a separate Cloudflare Worker at workers/pdf-renderer. The Worker is routed at /api/_pdf-renderer/* and uses Cloudflare Browser Rendering (@cloudflare/puppeteer) to open the styled report page and export a color PDF with printBackground: true.
GitHub Actions deploys the Worker and the Pages app from this repository. It derives a shared PDF_RENDERER_SECRET from the existing CLOUDFLARE_API_TOKEN and writes it to both Cloudflare targets during deployment. If the Worker is unavailable at runtime, /api/report/[session_id]/pdf falls back to the edge-safe text PDF generator instead of failing the purchase flow.
The Cloudflare API token used by GitHub Actions needs permissions for:
- Cloudflare Pages edit
- Workers Scripts edit
- Workers Browser Rendering
- Zone route edit for
nbmecalc.com/api/_pdf-renderer - Account read
npm test # one-shot
npm run test:watch # watch modeVitest covers lib/data.ts (prediction + personalized analytics) and lib/rate-limit.ts (fail-open + bucket counting). The DB and Next route handlers are exercised end-to-end via npm run preview (wrangler) — there are no jsdom/React-side unit tests yet.
Magic-link login emails are sent through Postal's HTTP API. Configure these variables in local .env.local and in Cloudflare Pages environment variables for production.
POSTAL_API_URL=https://mail.removexif.com/api/v1/send/message
POSTAL_API_KEY=your-postal-api-key
SMTP_FROM=noreply@nbmecalc.comUse Cloudflare secret variables for POSTAL_API_KEY. Keep SMTP_FROM aligned with the current project domain.
| Keyword | Volume/mo | Status |
|---|---|---|
| nbme score converter | 880 | /nbme-score-conversion |
| nbme score conversion | 880 | /nbme-score-conversion |
| nbme score calculator | 480 | / |
| step 2 score predictor | 480 | /step-2-predictor |
| free 120 step 2 score conversion | 480 | /free-120-predictor |
| step 3 score predictor | 320 | /step-3-predictor |
| nbme cms forms | 320 | /cms-converter |
Volumes are US monthly averages and close variants may share Google Ads volume. Do not sum variants as independent demand.
nbmecalc is not affiliated with NBME, FSMB, USMLE, USMLE-Rx, AMBOSS, UWorld, or Kaplan. Predictions are statistical estimates for educational purposes only.
- Cloudflare D1 schema + write API (
/api/predict, predictions / reports / rate_limits / events) - Stripe Checkout for Single Report + Lifetime (server pricing, durable entitlement, signed webhook)
- PDF report generation (
@react-pdf/rendereron edge) - Personalized analytics layer (trajectory / source insight / target gap / postpone / weak)
- Magic Link auth + user accounts
-
/dashboardwith saved predictions timeline - Webhook → KV entitlement / D1 reports row
- Publish individual NBME form pages only after each page has verified form-to-exam mapping, unique evidence, and enough information gain to pass the sitemap admission checklist.
- Compare pages (
/compare/vs-predictmystepscore, ...) - Blog MDX pipeline
- Dynamic OG images
Reddit: u/nbmecalc · Email: hello@nbmecalc.com