Kaiwaflow 会話フロー

Turn the Japanese words you already half-know into words you can actually say.

8
SRS rungs · 1 to 240 days
2
LLM providers · env-flip rollback
3
TTS providers · cached
~52KB
prompt engine

A private beta, measured by decisions and craft rather than traction. The interesting part of an early bet is the reasoning, not a number I do not have yet.

The problem & the bet

You have drilled the cards. You still cannot speak. Anki and WaniKani built you a huge passive vocabulary, but recognition is not production, and the moment you have to actually say something in Japanese, nothing comes out.

Kaiwaflow’s bet is to own that one job: the output halfof a learner’s practice. Bring the words you already study, and an AI partner (Sensei) builds real conversations around them, weaving in the ones you are due to review on purpose, so the vocabulary finally comes out of your mouth. It is explicitly complementary. Keep your Anki, keep your tutor. This is the place you use what they put in.

THE GAP other tools leave open
  recognize  ##########   "I know this word"      <- Anki / WaniKani stop here
  produce    ##........   "...but I froze when I had to say it"

HOW KAIWAFLOW CLOSES IT   (Living Recall)
   import deck -> word comes due -> Sensei steers the topic so YOU must say it
        ^                                                    |
        +------- SRS credit <- you produce it unaided <------+

Why this bet

I built and sunset two products before this one. Exityear and Vectorcache both taught the same lesson: I could ship a correct product, but the market was the problem. Both landed in saturated spaces with entrenched incumbents and no durable wedge for a solo builder. So the question for the next bet was not “can I build it,” which I had answered twice. It was “is there a job nobody has closed?” Choosing what to build on differentiation instead of buildability is the muscle those two sunsets trained.

It is also personal. [Personal hook, replace me: your real one-line reason, e.g. you studied Japanese for years and still froze the moment you had to speak.] Kaiwaflow’s Living Recall loop is the wedge: the one job the incumbents (Anki, Duolingo, tutors) structurally do not do, which is turning passive vocabulary into speech.

How Living Recall works

A chat turn is one streamed model call with defensive parsing on both ends. Your due words are selected by a plain database query, not a vector search, and woven into the prompt. As Sensei streams back, the server pulls structured signals out of the text and, when you have genuinely produced a due word unprompted, credits it back to your schedule.

KAIWAFLOW: one chat turn
========================================================================

  INPUT     browser speech-to-text (live)   or   typed text
            [ Whisper route built, shipped dark: Pro-gated + unwired ]
              |
              v  POST /api/chat  (Server-Sent Events)
  GATES     auth . plan/scenario gate . daily chat cap (free)
            daily token cap 500k   [fail CLOSED on read error]
              |
              v
  PROMPT    ~52KB builder: persona / tone / mode (7 modes)
  ASSEMBLY  + context, tiered  full | summary | minimal | off
            + DUE WORDS  (PocketBase filter query, not vectors)
              |
              v
  PROVIDER  lib/llm.ts, one env var flips the backend:
  SEAM        openrouter (OpenAI format)  ->  Gemini flash-lite
              @google/genai               ->  Gemini  (rollback)
            max_tokens = 1500 + 2048 thinking headroom
            idle-timeout abort, re-armed on every chunk
              |
              v  token-by-token stream
  PARSE     assume the model lies:
            <correction>{json}</correction>   stale / punctuation guards
            <recalled>WORD</recalled>         recall signal
            drop truncated + hallucinated <filler> tags
              |                                    |
       clean prose                           recall signal
              v                                    v
  OUTPUT    stripForTts ->                 STATE  SRS credit (positive-only,
            Azure -> Gemini -> browser TTS         still-due words only)
            SHA-256 disk cache,                  usage metered and awaited
            atomic tmp + rename                  BEFORE the stream closes

The import path is four steps. The AI touches only the second one:

1. EXPORT              2. IMPORT              3. ACTIVATE            4. CREDIT
your Anki / WaniKani    one AI pass pulls      due words woven into    say it unaided
JPDB / Migaku deck  ->  words + context    ->  real spoken chat    ->  -> SRS advances
                       (you skim and fix)     (you speak, not tap)     (Living Recall)

