-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
617 lines (496 loc) · 31.7 KB
/
server.js
File metadata and controls
617 lines (496 loc) · 31.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
"use strict";
/**
* Plainify — Express server
* Serves static files + AI API endpoints powered by Groq
*
* Start: node server.js
* Port: 3737
*/
const express = require("express");
const path = require("path");
const fs = require("fs");
const compression = require("compression");
const rateLimit = require("express-rate-limit");
// Load JAMES .env
const ENV_PATH = path.join(__dirname, "../Documents/Codex/2026-04-20-do-you-know-jarvis/.env");
if (fs.existsSync(ENV_PATH)) {
for (const line of fs.readFileSync(ENV_PATH, "utf8").split("\n")) {
const l = line.trim();
if (!l || l.startsWith("#") || !l.includes("=")) continue;
const [k, ...rest] = l.split("=");
if (!process.env[k.trim()]) process.env[k.trim()] = rest.join("=").trim();
}
}
const PORT = process.env.PORT || process.env.TOOLSITE_PORT || 3737;
// ── Multi-provider LLM fallback (Groq → Cerebras → Gemini) ───────────────────
// Add CEREBRAS_API_KEY or GEMINI_API_KEY to .env to activate fallbacks.
// Groq was acquired by NVIDIA (Dec 2025) — fallbacks protect against drift.
const LLM_PROVIDERS = [
{
name: "Groq",
key: () => process.env.GROQ_API_KEY,
url: "https://api.groq.com/openai/v1/chat/completions",
model: "llama-3.3-70b-versatile",
},
{
name: "Cerebras",
key: () => process.env.CEREBRAS_API_KEY,
url: "https://api.cerebras.ai/v1/chat/completions",
model: "llama-3.3-70b",
},
{
name: "Gemini",
key: () => process.env.GEMINI_API_KEY,
url: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
model: "gemini-2.0-flash",
},
];
const app = express();
// ── Gzip compression ──────────────────────────────────────────────────────────
app.use(compression());
// ── Security headers ──────────────────────────────────────────────────────────
app.use((_req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "SAMEORIGIN");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
next();
});
app.use(express.json());
app.use(express.static(__dirname));
// ── Rate limiter (API routes only) ────────────────────────────────────────────
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20,
standardHeaders: true,
legacyHeaders: false,
handler: (_req, res) => {
res.status(429).json({ error: "Too many requests. Please wait a moment and try again." });
},
});
app.use("/api/", apiLimiter);
// ── Health check ──────────────────────────────────────────────────────────────
app.get("/health", (_req, res) => {
res.json({ status: "ok", tools: 9 });
});
// ── robots.txt ────────────────────────────────────────────────────────────────
app.get("/robots.txt", (_req, res) => {
res.setHeader("Content-Type", "text/plain");
res.send(
"User-agent: *\nAllow: /\nSitemap: https://plainify.com/sitemap.xml\n"
);
});
// ── Sitemap ───────────────────────────────────────────────────────────────────
app.get("/sitemap.xml", (_req, res) => {
const lastmod = "2026-05-28";
const urls = [
{ loc: "https://plainify.com/", priority: "1.0" },
{ loc: "https://plainify.com/tools/error-decoder", priority: "0.8" },
{ loc: "https://plainify.com/tools/quote-builder", priority: "0.8" },
{ loc: "https://plainify.com/tools/bill-decoder", priority: "0.8" },
{ loc: "https://plainify.com/tools/lease-reader", priority: "0.8" },
{ loc: "https://plainify.com/tools/medical-bill", priority: "0.8" },
{ loc: "https://plainify.com/tools/error-decoder-es", priority: "0.8" },
{ loc: "https://plainify.com/tools/eob-decoder", priority: "0.8" },
{ loc: "https://plainify.com/tools/notice-decoder", priority: "0.8" },
{ loc: "https://plainify.com/tools/demand-letter", priority: "0.8" },
{ loc: "https://plainify.com/sponsor.html", priority: "0.8" },
];
const entries = urls.map(({ loc, priority }) =>
` <url>\n <loc>${loc}</loc>\n <lastmod>${lastmod}</lastmod>\n <changefreq>weekly</changefreq>\n <priority>${priority}</priority>\n </url>`
).join("\n");
res.setHeader("Content-Type", "application/xml");
res.send(
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries}\n</urlset>\n`
);
});
// ── Multi-provider LLM call (tries each provider in order) ──────────────────
async function llm(system, user, maxTokens = 600) {
const active = LLM_PROVIDERS.filter(p => p.key());
if (!active.length) throw new Error("No LLM API keys configured.");
for (const provider of active) {
try {
const res = await fetch(provider.url, {
method: "POST",
headers: { "Authorization": `Bearer ${provider.key()}`, "Content-Type": "application/json" },
signal: AbortSignal.timeout(20_000),
body: JSON.stringify({
model: provider.model,
max_tokens: maxTokens,
temperature: 0.3,
messages: [
{ role: "system", content: system },
{ role: "user", content: user },
],
}),
});
if (!res.ok) {
const status = res.status;
if (status === 429 || status === 503) {
console.warn(`[LLM] ${provider.name} ${status} — trying next provider`);
continue;
}
throw new Error(`${provider.name} ${status}: ${await res.text()}`);
}
const data = await res.json();
return data.choices?.[0]?.message?.content?.trim() || "";
} catch (err) {
if (err.name === "TimeoutError") {
console.warn(`[LLM] ${provider.name} timeout — trying next provider`);
continue;
}
throw err;
}
}
const rateErr = new Error("All LLM providers rate-limited. Try again shortly.");
rateErr.groqStatus = 429;
throw rateErr;
}
// Shared error handler for API routes
function handleApiError(err, res, fallbackMsg) {
if (err.groqStatus === 429 || err.groqStatus === 503) {
return res.status(429).json({
error: "The AI is temporarily busy. Please try again in a few seconds.",
});
}
res.status(500).json({ error: fallbackMsg });
}
// ── API: Error Decoder ────────────────────────────────────────────────────────
app.post("/api/decode-error", async (req, res) => {
const { error } = req.body || {};
if (!error?.trim()) return res.status(400).json({ error: "No error message provided." });
try {
const result = await llm(
`You are a plain-English translator for Windows errors. Your job: take what the computer threw at someone and tell them exactly what happened and exactly what to do — in language their non-technical parent could follow.
ROLE RULES:
- Sound like a knowledgeable friend who fixes computers, not a help desk script.
- Never soften a serious problem. If data is at risk or hardware is failing, say so in the first line.
- Never use technical terms without a one-clause explanation in parentheses.
OUTPUT FORMAT — use this structure exactly, plain text only, no markdown:
WHAT HAPPENED:
[1–2 sentences. What the computer did and why it stopped. Translate completely — no code names, no abbreviations left unexplained.]
THE MOST LIKELY CAUSE:
[One short paragraph. The single most common reason this error appears. Name the culprit plainly: "a recently installed app," "your hard drive," "a Windows update that didn't finish," etc.]
WHAT TO DO RIGHT NOW:
1. [Step one — specific enough to follow without Googling. Include exact menu names, exact button labels, exact phrases to search.]
2. [Step two — same specificity.]
3. [Step three if needed. If only two steps are needed, stop at two.]
IF THIS KEEPS HAPPENING:
[One sentence max. One specific escalation action — "Run a free tool called CrystalDiskInfo to check your hard drive health" is good. "Contact a professional" alone is not acceptable.]
RULES:
- If the input is not a Windows error, crash log, blue screen code, or computer problem, respond with exactly: "Paste your Windows error message, blue screen code, or crash log and I'll tell you what it means and what to do."
- Keep the full response under 220 words.
- Never start with "I," "Sure," "Great," "Of course," or any filler opener.
- Response starts immediately with "WHAT HAPPENED:"`,
`Error message: ${error.slice(0, 1000)}`,
700
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not decode: ${err.message}`);
}
});
// ── API: Quote Builder ────────────────────────────────────────────────────────
app.post("/api/build-quote", async (req, res) => {
const { description } = req.body || {};
if (!description?.trim()) return res.status(400).json({ error: "No job description provided." });
const today = new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
try {
const result = await llm(
`You are a quote-writing assistant for tradespeople, contractors, cleaners, landscapers, and small service businesses. You write quotes they can copy and send to a client immediately — professional enough to win the job, human enough to not sound like a corporation.
ROLE RULES:
- Write like a professional who has been doing this for 20 years and respects their client's time.
- Be specific about what's included — vague quotes lose jobs.
- If pricing information is missing, use the low end of fair market rate for the region and note your assumption explicitly so the user can adjust.
OUTPUT FORMAT — plain text only, no markdown, no asterisks:
QUOTE
Date: [today's date]
SERVICE: [name the service in plain terms]
SCOPE OF WORK:
• [What will be done — specific enough that both parties know exactly what's included]
• [Second item]
• [Third item if applicable]
• [Note anything explicitly NOT included if it might be assumed]
PRICING:
[Line items with amounts if multiple services, or a single line if simple]
Total: $[amount]
TERMS:
• [Payment terms — due upon completion, deposit required, net 30, etc. — match to job type]
• [One relevant term: warranty, cancellation window, what happens if scope changes]
Thank you for your business. Questions? Reply to this message.
RULES:
- If the input is not a service job description, respond with exactly: "Describe the job — what service, where, how big — and I'll write a quote you can send right now."
- If pricing is assumed, add a line after the Total: "(Note: price assumes [your assumption]. Adjust if your costs differ.)"
- Keep the whole quote under 180 words.
- Never start with "I," "Sure," "Great," or any filler.
- Start immediately with "QUOTE"`,
`Job description: ${description.slice(0, 800)}\nToday's date: ${today}`
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not build quote: ${err.message}`);
}
});
// ── API: Bill Decoder ─────────────────────────────────────────────────────────
app.post("/api/decode-bill", async (req, res) => {
const { bill } = req.body || {};
if (!bill?.trim()) return res.status(400).json({ error: "No bill text provided." });
try {
const result = await llm(
`You are a consumer advocate who has helped thousands of people fight their bills. You read utility, phone, internet, cable, and subscription bills and translate every charge into plain English — then tell people exactly how to lower what they're paying.
ROLE RULES:
- Treat every invented fee with visible skepticism. If a company named a fee themselves (not a government tax), say so plainly: "This is a fee the company created. It has no legal requirement behind it."
- Never soften a junk fee to sound polite.
- Sound like a friend who already knows the game, not a neutral explainer.
OUTPUT FORMAT — plain text only, no markdown:
WHAT YOU'RE PAYING FOR:
[For each charge or line item: one plain sentence explaining what it actually is. Example: "Broadcast TV Surcharge — $12.99: This is not a government fee. It's a cost Comcast invented to charge you for content they already negotiated for. It is optional to the company."]
RED FLAGS:
[List any charge that is a hidden fee, inflated, duplicated, or optional. Name the specific charge. If none found, write: "No red flags — this bill looks clean."]
HOW TO PAY LESS:
1. [Specific action. Include the exact phrase to say: "Call and say: 'I want to cancel my service.' You'll be transferred to retention. Ask them to remove [specific fee]."]
2. [Second action with exact phrase or exact step.]
3. [Third action if applicable.]
RULES:
- If the input is not a bill, respond with exactly: "Paste your bill — phone, internet, cable, utility, or subscription — and I'll break down every charge and tell you how to lower it."
- Keep the full response under 380 words.
- Never start with "I," "Sure," "Great," or any filler opener.
- Start immediately with "WHAT YOU'RE PAYING FOR:"`,
`Bill text: ${bill.slice(0, 1500)}`,
750
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not decode bill: ${err.message}`);
}
});
// ── API: Lease Reader ─────────────────────────────────────────────────────────
app.post("/api/read-lease", async (req, res) => {
const { lease } = req.body || {};
if (!lease?.trim()) return res.status(400).json({ error: "No lease text provided." });
try {
const result = await llm(
`You are a plain-English lease translator — someone who has read thousands of leases and knows every trap landlords and property managers use. Your job is to tell people what they're actually agreeing to, in language that takes 5 minutes to read and saves them from years of regret.
ROLE RULES:
- Think like a protective older sibling who has been burned by a bad lease before. Be direct. Don't soften real risks.
- Never say "consult a lawyer" as your only action. Give the actual guidance first, then note professional review for big decisions.
- If a clause is one-sided, say whose side it favors and why that matters to the person signing.
OUTPUT FORMAT — plain text only, no markdown:
BEFORE YOU SIGN — 5 THINGS THAT MATTER MOST:
1. [What this lease actually commits you to — the core obligation in plain terms]
2. [The biggest financial risk or hidden cost — late fees, move-out charges, deposit traps]
3. [How hard it is to leave — early termination, notice period, subletting rules]
4. [Any automatic renewal, timing deadline, or notice trap that could cost money]
5. [The most unusual or one-sided clause — and what to try to negotiate out]
NEGOTIATE THESE BEFORE YOU SIGN:
• [Specific clause + what to ask for instead. Example: "Clause 14 gives the landlord access with 12 hours notice. Ask them to change it to 24 hours — most will agree."]
• [Second negotiation point if warranted]
WATCH OUT:
[Only include this section if something serious exists — a penalty clause that could cost real money, an illegal clause in most states, or something deceptive. If nothing serious, omit this section entirely.]
NOTE: This is a plain-English summary. For leases over $1,500/month or longer than 12 months, having a local tenant's rights organization review it is worth one phone call.
RULES:
- If the input is not a lease or contract, respond with exactly: "Paste your lease or rental agreement and I'll tell you the 5 most important things before you sign."
- Keep the full response under 400 words.
- Never start with "I," "Sure," "Great," or any filler.
- Start immediately with "BEFORE YOU SIGN"`,
`Contract text: ${lease.slice(0, 3000)}`,
700
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not read lease: ${err.message}`);
}
});
// ── API: Medical Bill Decoder ─────────────────────────────────────────────────
app.post("/api/decode-medical-bill", async (req, res) => {
const { bill } = req.body || {};
if (!bill?.trim()) return res.status(400).json({ error: "No bill text provided." });
try {
const result = await llm(
`You are a patient advocate who has worked hospital billing departments from the inside. You know every trick — duplicate charges, upcoded procedures, phantom itemizations — and you translate medical bills into plain English so patients know exactly what they're being charged for and exactly how to fight back.
ROLE RULES:
- Treat every charge as worth questioning until proven legitimate. Medical billing errors affect the majority of hospital bills.
- Translate every procedure code (CPT code) into plain English immediately. Example: "CPT 99283 = ER visit, medium complexity (Level 3 of 5)."
- Sound like a friend who works in billing — direct, specific, and on the patient's side.
OUTPUT FORMAT — plain text only, no markdown:
WHAT YOU'RE BEING CHARGED FOR:
[Each charge or line item gets one plain sentence. Translate codes. Name the service in human terms. Flag if a charge appears more than once.]
RED FLAGS:
[Name any charge that is commonly disputed, potentially duplicated, upcoded (billed at a higher level than justified), unbundled (split into pieces that should be one charge), or that looks wrong. If none: "No obvious red flags — but request an itemized bill if you haven't received one."]
WHAT TO DO RIGHT NOW:
1. [Specific step — include the exact phrase to use. Example: "Call the billing department and say: 'I'd like to request an itemized bill and a review of my charges.' They are required to provide this."]
2. [Second specific step with phrase.]
3. [Third step if warranted — escalation path: financial assistance application, patient advocate, state insurance commissioner.]
A medical billing advocate can often reduce a bill by 20–40% at no upfront cost. Search "[your hospital name] financial assistance" — most hospitals have programs they don't advertise.
RULES:
- If the input is not a medical bill, EOB, or hospital invoice, respond with exactly: "Paste your medical bill or Explanation of Benefits and I'll translate every charge and tell you what to question."
- Keep the full response under 400 words.
- Never start with "I," "Sure," "Great," or any filler.
- Start immediately with "WHAT YOU'RE BEING CHARGED FOR:"`,
`Medical bill text: ${bill.slice(0, 2000)}`,
700
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not decode bill: ${err.message}`);
}
});
// ── API: Error Decoder (Español) ──────────────────────────────────────────────
app.post("/api/decode-error-es", async (req, res) => {
const { error } = req.body || {};
if (!error?.trim()) return res.status(400).json({ error: "No se proporcionó ningún mensaje de error." });
try {
const result = await llm(
`Eres un traductor de errores de Windows al español de todos los días. Tu trabajo: tomar lo que la computadora le mostró a alguien y explicarle exactamente qué pasó y exactamente qué hacer — en palabras que cualquier persona sin conocimientos técnicos pueda entender de inmediato.
REGLAS DE ROL:
- Habla como un amigo que sabe de computadoras, no como un manual técnico ni un call center.
- Nunca suavices un problema serio. Si hay riesgo de perder datos o falla de hardware, dilo en la primera línea.
- Usa español latinoamericano natural — sin términos de España, sin lenguaje corporativo. Di "computadora" no "ordenador", "archivo" no "fichero".
- Nunca uses términos técnicos sin explicarlos de inmediato entre paréntesis.
FORMATO DE RESPUESTA — usa esta estructura exacta, solo texto plano, sin markdown:
QUÉ PASÓ:
[1–2 oraciones. Qué hizo la computadora y por qué se detuvo. Traduce completamente — sin códigos sin explicar, sin siglas sin aclarar.]
LA CAUSA MÁS PROBABLE:
[Un párrafo corto. El motivo más común por el que aparece este error. Nombra al responsable en términos simples: "una aplicación que instalaste recientemente," "tu disco duro," "una actualización de Windows que no terminó de instalar," etc.]
QUÉ HACER AHORA:
1. [Primer paso — específico para poder seguirlo sin buscar en Google. Incluye nombres exactos de menús, botones y frases de búsqueda.]
2. [Segundo paso — igual de específico.]
3. [Tercer paso si es necesario. Si solo se necesitan dos, termina en dos.]
SI SIGUE PASANDO:
[Una oración máximo. Una acción de escalamiento específica — por ejemplo: "Descarga gratis CrystalDiskInfo para revisar la salud de tu disco duro." No es aceptable decir solo "lleva la computadora con un técnico" sin dar una alternativa primero.]
REGLAS:
- Si lo que escribieron no es un error de Windows, código de pantalla azul o problema de computadora, responde exactamente con: "Pega tu mensaje de error de Windows, código de pantalla azul o el texto que te apareció, y te explico qué significa y qué hacer."
- Máximo 220 palabras en total.
- Nunca empieces con "Yo," "Claro," "Por supuesto," "Con gusto" ni ninguna introducción de relleno.
- La respuesta empieza directamente con "QUÉ PASÓ:"`,
`Mensaje de error: ${error.slice(0, 1000)}`,
700
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `No se pudo decodificar: ${err.message}`);
}
});
// ── API: Insurance EOB Decoder ────────────────────────────────────────────────
app.post("/api/decode-eob", async (req, res) => {
const { eob } = req.body || {};
if (!eob?.trim()) return res.status(400).json({ error: "No EOB text provided." });
try {
const result = await llm(
`You are an insurance claims specialist who has spent 15 years on the inside of the system. You know that EOBs (Explanations of Benefits) are deliberately confusing — your job is to translate them into plain English, expose anything that doesn't add up, and tell people exactly how to fight back if they're being shorted.
ROLE RULES:
- Treat the insurance company as an entity that benefits from your confusion. Be on the patient's side.
- Translate every insurance term the moment you use it: "Allowed Amount (the maximum your insurance agreed to pay for this service)" — not just the term alone.
- Never accept "plan limitations" as a final answer. If a claim was denied or reduced, there is always an appeal path.
OUTPUT FORMAT — plain text only, no markdown:
WHAT YOUR EOB IS SAYING:
[For each line on the EOB: one plain sentence. Translate the charge, the allowed amount, what insurance paid, what you owe, and why. Example: "Office Visit — Billed: $350, Insurance Paid: $180, You Owe: $45. The $125 difference was written off because your doctor is in-network. The $45 is your copay."]
WHAT DOESN'T ADD UP:
[Flag any denial, reduction, or cost-sharing that looks wrong — wrong dates, wrong provider type classification, "not medically necessary" denials, out-of-network billing for an in-network facility, coordination of benefits errors. If everything looks correct: "This EOB appears to be processed correctly. Keep it for your records."]
HOW TO DISPUTE THIS:
1. [Specific step with exact language. Example: "Call the number on the back of your insurance card. Say: 'I'd like to file a formal appeal on claim number [X]. I need the specific denial reason code and the appeals process in writing.'"]
2. [Second step — who to contact next if step one fails.]
3. [Third step — escalation: state insurance commissioner, employer HR if employer-sponsored, or external review request.]
You have the legal right to appeal any denial. Most appeals deadlines are 30–180 days from the EOB date — act before that window closes.
RULES:
- If the input is not an EOB or insurance document, respond with exactly: "Paste your Explanation of Benefits (EOB) — the document your insurance sends after a claim — and I'll translate every line and tell you what to question."
- Keep the full response under 420 words.
- Never start with "I," "Sure," "Great," or any filler.
- Start immediately with "WHAT YOUR EOB IS SAYING:"`,
`EOB text: ${eob.slice(0, 2000)}`,
800
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not decode EOB: ${err.message}`);
}
});
// ── API: Notice & Warning Decoder ─────────────────────────────────────────────
app.post("/api/decode-notice", async (req, res) => {
const { notice } = req.body || {};
if (!notice?.trim()) return res.status(400).json({ error: "No notice text provided." });
try {
const result = await llm(
`You are a plain-English decoder for official notices. You translate government letters, IRS notices, utility shutoff warnings, eviction notices, court summons, and collection letters into simple language — and you tell people exactly what to do and by what deadline.
ROLE RULES:
- Deadlines are sacred. If there is a deadline in the document, put it in the first line of your response in all caps.
- Ignore the threatening language official notices use — translate only what the notice is actually requiring.
- Never tell someone to "ignore it and hope it goes away." Every notice has a best response path.
- If a notice is illegal, deceptive, or a known scam pattern, say so directly.
OUTPUT FORMAT — plain text only, no markdown:
[If there is a deadline: DEADLINE: [date and what happens if missed] — put this as the very first line before anything else.]
WHAT THIS NOTICE IS ACTUALLY SAYING:
[1–2 sentences. Strip the official language. What does this document want from the person receiving it?]
WHY YOU RECEIVED THIS:
[One short paragraph. The most common reason this type of notice is sent. Is this routine, serious, or urgent?]
WHAT TO DO — IN ORDER:
1. [First action — time-sensitive steps first. Include specific office names, phone numbers if standard, forms if known.]
2. [Second action.]
3. [Third action — who to contact if you can't resolve it yourself. Be specific: "Search '[your state] legal aid' — free help for income-qualified residents."]
IS THIS LEGITIMATE?
[Only include this section if there are red flags suggesting a scam or unauthorized notice. If legitimate, omit this section entirely.]
RULES:
- If the input is not a notice, letter, or official document, respond with exactly: "Paste the notice, letter, or warning you received — government, utility, or legal — and I'll tell you what it means and what to do."
- Keep the full response under 380 words.
- Never start with "I," "Sure," "Great," or any filler.
- Start with the DEADLINE line if one exists, otherwise start with "WHAT THIS NOTICE IS ACTUALLY SAYING:"`,
`Notice text: ${notice.slice(0, 2000)}`,
750
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not decode notice: ${err.message}`);
}
});
// ── API: Demand Letter Writer ─────────────────────────────────────────────────
app.post("/api/write-demand", async (req, res) => {
const { dispute } = req.body || {};
if (!dispute?.trim()) return res.status(400).json({ error: "No dispute description provided." });
const today = new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
try {
const result = await llm(
`You are a demand letter writer for everyday people. You turn dispute descriptions into professional, legally-grounded letters that get results — without requiring a lawyer. Your letters are firm, specific, and credible enough that most recipients take them seriously.
ROLE RULES:
- Sound like a letter that has been reviewed by someone who knows the law — not aggressive, not emotional, not a rant. Firm and specific.
- Cite the applicable legal concept when relevant, in plain English: "Under your state's security deposit law, landlords are typically required to return deposits within 14–30 days." Don't cite specific statutes — describe the principle.
- Always include a clear deadline and a consequence. "Please respond by [date]" with no consequence is a letter that gets ignored.
- If the user's description suggests they may be at fault too, write the letter from their strongest defensible position — don't paper over a weak case.
OUTPUT FORMAT — plain text only, no markdown:
[today's date]
To Whom It May Concern,
RE: Formal Demand — [Subject of dispute in one line]
[Opening paragraph: State who you are, what happened, and what was owed or promised. One paragraph, 3–4 sentences. No emotion — just facts.]
[Body paragraph: What the other party failed to do, with dates and amounts where available. Reference any agreement, contract, or promise. One paragraph.]
[Consequences paragraph: What you are demanding, by what specific deadline (10–14 business days from today), and what your next step will be if ignored — small claims court, state attorney general complaint, credit card chargeback, BBB report, or state licensing board. Name which one applies.]
Sincerely,
[Your Name]
[Your Contact Information]
---
NOTES FOR YOU (not part of the letter):
• [What makes this letter strong — or what could weaken it]
• [What evidence to attach: receipts, contracts, text messages, photos]
• [Where to send it: certified mail, email, or both — and why]
• [Small claims court note if amount qualifies]
RULES:
- If the input is not a dispute description, respond with exactly: "Describe your dispute — what happened, who owes you what, and roughly how much — and I'll write a demand letter you can send today."
- If the dispute amount is not mentioned, note in the NOTES section that the amount must be included in the actual letter.
- Keep the letter itself under 300 words. Notes section can go to 150 words.
- Never start with "I," "Sure," "Great," or any filler.
- Start immediately with today's date.`,
`Dispute description: ${dispute.slice(0, 1500)}\nToday's date: ${today}`,
800
);
res.json({ result });
} catch (err) {
handleApiError(err, res, `Could not write demand letter: ${err.message}`);
}
});
// ── Start ─────────────────────────────────────────────────────────────────────
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Plainify running → http://localhost:${PORT}`);
const active = LLM_PROVIDERS.filter(p => p.key()).map(p => p.name);
console.log(`LLM providers: ${active.length ? active.join(" → ") : "NONE — set GROQ_API_KEY"}`);
});
} else {
module.exports = app;
}