Mottainai


>

"Consider deeply before you acquire."

Next.js 16 React 19 TypeScript OpenRouter IndexedDB Vercel
Table of Contents

What it is

Mottainai is a mindful purchasing decision tool. The name comes from a Japanese concept expressing regret over waste — the idea that every object has inherent value and should not be acquired carelessly.

Instead of letting impulse drive purchases, users add items they want to buy, and a conversational AI engages them in a focused 3-10 turn dialogue, probing motivations before delivering a verdict: Spend, Save, or Repair/Upgrade.

Live demo  |  Source

How it works

User adds item → IndexedDB stores it (status: pending) ↓ ChatModal opens → POST /api/chat → OpenRouter (GPT-OSS-120B) ↓ AI asks ONE question → User replies → Repeat 3-10 turns ↓ AI delivers verdict → Decision parsed via regex → Saved to IndexedDB

User adds an item they are tempted to buy. AI opens a full-screen chat and starts with a real-world insight about the item. Multi-turn dialogue probes need vs. want, alternatives, total cost of ownership, urgency, and emotional motivation. Once the AI has enough clarity, it delivers a structured verdict with reasoning. Verdict and full chat history are persisted client-side.

Screenshots

Item list — items queued for reflection
Decision chat — the AI probes before verdict

Architecture Decisions

Why Next.js (App Router, v16)?

> Provides server-side API routes as a single backend without a separate server.

The app needs exactly one server-side function: proxying requests to OpenRouter so the API key never reaches the client. Next.js API routes (/api/chat/route.ts) handle this with zero deployment overhead on Vercel. No Express server, no separate backend service.

App Router with React Server Components was used for the layout (SEO metadata, analytics), while the actual app is entirely client-side ('use client' on page.tsx). This hybrid approach gives server-rendered metadata without the complexity of SSR for the interactive parts.

> Tradeoff: Next.js 16 has breaking changes from earlier versions. AGENTS.md was created specifically to warn AI assistants about this.

Why OpenRouter + GPT-OSS-120B?

> Cost-effective access to capable models without vendor lock-in.

The project started with nvidia/nemotron-3-super-120b-a12b:free — a completely free model. The quality was insufficient for the nuanced conversational requirements (the AI needs to be witty, probing, and follow strict formatting rules). It was upgraded to openai/gpt-oss-120b which balances cost and quality for the short, structured completions this app produces.

OpenRouter provides a single API that gives access to multiple model providers. If GPT-OSS-120B becomes unavailable or too expensive, switching models requires changing one string in route.ts.

> Tradeoff: The Vercel AI SDK's @openrouter/ai-sdk-provider was installed but never used. Raw fetch() was chosen for simplicity — the SDK added abstraction without value for this use case.

Why no streaming? (tried and removed)

> Responses are short (2-4 sentences). Streaming adds complexity without meaningful UX improvement.

Evidence in the git history shows streaming was attempted: a deleted stream.txt file contains SSE data with text-delta chunks in the Vercel AI SDK format. A verify.js mock server was used to test the useChat hook. Both were abandoned.

The current design shows a typing indicator, then the full message appears at once. With max_tokens: 512 and 2-4 sentence responses, the wait is under a second. Parsing the <<<DECISION>>> block is also simpler with the complete response.

Why no tools or function-calling?

> The AI doesn't need external capabilities. All intelligence is in the prompt engineering.

The app is a conversation, not an agent with capabilities. The AI doesn't look up prices, search the web, or take actions. It uses its training knowledge for pricing context, alternative suggestions, and ownership cost reasoning. Simpler implementation, no tool schema maintenance, no error handling for tool call failures.

Why IndexedDB (via idb) instead of localStorage or cloud DB?

> No size limit, async API, structured data with indexes, completely client-side.

vs. localStorage: 5MB limit, synchronous/blocking API, can't store complex objects without manual serialization, no indexing. IndexedDB supports practically unlimited storage, async operations, and native structured data.

vs. Cloud DB: Would require user accounts, server-side database, authentication — directly contradicting the core philosophy of "no accounts, no cloud sync, no data collection."

The idb package by Jake Archibald wraps IndexedDB's complex API in a clean Promise-based interface. The schema was finalized from day one (it hasn't changed since the initial commit): a single items object store with auto-incrementing keys and a by-date index.

> Tradeoff: Data is lost if browser storage is cleared. This is an accepted tradeoff for privacy.

Why no accounts / no cloud sync?

> Philosophical alignment with the Mottainai concept: minimize waste, including data waste.

From the README: "The app is intentionally minimal on the UI side — no accounts, no cloud sync, no data collection. Everything stays in your browser. The friction is in the thinking, not the interface."

This eliminates: privacy concerns, server-side database costs, auth/session management complexity, and the entire category of "where is my data" questions. The tradeoff is accepted: data only exists in one browser.

Why the 3-tier currency detection system?

> Progressive enhancement: show something immediately, upgrade when possible.

