{
  "markdown": "# Expense\n\n> Expense is a seamless receipt tracking solution: receipts are automatically\n> sent via email, mileage uses the tax rate provided by the IRS, and a smart AI\n> assistant does your data entry so that you can forget about the process\n> altogether.\n\n## Screenshots\n\n![Expense list: reports, receipts, and a mileage route](public/screenshot-home.png)\n\n![Receipt editor with the receipt image](public/screenshot-expense.png)\n\nWhat it does:\n\n- Log receipt expenses (upload/scan the image, specify the date, report,\n  category, merchant, amount) and mileage expenses (map a route on the map,\n  powered by Leaflet + OSRM, using the per year mileage rate)\n- Forward your receipt to the email associated with your Expense account, and\n  the receipt will be parsed and automatically added (see below)\n- Reports and categories to organize your expenses into; every receipt image is\n  saved and automatically renamed to `YYYY-MM-DD_Report_Name.jpg`\n- Export: as a PDF (one PDF per report with receipt images attached) and a ZIP\n  archive\n\nStack:\n\n- React Router v8 (framework mode) + Tailwind v4, TypeScript\n- Postgres via Prisma: accounts, users, expenses, reports, categories,\n  settings, mileage, image blobs\n- Images: Postgres BYTEA (production and development/test), with no external\n  storage\n- Deployed on Vercel + Supabase Postgres (git push to main automatically\n  deploys)\n\nAuth and accounts:\n\n- Email/password login (hashed with scrypt), sessions with signed cookies\n- Multi-user accounts: everything in one account is shared between users, each\n  account is separate from other ones\n- A user can join an account by entering an 8-character invite code (visible in\n  Settings) and can sign up to a new account from scratch; image keys are\n  namespaced by an account so that two accounts won't interfere\n\n## Accounts and sharing\n\nEmail/password login: email is the login name, stored in lowercase and\nvalidated, and unique at signup/join. All expenses, reports, categories, and\nsettings belong to an account; everything in an account is shared between users,\nand accounts are fully isolated from each other.\n\n- **Sign up** -> creates an entirely new account (empty).\n- **Join** -> enters an account invite code (Settings -> Account) to join an\n  existing account and share its data.\n- The first account/user is bootstrapped from `APP_EMAIL`/`APP_PASSWORD` when\n  the database is empty; pre-existing accounts get their login restored from\n  `APP_EMAIL` on the first launch (initStore).\n\n## SEO and AI discovery\n\nMarketing pages that are available publicly double as the AI search surface: if\nan assistant is asked for an expense tracker, the GPTBot / OAI-SearchBot /\nClaudeBot / PerplexityBot crawler quotes them. The copy is written in such a way\nthat it's easily quotable and includes the app name and the URL and lives only\nin one file, `app/lib/seo-content.ts`, which renders all the surfaces:\n\n| Page                                     | Purpose                                                       |\n| ---------------------------------------- | ------------------------------------------------------------- |\n| `/`                                      | Landing page (SoftwareApplication JSON-LD)                    |\n| `/about`                                 | Full feature/benefit list (AboutPage JSON-LD)                 |\n| `/faq`                                   | 13 Q&As matching real AI queries (FAQPage JSON-LD)            |\n| `/alternatives`                          | Expense vs Expensify comparison (WebPage + FAQPage JSON-LD)   |\n| `/llms.txt`                              | The llmstxt.org file, the curated overview AI assistants read |\n| `/about.md` `/faq.md` `/alternatives.md` | Markdown mirrors per the llms.txt convention                  |\n\nPlumbing to support it: `public/robots.txt` explicitly permits the AI crawlers\nwhile app routes are blocked, and `public/sitemap.xml` lists the public pages.\n\nThese routes are public (see the root loader in `app/root.tsx`); everything\nelse still requires a session.\n\n## What it does\n\n- Track **receipt** expenses (date, merchant, amount, image, category, report)\n  and **mileage** expenses (date, 2+ addresses, distance, amount, report).\n- Mileage routes run **Home → stops → Home**; distance is calculated using OSRM\n  and the amount based on a per-year mileage rate. Maps is powered by Leaflet +\n  OpenStreetMap; **no API keys necessary**.\n- Incomplete expenses are highlighted to make it easy to complete them.\n- Paste (⌘V) or upload an image anywhere to create a new receipt.\n- **Export** each report as a PDF (grouped by category, with all receipt images\n  attached) and a ZIP archive (CSV + images named `YYYY-MM-DD_REPORT_FILE.ext`).\n- **AI assistants (MCP)**: any MCP client (Claude, OpenAI) can connect by\n  logging in with your account (OAuth; no API keys). See [AI\n  assistants](#ai-assistants-mcp) below.\n\n## AI assistants (MCP)\n\nThe app speaks the Model Context Protocol at `https://expense.labnotes.org/mcp`\n(auth: OAuth 2.1 authorization-code + PKCE, where you sign in and authorize\nthe connection; no API keys). An assistant linked with to your account can:\n\n- **Upload a receipt**: drag and drop a picture or PDF into chat; it processes\n  the same way as the web app and uses the same OCR and extraction as well as\n  your merchant history for categorization.\n- **Log a drive**: drag and drop stops written in plain English; it geocodes,\n  routs, and prices the route according to the year's IRS rates.\n- **Answer questions about spending**: for example \"how much have I spent on\n  flights last quarter?\" You'll receive the answer based on your data.\n- **Create reports**: create/close a report, move expenses into it, export\n  a report PDF.\n- **Reconcile**: upload your bank statement as a CSV; it will match all charges\n  without a matching receipt (read-only).\n\nConnect any MCP client:\n\n```json\n// Claude — .mcp.json (no headers needed: the client discovers OAuth)\n{\n  \"mcpServers\": {\n    \"expense\": {\n      \"type\": \"http\",\n      \"url\": \"https://expense.labnotes.org/mcp\"\n    }\n  }\n}\n```\n\nThe client will open your browser; you'll log in and click Allow. You can manage\nconnections (delete per token, disconnect completely) in **Settings -> Agents &\nAPI (MCP)**. For the full reference, see [`docs/mcp.md`](docs/mcp.md) and for\ndirectory listings: [`docs/mcp-directories.md`](docs/mcp-directories.md).\n\n## State\n\nThe storage is Postgres-only via **Prisma 8** (`prisma/contract.prisma` is\nthe source of truth; `pnpm build:prisma` emits the contract artifacts the\nruntime reads). `DATABASE_URL` is required upon launch (otherwise the app will\ncrash with an error). Image blobs are stored inside Postgres BYTEA\n(`image_blobs`) in production and development; there is no additional storage\nservice.\n\n| Data                        | Images                         |\n| --------------------------- | ------------------------------ |\n| `accounts` / `users` /      | Postgres BYTEA (`image_blobs`, |\n| `expenses` / `reports` /    | prod and dev)                  |\n| `categories` / `settings` / |                                |\n| `mileage` / `image_blobs`   |                                |\n\nAll reads/writes are done via `app/lib/database.ts` (Prisma queries scoped by\n`accountId`); image storage is handled by `app/lib/images.server.ts` (Prisma\n`imageBlob`). Image blobs are kept in `images/{accountId}/...` pathnames on all\nbackends. They are namespaced per account, so two accounts can never have a\nname conflict. Schema changes: edit `prisma/contract.prisma`, run\n`pnpm build:prisma` (contract emit), then `pnpm db:push` locally and on\ndeploy (see `docs/deploy.md`).\n\n## Quick start\n\n## Environment variables\n\nLoad order: real `process.env` (Vercel dashboard, or inline) wins; a local\n`.env` is used to fill holes. `DATABASE_URL` is required; `.env` is gitignored.\nif (!hasDatabase()) {\n\n**dev / test (local `.env`):**\n\n```bash\n# .env (project root, gitignored)\nDATABASE_URL=postgres://assaf@localhost/expense_dev   # include the local user\nSESSION_SECRET=…         # signs the session cookie (random hex)\n```\n\nOn an empty database the first account + user are bootstrapped from\n`APP_EMAIL`/`APP_PASSWORD` (fail-closed if missing); thereafter, users are\ncreated via the app's signup/join flow. `SESSION_SECRET` is always required.\n`APP_EMAIL`/`APP_PASSWORD` can be omitted from `.env` after you have at least\none user.\n\nAccounts created before email login (username era) retain their original\nusername as the stored email until `APP_EMAIL` is set; `initStore` then adds\nthat address to the bootstrap (oldest) user, so the configured credentials\ncontinue to work.\n\nTests deliberately hardcode `expense_test` (Postgres with blobs/images), ignore\nthe local database, and reset the schema from Prisma on every run (`pnpm\ntest:db:push` in the test setup).\n\n**prod (Vercel):** set env vars in the project dashboard (Settings →\nEnvironment Variables): `DATABASE_URL` (Supabase Supavisor pooled URL),\n`SESSION_SECRET`,\nand (only until the first user exists) `APP_EMAIL` / `APP_PASSWORD`.\nVercel sets them during runtime; `.env` does not exist.\n\n## Receipts by email\n\nForward an email receipt to your inbox address and it will be parsed and\nimported automatically: the merchant, amount, and category are extracted, the\nreceipt is uploaded as an image, and the expense date is the date when the email\nwas forwarded. In case something could not be parsed, an explanation email is\nsent back.\n\nHow it determines what to import:\n\n- Receipt **attached as PDF/image** → the attachment is uploaded as the receipt\n  image; text is extracted from the PDF text layer (or OCR'd) in order to parse\n  the merchant/amount/category.\n- Receipt **inline in the email** (ASCII/HTML) → the email body is converted to\n  an image and uploaded; text is parsed similarly.\n- Several attachments (for example, a receipt and some logo or signature) → only\n  the actual receipt is processed (with heuristics + model tie-break).\n- Sender email must be in the account's **allowed senders list** (Settings →\n  Receipts by email); otherwise the email is replied with \"sender not\n  recognized\".\n- Successful imports trigger a response with all the parsed data; incomplete\n  ones (missing merchant, amount, …) create the expense anyway and reply listing\n  what's missing.\n- Each email is processed at most once (idempotent on email id).\n\n### Setup DeepSeek\n\n- **DeepSeek vision**: the hosted DeepSeek API is text-only, so receipt images\n  are OCR'd locally with tesseract.js (worker/fonts fetched from a CDN at\n  runtime). Set `RECEIPT_OCR_MODE=deepseek` if/when the hosted model will be\n  able to handle receipt images (vision tries first and falls back on `auto`).\n- **Scanned PDFs** (without the text layer) are rasterized and OCR'd; the first\n  pages are uploaded as receipt image.\n- **HTML receipts** are converted to a text image (receipt form on the paper);\n  no headless browser needed.\n- Forwarding **as attachment (.eml)**: the receipt enclosed in `.eml` is not\n  parsed; use normal inline forwarding (for example, Gmail/iOS includes original\n  email in the body).\n- Webhook processing time limit is 60 seconds (max Vercel `maxDuration`),\n  enough to download attachment + OCR + parse.\n\n## Maps & geocoding\n\nOpenStreetMap services (Nominatim geocoding, OSRM routing, OSM raster tiles\nmaps). Rate limited but good enough for personal use. If OSRM is not available,\ndistance calculation fallbacks to the straight line (marked \"approx.\").\n",
  "bytes": 11500,
  "sha": "a5b762a6204d5caf1d0afbde41d4f02c10c11b10177efa4da520c4c1b0620a88",
  "repo_slug": "assaf/expense",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_assaf_expense_1ec23f1f/readme"
}