The schedule itself is a plain interval ladder. Nail a word and it fades toward once a year. Miss it and it comes back tomorrow.

Days until a word comes back      Good = +1 rung    Easy = +2    Again = back to day 1

240 |                                             __________
120 |                                    ________|
 60 |                              ______|
 30 |                       ______|
 14 |                 _____|
  7 |           _____|
  3 |     _____|
  1 |_____|
    +-----+-----+-----+-----+-----+-----+-----+-----+
      1st   2nd   3rd   4th   5th   6th   7th   8th     clean production

The schedule is the only progress bar, and the learner never sees it. The app rewards you by getting quieter as you succeed.

Decisions & trade-offs

The interesting part is not the feature list. It is the calls made under real constraints, and what they cost.

01

Credit production, not recognition (Living Recall)

Decision
Make the spaced-repetition credit event unprompted production in a live conversation, not a self-graded 'did you remember this' card flip. A word only advances when you actually say it, unaided.
Trade-off
Recognition is trivial to detect; production is not. The signal comes from an unreliable model, so the loop is deliberately asymmetric: it only ever credits (never penalizes), only credits words still due, and treats the model's recall tag as a hint to verify, not a verdict.
Outcome
The schedule finally measures the thing learners care about, which is 'can I speak this word.' The worst failure mode is a real recall going uncredited, never a corrupted schedule.
If I did it again
The credit is a flat 'good.' I would weight it by how long the word had been dormant, and add a per-turn idempotency key so a re-delivered stream event can never double-count.
02

The output-half wedge: complement, do not compete

Decision
Position Kaiwaflow as the output half of an existing stack and tell users to keep Anki and their tutor, rather than build another all-in-one Japanese app.
Trade-off
A narrow tool is a harder pitch than a full course, and it depends on the very incumbents it refuses to replace. But it owns the one job none of them do: turning passive vocabulary into speech.
Outcome
The whole product, especially the import pipeline, plugs into the existing stack instead of fighting it. That is a cleaner wedge than competing with Duolingo on breadth.
If I did it again
The risk is discovery: a complement lives or dies on the incumbent's export. I would invest earlier in frictionless Anki and WaniKani import and in proving the wedge with real learners before widening scope.
03

Anti-gamification as a segment bet

Decision
Cut the entire gamification playbook: no streaks, no XP, no levels, no guilt loops.
Trade-off
Gamification exists because it drives daily active use, so I am giving up a proven retention lever on a bet that my segment, adults who froze despite years of study, is repelled by it.
Outcome
The SRS schedule is the only progress system, and the learner never sees it. Intrinsic 'keep your words warm' replaces extrinsic streak anxiety.
If I did it again
This is a bet I have to prove, not a truth. If activation stalls, the test is whether a calm 'your words are staying warm' receipt beats a streak without quietly becoming one.
04

Fail-closed cost caps for a solo-run LLM product

Decision
Put a hard daily token cap on every account, free and paid, as an abuse breaker on the LLM bill, and reason about which direction each cap should fail.
Trade-off
A database read hiccup now denies chat instead of letting spend run unbounded. I accept rare false denials so the spend guard can never be switched off by the same outage that makes abuse most likely.
Outcome
The bill has a per-user daily ceiling that survives a partial database failure. The cap fails closed on a read error but open on a genuine 'no usage yet today,' so normal traffic stays fast and open.
If I did it again
At scale I would move the counter to a database-atomic increment to close a lost-update window, and warn a legitimate power user before the wall instead of at it.

Architecture at a glance

