# Gaps: what the product needs and nobody has written yet

**This file exists so that nothing gets invented.** `docs/spec/microcopy.md` owns every
interface string in Tendd, and the rule is absolute: a line that is not in it does not exist
yet. When building a screen turns up a state nobody drew, the honest move is to leave the words
out and record the hole here, not to write a plausible sentence that no inventory owns and no
one reviewed.

Each row is addressed to a person, because a gap with no owner is a gap nobody closes. The
owners are roles and not repositories: **Voice** writes the line, **IA** decides the structure
behind it, **the design system** owns a class that does not exist, **the founder** decides what
the product does, and **this repository** means it is waiting on a service, a deploy or a
screen that is not built.

**How a row is closed, rewritten 2026-09-02.** The line is written **here**, in
`docs/spec/microcopy.md`, against `docs/spec/voice.md`, and only then goes on a screen. It used
to be written in the design repository and copied in; that repository is frozen and this one
owns its own words now.

**The friction that protected this file is gone, so the rule has to hold on its own.** Closing
a gap used to mean opening another repository, and thirty-two rows below are what that cost
bought. Adding a row to `microcopy.md` is now four keystrokes away from typing the sentence
straight onto the screen, and only the first of those is legal.

| # | Where | What is missing | What was done instead | Owner |
|---|---|---|---|---|
| G1 | `/sign-in`, a send that fails | **CLOSED 2026-09-27.** No screen and no line anywhere for "the link could not be sent". `sign-in` shipped with `sent` and `expired` and nothing else | The founder met the silence himself: a resend that looked ready did nothing. The line is now "We could not send a link just now. Try again a little later.", written to voice.md's error rule and owned in microcopy.md; when the provider says how long is left, the resend button counts it down instead (N5). The refusal code is logged, never the address | Closed |
| G2 | `/reveal`, step 1 | The owned line is "Found in your Chase account. No total yet, and no verdict." The first sentence names a bank, and there is no edition of it for a list somebody typed by hand | Only the second sentence is printed. The source clause is absent rather than guessed | Voice |
| G3 | `/`, the trust line | ~~The bank edition cannot be composed until a provider is wired~~ **CLOSED 2026-09-28.** Plaid landed on 2026-08-27 and both editions composed from owned halves; the THIRD edition D-Gate needs, a bank that read once and reads nothing now, was owned on 2026-09-02 and printed by nobody, because nothing removed a free Item until P20 | "Found in Chase on Aug 30. Nothing is being read now, and the list is yours to keep up to date." is printed whenever no bank is live and a stopped one still has rows on the list, dated by its last read. `check:bank` asserts it on a real sandbox scan | Closed |
| G4 | `/`, the trust line | "Last updated by you, today, 9:14 AM." is owned for TODAY. There is no published form for a timestamp that is not today | The line is not printed at all yet. A freshness line that can only say one word is worse than none | Voice |
| G5 | Every row, a cycle that is not monthly | Every fixture in the design is monthly, so "a month" is the only published phrasing. A yearly subscription's accessible name has no owned form | `spokenAmount()` reuses the Billing frequency labels, lower-cased, because those strings do have an owner. It is a reuse and not a new line, but it was never reviewed as copy | Voice |
| G6 | `/reveal`, step 2 | The owned line spells the count as a word: "Five categories, and the real names behind the charges." | Rendered with a numeral, matching how the summary strip renders its own count ("13", "3", "1"). A one-character judgement, recorded because it is a judgement | Voice |
| G7 | Home, grouping | A subscription typed by hand with no service has no category, and the design has no group for that case: every fixture came from the catalogue | Such rows group under "Other", a word that appears nowhere in the inventory | Voice, then IA |
| G8 | `/subscription/[id]`, a row typed by hand | **Every fixture of node 2.7 came from a bank.** "Charged to", "Paid so far", the recent charges, the removal sentence ("This one came from Chase, so removing it hides the row") and the trust line ("From Chase, read-only") all have owned text for a Chase row and no edition for a typed one | Each is omitted when there is no bank behind the row. A fact with no value is not a fact | Voice |
| G9 | `/subscription/[id]`, a price that went DOWN | The owned line is "Netflix went up by $2.50 on Jul 28." There is no form for a decrease, and the product will detect one | The banner is drawn only when the new amount is higher. A price drop currently shows no banner at all | Voice |
| G10 | `/subscription/[id]`, the failed-payment line | The owned line shortens the merchant: "A payment to **Amazon Prime** did not go through on Jul 20. **Amazon** usually tries again..." A short name cannot be derived from a full one without guessing | The full subscription name is used in both slots. One word off an owned line, and it is a substitution rather than a new sentence | Voice |
| G11 | `/subscription/[id]`, the Pro gate | "Maybe later" is an owned button with no defined behaviour: nothing says whether dismissing the gate lasts for a session, for a device, or for the account | The button is not drawn. It cannot render today anyway, because the gate needs charges and only a bank feed produces them | Behaviour, then the founder |
| G13 | `/settings`, Email | Changing the address on an account whose only key IS the address means confirming the new one and holding the old until it answers. That flow has no screen and no line in the design | The field is shown disabled. A control that takes a change and drops it is worse than one that says it cannot | Voice, then the founder |
| G14 | `/settings`, "Sign out" and `/subscription/[id]`, "Remove from your list" | **A visual deviation, twice, for the same reason.** Both must be a POST: a GET that ends a session or deletes a row is followed by prefetchers and tab restores. `.navrow` and `.muted a` were written for anchors and reset none of a button's own chrome, so a button wearing either renders as a grey box | Both use `.btn`, a defined component that renders correctly. The system has no quiet submit, and inventing a class would break its growth rule | The design system |
| G12 | `/subscription/[id]`, "Remove from your list" | **A visual deviation, recorded rather than smuggled.** The design draws a quiet link; removal must be a POST, because a GET that deletes is followed by prefetchers and tab restores. `.muted a` is styled by the system and a link-shaped button is not | The control is an outline `.btn`. Inventing a class would break the system's growth rule, and leaving the control out would strand anybody who typed a subscription by mistake | The design system, if it wants a quiet submit |
| G16 | `lib/chart.ts` | The chart geometry exists twice: in `system/behaviour.js`, which only a browser runs, and here, because a server-rendered page has to arrive carrying a line | The duplication is CHECKED: `lib/chart.test.ts` asserts the exact path the design published. The system should own one copy that both can read | The design system |
| G17 | `/trends`, the y axis | The design chose `data-scale="160 200"` for a flat $192.90 and states no rule for choosing a band | A rule is written in `scaleFor()` and reproduces that pair: the step is the smallest round number at least a tenth of the top, and the floor is two steps below it. Stated rather than fitted | The design system |
| G18 | `/trends`, the Pro view | **CLOSED 2026-09-29, by P25.** `history-trends.html` with its three ranges and trend list was not built, because nobody could be on Pro. Stripe made it reachable and the founder, on Pro, met the lock | `app/trends/ProView.tsx`: the total, the sentence per range, the chart, What moved, By category and the CSV export, every figure derived by `lib/trends.ts` from the list (P3). Free is unchanged | Closed |
| G19 | `/trends`, the reading | "the same as it was in June" is owned. A rise or a fall between two months has no owned form; the Pro screen says "up about $20 across three months" over a longer window | "up about $X from June" and "down about $X from June", reusing the Pro screen's own words with its window clause replaced | Voice |
| G20 | `/trends`, still gathering | The owned line says "we have less than three so far", which is written for a Pro person waiting on three months. A free plan needs two | Printed unchanged. It is the only owned text for the state, and rewording it here would fork the line | Voice |
| G21 | Every screen, a bank-reported cycle | Plaid reports weekly, biweekly and semi-monthly. The Billing frequency select offers Monthly, Every 4 weeks, Quarterly, Yearly, Custom, and neither set contains the other | The DATA keeps the exact cycle, so the arithmetic is right; the LABEL prints the owned "Custom". Semi-monthly through a fifteen-day interval would have been 1.5% wrong on the monthly total | Voice |
| G22 | `/connect-bank`, the email field | On the static prototype this screen creates the account, because the bank path had nowhere else to do it. Here a session already exists before anybody reaches the route | The field is not drawn. It would be asking for something already held | IA, then Voice |
| G23 | `/connect-bank`, the wait | **CLOSED 2026-09-29, by P28, on every deployed address.** `bank-connection.md` section 7 says the product does not poll and syncs on the `SYNC_UPDATES_AVAILABLE` webhook, and a webhook needed a public URL a development machine does not have | `/api/plaid/webhook` verifies Plaid's JWT and reads the Item again on the transactions events. Every Item made from a link token on an https address reports there; dev.tendd.co is reached through Vercel's bypass. On localhost there is still no webhook, and the connect-time read still waits thirty seconds, which is the one moment somebody is watching. `check:plaid-webhook` walks it | Closed |
| G24 | Home and the reveal, a charge with no merchant | A recurring charge Plaid cannot name ("AUTOMATIC PAYMENT - THANK") is marked "Not identified" and left out of the total, because it is usually a transfer or a card payment rather than a subscription. The design never drew what a person sees when a bank feed yields only those | Nothing pretends: the rows exist and say what they are, the total counts what it can name. On the Plaid sandbox that means a reveal reading two subscriptions over seven rows | Behaviour, then the founder |
| G25 | `/connect-bank`, the title body | **CLOSED 2026-09-28.** "Next you'll pick your bank and sign in on your bank's own site." was false: Plaid shows its own returning-user screen first, a phone number, a code by text and "Continue as guest", on by default for every customer and with no parameter to refuse it | The line now says it before the button: "Next, Plaid opens. It may first ask for your phone number, to remember you in other apps that use Plaid; you can skip that with Continue as guest. Then you pick your bank..." Written to Principle 4 in `microcopy.md`. If Plaid ever lets us switch the screen off, the clause goes with it | Closed |
| G26 | Every screen, grouping | **The design draws five categories and the world has more.** Streaming, Software, News, Fitness, Music. A gaming pass, a food delivery membership, a retail membership and a meal kit are ordinary recurring charges with no group to sit in, so the catalogue cannot hold them and a bank feed that reports one has nowhere to put it. Related to G7, which is the same hole reached from the manual side | `0007_service_catalogue.sql` adds 97 services and every one of them fits the five. The rest are left out rather than filed under a category invented here, because the groups are printed on Home and a sixth is an IA decision | IA, then Voice |
| G27 | Onboarding, the whole of it | **CLOSED 2026-09-02 by D-Gate.** The founder decided on 2026-08-29 that the bank is Pro and chose the shape on 2026-09-02: one free scan returning the count, the total and three names, the rest drawn and withheld. The three places the frozen design contradicted are answered, and the fourth turned out not to be a contradiction: Emma can still carry a Free chip beside a bank, because a free person really does get one scan | The decision is in `docs/decisions.md`, the reversed rules are in `CLAUDE.md`, and the lines exist in `docs/spec/microcopy.md`: node 1.2's promise rewritten, reveal step 2 cut to three, the locked list on both screens, the post-scan trust line, and two cards on the upgrade screen. **What is still not built is the locked-list component and the screens that wear it** | Closed. The build is G33 |
| G28 | `/data-privacy/delete`, the consequence | **An owned sentence written for one person.** "This deletes your 14 subscriptions, both of your sources, and your account", and under it "deleted, including the 3 you typed" and "disconnected at Chase, and the read access ends". Every figure is Emma's. "both of your sources" is a phrase for exactly two and there is no published form for one or for three, and no form exists for a person with nothing on their list | The sentence is composed from its own clauses and each appears only when it is true: the sources clause at exactly two, the subscriptions clause above zero, the bank pair only where a bank is connected. Nothing is reworded and no clause is invented. Same move as G2, one clause lower | Voice |
| G29 | `/connections`, every timestamp | **The product has no idea what time it is where you are.** "today, 9:14 AM" and "Jul 29, 8:02 AM" are published forms, and both need a zone to be true. Node 6.16 draws an email and a currency and nothing else, so the profile holds no timezone and nothing on any screen asks for one | The runtime's own zone, stated in `lib/when.ts`. D5 puts the product in the US first, so one zone is a smaller lie than rendering a bank check at 9:14 AM UTC and calling it somebody's morning. It becomes wrong the day the second region lands, which is the day the column is needed anyway | IA, then the founder |
| G30 | `/data-privacy`, the policy | **CLOSED 2026-09-20.** "The full privacy policy is at tendd.com/privacy." was owned, and the design linked it to `href="#"`. There was no policy, no page and no address that resolved, so the line printed a URL a person would have had to retype into a browser to reach a 404 | `/privacy` is built and public, the line became "Read the full privacy policy" and links to it, and the two screens now name each other at the foot. P12 | Closed |
| G31 | `/connections`, the reconnect state | **CLOSED 2026-09-29, by P28, both halves on one day as this row asked.** Nothing set `reconnect_needed`, and "Reconnect Chase" led to the ordinary connect route, which would have made a second Item | ITEM_LOGIN_REQUIRED, PENDING_EXPIRATION and PENDING_DISCONNECT set it; LOGIN_REPAIRED clears it and reads the bank. The button opens Link in update mode on the same Item (`ReconnectButton`), and `/api/plaid/repaired` finishes it while the person is watching. Measured on dev.tendd.co: the chip, the enabled button, and the way back to connected | Closed |
| G32 | `/help`, all of it | **CLOSED 2026-09-30, by P36.** A screen with a link and no design | `/help` answers fourteen questions in six groups, each from a line the product already says (the landing FAQ, the terms, the privacy page) or from where a thing is on screen, and says how to reach a person. Public, like the terms | Closed |
| G33 | The locked list, on the two screens that need it | **CLOSED 2026-09-02.** The reveal names three at step 2 and Home draws the slot where a name is withheld, both driven by `lib/gate.ts` and the `plan` column. What is newly open is nothing: the two side doors were closed with it, and a withheld row answers 404 on its detail and on its cancel guide rather than printing the merchant, the raw bank line and the decoder | Measured on a real sandbox scan, not on a fixture: 28 checks in `npm run check:plan`, and the one that matters is an absence, that no withheld name appears anywhere in the served html of either screen | Closed |
| G34 | `/upgrade`, the two priced buttons | **HALF CLOSED 2026-09-28, by P24.** Node 5.13 draws "Start Tendd Pro - $69 a year" and "$7.99 a month" and there was no payment provider | Where `STRIPE_SECRET_KEY` is set the two buttons are drawn and lead to Stripe Checkout; where it is not, the owned "Not open yet. Tendd cannot take a payment, so these have no button." still stands and is still true. Test keys only, on tendd-dev's addresses; the live site keeps the sentence until G54 | This repository, then G54 |
| G35 | `/upgrade`, the Pro shape | **CLOSED 2026-09-28, by P24.** Node 5.13.3 prints "Tendd Pro, yearly" and "Renews 1 May 2027, at $69", and nothing stored either | The Stripe webhook writes the interval, the amount and the period end from a fresh read of the subscription, and the screen prints them with the consequence sentence under them. A plan set by hand (the instruments) still has neither and prints neither | Closed |
| G36 | The reveal and Home, a scan with no categories | The Plaid sandbox returns seven recurring streams and most match nothing in the catalogue, so they carry no category at all. The design never drew a reveal whose categories are mostly absent, and "Three of the names are below" is a promise the screen makes out loud | Three is kept as the promise and the category spread demoted to a preference: `lib/gate.ts` names one row per new category first, then fills to three from what is left. Without the second pass an uncategorised scan would have printed that sentence over an empty list. Related to G26, which is the same hole from the catalogue side | Behaviour, then the founder |
| G37 | `/path-choice`, node 1.2 | **CLOSED 2026-09-03.** The route is built, and it is the one screen besides sign-in that a stranger may open. What was newly open is a question the design never had to answer: on the prototype the doors led straight to the bank and the form, and here both need a session (G22), so a door that linked to them would have thrown the choice away at the bounce | The choice is carried across the mail round trip as a token and not a URL, P9, and the two side questions were measured rather than assumed: a forged cookie names nothing, and a link opened on a second device lands on the empty list, which offers both doors in the same words this screen used. 30 checks in `npm run check:path`, and the width sweep gained a twentieth route | Closed |
| G38 | `welcome`, `/path-choice`, `/sign-in`, everything that linked to the terms | **CLOSED 2026-09-20.** Node 1.2 ended "By starting you agree to our Terms and Privacy Policy" and the landing said it three more ways, and all of them pointed at `/data-privacy`, which guards itself. **A stranger asked to agree to two documents was answered with a sign-in form**, on the screens whose whole job is to be trusted by somebody who has not decided yet | Two public documents, `/terms` and `/privacy`, written against `voice.md` with every claim read out of the code first, and six links repointed on one rule: a public screen links to a public document, an in-app screen links to the in-app screen. `/data-privacy` keeps the controls and loses the prose. P12, and 1918 checks in the width sweep across 23 routes | Closed |
| G39 | `/sign-in`, arriving without an account | **CLOSED 2026-09-27 by P17, recorded 2026-09-28.** "Start here" led to node 1.2 and node 1.2 led back to a screen reading "Sign in" and "Type the email you used", words for somebody who already has an account | The panel's first step says "New to Tendd? Either way starts your account.", and "Type the email you used" is gone from the product and from `microcopy.md`. The heading still reads "Sign in" for both people, which is one word serving two and the founder's choice in P17 | Closed |
| G40 | `welcome`, the footer's Company column | **CLOSED 2026-09-30, by P36.** "About", "Contact" and "Careers" pointed at `#` | `/about` and `/contact` exist and the footer links them; "Careers" became "Help", because there are no jobs and a page saying so would be a link for the sake of a column | Closed |
| G41 | `/sign-in`, the second way in | **CLOSED 2026-09-20.** Node 1.6 was drawn as an email and a link, and the founder could not get in on 2026-09-08, because a link has to survive a mail client, a redirect allow list and whichever device the mail was opened on. The one step nobody here could take was Google's: **an OAuth client for a web application exists only through the Console**, since the consent screen is a legal statement about the company | Built on both sides and left waiting, then switched on by the founder and walked end to end on production. The door travels through OAuth as the same token it rides through the mail, the button is drawn only when Supabase says the provider exists, and `npm run check:origin` asserts that the screen and the provider agree in both directions: 10 checks, 0 failing. P11. Going past Google's Testing mode, where only listed accounts may sign in, still needs a privacy policy and terms, which is G38 and G40 | Closed |
| G42 | The production database | **CLOSED 2026-09-28, on the founder's choice of the three answers.** Every way into Tendd died on 2026-09-20 and nothing said so: a free Supabase project pauses itself after a week with no traffic, loses its hostname (NXDOMAIN), and the site keeps serving pages while nobody can sign in. Found only because `check:origin` was run for an unrelated reason, and fixed by the founder pressing Restore | A scheduled probe, not a paid plan and not an accepted risk. `.github/workflows/keepalive.yml` runs `tools/keepalive.cjs` every day at 06:17 UTC against BOTH projects, as a stranger with the public key: one real query each, answered with an empty list by row level security. A paused or unreachable project fails the run and GitHub mails the owner, so the watch arrived with the fix. It runs from `main` only, so it starts with the merge. `npm run keepalive` asks the same question from a laptop | Closed |
| G43 | `/terms` and `/privacy`, the closing section | **HALF CLOSED 2026-09-23.** A privacy policy names who is answerable and gives an address, and both are facts about a person and a company rather than about this product. **The address exists now**: the founder bought `tendd.co`, and `privacy@tendd.co` forwards to a person through Cloudflare, so both documents end by naming it. **The entity does not.** The founder's answer on 2026-09-20 was that it is him as a private person today and will be a company abroad later, and he asked for that to be written down so the change is not missed | Both pages say "Tendd is run by one person and not by a company. There is no business registered behind it yet, and when there is, this page will name it", which is true, checkable and refuses to imply an office. The founder's own name is not printed, because publishing a private person's legal name is his decision and he has not made it. **Google needs a named entity to publish the app out of Testing mode**, so this is also what keeps Google sign-in limited to listed accounts **2026-09-28: the founder named the shape.** A Wyoming LLC. That fixes the country everything else is filed under: the Stripe account is United States (it cannot be changed later), and the same LLC is what the Plaid production application (G45) and Google's app verification will name | The founder: the entity, and whether to print his own name meanwhile |
| G44 | The database, all three environments | **CLOSED 2026-09-25.** There was one Supabase project and it served local, preview and production alike, so every instrument run on this laptop wrote into the database the public site reads from, `npm run demo` rebuilt a person there, and a migration tried locally was a migration applied to production. **Now:** a second project, `tendd-dev`, in the same region, carries all eight migrations and the seed, and passed `check:rls` (19), `check:screens` (1918), `check:flow`, `check:bank`, `check:plan` (28) and `check:path` (27) on its first day. `.env.local` and every Vercel preview read it; the live project is in `.env.prod` and only three instruments open it; the CLI is linked to it; `check:origin` now fails if a preview or a laptop reads the live database, or if the live project would send a link to either. P15 | The live project's allow list now holds only `tendd.co` and `tendd-theta.vercel.app`, `dev.tendd.co` answers on the `dev` branch behind Vercel's own sign-in, and `check:origin` passes 15 checks with the wall included. Measured the same day: `tendd-dev` would send a link to `dev.tendd.co`, the live project refuses to | Closed |
| G45 | The bank connection, in production | **The main feature of the product does not work with a real bank yet.** Plaid runs in Sandbox, which is a set of fake institutions with fake credentials: `PLAID_ENV=sandbox` locally, and production's copy of the variable is hidden in Vercel but cannot be anything else, because **Plaid Production access is granted to an approved application** and an application names a company, a policy and a use case. G43 is the company. So the first bank scan, which is the whole of node 1.3 and the reason node 1.5 exists, can be demonstrated and cannot be sold | Sandbox is the correct setting for today and `npm run check:bank` walks a real sandbox institution end to end through the route a browser uses, 14 checks, so what is unproven is the credential and not the code. What has to happen in order: the entity, then the Plaid application, then a production key, then one real bank connected by a real person | The founder: a Plaid application, once there is a company |
| G46 | Every sign-in email | **CLOSED 2026-09-24. The founder signed in on his phone twice from the new letter, the second time through the link that goes to our own callback and not through Supabase's host.** The magic link was sent by Supabase's own shared sender, rate limited to a couple of messages an hour and which Supabase itself says is not for production use: invite three people on one evening and the third does not get a link, while the screen reports that the mail was sent. It came from an address the recipient had no reason to trust, in Supabase's default words ("Magic Link"), which nobody here wrote. **Now:** Supabase sends through Resend as `Tendd <signin@tendd.co>`, at most 30 an hour; `tendd.co` carries DKIM, SPF on `send.` and a DMARC record; both letters are the owned `sign-in-mail` rows, pushed from `supabase/templates/sign-in.html`; `/privacy` names Resend, and Cloudflare for letters written to `privacy@`. Every address on `tendd.co` forwards to the founder's projects inbox, and replies leave as `privacy@tendd.co` through the same sender. `npm run check:mail`: 52 checks, 0 failing. P14 | Closed | **What is left:** Resend's free plan is 100 letters a day and one domain, which is the next ceiling, and `check:mail` needs a Supabase personal token that expires; without one it checks the words and says it could not check the sender |
| G47 | `/`, the story section, released | **CLOSED 2026-09-25, the same day it was opened.** The landing's story section stops animating for a visitor who asks for less motion and in a window under 680px tall, and both releases were half written in four places, each of them text printed over text at full opacity: the promise and the section heading in one cell (a friend's phone, in a messenger's browser); past 60rem the heading lifted out absolutely over the h1; "14 subscriptions" across the heading's lead (the founder's own screen, 1000 x 511); and the three reason cards stacked in one cell at every width, which had been live all along. Each release now takes every block out of its shared cell and its stage offsets: the list drops its `top: 120px`, the two figures become one row under the list, the cards a row that wraps | `check:screens` asks two new questions of every screen: nothing visible is printed over the h1, and on the landing no two visible text blocks overlap at all. Run against the live site before the fix, the second one reports the stacked cards at all five widths it was asked at; after it, 2562 checks, 0 failing | Closed |
| G48 | Home, the first request after signing in | **CLOSED 2026-09-26.** The live product answered 500 on `/` once, at 15:20 on 2026-09-25, straight after a sign-in.** Vercel's log: `PGRST303, JWT issued at future`. The session Supabase had just issued carried an issue time a fraction of a second ahead of the clock of the database API that read it, so the first read of the list was refused and the page failed. `check:prod` caught it on its signed-in walk and passed on the next run with nothing changed, so it is a race and not a state. A person who opens a sign-in link and lands on an error page does not know it was a race | Hosted Supabase has no setting for the skew, so the server's client retries that one refusal and nothing else: `lib/skew-fetch.ts`, wired in `lib/supabase/server.ts`. Only a 401 carrying PGRST303, only against the database API, twice, after 500ms and 1000ms; any other refusal comes back as it came. Five cases in `npm test`, and the signed-in width sweep and `check:prod` pass through it | Closed |
| G49 | `/`, the story section and the footer | **The public page shows fourteen other companies' marks, and nobody who knows the law has looked.** From 2026-09-26 the tiles are the services' own published logos (Simple Icons, CC0 for the drawing only; Disney+ and The Economist from Wikimedia Commons), and the landing animates them as decoration: they drift, get swallowed and are cancelled. Since Light 2 (P16, the same day) they are also the largest objects in the hero, fourteen 3D tiles floating beside the promise, which makes the partner reading easier, not harder. In a person's own list a logo names the service they pay for, which is the ordinary nominative use every subscription tracker makes; on a marketing page the same logo can read as a partner. CC0 covers the drawing and grants nothing on the trademark | The footer now says it: "Tendd is not affiliated with the services shown. Their names and logos belong to their owners." (microcopy, footer-bar). The marks are one file each in `public/logos`, so any one of them can be replaced with a neutral tile in minutes if an owner asks | A US lawyer, in the same package as the Terms review: is the footer line enough, and may the marks stay in the animation, or only in a person's own list |
| G50 | every page, the Content-Security-Policy | **CLOSED 2026-09-28.** The policy only reported (P18). Enforcing it needed a nonce for the two inline scripts that must run before paint, the theme in `app/layout.tsx` and the landing's curtain, and a proxy that writes one per request; without it `'unsafe-inline'` stayed in `script-src` and the policy stopped little | Enforced, P22. `proxy.ts` writes a fresh nonce and the policy from `lib/csp.ts` on every request, Next puts the nonce on its own scripts and both of ours wear it. Walked on a production build and on dev before the commit: every public screen, the sign-in dialog with Turnstile, Google's redirect to accounts.google.com, every signed-in screen with the dark theme chosen, and Plaid Link opened for a person who had not scanned. 0 violations, and every instrument passes on the build | Closed |
| G51 | `/connect-bank`, a free scan that is not ready | **CLOSED 2026-09-29, by P28, on deployed addresses.** Plaid answers PRODUCT_NOT_READY for the first minutes of an Item's life; a free Item still not ready after thirty seconds stayed alive with nothing reading it | The Item now reports to the webhook, and the first transactions event finishes the read and then removes the Item exactly as every free scan ends (P20). Not walked end to end: the sandbox cannot be made to hold PRODUCT_NOT_READY on demand. On localhost the old behaviour stands | Closed |
| G52 | `/connections`, after the free scan | **CLOSED 2026-09-28, the day it was opened.** Node 6.14 drew no card for a disconnected source, so under P20 a person whose whole list came from one scan read "No sources yet. Add one and your subscriptions appear on your list." over a list a bank filled | A stopped bank is a card while it has rows on the list: the chip "Disconnected", the last check, the count, and the Home trust line's third edition reused whole. One new word, owned in `microcopy.md`. With no rows left it is still not drawn, which keeps the design's own flow where it is true | Closed |
| G53 | D-Gate against D-Export, and against the person's own key | **CLOSED 2026-09-29, by P31 with (b), and whole since P32.** A former Pro kept names the screen hid; since P32 a stored name is never hidden, so nothing hidden is stored anywhere, and paying reads the held scan so no name is lost either. The names a free scan withheld were stored in the person's own rows, so "Download everything" and the person's own session key returned every one, and the gate stood on the screen and not in the data | A free scan now keeps no name for a row it does not show: no name, no service, no bank line, only the figures the total needs (migration 0012). A Pro read of the same bank names them again, matched by cadence, category and a close amount, one candidate or none, and a placeholder no stream claims is removed so nothing counts twice. Home and the reveal draw the named rows and one block for the rest, with its count, its amount and two ways on (the founder's shape). `check:bank` proves the rows keep nothing at the scan and get everything back on Pro; `check:plan` proves it on the database before the html. Not closed for someone who was Pro and is Free again: they saw the names and keep them, and the screen still withholds them | Closed |
| G54 | Taking real money | **Stripe is built and runs in test mode only (P24).** **NARROWED 2026-09-29 by P35: the terms now say how paying works, the refund policy is written (14 days, full, for a mistaken charge), and `/privacy` names Stripe and what it sees.** What is left is the business and the money question. The original four, for the record: the terms promise "these terms will say how paying works before you are ever asked for money" and do not yet (the price, the renewal, how to cancel, what happens to a refund); a refund policy is a decision nobody has made; `/privacy` must name Stripe and say what it sees (the card and the billing email, on Stripe's own page); a live Stripe account needs the business behind it, which is G43 (a Wyoming LLC); and one money question for an accountant: whether Stripe is the merchant of record (Managed Payments, +3.5% a charge, Stripe owes the sales tax and VAT) or Tendd is (Stripe Tax at about 0.5%, or neither while US sales stay under the states' thresholds). The sandbox was set up with neither, and it can be switched on later | Nothing live. `/terms` still says Tendd Pro is not on sale, and on the live site that is true, because the live site has no key and draws no button | The founder: the refund policy and the entity. Then this repository writes the terms and the privacy rows |
| G55 | `/upgrade`, the renewal-failed state | Node 5.13 draws "Tendd Pro did not renew" with two dates, the failed renewal and the end of the grace period. Stripe's retry schedule decides the second one, and the subscription does not carry it | A person whose renewal failed (`past_due`) sees the ordinary Pro screen with "Update your payment method" as its button, leading to Stripe's portal. The dated screen is not drawn, because its dates would be guesses | This repository, with the invoice's next retry date |
| G56 | Tendd Pro, what it actually opens | **NARROWED 2026-09-29, by P30: one of six is left, and it is being built.** Node 5.13 lists six things in Pro. Four were built (every name in the scan, a bank that keeps the list current since P28, history and trends and export since P25). The founder chose to make the other two true rather than cut them, and without new access to anybody's accounts: "full cancel guides" is now the free steps plus, on Pro, a direct link to the service's own cancel page (11 services, each read off the service's own help page), and the promises nobody could keep (screenshots, "skips the retention screens", unusual and duplicate charges) are gone from every screen | **What is left: advanced alerts**, narrowed to "a trial ending, and a charge coming up", which are the email letters being built as P29. Until they send, that one line on the Pro list is ahead of the product. The eleven links are also a list that rots: each carries the date it was read, and a service with no published cancel page has no link and draws no Pro panel | This repository, with P29 |
