Static demo · Getting started · Features · Documentation
A fully functional clone of the AI math solver math-gpt.org, built from scratch with Node.js and vanilla JavaScript. Type a problem, paste LaTeX or upload a photo and get an instant step-by-step solution with rendered math, interactive graphs and consistent, repeatable output.
Not affiliated with math-gpt.org. Built for educational purposes and containing no code or proprietary prompts from the original site.
Live static demo: https://outblade.github.io/mathgpt-clone/ (GitHub Pages, from docs/)
GitHub Pages only serves static files, so the hosted demo is a browser-only build: text/LaTeX problems are solved client-side by a mathjs port of the local solver, with history kept in this browser via localStorage. Image upload, AI solving, and accounts (email/password + Google/Apple) need the Express backend and only run when you clone the repo and npm start (see below).
| Feature | How it works |
|---|---|
| Text input | Free-form problems: solve x^2 - 5x + 6 = 0, derivative of sin(x)*x^2, 2^10 / 8 |
| LaTeX input | Paste LaTeX directly: \frac{3}{4} + \frac{1}{6}, \sqrt{18} |
| Image upload | Photograph a handwritten or printed problem; a vision model reads and solves it |
| AI solving | Provider-agnostic backend: OpenRouter (Qwen), Groq, Gemini or OpenAI |
| Offline fallback | A built-in mathjs engine solves equations, derivatives, simplification, evaluation and plotting with no API key |
| Output consistency | Identical or near-identical submissions return identical output (see below) |
| Step-by-step solutions | Titled steps with plain-language detail and KaTeX-rendered math |
| Interactive graphs | Functions are sampled and drawn with Chart.js (hover for values) |
| Accounts | Email/password (bcrypt) plus Sign in with Google and Sign in with Apple |
| Sessions | HttpOnly cookie sessions, persisted server-side, 30-day lifetime |
| History | Every problem a logged-in user solves is stored, reopenable and deletable |
| Error handling | Invalid input, unsupported problems, API failures and rate limits return clear messages |
| Security | Rate limiting, input sanitization, OAuth state/CSRF cookie, JWKS id_token verification, CSP, upload type/size limits |
Requirements: Node.js 18+ (no native build tools; all dependencies are pure JavaScript, which matters on ARM64).
cd mathgpt-clone
npm install
copy .env.example .env # macOS/Linux: cp .env.example .env
npm startOpen http://localhost:3000. The app works immediately: without any API key the built-in solver answers text problems (equations, derivatives, simplification, arithmetic, plotting). Image solving and AI text solving need a provider key.
The backend is provider-agnostic; every supported provider speaks the OpenAI-compatible /chat/completions format, so switching is a config change, not a code change. Candidates were compared on the three axes that matter here:
| Provider | Model (default) | Accuracy on math | Latency | Consistency support | Free tier |
|---|---|---|---|---|---|
| OpenRouter (default) | qwen/qwen-2.5-72b-instruct + qwen-2.5-vl-72b-instruct (vision) |
Strong; Qwen2.5 is a top open math model | Medium | seed + temperature honored |
Yes (free-tier keys and :free models) |
| Groq | qwen-2.5-32b + llama-3.2-90b-vision |
Good | Very low (fastest) | seed supported |
Yes |
| Gemini | gemini-2.0-flash |
Good | Low | temperature only (no seed) |
Yes |
| OpenAI | gpt-4o |
Strong | Medium | seed supported |
No (paid) |
Default: OpenRouter with Qwen2.5 / Qwen2.5-VL. Rationale: Qwen2.5 is one of the strongest openly available models on mathematical reasoning, OpenRouter exposes both a text and a vision variant through one key, and it honors seed + temperature=0, which is essential for the consistency requirement. Groq is the pick if latency dominates; Gemini is the simplest key to obtain. Any of them is selected automatically from whichever key you set.
To choose a provider, set its key in .env (get one from the URL in .env.example). If several are set, AI_PROVIDER decides; otherwise the first configured wins in the order openrouter → groq → gemini → openai. Restart after editing .env.
The requirement: identical or highly similar problems/images must consistently produce identical or near-identical output. This is met with two layers.
Layer A - a hard cache guarantee (independent of the LLM).
- Every text problem is canonicalized (whitespace/case/punctuation normalized) and hashed. The same problem returns the exact stored solution.
- Every image is hashed two ways: a content hash (SHA-256 of the bytes) for exact duplicates, and a 64-bit perceptual hash (average hash) for near-duplicates. A re-encoded, rescaled or lightly cropped copy lands within a small Hamming distance (threshold 5/64) and maps to the same stored solution.
- The cache is namespaced by
provider:model:promptVersion:mode, so changing the model or prompt never serves a stale answer. It persists todata/cache.json, so consistency survives restarts.
This means the same image always yields the same output, and highly similar images yield the same output, regardless of any run-to-run LLM variation.
Layer B - deterministic model settings (for the first, uncached solve).
temperature: 0,top_p: 1, a fixedseed(default 42), and a strictresponse_format: json_object.- A rigid JSON output schema and explicit canonicalization rules in the prompt (simplest form, ascending solution order, one operation per step, fixed step wording), which remove the main sources of wording/structure drift.
Verify it:
npm start # in one terminal
node scripts/consistency-test.js # in another
# or point at a custom host / your own image:
BASE=http://localhost:3000 node scripts/consistency-test.js path/to/problem.pngThe harness checks the hash/cache machinery (no key needed), then submits the same text problem 6x and the same image Nx over HTTP and asserts every response is identical. With no AI key it still proves the exact/near-match cache guarantee.
The full prompt lives in lib/prompt.js and is versioned (PROMPT_VERSION). It is an original, engineered approximation of how a step-by-step tutor product prompts its model - not a copy of math-gpt.org's proprietary prompt (which is not public). It is organized around consistency and quality:
- Role - expert math tutor/solver over text, LaTeX or image input.
- Chain-of-thought - instruction to reason and verify internally, but keep reasoning out of the output.
- Strict output schema - a single JSON object (
interpreted,answer,answerLatex,steps[],graphExpression,graphRange); rigid structure is the biggest anti-drift lever. - Determinism/canonicalization rules - simplest canonical final form; ascending order for multiple solutions; one operation per step in textbook order; one canonical method per problem type; identical wording for identical operations; no conversational filler.
- Notation guidelines - valid KaTeX in
latexfields (no$delimiters), plain language indetail/answer. - Graphing rule - emit a single-variable function in mathjs syntax when a plot helps.
- Fallback - non-math or unreadable input returns a brief explanation and no steps.
The user-prompt structure is fixed in the same file (buildTextMessages, buildImageMessages) so the message sent to the model is byte-stable for a given input.
Both are standard OpenID Connect authorization-code flows. The server verifies the provider's signed id_token against its published JWKS (via Node's native crypto - no extra dependency) and reads the email/name/subject. A CSRF state value is stored in an HttpOnly cookie and checked on callback. Accounts created via OAuth have no local password; if the email already exists, the identity is linked.
Google (https://console.cloud.google.com/apis/credentials): create an OAuth client ID (Web application), add redirect URI <BASE_URL>/api/auth/google/callback, put the client ID/secret in .env.
Apple (https://developer.apple.com/account/resources): create a Services ID and a "Sign in with Apple" key, download the .p8, register return URL <BASE_URL>/api/auth/apple/callback. Put the Services ID, Team ID, Key ID and the .p8 contents (newlines as \n) in .env. Apple posts the callback as a form (response_mode=form_post), which the server handles.
Buttons appear in the sign-in modal and are automatically disabled with a tooltip when a provider is not configured, so the app runs fine with no OAuth credentials. Discord is intentionally not included.
docs/ is a separate, self-contained browser-only build published to https://outblade.github.io/mathgpt-clone/ via Pages (source: master branch, /docs folder). It is not generated from public/ automatically; it's a manually maintained port:
docs/js/solver.js- the same logic aslib/solver.js, adapted to use a globalmath(mathjs loaded from jsdelivr'slib/browser/math.jsUMD bundle) instead ofrequire("mathjs"), exposingwindow.MathSolverinstead ofmodule.exports.docs/js/app.js- a trimmed frontend: nofetch("/api/...")calls (there is no backend), problems are solved synchronously viawindow.MathSolver, history is kept inlocalStorage(mathgpt_demo_history, no login), and image upload / accounts show an explanatory message pointing back to this repo instead of attempting a network call.docs/index.html/docs/css/style.css- same look aspublic/, with a top banner disclosing the static-demo scope and a "View on GitHub" link. Asset paths are relative (css/style.css, not/css/style.css), which matters because project Pages sites are served from a sub-path (/mathgpt-clone/), not domain root.
If you change public/ in a way that should also apply to the demo, port the change into docs/ by hand and push to master; Pages rebuilds automatically (check status with gh api repos/OutBlade/mathgpt-clone/pages/builds/latest). Preview it locally with the dependency-free static server: node scripts/static-server.js --port=5000.
mathgpt-clone/
server.js Express app: routes, sessions, OAuth, rate limits, security headers
lib/
prompt.js Versioned system + user prompt architecture
ai.js Provider-agnostic AI backend (OpenRouter/Qwen, Groq, Gemini, OpenAI)
solver.js Local math engine (mathjs) fallback
imagehash.js SHA-256 content hash + perceptual (average) hash + text key
cache.js Consistency cache (exact + perceptual near-match), persisted
oauth.js Google/Apple OAuth: auth URLs, code exchange, JWKS id_token verify
store.js JSON persistence: users (local + OAuth), sessions, history
openai.js Backward-compat shim -> ai.js
public/ Full app frontend (index.html, css/style.css, js/app.js) - served by server.js
docs/ Static GitHub Pages demo (browser-only port, see section 5 above)
scripts/
consistency-test.js Consistency test harness (unit + HTTP)
static-server.js Dependency-free local preview server for docs/
data/ Created at runtime: users/sessions/history/cache JSON
.env.example Environment template
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /api/solve | { "problem": "..." } |
Text/LaTeX; returns cached flag |
| POST | /api/solve-image | multipart: image, note |
Returns cached + match (exact/perceptual) |
| POST | /api/auth/register | { name, email, password } |
Password min 8 chars |
| POST | /api/auth/login | { email, password } |
Sets session cookie |
| POST | /api/auth/logout | - | Clears session |
| GET | /api/auth/me | - | Current user or null |
| GET | /api/auth/google | - | Redirects to Google consent |
| GET | /api/auth/google/callback | ?code&state |
Verifies, signs in, redirects to / |
| GET | /api/auth/apple | - | Redirects to Apple consent |
| POST | /api/auth/apple/callback | form_post | Verifies, signs in, redirects to / |
| GET | /api/history | - | Latest 50 entries (auth) |
| GET | /api/history/:id | - | Full stored solution (auth) |
| DELETE | /api/history/:id | - | Removes an entry (auth) |
| GET | /api/config | - | OAuth + AI availability for the frontend |
| GET | /api/health | - | { ok, aiConfigured, provider, oauth } |
| math-gpt.org | This clone |
|---|---|
| Hero with problem input box | Centered hero, input card with image button and solve button |
| Example problems | Clickable example chips under the input |
| AI step-by-step answers | Steps with title, plain-language detail and KaTeX math per step |
| Graphing calculator | Automatic Chart.js plot for single-variable functions |
| Photo solving | Image upload to a vision model |
| Sign in with Google / Apple | Full OIDC flows with JWKS verification (Discord omitted by request) |
| Account and saved history | Register/login, per-user history drawer with reopen and delete |
- Behind a reverse proxy, serve over HTTPS, add
Secureto the session cookie inserver.js, and setapp.set("trust proxy", 1)so rate limits and OAuth redirects use the real client host. SetBASE_URLto the public HTTPS origin and register that redirect URI with Google/Apple. - The JSON store and cache are single-instance; for horizontal scaling, swap
lib/store.jsandlib/cache.jsfor a database/Redis implementation exposing the same functions.
- "Image solving requires an AI backend": set a provider key in
.envand restart (an already-cached identical image still works without a key). - "The API key was rejected": wrong key or no quota; check the provider dashboard.
- Google/Apple button disabled: that provider is not configured; see OAuth setup.
- Math or graphs not rendering: the page loads KaTeX and Chart.js from cdn.jsdelivr.net; check the network or vendor them into
public/. - Port already in use: change
PORTin.envor runnode server.js --port=3001.