The currency detection in src/lib/currency.ts uses a 3-tier fallback:

  • Tier 1 — GPS Geolocation: navigator.geolocation.getCurrentPosition() + BigDataCloud reverse geocode (free, no API key). Maps country code to currency via a hardcoded lookup table of 40+ countries.
  • Tier 2 — Timezone Fallback: Intl.DateTimeFormat().resolvedOptions().timeZone mapped through a timezone dictionary. Synchronous, instant, no permission needed.
  • Tier 3 — Default INR: Falls back to Indian Rupees, reflecting the developer's primary market.

The page uses a progressive enhancement pattern: detectCurrency() returns immediately from timezone (Tier 2), then resolveLocationCurrency() upgrades asynchronously when GPS permission is granted. No loading delay for the user.

> Tradeoff: The GPS tier requires browser permission, which many users deny. Timezone is the practical default for most sessions.

Why the decision gate pattern (3-10 questions then verdict)?

> The AI must be thorough before judging. The system prompt mandates clarity on all dimensions.

The system prompt enforces: "You are NOT allowed to give a verdict until you have clarity on ALL (if relevant): Frequency of use, Budget range, Alternatives (repair / borrow / second-hand / cheaper), Core motivation (need vs want)."

The turn count is flexible (4-10 turns) based on conversation complexity. The verdict bias is explicitly anti-buying: "buy" is a high bar. The 8 topics to cover (Need vs Want, Real Value, Cheaper Alternatives, Why Not Buy, Use Case Fit, Ownership Cost, Urgency, Emotion Check) ensure comprehensive evaluation.

Why regex-based verdict parsing instead of structured output?

> Simplicity over reliability. The <<<DECISION>>> block is a convention enforced by prompt instructions.

The verdict is parsed via: content.match(/<<<DECISION>>>\s*([\s\S]*?)\s*<<<END>>>/). If the LLM doesn't follow the format, the decision silently fails (the catch block doesn't retry). This is a deliberate choice: structured output APIs add latency and cost, and the prompt engineering is reliable enough for the use case.

> Tradeoff: ~5% of conversations may not produce a parseable verdict. The user sees the chat but no formal verdict panel.

Why are ai, @ai-sdk/react, @openrouter/ai-sdk-provider, and zod in package.json but unused?

> Vestiges of an earlier architecture that was abandoned during development.

The git history tells the story: the project started with Vercel AI SDK's useChat hook (evidenced by the deleted test-usechat.js and verify.js mock server). It was tested with Puppeteer browser automation. Eventually, the developer moved to raw fetch() for simplicity and removed all test infrastructure. The packages remain as dead weight in package.json.

The System Prompt is the Product

The ~220-line system prompt in src/app/api/chat/route.ts IS the entire product. There is no agent logic, no tool definitions, no chain-of-thought orchestration. The prompt encodes:

  • Identity: A sharp, honest friend, not a shopping assistant
  • Waste hierarchy: Use what you have > Repair > Borrow > Buy second-hand > Buy new (last resort)
  • Pacing rules: EXACTLY ONE question per reply, 2-4 sentences max, never more than one ?
  • Format rules: Plain conversational text, no bullets/asterisks/markdown. Exception: the <<<DECISION>>> block
  • Self-check: Mandatory pre-send validation for question count, turn count, and formatting
  • Decision gate: Cannot give verdict until clarity on frequency, budget, alternatives, and motivation
  • Verdict bias: Default: do NOT buy unless clearly justified
  • Dynamic variables: ${itemName}, ${userRegion}, ${currency}, ${symbol}

The prompt was expanded from ~70 lines to ~220 lines in commit f445faa, which was the most significant change in the project's history. This single commit also fixed React stale-closure bugs and added the Strict Mode double-fire guard.

Tech Stack

  • Framework: Next.js 16 (App Router) — API routes as serverless backend, Vercel deployment
  • UI: React 19, TypeScript, CSS Modules + global CSS custom properties (Tailwind installed but unused)
  • AI: OpenRouter API, GPT-OSS-120B model, raw fetch() (no SDK abstraction)
  • Storage: IndexedDB via idb — items, chat history, decisions, all client-side
  • Currency: 3-tier detection (GPS → Timezone → Default INR), 40+ country mapping
  • Analytics: Vercel Analytics (added in final commit)
  • Deployment: Vercel, zero-config (no vercel.json)

Key Numbers

  • ~2,300 lines of application code
  • 294 lines in the core API route (including the 220-line system prompt)
  • 220 lines in the system prompt — the entire product logic
  • 10 items/day daily reflection limit (client-side enforced)
  • 512 max_tokens per AI response
  • 3-10 turns per conversation before verdict
  • 11 commits across the project's history

What I learned

  • The system prompt IS the product. Prompt engineering at 220 lines is software engineering.
  • Streaming isn't always worth it. Short responses don't benefit from word-by-word delivery.
  • Frameworks get abandoned mid-project. The Vercel AI SDK was tried, tested with Puppeteer, and replaced with raw fetch.
  • Privacy-by-design eliminates entire categories of complexity (auth, sync, data deletion).
  • Progressive enhancement for location/currency works: show something fast, upgrade silently.