One box. A single Next.js app is the backend-for-frontend; PocketBase (an embedded Go binary over SQLite) is the database and auth; Caddy terminates TLS and splits traffic. Deployed by Coolify onto one VPS. It is a vertical-scaling design, chosen on purpose for a private beta, and the honest scale answer is Postgres plus a stateless app tier.

        BROWSER (React 19)   furigana + TTS playback + browser STT
              |
              |  HTTPS, pb_auth cookie on every request
              v
        +-----------------------------------------------+
        |          CADDY :443   (TLS, reverse proxy)    |
        |     /pb/* -> PocketBase       /* -> Next.js    |
        +---------------+-------------------+-----------+
            /pb/*       |                   |  /*
                        v                   v
        +---------------------+   +------------------------------+
        |  POCKETBASE :8090   |   |     NEXT.JS 16 BFF :3000      |
        |  Go + SQLite (0.25) |   |  middleware: local JWT check  |
        |  auth (pw/OTP/magic)|<--|  api routes: requireUser +    |
        |  46 migrations      |   |  plan gates + cost caps       |
        |  hooks: trial, beta |-->|  /api/chat  -> SSE stream     |
        |  gate, backups      |   |  /api/tts   -> cached audio   |
        +---------------------+   +----+--------+--------+-------+
                                       |        |        |
                                       v        v        v
                                   Gemini /   Azure /   Stripe
                                   OpenRouter Gemini    billing
                                   (LLM)      TTS

  [ built, shipped dark: OpenAI Whisper STT, Pro-gated. Live STT is the browser. ]
  Deploy: Coolify builds two images (app, pocketbase+migrations); Caddy fronts them.

The part I am proudest of is not the topology, it is the money. Every LLM call is metered, and the abuse breaker that reads that meter fails safe.

  POST /api/chat
      |
      v  requireUser -> effectivePlan
  daily chat cap (free: 30/day) --------------> over? 429
      |
      v  checkDailyTokenCap  (all plans, 500k/day)
      read llm_usage_daily:
        no row yet    -> used = 0     (fail OPEN, correct: really zero today)
        read error    -> used = cap   (fail CLOSED -> 429)
      |
      v  under cap
  stream Gemini -> SSE tokens -> client
      |
      v  (stream ends)
  await meter usage    (retry-safe upsert, cost priced at write time)
      |
      v
  close stream   <- metering is flushed BEFORE close, so tomorrow's cap
                    can never be undercounted by a dropped write

Engineering for trust

Most of the hard engineering here is governing an unreliable collaborator: the model. I treat every tag it emits as hostile input. Recall credit is positive-only and gated on words still due, with an anti-mimicry guard so parroting a word Sensei just said does not count. The prompt fences user-supplied persona and topics as data with a non-negotiable trailing instruction, and structured extraction runs with provider fallbacks turned off so attacker-influenced content cannot be rerouted to a model that formats differently.

I also do not trust the model’s Japanese readings. A deterministic morphological analyzer (kuroshiro and kuromoji) runs in the browser to verify and repair the furigana it emits, fixing truncations like 育[はぐ]む into 育[はぐく]む. And the whole model backend sits behind a one-module provider seam, so a vendor outage or price change is a one env-var rollback, not a code change.

The method under the hood

The product is a deliberate implementation of second-language-acquisition research, turned into mechanics. Comprehensible input becomes furigana that fades per kanji as your level rises. The output hypothesis becomes the recall loop, which forces production rather than recognition. Corrective feedback becomes a modeled correction (a recast) shown as a collapsible chip, never a red pen mid-sentence. Spaced retrieval becomes the interval ladder. The theory is the spec.

The business

Free is a real taste: unlimited free chat, browser voice, every Sensei dial, and up to 100 saved words at 30 chats a day. Pro is a flat $19 a month or $180 a year, with a 15-day trial that needs no card. In the code, the abuse breakers (token and character caps that protect the bill) are kept separate from the monetization levers (the 100-word line that sells Pro), because blurring the two is how you either bankrupt yourself or annoy a paying customer.

Distribution is an invite-gated private beta with an off-app waitlist. That is a deliberate growth choice, not an absence of one: I am metering demand and cost while I prove the loop. The activation funnel is already instrumented, from first seed to a word recalled in conversation, so what I watch is whether the recall loop closes. For a product this early, that is the honest version of a traction chart.

An honest note on how it was built

Kaiwaflow is built solo with heavy AI assistance. What I own, and what this page is about, is the thesis, the design of the recall loop, the trade-off calls, the sequencing, and governing the AI itself: the guardrails, the fail-safe caps, and the defensive parsing that keep an unreliable model from corrupting the product. Those are the parts a code generator does not decide for you.

Want in?

It is invite-only for now. Join the waitlist and I will send an invite as spots open.

Join the waitlist