<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jiahao luo</title>
    <description>The latest articles on DEV Community by jiahao luo (@jiahao_luo_f33d8988caf4e1).</description>
    <link>https://dev.to/jiahao_luo_f33d8988caf4e1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3876649%2Fdce1e942-9dfc-4d36-909b-6a8a04b54f81.png</url>
      <title>DEV Community: jiahao luo</title>
      <link>https://dev.to/jiahao_luo_f33d8988caf4e1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jiahao_luo_f33d8988caf4e1"/>
    <language>en</language>
    <item>
      <title>I built a free, no-login, unlimited AI invoice tool. Here are the 4 features and the 5 5 3 dunning matrix behind it.</title>
      <dc:creator>jiahao luo</dc:creator>
      <pubDate>Tue, 05 May 2026 06:23:50 +0000</pubDate>
      <link>https://dev.to/aiinvoicemaker/i-built-a-free-no-login-unlimited-ai-invoice-tool-here-are-the-4-features-and-the-5x5x3-dunning-4ank</link>
      <guid>https://dev.to/aiinvoicemaker/i-built-a-free-no-login-unlimited-ai-invoice-tool-here-are-the-4-features-and-the-5x5x3-dunning-4ank</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Cross-post notice: this is the canonical version. Reposts on Medium, Hashnode, and Indie Hackers will link back here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few months ago I shipped a free invoice generator. The kind of tool freelancers find at 11pm when a client wants an invoice and they don't want to register on a CRM just to send a PDF.&lt;/p&gt;

&lt;p&gt;The whole product runs under four hard constraints I refused to break:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100% online&lt;/strong&gt; — open the URL, use it. No install, no desktop app, no browser extension. Works on any laptop, tablet, or phone with a browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited use&lt;/strong&gt; — no per-invoice cap, no "after 5 invoices please sign up," no monthly quota. Generate one a day, generate a hundred a day. Same tool either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No login&lt;/strong&gt; — anonymous by default. No email capture, no account creation, no password reset flows, no session cookies tied to identity. Your data lives in your browser's localStorage; close the tab and nothing is sent anywhere we don't show you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free, period&lt;/strong&gt; — no paid tier, no credit card, no "premium templates," no watermark on PDFs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those four together are the actual product. Everything else (AI features, customization, multi-document) is what I built &lt;em&gt;given&lt;/em&gt; those constraints, not in spite of them.&lt;/p&gt;

&lt;p&gt;Generating invoices is the easy part. The hard parts are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Filling the damn form (people will give up halfway and bounce)&lt;/li&gt;
&lt;li&gt;Making it look like &lt;em&gt;theirs&lt;/em&gt;, not a generic template&lt;/li&gt;
&lt;li&gt;Actually getting paid 30 days later when the client goes quiet&lt;/li&gt;
&lt;li&gt;Not having to switch tools when they need a receipt or a quote next week&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post is a build-in-public walkthrough of the four AI features I shipped to address each of those, the architectural decision that surprised me most (a &lt;em&gt;relationship-aware&lt;/em&gt; dunning matrix), and a few real costs from the prompt design.&lt;/p&gt;

&lt;p&gt;Tool is at &lt;a href="https://ainvoicemaker.com" rel="noopener noreferrer"&gt;ainvoicemaker.com&lt;/a&gt; — open it, it works. Nothing to sign up for.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Smart Paste — text or image → form fields, in 5 seconds
&lt;/h2&gt;

&lt;p&gt;The single highest-friction step of every invoice tool I've ever used: &lt;strong&gt;filling the form&lt;/strong&gt;. Client name, address, line items, rates, dates. 12-20 fields of typing, every time, often pulled from an email.&lt;/p&gt;

&lt;p&gt;So Smart Paste eats the email instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkh0f2cq4lmz0fd9mfgy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkh0f2cq4lmz0fd9mfgy.png" alt="Smart Paste bar at the top of the invoice form, with a textarea, image-upload button, and example chips" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You paste anything — a brief, an email thread, a Slack message, a Notion doc — and the AI extracts what it can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client business name, address, contact email&lt;/li&gt;
&lt;li&gt;Line items (description + quantity + unit price)&lt;/li&gt;
&lt;li&gt;Currency (detected from &lt;code&gt;$ / € / £ / ¥&lt;/code&gt; or country mentions)&lt;/li&gt;
&lt;li&gt;Due date (from "net 30", "by Friday", "end of month")&lt;/li&gt;
&lt;li&gt;Project / invoice context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also accepts &lt;strong&gt;screenshots&lt;/strong&gt; (4MB max). Camera button → upload photo of a handwritten estimate, an email screenshot, or a competitor's invoice you're rebuilding. Same AI pipeline, vision input.&lt;/p&gt;

&lt;h3&gt;
  
  
  The implementation honest part
&lt;/h3&gt;

&lt;p&gt;I started with one big prompt asking for the entire invoice JSON in one shot. It hallucinated currencies and made up tax rates. Switching to an extraction-only prompt (pull entities, don't reason about them) and a separate validation pass cut hallucinations to near zero on real-world inputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SYSTEM (extraction only):
You extract structured fields from unstructured text. You do
NOT infer, calculate, or assume. If a field isn't in the
input, return null for it. Never invent currencies, tax
rates, or amounts. Never round.

INPUT: {raw text or OCR'd image}

OUTPUT: JSON matching the invoice schema. Empty fields = null.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three-line discipline, one big behavior change: the model stopped trying to "be helpful" and just reported what it actually saw.&lt;/p&gt;

&lt;p&gt;Smart Paste works for invoice, receipt, quote, superbill, and proforma — same component, same API endpoint, the document type is just a parameter.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Customizable preview — 14+ knobs, no design skill required
&lt;/h2&gt;

&lt;p&gt;Generic invoice tools all spit out the same Stripe-blue / black-Helvetica template. That's fine for one-off jobs but it's not &lt;em&gt;yours&lt;/em&gt; — and clients pattern-match the template they see most.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4m3gpxpmphydjuvxv0g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4m3gpxpmphydjuvxv0g.png" alt="Customize sheet open: 3 style presets (Minimal / Professional / Branded), 8 brand-color swatches, density selector (Compact / Standard / Roomy), and per-section toggles" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The customize sheet has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 paper styles&lt;/strong&gt; — classic, modern, minimal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8 brand colors&lt;/strong&gt; — pre-tuned for invoice contrast (no guesswork on accessibility)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paper size&lt;/strong&gt; — A4 or Letter (auto-detected from locale, override-able)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column visibility&lt;/strong&gt; — show/hide tax, discount, notes columns based on whether you actually use them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logo upload&lt;/strong&gt; — PNG with transparent background, auto-fits header&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13 languages&lt;/strong&gt; — full UI + invoice copy translations (not just labels — totals row, tax line, payment terms)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency&lt;/strong&gt; — 50+ currencies with proper formatting (e.g. €1.234,56 vs $1,234.56)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typography&lt;/strong&gt; — serif / sans / mono mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole customize sheet is &lt;strong&gt;pure form state&lt;/strong&gt; — no save button, no "draft" concept. Every change updates the live preview pane on the right within 100ms. Refresh and you're back to defaults; you only "commit" by downloading the PDF or sending it.&lt;/p&gt;

&lt;p&gt;That decision (no save state) cut roughly 40% of complexity out of the product. No accounts, no drafts, no "where did my invoice go" support questions. The local browser is the source of truth, and recent invoices are remembered in localStorage for re-use.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. AI Payment Reminders — a 5 × 5 × 3 dunning matrix
&lt;/h2&gt;

&lt;p&gt;This is the part I'm proudest of architecturally, so I'll go deeper.&lt;/p&gt;

&lt;p&gt;The naive way to do reminders is one tone, generic email, click-and-send. The slightly-less-naive way is N tones (friendly / firm / etc), pick one, send.&lt;/p&gt;

&lt;p&gt;What I shipped is a &lt;strong&gt;5 × 5 × 3 matrix&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 tones&lt;/strong&gt; — soft, standard, firm, final, demand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 escalation steps&lt;/strong&gt; — first reminder through last notice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 relationship types&lt;/strong&gt; — new, loyal, going-silent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjw3urfdbv88dq86s6jo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjw3urfdbv88dq86s6jo.png" alt="Payment Reminder generator with 4 tone tabs (Gentle / Second reminder / Firm / Final notice), each auto-recommended by days overdue, with full email body preview" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The matrix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     Step 1  Step 2  Step 3   Step 4   Step 5
Relationship:
  new (default)       soft   standard firm   final    demand
  loyal              soft   soft     standard firm    final
  going_silent       soft   standard firm    final    demand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A loyal customer (3+ paid invoices, on time) gets &lt;em&gt;benefit of doubt&lt;/em&gt; and escalates one step slower than a new client. A "going-silent" customer (was responsive, now isn't) gets standard escalation. A new client gets standard escalation but with no past relationship to lean on.&lt;/p&gt;

&lt;p&gt;Why this matters: the cost of using "firm" too early on a loyal $50K/year client is higher than the cost of using "soft" too long on a new $500 client. The matrix encodes the asymmetric risk so the user doesn't have to think about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "demand" tone never threatens legal action
&lt;/h3&gt;

&lt;p&gt;Step 5 is "demand" — the strongest tone the system will produce. It explicitly does &lt;strong&gt;not&lt;/strong&gt; include phrases like "small claims," "collections agency," or "legal action." Reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legal threats from a freelancer often violate their own contract or the client's jurisdiction&lt;/li&gt;
&lt;li&gt;AI-generated legal threats are an unbounded liability surface for the tool&lt;/li&gt;
&lt;li&gt;Most freelancers don't actually want to escalate; they want a strong-but-defensible last email before &lt;em&gt;they&lt;/em&gt; decide to escalate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to threaten legal action, you write that part yourself. The AI gets you up to "this is unacceptable and I need a date." You decide whether the next email mentions a lawyer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-language with per-tone, per-language prompts
&lt;/h3&gt;

&lt;p&gt;This is where it gets unglamorous. Each &lt;code&gt;(tone, language)&lt;/code&gt; pair has its own system prompt because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Firm" in Japanese is grammatically and culturally different from "firm" in German&lt;/li&gt;
&lt;li&gt;A polite German chase email is more direct than a polite English one&lt;/li&gt;
&lt;li&gt;Some languages (Korean, Japanese) have honorifics that change every sentence based on relationship — collapsing them under "firm" loses the entire point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;SYSTEM_PROMPTS[tone][language]&lt;/code&gt; is a 2D lookup, not one big prompt with a &lt;code&gt;{language}&lt;/code&gt; variable. ~50 prompts to maintain, but the output quality difference is significant — bilingual freelancers in the user pool spotted machine-translated tone within 2-3 emails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two reminder modes: self-nudge or auto-chase
&lt;/h3&gt;

&lt;p&gt;The matrix is the &lt;em&gt;content&lt;/em&gt; layer — what the email says. There's a second design decision on the &lt;em&gt;delivery&lt;/em&gt; side: who actually hits send?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsk2mhibkmvyi7m5h52bx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsk2mhibkmvyi7m5h52bx.png" alt="Send &amp;amp; get paid modal showing two delivery modes: " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When an invoice goes overdue, the user picks one of two modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode A — Remind me to chase (self-nudge).&lt;/strong&gt; The system emails &lt;em&gt;you&lt;/em&gt; a nudge with a 1-click AI draft. You read it, edit if needed, and send it from your own mailbox. Keeps your voice, your relationship, your control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode B — Auto-chase the client.&lt;/strong&gt; The system drafts and sends the reminder directly to the client on the matrix-recommended schedule. You can pause anytime, mark paid anytime, or override the tone. Hands-off until they pay.&lt;/p&gt;

&lt;p&gt;Most users start in Mode A and switch to Mode B for clients who go fully silent — the relationship signal already says "this one isn't worth your hand-holding anymore."&lt;/p&gt;

&lt;p&gt;The interesting design problem: Mode B has to be &lt;em&gt;trustworthy enough to delegate&lt;/em&gt; without being &lt;em&gt;aggressive enough to torch a relationship&lt;/em&gt;. The matrix already encodes that asymmetry, which is why Mode B exists at all. A naive "AI auto-sends emails to your clients" feature without the relationship + escalation logic would be a recipe for damaged accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real chase email samples
&lt;/h3&gt;

&lt;p&gt;Same fictional invoice — Acme Corp, $2,400, 14 days overdue, invoice #INV-1042 — through tones 1-4:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tone 1 — soft (gentle nudge)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Quick reminder — invoice #INV-1042&lt;/span&gt;

Hi Sarah,

Hope you're well. Just a quick nudge that invoice #INV-1042
($2,400) was due last week. I know things slip through —
totally understand.

Here it is again: [invoice link]

Let me know if you need anything from my end to wrap it up.

Thanks,
Jiahao
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tone 2 — standard (professional follow-up)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Following up on invoice #INV-1042&lt;/span&gt;

Hi Sarah,

I wanted to follow up on invoice #INV-1042 for $2,400, which
was due on April 21 and is now 14 days past due.

Could you confirm when I can expect payment? If there's an
issue with the invoice or anything you need from me, please
let me know.

Best regards,
Jiahao
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tone 3 — firm&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Overdue: invoice #INV-1042 — payment required&lt;/span&gt;

Hi Sarah,

Invoice #INV-1042 ($2,400) is now 14 days overdue. I haven't
received a response to my previous reminders.

Please process payment within the next 5 business days, or
let me know in writing if there is a dispute that needs to
be resolved.

Continuing to work on new projects together depends on
keeping current invoices settled.

Regards,
Jiahao
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tone 4 — final notice&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="na"&gt; Final notice — invoice #INV-1042&lt;/span&gt;

Sarah,

This is a final notice regarding invoice #INV-1042 ($2,400),
now 30 days past due. Despite multiple reminders, the
balance remains unpaid.

I would prefer to resolve this directly. Please reply within
7 days to confirm next steps.

— Jiahao
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Tone 5 — &lt;em&gt;demand&lt;/em&gt; — looks similar to "final notice" but stripped of conciliatory language. Reserved for clients who have ignored all prior steps. Notably, it still does not threaten legal action.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost numbers
&lt;/h3&gt;

&lt;p&gt;Per-reminder cost (Claude 3.5 Sonnet via OpenRouter):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;System prompt&lt;/td&gt;
&lt;td&gt;~400&lt;/td&gt;
&lt;td&gt;$0.0012&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User context (invoice + history)&lt;/td&gt;
&lt;td&gt;~600&lt;/td&gt;
&lt;td&gt;$0.0018&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output (email body)&lt;/td&gt;
&lt;td&gt;~250&lt;/td&gt;
&lt;td&gt;$0.0038&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total per draft&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1,250&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.0068&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;About &lt;strong&gt;2/3 of a cent per chase email&lt;/strong&gt;. At freelance volumes (5-30 chases/month per active user) the marginal cost is rounding error. The bigger cost is keeping prompts tuned across 13 languages.&lt;/p&gt;

&lt;p&gt;If you want to try the dunning generator without going through the invoice flow first, there's a &lt;a href="https://ainvoicemaker.com/payment-reminder-generator" rel="noopener noreferrer"&gt;standalone Payment Reminder generator&lt;/a&gt; that takes invoice number + amount + days overdue and outputs the email directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Multi-document — same engine, six form factors
&lt;/h2&gt;

&lt;p&gt;Once Smart Paste and the preview engine were stable for invoices, every other document type came almost free:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Try it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Invoice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standard invoice with line items, taxes, discounts&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ainvoicemaker.com" rel="noopener noreferrer"&gt;/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Receipt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Payment-received version, "PAID" stamp + transaction details&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ainvoicemaker.com/receipt" rel="noopener noreferrer"&gt;/receipt&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quote / Estimate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre-work pricing, optional "Convert to Invoice" on accept&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ainvoicemaker.com/quote" rel="noopener noreferrer"&gt;/quote&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Superbill&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;US medical billing — NPI / CPT / ICD-10 fields, HIPAA-conscious defaults&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ainvoicemaker.com/superbill-generator" rel="noopener noreferrer"&gt;/superbill-generator&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Contract&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Service agreement, scope + payment terms + termination, AI-fillable&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ainvoicemaker.com/contract" rel="noopener noreferrer"&gt;/contract&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NDA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mutual or one-way, jurisdiction-aware boilerplate&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ainvoicemaker.com/nda" rel="noopener noreferrer"&gt;/nda&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Payment Reminder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standalone chase-email generator&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ainvoicemaker.com/payment-reminder-generator" rel="noopener noreferrer"&gt;/payment-reminder-generator&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same Smart Paste, same theme, same engine — different schema configurations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdjx3va5plb5mfpx27y3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdjx3va5plb5mfpx27y3.png" alt="Receipt page using the same form/preview framework as invoice — only the schema and labels change" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbslyj0idv88lepv6jtd1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbslyj0idv88lepv6jtd1.png" alt="Quote page — same framework, different doctype, with extra " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They all share:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same Smart Paste extraction (the document type is a parameter)&lt;/li&gt;
&lt;li&gt;Same theme system (your brand color carries across all six)&lt;/li&gt;
&lt;li&gt;Same PDF export (no per-document templates to maintain)&lt;/li&gt;
&lt;li&gt;Same localStorage memory (your last-used "From" details auto-fill)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a seventh document type now costs about a day of work — schema definition + a small profile config. The framework cost was high upfront, the marginal cost is near zero now.&lt;/p&gt;




&lt;h2&gt;
  
  
  The dashboard ties it together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8ez4qewc1pzfkq5pgne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8ez4qewc1pzfkq5pgne.png" alt="Dashboard listing invoices on this device with status badges (Sent / Overdue / Paid) and a " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because there's no login, the dashboard is per-device — saved entirely in localStorage. It groups your local history into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overdue&lt;/strong&gt; — past due date, top of the list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Due soon&lt;/strong&gt; — under 7 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sent&lt;/strong&gt; — awaiting payment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafts&lt;/strong&gt; — not yet sent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each row gets a one-click &lt;strong&gt;Generate reminder&lt;/strong&gt; button (drops you into the dunning generator with all invoice context pre-filled) and &lt;strong&gt;Mark as paid&lt;/strong&gt; (closes the loop, also gives the AI relationship signal — paid-on-time history feeds into the &lt;em&gt;loyal&lt;/em&gt; classifier next time).&lt;/p&gt;

&lt;p&gt;This is the closest thing to "an account" the tool has, and it works without any account because everything is local.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;For anyone curious about the build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; — Next.js 15 (App Router), TypeScript strict, Tailwind, shadcn/ui&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI provider&lt;/strong&gt; — OpenRouter (single key, automatic fallback). Default model: Claude 3.5 Sonnet for invoice extraction + reminder generation. Lighter classification tasks fall through to GPT-4o-mini.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt; — Supabase Postgres (only used for the auto-chase delivery queue; everything else is localStorage)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF rendering&lt;/strong&gt; — client-side, no server roundtrip — same React tree that renders the live preview&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting&lt;/strong&gt; — Alibaba Cloud HK + Cloudflare CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt; — Playwright E2E (currently 226 tests, including a chained "switch industry then template then language" test that has caught more bugs than any unit test ever has)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost shape&lt;/strong&gt; — fixed (server + Cloudflare) ~$30/month. Variable (AI calls) ~$0.007/draft × volume. At zero revenue this is comfortably under personal-project budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single biggest leverage point: putting AI behind a unified &lt;code&gt;src/lib/ai/client.ts&lt;/code&gt; interface from day one. Every feature (Smart Paste, dunning, future stuff) goes through the same client, gets the same fallback + cost tracking + prompt-version logging, free of charge.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Ship the dunning matrix sooner.&lt;/strong&gt; I spent 6 weeks polishing the customize sheet before any reminder feature existed. The customize sheet is delightful but the chase emails are what made users come back — it's the only feature where I get unsolicited "this saved me $X" emails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Constraint-based prompts &amp;gt; example-based prompts.&lt;/strong&gt; Almost every prompt I tried to seed with examples ended up copying the example's structure too literally. Replacing examples with hard constraints ("don't use exclamation marks", "no &lt;code&gt;Sorry to bother you&lt;/code&gt;", "subject line must include invoice number") gave more variety and tighter tone separation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The relationship signal is the unfair advantage.&lt;/strong&gt; Most invoice tools are document generators. Adding the relationship dimension (new vs loyal vs going-silent) turned this into a &lt;em&gt;workflow&lt;/em&gt; tool. It's also the hardest part to copy because you need usage history before the signal exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Open the URL. That's the whole onboarding.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No install, no extension, runs in any browser&lt;/li&gt;
&lt;li&gt;No login, no email capture, no account&lt;/li&gt;
&lt;li&gt;Unlimited — generate as many invoices, receipts, quotes, contracts as you need&lt;/li&gt;
&lt;li&gt;Free forever — no paid tier, no credit card, no watermark&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;→ &lt;a href="https://ainvoicemaker.com" rel="noopener noreferrer"&gt;Free invoice generator with AI payment reminders&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build something similar, want to compare prompt structures, or have a &lt;code&gt;(tone, language)&lt;/code&gt; pair where my output is wrong, comments below or &lt;a href="https://dev.to/aiinvoicemaker"&gt;@ me on Dev.to&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Cross-posted to Medium, Hashnode, and Indie Hackers. Canonical version is on Dev.to.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>indiehackers</category>
      <category>sideprojects</category>
      <category>freelance</category>
    </item>
    <item>
      <title>Every Invoice Tool Wants My Data. I Built One That Doesn't.</title>
      <dc:creator>jiahao luo</dc:creator>
      <pubDate>Mon, 13 Apr 2026 12:28:13 +0000</pubDate>
      <link>https://dev.to/jiahao_luo_f33d8988caf4e1/every-invoice-tool-wants-my-data-i-built-one-that-doesnt-4b1m</link>
      <guid>https://dev.to/jiahao_luo_f33d8988caf4e1/every-invoice-tool-wants-my-data-i-built-one-that-doesnt-4b1m</guid>
      <description>&lt;p&gt;My girlfriend asked me to help her make an invoice. "You're good with computers."&lt;/p&gt;

&lt;p&gt;I googled "free invoice generator." Here's what happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First result: signup required&lt;/li&gt;
&lt;li&gt;Second: free trial expired, credit card&lt;/li&gt;
&lt;li&gt;Third: works but PDF has a watermark across the whole thing&lt;/li&gt;
&lt;li&gt;Fourth: actually decent but generates the PDF on their server — meaning they see my girlfriend's client name, her bank details, everything&lt;/li&gt;
&lt;li&gt;Fifth: Google Docs template from Pinterest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm a frontend developer. Seven years. I sat there thinking: this is a form with like 10 fields, a table that does multiplication, and a PDF export. Why does any of this need a backend? Why does it need my email? Why is the PDF being generated on someone else's server?&lt;/p&gt;

&lt;p&gt;It doesn't. So I built one where it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ainvoicemaker.com" rel="noopener noreferrer"&gt;ainvoicemaker.com&lt;/a&gt;. Next.js. Client-side PDF. Zero server storage. Your invoice data never leaves your browser. Open, fill, download.&lt;/p&gt;

&lt;p&gt;Here's everything that went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  jsPDF pretends Unicode doesn't exist
&lt;/h2&gt;

&lt;p&gt;jsPDF ships with three fonts. Helvetica, Times, Courier. End of list.&lt;/p&gt;

&lt;p&gt;User types Chinese → blank rectangles. Japanese → blank rectangles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasCJK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\u&lt;/span&gt;&lt;span class="sr"&gt;4e00-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;9fff&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;3400-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;4dbf&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;3040-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;30ff&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;ac00-&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="sr"&gt;d7af&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detect CJK → dynamically fetch NotoSansSC from Google Fonts. 10MB. Fine on fiber. Not fine on a phone in Jakarta.&lt;/p&gt;

&lt;p&gt;AbortController, 8-second timeout. Font doesn't load? Helvetica it is. The CJK will be garbled but at least the tab doesn't freeze. I'll take "wrong" over "frozen" every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image.naturalWidth returns 0 and I wasted two hours
&lt;/h2&gt;

&lt;p&gt;Logo rendering. Need aspect ratio. Did the obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero. Always zero. Because &lt;code&gt;.src&lt;/code&gt; assignment is async-ish and the browser hasn't decoded anything yet. I knew this conceptually. But when you're debugging at 11pm and the math looks right and the image is definitely there, you don't think "maybe the browser hasn't started loading this yet." You think "my ratio calculation is wrong" and you spend two hours refactoring perfectly correct math.&lt;/p&gt;

&lt;p&gt;Fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImageProperties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logoDataUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;jsPDF has a built-in method that actually works. Two hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intl.NumberFormat does everything and nobody talks about it
&lt;/h2&gt;

&lt;p&gt;55 currencies. Was building a lookup table. Got to currency #8, thought "I refuse to do this manually."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ja-JP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JPY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1234.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "￥1,235"  — knows JPY has zero decimals&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-DE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1234.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// "1.234,50 €"  — knows Germany uses comma decimals and puts € after&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser knows all of this. Has since 2016. I deleted my lookup table. Genuinely mad that I've seen a hundred "how to format currency in JavaScript" tutorials and none of them mention this API.&lt;/p&gt;

&lt;h2&gt;
  
  
  96% of my users were Playwright
&lt;/h2&gt;

&lt;p&gt;The bad one.&lt;/p&gt;

&lt;p&gt;Launched the app. Analytics: 175 sessions, 37 PDF downloads. Nice. Spent two days building dashboards, analyzing funnels.&lt;/p&gt;

&lt;p&gt;Then I filtered the data. Removed HeadlessChrome (Playwright E2E — 226 tests, each firing analytics events). My own dev IPs. Cloud scrapers.&lt;/p&gt;

&lt;p&gt;Real external humans: ~45. Real PDF downloads from non-developers: 1.&lt;/p&gt;

&lt;p&gt;I'd been making product decisions based on my own test suite. If you run E2E tests that trigger your analytics endpoint, filter at collection time. Not at query time. I now drop all HeadlessChrome requests before they hit the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldDrop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ua&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HeadlessChrome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should've been line 1 of the analytics API. Wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  localStorage: why I can't have nice things
&lt;/h2&gt;

&lt;p&gt;No signup = no database. Returning users shouldn't re-enter their business name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;BUSINESS_PROFILE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inv_business&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;RECENT_CLIENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inv_clients&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;INVOICE_SEQUENCE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inv_sequence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one thing that works well: auto-incrementing invoice numbers (&lt;code&gt;INV-0001&lt;/code&gt;, &lt;code&gt;INV-0002&lt;/code&gt;...) that persist across sessions. Come back next month and it picks up where you left off. Feels like the tool remembers you. It doesn't. It's 47 bytes in localStorage. But it feels nice.&lt;/p&gt;

&lt;p&gt;The problem nobody warns you about: user clears browser data, everything's gone. Switches to phone, nothing syncs. The real fix is "optional account" but then I'm adding signups, which is the thing I built this entire tool to avoid.&lt;/p&gt;

&lt;p&gt;Still don't have a good answer for this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dumb mistake
&lt;/h2&gt;

&lt;p&gt;Code works fine. PDF generates. Tool does what it should.&lt;/p&gt;

&lt;p&gt;My mistake was naming it "AI Invoice Generator" because AI sounds trendy. Nobody searches "AI invoice" — like 500 queries a month. "Invoice generator" gets 49,500. I optimized for vibes instead of for what people actually type into Google. Main keyword ranks position 72. That's page 8. Nobody scrolls to page 8.&lt;/p&gt;

&lt;p&gt;Also built 7 document types, 55 currencies, 9 languages, and an auto-reminder system before having more than 2 visitors a day. Developer brain. Building is fun. Marketing is not. So you build.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ainvoicemaker.com" rel="noopener noreferrer"&gt;ainvoicemaker.com&lt;/a&gt; — everything stays in your browser. No signup, no server storage. I just need humans to find it now.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>privacy</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
