What changed lately
Every change to Bullet or Signal that moves a number, changes what a customer sees, or changes how we work, newest first. This list is the change log (docs/changes.json), the same one the reports read: the monthly report shows a change under its month, the monthly email lists that month's changes, and the Customer read's write-up names a change when a number moved after it.
Every number in Signal has a tooltip: what it is, where it comes from, how to read it and what it compares with (Peter, 6 Oct; the standard is docs/TOOLTIPS.md).
| Reading the change log… |
The living-docs agent: this page keeps itself up to date
Peter, 5 Oct 2026: Signal is a live system. Every morning at 7 a Claude agent on Peter's Mac reads what changed in the last day: the code in Signal, the web front end and the old app (John's work), the to-do items ticked off, and the Signal tasks marked done. For each change that matters it:
- adds it to the change log above, with the date it went live and the number to watch;
- updates this page, so the words match what the system does;
- records a decision (Tools, Decisions) saying what changed and why;
- flags any known answer on the support desk that the change may have made wrong, as Needs a check on the Answers page, with a suggested wording. It never changes an answer a customer reads by itself: Mariana fixes it and Save clears the flag;
- sends a note to the web front end when a customer walkthrough (Tools, Walkthroughs) no longer matches the app, since the guides live there;
- updates the contract files John works from when a change alters one, and adds a to-do for John when he has something to do;
- puts Signal live, and leaves a one-line summary of what it changed in Signal's Tasks inbox.
If the Mac is asleep at 7, it runs when the Claude app next opens. Ticking the to-do line of a Tasks item marked done also runs by itself every hour, on the hour (a Vercel cron, since 5 Oct; hourly since 8 Oct), so it does not wait for the morning run.
Its notes are its own (5 Nov 2026). What the agent sends to a project's chat is marked as the agent's, never Peter's: it goes to a Claude chat only, never to John's Codex; it opens by saying the agent sent it and asking the chat to check with Peter before acting on it; and its words sit under their own heading. Only a person signed in to Signal writes Peter's notes on a task.
Today's check: is this page still true?
This product changes every week. The lines below are not typed in. They are read from the running system every time the page opens, and the daily run at 9am records the same check. If a line here disagrees with the words on this page, the words are out of date and the developer is told (the rule in the repo: any change to what runs by itself updates this page in the same commit).
| Reading the system… |
The numbers check, every morning
Every morning at 06:15 UTC (07:15 Irish summer time, 06:15 in winter) Signal checks its own numbers on the real rows, before anyone reads them (api/_lib/number-audit.js):
- Fresh: Stripe was read in full in the last 26 hours (last night's read finished), Google today, and Revenue's manuals in the last 2 days.
- Not far too small (2 Nov): Search Console has at least 100 clicks and Google Analytics at least 300 sessions in the last 28 settled days, and neither fell under a third of the 28 days before. A feed can be fresh and still read the wrong site: until 2 Nov Search Console read bullethq.com, which only redirects to bullethq.ie.
- Adds up: for every Irish month since the first customer, paying at the start plus started paying plus came back, less stopped paying, is paying at the end; chose to leave plus cards failed is customers lost; and monthly recurring revenue is each paying customer's own amount added up.
- No test accounts: no bullethq.com or bullethq.ie address, plus-address or demo row is behind any number.
- The same number everywhere: the Overview, the Monthly tab, last month's monthly email, the Ask bar's business numbers and the scorecard give the same figure for every number they share.
- The written words: Check this out, the marketing brief and the customer read carry at most two figures in a headline, and every figure is in the data they were written from.
The result is kept for 90 days and shows in Today's check above. When anything is red Peter gets an email saying which number, what disagrees and where, and one item goes in Signal's own Tasks inbox (at most one a day) for a chat to find the cause. The Overview shows the latest result at the top: green when all agree, red naming what disagrees. While the check is red, or missing, or older than 26 hours, the monthly email on the 1st, Monday's Check this out and the marketing brief are held back: nothing is written or sent until the check is green again (for the monthly email Peter gets a short note saying it was held back). An admin can run it again at any time (POST /api/number-audit?run=1).
The second brain: known answers
Every reply the desk sends is read once by the AI and judged: is this an answer another customer could ask for? A reusable one joins Answers by itself. A repeat of one already there counts as a use. A "thanks" or "let me check" is skipped. Nothing to tick. One brain (25 Sep): the library is the only place answers live. An answer ticked "Customers can see it" is served to the web app's Ask through /api/answers?public=1, with its other ways people ask it; staff answers never leave Signal.
When a new customer message lands, the AI looks in Answers for ONE that answers it. If it finds a match with good confidence, the desk shows Known answer with a draft and "Use this draft". If it does not, the desk says No known answer yet. The AI never writes an answer of its own. Every number and link in a draft must already be in the saved answer, or the saved answer goes in exactly as it was.
- Not right hides a draft on that ticket. The answer stays in the library.
- The Answers page (left menu, Customers) lets you edit, retire or write one by hand.
- A reply built on the accountant's answer carries the Around Finance line, added when it is sent.
UX tests by email
A new UX test that the web front end chat drops in Signal goes live each morning at 08:41 and is emailed to Peter and Mariana, each with their own recorded link. A test dated 22 Oct or later is offered and emailed only after Signal's own AI has passed a screenshot of every screen in it against the app's design rules. A test on hold (built but not live yet, or for one person first) is not emailed by itself; it waits until the chat that built it says it is live. The test opens in Signal when the link is opened. (Separately: at 08:30 each morning, when a chat has logged a new change for Peter to approve, he gets one email with the titles and a link to the Changes page; the same email lists any new answers to his questions on a change. A change reaches Peter only after Signal's own AI has checked a screenshot of every step of its demo against the caption and the app's design rules; a new version without a pass is refused. A question or an edit on a change card comes only from a person signed in to Signal, and a task on Peter's Mac passes those on to the card's chat every 10 minutes; a chat's own note is never passed on as his.) When the tester has been quiet for 15 minutes it counts as done. Within about 10 minutes after that, Signal turns the voice into text, writes the review, and emails Peter: the link to the review, the top things to fix, and, for our own testers, the changes they asked for out loud. In the review Peter ticks those, picks the chat, and sends them as one task or copies them as a prompt. If pieces of the voice failed and the voice text came out empty, the review and the email wait until the voice is read (the voice text says why), or until someone presses Review this session.
The tax brain: Revenue's manuals, the accountant, and what Bullet learns
Peter, 7 Oct: move away from asking the accountant the same tax questions. Three parts, all running by themselves.
- Revenue's manuals, read into Signal. Every current Tax and Duty Manual on revenue.ie (about 1,220 PDFs, free to reuse with credit) is read into text and indexed. Each day at 03:15 Signal walks Revenue's list of manuals; every hour, on the hour, it reads new ones and asks Revenue whether any it holds have changed (a weekly check per manual). A changed manual is read again. Ask Revenue (Tools) searches them and answers with every point linked to the page.
- Revenue's text goes with each question to the accountant. When the desk asks the accountant something, Signal looks it up in the manuals once (hourly,
/api/revenue?cron=asks) and keeps what Revenue says on the question. Her daily email and the desk show it under the question, with two choices: Yes, that is how I would handle it, or No, I would put it my way (the real-world view: what a small business actually does, which Revenue's text never says). - Bullet learns. Her answer, yes or her own words, is saved as a tax answer in Answers, with Revenue's pages as its references. Next time the same question comes up, Ask Revenue and the desk's draft show her answer first. When Revenue changes a manual one of her answers cites (after a Finance Act, say), that answer is marked "Needs a check" and goes back to her.
For customers too. The app's Ask box can send a tax question to /api/revenue?public=1 (20 a day per address). The answer always starts "Revenue's guidance says", names the manual, links the page, and carries the line "Revenue's published guidance, not advice for your situation", with the chat to the accountant as the way out. A plain question is first put into Revenue's own words by a small model (search words only, never an answer).
Guidance pages too (7 Oct, evening). Ask Revenue also reads about 790 of Revenue's guidance pages for small businesses (VAT, the self-employed, employing people, starting a business, corporation tax, pensions and CGT reliefs). It walks the pages every night and reads a page again when its text changes, and any approved answer that cites a changed page goes back to the accountant to check. Left out on purpose: customs, excise, VRT, local property tax, stamp duty, gift and inheritance tax.
Simple answers for customers (14 Oct). The customer's answer now leads with one line: Yes, No, It depends on one thing or I can't tell from Revenue's guidance, then one plain sentence. The app sends what Bullet already knows about the company (limited company or sole trader, VAT registered, has staff, kind of business, RCT) and the answer is tailored to it ("As a company with staff, ..."), so it does not ask what it knows. Only when the answer truly turns on something unknown does it return one question with two to four options to tap. Then one to three short steps (and the Bullet how-to when a saved answer has one). Revenue's own wording goes behind a link: each source carries a short quote, kept only if it is word for word in the passage. The fences: every fact comes from the passages or the approved answers; a sentence holding a figure that is not in them is dropped in code, and a one-line answer that loses its figure becomes "I can't tell". Nothing about the company is stored or logged.
Nothing here is advice: it is Revenue's text and the accountant's view, side by side. The code: api/_lib/revenue-index.js, api/revenue.js, learnTax in api/tickets.js.
The tax change watch: Budgets, Finance Acts, emergency bills
Peter, 7 Oct. Four times a day (06:20, 11:20, 16:20, 21:20) Signal reads where Ireland announces a tax change: Revenue's eBriefs (Revenue's notice of every new or changed manual, rate, form and date), the Oireachtas bills feed (every Finance Bill, emergency tax bill and Social Welfare bill, at every stage) and Revenue's press releases about rates and dates. Anything new is summarised from the notice text only (never a rate or date the notice does not give) and matched against docs/product-rates.json, the map of where Bullet holds each rate (the app's tax year files, company car rates, contractor tax, VAT rates, payroll's new year, the ROS limits). Peter gets one email: what changed, what Bullet has now, what should change and who changes it, plus what is coming up (the payroll new year, the Finance Act). It never changes a rate by itself: check with the accountant, then send the change to the project from Tools, Tasks. First run, 7 Oct: 4 changes, including Budget 2027's cut in capital gains tax to 31%.
Does this affect you? (14 Oct). For each relevant change the watch also keeps one plain sentence and one line on what to do, written from the notice's own text (a sentence with a figure the notice does not give is dropped in code). Who it affects is decided by rules, not the model: limited company or sole trader, VAT registered, has staff, and kinds of business the change names (food, construction, hairdressing, property, farming, transport). The app asks /api/law-watch?public=1&entity=&vat=&staff=§or= (open, cached for an hour) and gets only the changes from the last 90 days that apply to that company. An unknown answer never hides a change, except a change about a kind of business, which needs the company's sector to match. Nothing about the caller is stored or logged.
One Ask for everything
Ask now looks up the data itself (Peter, 22 Oct: "Signal reads all of Signal"). Every question the Ask bar does not answer from the page's own rows now goes first to Claude Opus 5.5 with 13 look-ups that only read: Stripe's monthly report, the scorecard against the industry, Google Analytics and Search Console, the support desk, one customer, the customer list, product health and the recorded visits, AI visibility, what Bullet does, the Changes log, the saved answers and Revenue's guidance, the Ask log and the change history. It picks what the question needs, reads the rows and answers from them, saying where each figure came from, and lists what it looked up under the answer. It answers on every page, so there is no hand-off to another page. Names, emails and the books never reach it, and anything a customer typed is scrubbed first. Before you see the answer, any sentence with a figure the look-ups did not return is taken out, and so is every dash. If it cannot answer in time, the routes below answer as before. The code: api/_lib/signal-brain.js.
Peter, 7 Oct. The Ask bar at the top of Signal answers three kinds of question. A small fast model only decides which kind; it never writes the answer.
- Tax ("can a company pay mileage at Civil Service rates?"): the tax brain answers, from Revenue's manuals and the accountant's approved answers, with the Revenue pages and a link to Ask Revenue.
- How to do something in the app ("how do I add an invoice?"): the app's own guide answers in its own words, with a link that starts the guide in the app.
- Everything else: Signal's own numbers and the tooltips on the page, as before.
The ROS chat's notes too (16 Oct). Ask Revenue also searches the ROS chat's own notes on how to do things in ROS (paying tax in parts, CT1, Form 11, VAT3, RCT, registrations and more, about 13 notes), read from the ros-agent repo on Bitbucket every night at 03:25. A note the ROS chat changes is read again. They are our notes, not Revenue's text: the answer names them as Bullet's ROS notes, the filter lists them apart, and a customer's answer never uses them.
One tax brain only: the ROS agent keeps the filing knowledge (forms, schemas, how to file) and asks Signal for tax rules; it holds no rates or rules of its own (checked with the ROS chat, 7 Oct).
Two funnels (Peter, 9 Oct). Every page has the same Ask bar at the top, and each page says which funnel it is in. Business: the Overview, the Onboarding board and Product (with Product health), answered from Signal's own numbers: Stripe, Google Analytics and the ads, the onboarding, product health, the replays and the monthly report. Support: the Support desk and the Accountant page, answered for customers, their accounts, tax (the tax brain above), how to do it in the app, and tickets. The same small model also says which funnel a question belongs to. A question asked in the other funnel is not guessed at: the answer is one line, "That is a support question: ask it on the Support desk" (or a business question, on the Overview), with a link that opens that page with the question already asked. On the board, the desk and the Accountant page the question bank answers its own questions first, from the rows on the page, every figure counted by our code, and a bank question from the other funnel gets the same one line. Other pages have no funnel and answer everything, as before. The code: dashboard/js/ask-chat.js (the bar) and api/answer.js (the sort).
What the 383-question test changed (9 Oct). The small model's word alone no longer sends a question away. It goes to the other funnel only when its own words clearly belong there (MRR, churn, sign-ups, ads for business; tickets, replies, response or resolution times, saved answers, clients, health checks for support) and say nothing about the page it was asked on. So a desk question on the desk, like "median first response time this week", is answered there, never sent to the Overview. Tax and how-to questions still go to the support funnel. Sums staff ask for are done in code from the pack's own numbers and sent with it (ARR as MRR times 12, MRR change month to month, ARPA as MRR over paying customers); the model may also do one sum on two numbers it was given, and says how it worked it out. The answer model thinks before it writes, so it now has room for both; an empty reply is tried once more without the page's tooltips, and if that is empty too the answer is one plain line saying Signal does not have it just now, never an error. The tooltips sent with a question are words only: a tooltip in a table row is labelled by its column heading, the value shown on the page is taken out of it, and the names on the page (a client's or customer's) are taken out of every tooltip, so Ask cannot repeat a client's name; it only writes a name when the question names that record.
Why did it happen? Investigate (Peter, 9 Oct). A question like "what happened with the drop in sign-ups in April and May?" is answered the way a good analyst would, in either funnel. Code decides which questions are investigations (since 9 Oct, after the 383-question test sent plain lookups like "MRR growth this month" here): a why, how come, explain or look into question about a number that rose, fell or changed, or a what happened or what changed question about a period. Every other question, "what is the drop-off at each step", "where do users rage click", goes to the pack, which holds those numbers. A why question also skips the bar's own question bank. Everything else is counted in code (api/_lib/ask-investigate.js). The answer has six parts: a one-line verdict; what Signal can see for those months, each source with the first day it holds, read from its rows each time and never typed in; what Signal cannot see and why (sources that start later, support on user.com before Signal's desk, the old app's onboarding screens that were never sent, the new web front end that is not live yet, months GA4 did not record the sign-up forms, agency spend that is typed, not read); a small table of new Stripe customers, paid, GA4 visits and sign-up forms, Google Ads spend, tickets and new onboarding sign-ups for the months asked, the 3 before and the 2 after, each number opening its page; what would make the answer better; and questions back to you. A month in which Signal only started reading a source part way through shows as not held. The answer leads with the verdict and the numbers; what Signal can and cannot see comes last, closed until opened, and only once a conversation (a follow-up points back to it). When no month was named it says which months it read. The model may do one sum on two numbers in the table (a difference, a total, a change against the earlier month, one count as a share of another), written with its working and a percentage to one decimal place; any other number it writes is dropped.
Answered or not, and the Ask log (Peter, 9 Oct: "it should keep a list of questions we can't answer"). The second run of the 383-question test found 275 answers opening "I do not have that", 57 of which then did answer, so "Send to dev" showed under good answers. Now the answer leads with the number, and ends with a line only the server reads: answered, partly, or not. The bar shows Send to dev only when it was not answered, read from that line, never from the words. Every answer also has a small Flag: mark it wrong or not answered, with a note. Every question asked in the bar is kept for 90 days with its page, its funnel, whether it was answered and the answer (an answer drawn from a page's own rows can name customers, so only its question is kept). Questions Ask could not answer (Tools, and a tab on Answers) lists the ones not answered, answered in part or flagged, newest first, with the count by page; an admin marks each one answered (what was built, or an Answers entry) or sends it to dev as a task, the same form the desk uses. Test runs (sent with a tab ending "eval-" and a number) are kept in the table but left out of every list and count. The code: api/_lib/ask-log.js, /api/answer?log=1.
The named number, and the check after (9 Oct). The same run found six misreads, each a number the pack holds under a name: customer churn called revenue churn, DAU/MAU called WAU/MAU, the wrong count of tickets opened or closed this week, "of the 48 tickets" when the desk holds 10, "failed on the first attempt" when Stripe counts every failed attempt, and a median of 0 hours written as "1 minute". The sums are now done in code and sent with the pack (gross revenue churn as churned MRR over the MRR at the start of the month, WAU/MAU, and the desk's last 7 days opened and closed), and when the page's tooltips and the pack both give a figure the pack's named one wins, one number, one definition (a ticket touched this week is not a ticket opened). After the answer is written, code checks it for those six and for a forecast: one it catches is asked for once more with the fix, and if the fix does not take, the answer counts as answered in part and the Ask log says what Signal doubted (api/_lib/ask-fence.js). Signal does not forecast: a question that asks what a number will be later (next month, next quarter, at year end, on the current trend) or what growth a target needs gets those words and the reason (Peter, 9 Oct: small business income comes in ad hoc, so a forecast would not be reliable), then the number now and its trend. Since the third run (9 Oct) that is all it covers: "Signal does not forecast" had opened 21 answers, several not forecasts. A one-step what-if on numbers Signal holds ("payback if CAC rose 20%") is worked out, with the sum shown in the same sentence and called a what-if on today's numbers, and a rate so far (this month, year on year, over past months) is answered from the months held. An answer that says "Signal does not forecast" to a question that is not a forecast is asked for again.
Money, worked out in code, and checked (9 Oct, the third run). Five answers were wrong, all money Ask had worked out itself: three months of marketing spend over one month's MRR, twelve rows added up wrong, all MRR lost put down to failed cards, July's expansion given as September's, and "7,800 of the 13 lost". The pack's core now carries those sums done in code, both sides over the same months: marketing spend as a share of revenue (each month and the last 3 full months), churned MRR split by why the customer left (chose to leave, card failed for good, or no reason held, matched to the loss nearest the day they stopped paying; contraction has no reason), expansion and contraction by the month's name, failed payments added up (failed, recovered, not recovered, over 12 months and the last 3 full months), customers lost with their base, the segments (VAT registered and entity) and Stripe sign-ups in the last 7 days. After the answer is written, code also checks that a figure set beside a month is that month's, that both sides of a share come from the same period, that a euro amount is one the pack holds or a sum the answer shows, that a cause is not put on a total covering every cause, and that in "X of N" the part is not bigger than its base.
What the fourth run changed (9 Oct). Three answers were wrong and two of them came back marked answered. A what-if on churn ("what is a 1 point drop worth") was worked on 847, the customers paying at the start of three months added up, so it gave three months' worth as one month's: the pack now carries each month's own base (about 282 paying at the start of a month), the average month and what one point of churn is a month in customers and in MRR, and code catches a churn what-if built on the three-month figure. The share lost at the bank step (63%) was read as the activation rate: the activation rate is now a named number (numbers.js, activated over signed up, 6 of 16, 38%) beside each step's reach, its drop and the median hours to it, with the industry figure and its source, and code catches a drop called activation. The saved answers' uses were read as help-article views: they are now called staff reply uses, and the pack says Signal holds no article views; code catches the mix-up. Answered now means the check found nothing: any doubt left after the second try makes it answered in part. The desk's weeks are rows newest first, each with the date it starts, so they cannot be read backwards, and code catches a week given the wrong row or the desk chart's touched counts given as tickets opened. The core also carries the visit to sign-up rate as a percentage, first reply this week against last week, and the money still being chased (failed invoices from the last 30 days not paid yet); each kind of flag carries its own top screens, so rage clicks have a screen. Customers lost over July to September has two counts and the pack names both: 36 losses (the scorecard's, a customer lost in two of the months counts twice) and 35 different customers, from one function.
What the fifth run changed (9 Oct). Three answers were wrong. The website's weeks were read backwards, so a fall of 11% this week read as no drop: the Google Analytics weeks now run newest first with their own dates and a weeks-ago number, like the desk's, this week and last week are named in the key numbers, and the check catches a figure given for this week that belongs to an older week. Two answers made up hours and counts ("201 hours", "7 to 21 sign-ups a week"): the check now covers hours, minutes, days and plain counts too, so each must be a figure Signal holds or a sum shown from figures in the answer, a range needs both ends held (and over the last three months, both from those months), and the same figure given two values in one answer ("1 downgrade, actually 0") is caught. Anything the check still doubts is never marked answered. Each desk ticket now carries its hours to resolve, and the desk's counts by topic, plan and stage are counted in code. Forecasts: a question that changes one held number once and names no time ahead ("if trial conversion improves by 5 points") is a what-if even when it says forecast, and is worked out with the sum shown; a future value over time (next month, by year end, in six months, on the current trend) is still not forecast. The desk rows' staff, plan and stage were empty on every live ticket: one customer record with a part-second sign-up time broke the read of all of them, the desk's replies carried no staff address, and a tester's ticket address is often not their Stripe address. The read no longer breaks, a new reply is stamped with the staff member who sent it (older replies say not recorded, and Signal's own quiet-days close says so), and a ticket is joined to Stripe by the customer's own Stripe id first. "Which customers", "who", "list" and "top" questions about money now get Stripe customer by customer, and the key numbers add cost per sign-up, onboarding completion and tickets per active customer over the same 30 days.
Answered, checked in code (9 Oct, the third run: "partly" came back 184 times, 37 of them full answers and 66 not answered at all). Partly now means part of what was asked is answered with numbers Signal holds and part is not. Code then checks the flag against the words: no figure and a "not held" sentence, or an answer that opens "Signal does not hold that", is not answered; a "partly" with figures and nothing said to be missing is answered. The page's own tab or title is never repeated in an answer: a word of it carrying a number (a test marker such as "eval-3") is taken out.
How sure, the one rule. The months asked are set against the average of the 3 months before. When the two together are under 10, or the gap is smaller than twice the square root of their sum (the range chance alone gives), the answer says "too few to tell". Otherwise it is a change beyond chance. Money gets no chance test, and test data is labelled, never read as customers. The model never decides this. It writes the verdict, the reading, what to test next and the questions from those facts only: any sentence with a number that is not in the facts, or that states a cause as fact, is dropped before you see it. A dated change from the change log or the change history only ever "lines up with" a number. No published benchmark covers a month against the months before, so the answer says so, and it names the one number to keep watching. If the model cannot answer, code writes the verdict from the same facts.
The prompts are cached (Peter, 11 Oct: Anthropic wrote that our cache hit rate was low). Every call Signal makes to Claude sends the part that is the same each time first and marks it for Anthropic's cache: the instructions, and for Ask the pack's core (the counts, the key numbers and their definitions, the pages), which is the same for every question until the pack is rebuilt five minutes later. The question, the blocks picked for it, the page's tooltips and the conversation come after the mark. A cached read costs about a tenth of a normal one, and the words the model reads are exactly the same. The instructions stay cached for an hour, the pack's core for five minutes. The same is done for the website Ask bot's pack (an hour), the desk's library of known answers, the Ask box's question list, the marketing brief and the customer read's benchmarks, and the law watch's product map. A short prompt below what the model will cache (Haiku caches nothing under about 4,000 tokens) is marked anyway and costs nothing extra. Every call's cache reads and writes are counted by the hour; staff can see the hit rate over the last 24 hours at /api/answer?cacheStats=1. The code: api/_lib/claude-usage.js.
The numbers, corrected (7 Oct review)
- Customers lost counts a cancel in the month it was pressed, even when they keep paying to the end of the month, as the monthly email always said. The latest month or two go up by the cancels still running out.
- Against the industry's 2% a month, Signal now compares all customers lost (chose to leave plus card failed), the way ChartMogul counts it; chose to leave is shown underneath.
- 12-month retention has one meaning everywhere, ChartMogul's cohort retention at month 12: of the customers whose first payment was 12 months before a day in the last 3 full Irish months, the share still paying on that day, with Bullet's own test accounts left out. Under 15 customers is too few to tell, so no verdict.
- Monthly revenue divides a yearly invoice by twelve instead of counting it all in one month.
- Ad sign-up forms are marked "tracking broken since 1 Sep 2026": Google stopped counting most forms while Stripe sign-ups held, so no verdict and no "pause these keywords" advice until it is fixed.
- Conversion in the monthly email is compared with last month, not with an industry figure that measures something else. Product health carries the Test data label while the new app is not live.
Who can get in, and what the open doors allow (7 Oct and 5 Nov reviews)
- Sign-in. A 6-digit code allows 5 tries, then it is dead, and works once. At most 5 sign-in emails an hour per address and 10 per computer. After sign-in Signal only follows a Signal page, never another site.
- Only from Signal's own pages. A change made with a staff login (saving, sending, deleting) only counts when it comes from a Signal page, so another website cannot act as a signed-in member of staff.
- Off the team means out. Removing someone on the Team page stops their sign-in and their inbox key straight away, on every door: since 5 Nov each one reads the team list every time, not only the sign-in. If the database is down, nobody new can sign in, and anyone already signed in stays signed in: the pages say to try again, rather than sending them to the sign-in page.
- The accountant reads; she does not change. An adviser (the accountant) reads every page and uses Ask, except customers' card details, home addresses and recordings (screen, camera and voice, and a tester's words quoted from them; in a test review her copy shows those as [their words]). Where one of these would be, her page says "Not shown to advisers." In Tools she sees only the bullet-migration repo, and the menu leaves out links into the others. She cannot change jobs, decisions, tasks, tests, the roster or the Tasks inbox. She answers on the desk, as before.
- What Bullet does. The product's own facts (what Bullet files, charges, sends and covers), each with who said it and when, live on Admin, What Bullet does. Every chat reads them (
/api/bullet-facts) before it changes what the product says; only the team changes a fact, and a replaced fact is kept, crossed out. - Open doors have limits. Tester sign-ups and the website bot's hand-over to a person: 5 a day per computer and 100 a day in all. The customer tax door: 20 a day per computer; the brain exam on Peter's Mac has its own 60 a day, with a pass that changes every day, so it never uses up his. The exam judge (
/api/exam-judge), which grades the exam's free-text answers with Opus 5.5, takes the same pass, 400 a day. The certificate: once per address, with a cap on how many go by themselves each day, and before the web front end is live only to a tester's own address (see The first payment certificate). The AI doors that only Signal uses need a staff login. - The app's doors have limits too (5 Nov 2026). Chat messages and new tickets, screen recordings sent from the chat, replay film and new visits, and onboarding posts and new sessions are each counted per computer. Past a limit, what came in is not kept: a chat post is refused and the app sends the whole chat again with its next message, the app says a recording did not send, and the replay recorder is told all is well, so it never tries again. A tester Signal knows from its own Testers list is counted on their own instead, across all their sessions, with room no real test comes near. A team member's own uploads are never counted. If far more messages arrive in one day than any real day brings, the desk still saves every ticket, but Signal stops the email for each message and the AI reads of them until midnight (UTC), and emails the team to say so.
- Pages from Peter's Mac are sealed (5 Nov 2026). Review and Walkthroughs in Tools are published from Peter's Mac every 5 minutes. Signal shows them sealed: they run without your sign-in, carry only what they show (of Signal's own tasks, only the ones not done, with no owner and no source), and can ask the Tools page for two things only: open another Tools tab, or move one of Signal's own tasks, one task for each click inside the page.
- Only the site is served (5 Nov 2026). Internal notes, tooling and logs are kept off the site, and after every deploy Signal checks that none of them can be opened.
- The Tasks done sweep reads each project's to-do list once per run, so it cannot run Bitbucket out of calls.
The New Stripe customer email
Peter, 7 Oct. When Stripe creates a customer, its webhook tells Signal, and Signal emails John, Peter and Mariana once (a lock stops a Stripe retry sending it twice): the name, the email, the time in Irish time, whether a card is on file, where they are in onboarding if Signal knows them, and buttons to open them in Signal and in Stripe. Test-mode customers and old resent events send nothing. Who gets it: NEW_CUSTOMER_NOTIFY on the server, else those three. The code: api/stripe.js.
Maybe a test company (9 Oct). People often make a test company because they are afraid of getting Revenue wrong. The email says "maybe a test company" in its subject, and gives the reason in a box, when the name starts with a word like test, demo or sample, or when another Stripe customer already has the same email or the same name. It names that other customer and when it was made. It never guesses: it only says what the Stripe rows show. Mariana can then call to help them set up their real company.
The Revenue inbox agent
The ROS agent runs at 08:00 Dublin time every day, today from Peter's Mac. A hosted copy on Bullet's own cloud is built (4 Oct) and Signal already takes its signed runs; its schedule stays off until its manual tests pass and the Mac job is switched off. It reads Revenue notices only, saves the exact run evidence privately, and sends the result to Signal. Recent real runs appear under Accountant → Agent runs. Notice PDFs and screenshots open through a five-minute staff-only link.
Signal checks at 11:00 Dublin time that the run arrived, for each company in the setting ROS_EXPECTED_COMPANY_IDS. A failed or missing run creates one task for Mariana; repeats update that task rather than making a new one every morning. Signal does not message the customer because ROS itself may be temporarily unavailable. Peter gets an email after two consecutive failures, after 24 hours unresolved, or straight away for a certificate or infrastructure problem.
What changed, from the commits
Under the Journey charts, What changed, month by month lists what went out, so a month that moved can be read against what changed in it, back to January 2025. It comes from three places, in their own words, never written by a model: the commits on Bitbucket to the marketing site (bullet-marketing), the web front end (bullet-web-frontend) and the app and its backend (accounts-web-application, every branch), and the change log (docs/changes.json). One repo's commits on one Irish day are one line, with the rest of that day inside it; plain branch merges are left out. Each line gets an area (pricing, billing, sign-up and onboarding, ads and tracking, marketing page, app feature, support) by fixed rules on its words and the files it touched; commits that only touch notes and docs are kept but hidden unless asked for. The area is a guess, it says what a change touched, never why a number moved.
Every morning at 05:10 UTC (/api/change-history?cron=1) Signal reads the last four days again, with the files each new commit changed (60 at most a run), so a day read twice is replaced, never doubled. Google Ads changes are not in it: Signal's Google key reads Analytics and Search Console only, and the Ads change history needs the Ads API. The Ask reads the same rows (getChanges in api/_lib/change-history.js). An admin loads the full history once with /api/change-history?backfill=1 (a dry run that shows what it would load) and then &go=1.
Our journey, month by month
The Journey tab on Overview shows the four numbers every subscription business watches, for the last 18 months, worked out from Stripe and Google Ads in code: monthly recurring revenue, paying customers, customers lost each month (split: their card failed, or they chose to leave), and how many months a new customer takes to pay back what it cost to win them. Two have a dashed benchmark line with its source: good churn is under 2% a month (ChartMogul), and the median payback for SaaS under $5K a year is 11 months (Aleph x Benchmarkit). The cost to win a customer is all the marketing spend (agency and ads, typed in on the Monthly tab, staff time left out) over the new paid customers from every channel; it is paid back from what a customer pays a month, before VAT and net of refunds, a yearly invoice divided by 12. Google Ads spend alone per new paid customer is in the tooltip, with no industry figure.
The brief: what they are asking
Every incoming customer message is read once. Above the thread the desk shows what they are asking in plain English, every number they gave exactly as they wrote it, what we still need from them, and who it is for (desk, accountant, developer). A number the customer did not write is dropped. A screen recording is turned into text first and its main points are listed. The points are written twice: once for the desk, and once to the customer as "you", which is the copy the suggested reply uses. A recording with no sound says so.
The questions we still need answered go into the draft. After a known answer, or as the whole first reply when there is none. A question that asks the customer to do something in the app carries its how-to link when the guide exists.
The chase agent: the customer hears from Bullet, not from us
The monthly health check finds what is wrong on each client. Most of it needs the client to do something. The chase agent tells them, by itself, and only escalates to a human when it has gone on too long. (In the code it is called the ladder: ladder-rules.js, api/ladder.js.) Peter's rule: always real, never made up. Every line in a note is the check's own name and its own question. Nothing is written by a model.
Each failing check is one row in the ledger with the day it first failed. The chase agent counts from that day.
| Step | When | What happens |
|---|---|---|
| Quiet | Day 0 to 3 | Nothing. Many things fix themselves. A bank feed often comes back. |
| Note 1 | Day 3, in the next Tuesday recap | Friendly. "A few things we need for your books." Each item with its how-to link. Email, chat, and the to-do list in the app. |
| Note 2 | Day 10, in the next Tuesday recap | Firmer. "Until these are done we cannot finish filing your VAT return." The banner takes over the yellow card in the app and on mobile from here. |
| Countdown | Day 17 to 21, every day | "Your account will be limited in 4 days", then 3, 2, 1. Email and push daily. The banner shows the number. A human is flagged the same day. |
| Lock | Day 21 | The app shows one screen: the things to do, with their links. The part of the app that depends on them (filing VAT, running payroll) stays shut until they are done. The way out is built in. One last email and chat note say so. |
The check passing again clears the row and the chase agent resets for that item. If it fails again later, it starts from day 0.
Which check is which
Every health check has a kind. The kind decides what the chase agent does with it.
| Kind | Means | What the chase agent does |
|---|---|---|
| Blocking | One thing is broken until the client acts. Bank feed disconnected. Cert expired. Agent link not signed. VAT direct debit bounced. A statement or a yes/no we need from them. | Goes up the steps: note, note, countdown, lock. |
| Housekeeping | Happens every day by nature. Receipts not attached. Payments not categorised. VAT number missing on a zero-rated invoice. Unpaid invoices to chase. | One line in the Tuesday recap, never the steps, never a lock. If it has been failing for 90 days it becomes blocking and the steps start. |
| Ours | Bullet fixes it. Book does not match bank. Control accounts off. A return we file. A ROS agent that broke. | The client is never told. A human is flagged when it is older than 3 days. |
The full list, check by check
The days
The recap goes on Tuesday at 9am. Monday the inbox is full, Friday gets lost in the weekend, and Wednesday is when most people are in the app, so a Tuesday note is on top of the pile. One recap a week at most. The countdown is the only thing that writes daily, and only during a countdown.
The numbers (3, 10, 17, 21) are one setting on the server (LADDER_DAYS). The daily run is at 09:00 every day; on a Tuesday it sends the recap, every day it checks the countdowns and locks.
Where a customer hears from us
- Email from Bullet (signal@bullethq.com). Sent by Signal.
- The chat in the app. A message from Bullet on their thread, or a new thread Bullet starts. Sent by Signal. Their reply lands on the desk like any ticket.
- The to-do list on the app's homepage. The app reads its list from Signal.
- The banner from Note 2, on the homepage and on mobile. The countdown number sits in it. It carries a Not now button that hides it for one day. Their dates do not move and the emails still go out. A locked account has no such button.
- Push on the mobile app during a countdown.
- The lock screen: the to-do list and nothing else, on the app and on mobile.
The app and the mobile app read one thing from Signal: what to show this customer (their to-dos, the step, the days left, the lock date). Signal never tells the app anything about money or staff.
A Messages banner or pop-up carries the same two doors. Not now brings it back tomorrow, once. The X closes it for good. Either way the chase agent's banner wins over a Messages banner.
Journal entries: the accountant's job, never the customer's
Bullet took manual journal entries out of the customer's product. The owner never sees one and never will: they make a mess of them, and the accountant is the one who should be posting them.
On their Bank screen one of the categories is "I don't know what this is". Picking it does NOT make a journal and does NOT go to the accountant. It opens the support chat with the transaction already in it and asks them to say what they are looking at, or to record their screen.
So it lands on the desk as an ordinary ticket, with a Don't know chip of its own and a badge on the row. Triage it first. Most of these are not journals at all: a missing receipt, a category they could not find, a transfer they did not recognise. Only when it really is a question about the books does it go to the accountant, with Ask the accountant, the same button as always. The customer never knows: they are still talking to support.
The accountant's queue (Peter, 29 Sep). A question to the accountant (Ask the accountant, or @Deirdre in a note) no longer emails her one at a time. It waits in Asked of me on the desk, where she lands when she signs in, and every day at 16:00 UTC (17:00 in Irish summer time) she gets one email listing every question still waiting on her, oldest first, each a link back into that list. No questions waiting, no email. On each question she has two buttons. Send to customer goes as a reply from Bullet with the Around Finance line added by the server, so it cannot be left off, and the desk labels it "from the accountant". Send to Mariana is a draft for Mariana, or a question back. A question counts as answered once she has done either. The rule is one file, dashboard/js/ask-queue.js, shared by the chip and the email. A question to a developer set up as an adviser on the Team page waits the same way; only the accountant has Send to customer. A question to a desk person (@Mariana) still emails at once. The desk people with support emails on (Peter, Mariana) get a copy of her email. While her own emails are off on the Team page, the email still goes to them as a Preview, and not to her. An admin can send a test at any time (POST /api/tickets?advisers=digest).
- The ticket carries a link straight to that exact transaction in the customer's own Bullet. One click. The app finds the row by who the payment was with and how much it was, so on the rare ticket that did not carry that, there is no link and the line says so rather than sending you somewhere wrong.
- The accountant posts the entry on the ticket, inside Signal. They do not have to go anywhere.
- Half the entry is already filled in. It started from a bank transaction, so the account, the date and the amount are facts. That line is written in with a padlock and cannot be changed. The accountant only says where the other half goes.
- Nobody picks the side. Money out of the bank credits the bank, so the other half is a debit. Money in is the other way round. Signal works it out, because picking the side is where the mistakes happen.
- The line under the form says what is left, like "€24.50 still to put somewhere, on the debit side". It will not post until that is nil.
- The other half can be split. One payment of €1,230 might be €1,000 of equipment and €230 of VAT.
- Every line with a figure needs an account. A line is a debit or a credit, never both.
- Only the accountant and an admin see the button. Signal decides that from the team list, not from anything the browser says, and the server checks it again on the way in with the very same rule.
- A posted entry is never edited or deleted. It is voided, with a reason, and the reason stays on the record.
- No accruals. They came out of the product on Deirdre's advice (21 May 2026). A year-end adjustment that genuinely needs one is what this journal is for.
The one rule. A journal entry never reaches anywhere the customer can see. Not on their transaction, not in an export, not even a note saying one exists.
We never chase for a receipt that does not exist
Some payments have no receipt to find. Moving your own money between your own accounts, a loan repayment, a director's loan, paying yourself, paying Revenue. Nobody issues a document for any of those.
The chase agent used to say "Add the missing receipts" on these, by itself, with nobody reading it first. It now says "Tell us what these payments were for", and adds that a transfer between their own accounts just needs them to say so. The seven categories are one list that Signal and the app both read, so neither keeps its own version. Entertainment and meals is a near miss: no VAT back on it, but a meal has a receipt, so that one is still asked for.
Insurance is not on that list, on purpose. It is exempt from VAT, which is a third thing: not a rate, not outside the scope. It looks the same to a customer but it is not the same in the books. Deirdre is being asked. Nothing guesses until she answers.
What a human sees
How the desk stays current. The desk reads the whole ticket list once when it opens. After that it asks the store every 15 seconds for tickets that changed since its last look, and only those. A tab in the background asks once a minute; bringing it to the front asks at once. While a conversation is live (the customer wrote in the last 5 minutes, or you are typing a reply) the desk asks every 3 seconds instead, so a quick back and forth is quick without pressing refresh. In that same live window the desk shows "Name is typing" under the thread when the customer is typing, and the app shows "Mariana is typing" to them. Only the time of the last keystroke crosses, never the words. The roster refreshes every 5 minutes. Before 21 Sep 2026 every open desk pulled every ticket every 4 seconds, which used up the database's monthly transfer allowance and took Signal down for a morning.
Recordings are private. Since 7 Oct everything a customer or tester records (screen recordings from the chat, replay batches, a tester's camera and voice) lives in a private store (Backblaze B2). Nothing sits on a public link. Staff play them through Signal with their login; an adviser (the accountant) never does, and her page says "Not shown to advisers." in their place. A customer can play back their own clip in the chat, and nobody else's. An upload in the old way is moved within seconds, and the 03:00 job moves anything left over, on open and closed tickets alike. The transcript stays on the ticket. Nothing a customer sends is ever deleted, except a recording that lands past the upload limits (see the open doors): it is deleted at once and never reaches the desk. A tester's camera and voice go after 90 days (TEST_MEDIA_KEEP_DAYS).
The demo company is not real activity (9 Oct). The app has a "Demo company" in its company switcher. What it sends is marked (demo, and the plan demo), under the person's own login. Signal keeps the recording, but it never counts as that customer's activity: no events, no change to their company details, and it is left out of product health, the customer page, the tester counts and the connections check. A support ticket sent from inside the demo is a real person asking, so it is answered as normal.
The Accountant view has one tab per section, each with its count, and opens on the first that needs a human: Flagged, Agent runs (only when there are runs), Actions, Monthly check (the health check grid), Everything running, and How-tos (only when a guide is wanted). The work tabs are these:
- Flagged by the chase agent. A customer in countdown or locked (call them). Our own fix older than three days. Housekeeping that turned blocking.
- Agent runs needing attention and Actions needed: a Revenue notice on a clock, a cert about to expire, a failed agent login. The same as before.
- The health check grid, showing only red and amber clients by default. All-clear clients are one number in the line above it.
Everything running is the other view: every failing check on every client, its kind, how long it has been failing, which step, when the next step falls, what was sent. The record of what the system is doing on its own. Search a client when you are on a call with them. It is not the work view.
On the Support desk, a ticket from a customer who is in countdown or locked, or who has unanswered notes, says so at the top, so nobody replies blind.
Safety rails
Dry run first. The chase agent starts in dry run: it writes what it would send into the ledger and sends nothing. Look at a week of dry runs, then flip one switch on the server (LADDER_LIVE=1). The state of the switch is shown at the top of this page and on the Accountant view.
Safety rails (7 Oct review). Until the first live run, the app shows no banner, no countdown and no lock, only the to-dos. The first live run starts every client again at note 1, so nobody jumps straight to a countdown because of the dry-run days. If Signal has had no fresh observations for 26 hours, the agent sends nothing and moves no step (a fixed client is never chased on old news). A client written to in the last 20 hours is never written to again that day. The Revenue agent link permission is ours to fix, so it never chases or locks a client.
Mock and test clients are never emailed. The checks run on mock data today. A client whose email is example.com, or whose id starts with test, anon, mock or id-, gets nothing, live or not.
Nothing invented. A note lists failed checks by their own name and their own question. Drafts for the desk only ever come from Answers. The AI reads and sorts. It does not write to customers.
Patterns
Every Monday at 7am (and on "Find patterns" on the desk) the AI reads the last four weeks of tickets and groups the ones that ask the same underlying question. For each group: a topic, the tickets, the likely cause (a guide wanted, a product fix, wording, pricing), and one sentence on what would remove the need for those tickets. Counts and trends are computed from the tickets, never by the model. A row filters the desk to its tickets. "Send to dev" makes the fix a task.
The score
On the desk, under "The loop": the share of replies built on a known answer, per week since day one (12 September 2026, 0%). A reply answers itself when the desk sends a known answer the brain drafted. Every reply teaches it, so the number should climb.
One chat, many tickets
The customer sees one chat. The desk sees tickets. Every ticket has a number (#18) that is on the desk, in the list, and in the subject of every email about it. A ticket that has waited on the customer for 7 quiet days closes by itself (a ticket waiting on us never does). The next message from them opens a new numbered ticket, linked both ways ("continues #17", "continued by #19"), and their chat shows a thin line "New conversation #19". This is how Zendesk and Help Scout do it: one thread for the person, one ticket per issue for the desk, so the brief, the known-answer match, the patterns and "waiting on us" stay honest.
Replies by email
Every mail Signal sends carries a reply-to. A chase note's reply-to is reply+<ticket>@reply.bullethq.com, so a reply lands straight on that ticket in Signal as a customer line, and the brief and the known-answer match run on it like a chat message. (7 Oct review) An email is only added to an existing ticket by its reply address, or by a [#number] in the subject when the sender is that ticket's customer; a reply from a team address becomes an internal note, and a webhook retry of the same email is skipped. A reply that cannot be matched to a ticket by its address is matched by the sender's email; failing that it becomes a new ticket marked by email. The quoted mail underneath a reply is cut off. Attachments are listed by name. Until receiving is switched on in Resend (an MX record on reply.bullethq.com and a webhook to /api/inbound), the reply-to is the help address.
Messages: broadcasts and triggers
Who counts as a customer (Peter, 30 Sep). Signing up is not active. Active: a real action, ever, read from the app's events: an invoice sent to a client who is not Bullet (never the onboarding's €1), or a real bank connected with transactions (never the practice bank). New: signed up under 14 days ago, no real action yet. Browser: signed up 14 days ago or more, still no real action. What they are (limited company or sole trader, VAT or not) is their latest answer to the app's setup check, so someone who fixed a wrong answer is still one customer. One rule, dashboard/js/customer-status.js, used by the roster; the Customer filter in Messages picks by it. The onboarding funnel's "activated" (they finished the onboarding) is a different thing and is unchanged.
Messages (left menu, Customers) is how Bullet talks to some or all customers. Write it once, in plain words, like you are sitting beside them. Pick who gets it with the same chips as the board (type, activated, signed up, last seen, VAT, bank, first invoice, support, chase agent, situation, funnel, customer: active, new or browser). The count updates as you tick, the grid shows who, Export gives the CSV. Pick where: email, a pop-up in the app, a banner in the app, push on mobile. Then Send now, or save it as a trigger: the same message sent once to anyone who matches from now on, checked every morning at 9:15.
The writing check. Stop typing for a moment and the AI reads the subject and the message. One grey line under the box says whether the spelling is right, with a "Fix spelling" link, and how it reads for someone who does not want to think about accounts. It never rewrites, and it never touches a figure, a date or a link.
Send a test to me. The email goes to your own address. The pop-up, the banner and the push are planted on your own Bullet account, the one with your email on the roster, so you see them in the web app and on the phone the way a customer would. A test never counts in the results.
The writer. Two doors, one engine. "Rewrite it" on the check line rewrites the subject and the message in Bullet's voice, plain and short, keeping every fact and link. The "Ask the AI to write it" field at the top writes them from whatever you type. Both keep the old text behind an Undo. It refuses to add a figure or a link that is not already there, and says so.
Where it opens. The button link is a web address, for email and the web app. A phone cannot open a web address inside the app, so a push or a pop-up on mobile needs a screen: pick it in Opens on mobile. Leave it blank and Signal matches it from the web link when it can. A push never opens a browser.
- From. Broadcasts go as "Pete from Bullet" at the help address. Support and chase notes go as "Bullet Support" at the same address. One address, two names.
- The email. Plain, one column, one button, signed "Pete", the postal address and an unsubscribe link in the footer. The unsubscribe is required by law for news; support and chase notes never carry it and never stop. Opening the unsubscribe link shows a button; only the button unsubscribes (mail scanners open links), and the page says unsubscribed only when it saved.
- Confirm the count. Send now shows "Send to 412 people?" with the first three names, and how many unsubscribed and test addresses are left out. Admins only. Test addresses and mock clients are never mailed. After Yes, it waits ten seconds with an Undo before anything leaves.
- Schedule (Mariana, 29 Sep). A send-once message can go at a set day and time instead of now. Every half hour, on the hour and at half past, Signal sends the ones that are due (so a message set for 10:10 goes at 10:30), to whoever matches at that moment, the same as Send now: unsubscribed and test addresses left out, no weekly cap. "Cancel the schedule" makes it a draft again.
- When there is no first name, {{first}} says the word you type beside the message, or "there". Preview as picks anyone in the audience, or "Someone with no first name", and shows the email, pop-up, banner and push as that person would get them.
- Push words. A push can have its own short title and text. Blank uses the subject and the start of the message.
- How long a pop-up or banner shows. Until they close it (the default), or once: it goes away as soon as the app says it was seen. Stop on a day, and after that day nothing in the app shows it and a trigger sends to nobody new (the daily run marks it ended).
- Who a trigger reaches. Everyone who matches, now and later (the first run sends to everyone who already matches), or only people who match from now on: when it is first switched on, everyone who already matches is marked and left out for good.
- The list. Every row has a date. Tabs for Drafts, Scheduled, Sent, Triggers and Archived, a search by name or subject, Duplicate for a fresh draft copy, and Restore on an archived one.
- The pause switch and the cap. "Pause all triggers" for migration week. At most one automated email per customer per week across triggers and the chase agent (a setting). Manual sends are not capped: you confirmed them.
- Dry run. Triggers write what they would send and send nothing until
MESSAGES_LIVE=1. Manual sends go once confirmed. - Results. Every send is a row: sent, delivered, opened, clicked, bounced, unsubscribed; in-app: shown, seen, clicked, dismissed. Opens and clicks come from Resend's events; the app reports the pop-up. Each row links to the customer.
- Replies land on the desk as tickets, like any email reply.
Testers: the people trying the app before everyone else
Testers (left menu, Testing) is the list of people invited to test Bullet, 15 to 100 of them, users and people from socials. Paste them in, one per line: name, email, where they came from. Someone already on the list is updated, not doubled. Every sign-up on the test page mails the team at once (the same people a new ticket mails), saying whether the invite went out by itself or is waiting on the Testers page.
- The invite. The link opens the onboarding, which lives in the web front end since 29 Sep (app.bullethq.ie/onboarding/; the old accounts-onboarding /v8/ addresses redirect there), so the tester is one site from the first screen to the app and is recorded from the first screen. "Send invite" on a row emails that person once, from the support address, subject "You are invited to test Bullet". Short, plain, signed Peter. It carries the invite link, then nine things to try on the sample company (the same as Part 4 of the customer test run), each with a Start link that tells the recording which task it is (so the replay is scored against the app's own guide) and carries their invite (so a task opened on another device still lands on their row). Before it signs off it tells them the app will ask to record their face, voice and screen, that they can say no, that only the Bullet team sees it, that it is kept for 90 days (TEST_MEDIA_KEEP_DAYS) and deleted sooner if they reply, and that testers get 2 months of Bullet free, worth €178 (Peter, 29 Sep). Test addresses (example.com, anything ending @test) are never mailed; copy the link instead. Resend sends the same email again.
- The link. Opens the onboarding straight away (no welcome page, since 24 Sep) with
?invite=and a signed token that names one tester and nothing else. The app asks Signal who this is and gets the name and email back, so the tester's name is already in when onboarding starts. The token does not expire; it only ever returns a name. - What Signal records, by itself. Opened (the moment the app asked), the session the tester became (the app tells Signal once it has one; that also makes the roster row, source "tester"), then from the roster: signed up, bank connected, last step, last seen; and from the desk: how many tickets they wrote and how many were feedback (a ticket whose source is "feedback", the app's Give feedback door), counted through every session the tester has ever had, so a seat that was freed and claimed again keeps what was written under the old one. The page reads it all again every 30 seconds.
- The summary line above the table is the funnel: on the list, invited, opened, signed up, bank connected, tickets, feedback.
- The landing form. The marketing site's test page posts name and email to
POST /api/testers?signup=1; the person lands on the list with source "landing". IfTESTER_AUTO_INVITE=1is set the invite goes at once, up to a small number a day from one computer and to one address, and a total a day for everyone (5 Nov 2026). Past any of these, or with the switch off, it waits for Send invite, and the team's email says so. Test addresses are never mailed either way. - The onboarding feed. Every screen of the onboarding in the web front end is posted to
POST /api/onboardingwith what they have typed so far (name, email, business type, VAT, situation, worries, business name, industry, how they heard, what made them look, and the link that brought them) and four events: signed up, invoice sent, bank connected, finished. Signal keeps it on the customer row: the last screen, the last 200 screens with times, the state, the events. The step only ever climbs, so going back to fix a name does not move a card backwards. A valid invite token on the post joins the session to its tester and lends the tester's name and email until the person types their own.dashboard/js/onboarding-steps.jsis the one table that turns a screen name into a board step. - The live board. On signal.bullethq.com the Onboarding Ops board draws the real testers from that feed instead of the mock accounts sessions (the yellow line at the top says how many). A tester's card carries a grey "Tester · where they came from" line. The formations lanes hold real rows the moment the formations app posts one, and the mock formations cards go when it does (see Formations); Mariana's review and the benchmarks are still presentation data. Localhost keeps the whole mock.
Feedback on the desk, and the 360 test. A ticket a tester opens with the app's Give feedback door carries the screen they were on, so the desk has a Feedback chip beside Don't know (the count is every such ticket, open or closed), the line under the title reads "Feedback from the Bank transactions screen" (or, when they pressed "This isn't right" on an Ask answer, the question they asked and the answer they flagged, 29 Sep), and any ticket from someone on the Testers list carries a grey Tester word with a Testers page link in the customer panel. Part 7 of Mariana's test run (Testing) walks the whole thing as one person: join on the landing page, get the invite, do the onboarding while the board moves lane by lane, check every field reached the card (a blank one is a black spot), then write a message and give feedback and see both land here. John's prompt docs/john-prompts/onboarding-feed.md has the same list as its Done when, for the real backend.
Formations: the other flow through the same door
Company formation is its own flow, in its own app, and it lands on the same door as accounts onboarding. A post to POST /api/onboarding carrying flow: "formations" reads its screen through dashboard/js/formations-steps.js instead of the accounts table: the wizard and its five questions, then whichever branch the person took, the limited company one of twelve screens or the sole trader one of ten, then done. The step only ever climbs, a screen the table has never seen keeps the step the row had, and the two branches are never added together. On the board those rows go into the three formations lanes, deciding up to the recommendation, forming inside a branch, formed at done; an accounts row is untouched and goes into the step lanes as before. The yellow line at the top says how many of each are real, for example "Live. 12 testers and 3 formations from the feed." The Formations page is the funnel of those same rows: one line per screen with how many reached it and the median hours from their first screen, the biggest drop in a sentence underneath, and every number opens the people behind it. Where the formations app has sent nothing, the page says so in one line and shows nothing else. That flow takes no money and files nothing yet: its payment and filing screens are screens reached, its "paid" and "submitted" events mark reaching them, and Signal never says paid or filed, on the page, on a card or on a customer's timeline.
The jobs behind a formed company
When a formations flow reports itself done, Signal makes the jobs behind that company as real rows (formations_jobs, 5 Oct 2026), once per session: a limited company gets File beneficial owners with the RBO, due five calendar months from that day (the real incorporation date replaces it when the formations handoff carries it), and the three tax registrations, corporation tax, VAT and employer PAYE, with no due date; a sole trader with a business name gets the RBN1 only. Every job starts Pending. On the board card the chip says how many are filed ("1 of 4 filed", orange while one is stopped); the drawer and the customer page list them with the status, the due date and, when the agent needs something, what it needs in plain English. A person presses Ready when the data is complete; that is the one thing a human does. From there the cloud agent in the ros-agent project does the rest through one door, /api/formations-jobs, with its own bearer: it claims the ready job of that kind that is due soonest, then the oldest (the job becomes Agent has it), files it, and reports Filed with the receipt, Rejected with the reason, or Needs info with the words for the customer. A filed RBO also writes "RBO filed, submission n" on the customer, so it shows in their notes and on their page. After Rejected or Needs info, a person presses Ready again. Hold takes a job back from the agent's queue, Cancel drops it. The agent never sets Ready and never lists; staff never set Filed. Localhost shows a mock set in the same shape so the drawer can be designed; the live site shows the rows.
Formations leads: every new company, scored for the letter
The direct-mail pilot. Every weekday at 08:10 (07:10 UTC, /api/formations-leads?cron=1) Signal asks the formations app's feed for the companies the CRO registered the day before and puts each one on the Formations leads board, keyed by its CRO number: name, type, county, address, and the name sieve's verdict (TARGET, REVIEW or SKIP) with the reason. Business names are left out; a company already on the board is updated, never doubled, and never loses its status, its human verdict or its notes. A hand-scored sheet goes in the same door (scripts/import-formations-leads.js) and adds the score out of 10; the June workbook (99 companies registered 10 Jun, scored by hand before the board existed, with its mail-queue sheet of emails, phones and next steps) went in through scripts/import-june-sample.js. A sheet may carry a person's fields (human verdict, email, phone, filed by, next step, notes) and a status: on a company already here they fill a blank only, and the status moves only off new, so re-running a sheet never undoes what a person set on the board. The board shows NACE and the officers on every row (needs lookup when the feed gave none), runs newest registered first then by score, and has a card per day registered (the batch) to look at one day's list. Each company gets a five letter slug on arrival (the first distinctive word of its name, a digit added when taken) that never changes: it is printed on the letter and inside the QR. The QR points at signal.bullethq.com/f/<slug>, open to anyone, which logs a scanned event against that company, moves a new, queued or sent row to scanned, and forwards to the formations app with ?ref=<slug>. A slug nobody has still forwards, without the ref. The board has two views, Board and List (the choice is kept in the browser): the board is one lane per status (new, queued, sent, scanned, viewed, signup, won, skip) and a card is dragged to another lane to change its status (won and skip ask first; the detail's Move to does the same from the keyboard); the human verdict is the Target / Review / Skip control on the card. Every change is in the company's log.
The statuses move by themselves (3 Oct). A person sets queued and skip; Mark queued as sent at the top of the board moves the whole queue to sent once the letters are printed, and that is the last thing anyone types. From there the QR door sets scanned, and the formations app's own feed (POST /api/onboarding, flow formations, carrying the letter's ref on every payload) sets viewed when they open the app, signup when they start signing up and won when they reach payment or finish. Forward only, never back, skip never moved by a machine, one log line per change and none when nothing moved. The company's detail shows the Signal customer that feed joined to it. An admin can put a company back to new from the row menu (the linked session unlinked, the system's log lines wiped, notes kept, the reset itself logged).
Session replay: watching a tester hit the wall
The app records the screen while a tester uses it (rrweb, the open engine under the big replay tools) and posts the recording to Signal in five-second batches. Peter's line: "we're really trying to catch people hitting cul-de-sacs and not being able to work something out." Signal keeps it, plays it, and marks the moments worth watching.
The phone has no replay, and will not have one. rrweb records a web page, and the mobile app has no page to record, so a mobile session has no film. What it sends instead is its named events as plain JSON (POST /api/onboarding?events=1), which land in the same place the web app's do, so the Product report counts them the same. Anything that plays a film has nothing to play for a phone: that is the feed missing, not the person doing nothing. Since 5 Nov 2026 the phone also sends where people tap (and whether anything there acts), each screen it shows, the app's own errors, and when the app opens and goes to the background. The words on a tap and in an error are cleaned on the way in as well as on the phone: an email, a bank account, a PPSN or a long number becomes [removed]. Every post from the app also says which phone and which version of the app it is (a model name and version numbers, nothing personal), and Signal keeps the latest on the customer's row.
- What is recorded. The page as it was drawn, every mouse move, click, scroll and page load, for one browser tab visit from open to close. What is not: anything typed into an input, and any figure marked to be masked (money on a customer's accounts) is stars before it leaves the browser. Invite tokens and customer keys never appear. Invited testers are told in the invite email; the marketing site records only after yes on the cookie banner.
- The mouse leaves a trail, and every click throws a ring. The pointer is a red ring with a fading tail behind it, so you can see where they went; a press draws a ring that grows and fades where they pressed. Clicks are read from the recording itself, so what you see is what they did.
- One page load at a time. A visit can run for hours over a dozen page loads. The player draws one load, with the others as numbered buttons above it, so it opens in a second instead of a minute and does not jump every time the page reloaded. A flag on another load takes you there.
- One visit, not three. The app stops recording a tab that has been idle 15 minutes and starts again on the next click, so a day-long tab arrives as several short recordings. The Product page merges them back: recordings of the same person less than 30 minutes apart are one visit, the usual analytics rule, so "sessions a week" and the habit line stay honest.
- Everyone, not just testers. Since 23 Sep the app records every session, so most visits hang off no tester at all. The replay page with nothing named lists the latest visits; a ticket's Watch link and the Testers page still open their own. One visit that never ends stops being stored past 40 MB, or 400 MB for a tester (since 30 Sep, so a 90 minute session keeps its end), so a tab left open all day cannot fill the store. Past the cap only the film stops: the named events and the profile are still kept, and the tester session page says when the film stopped.
- Kept until Peter says otherwise. Nothing ages out (his reminder is late January 2027); the 03:30 run only works out the flags for new replays. Staff only: every read is behind the sign-in.
- Watching. A ticket sent from the chat while the app was recording carries "Watch the replay" in its header line, opening the film at the second they wrote in. The Testers page has a Replays count per person; it opens the list of their visits. The player has speed and skip-inactive controls; the strip under it holds the flags, and clicking one jumps there.
- The flags, worked out from the recording by itself, one dot each on the strip (red for the ones that hurt, orange for the slow ones, grey for the rest):
rage click, three or more clicks on the same spot inside a second;
dead click, a click on something that is not a button or a link, and nothing on the page changes;
ping-pong, the same two screens back and forth three times;
stuck, thirty seconds on one screen with the mouse moving and not one click;
bounce back, a page opened and left for the one before inside five seconds;
form abandoned, typing on a form, then leaving the screen with no submit;
empty search, typing into a search box, then leaving inside ten seconds with no click;
hesitation, the pointer crossing a button three times, or sitting on it three seconds, before the press;
page error, a script error in the browser. The numbers are constants at the top of
api/_lib/replay-flags.js. - Task scoring. When a tester picked a task (one of the app's guides: "Match a tax payment", "Send an invoice") the recording is stamped with it, and Signal compares what they did with the guide's ordered steps, screen by screen and click by click: finished or not, how long it took, the step where they first left the path, and the seconds spent off it. The answer key is the app's own guides file, read fresh every ten minutes. The score sits under the player and, per tester, as "finished of stamped" in the Tasks column.
- Later. Day-to-day customers on the same store, the same masking; heat maps once there are hundreds of visits per page.
Proposal reading. When a client opens a customer's proposal link, their reading of it is recorded the same way, quietly, with nothing kept in the reader's browser (Peter dropped the consent line on 22 Sep). The customer who sent it sees "opened N times" and watches each reading inside the app, on the app's own page; the customer never opens Signal. The sender's own previews are left out of the count. Every replay is kept, tester, customer and proposal, until Peter says otherwise (his reminder is late January 2027); when the app deletes a proposal it tells Signal and the readings go with it. Their key binds to the proposal the first time it is used; any other key is refused.
The tenth flag, and their voice. When a customer presses Record in the app, the replay keeps running and the moment is marked "Pressed Record" on the strip, ranked with the other nine on the Product page. The recording already holds what they said, so the ticket's Watch link opens the film with "Play with their voice": the sound of the recording plays under the film from the second they wrote in. Nothing is uploaded twice.
Tester sessions: a real test, watched and repeatable
Two short tests, never one long one (Peter, 29 Sep, after Mariana's session: she loved the onboarding and was underwhelmed by the product). At Start you pick the test and the tester's business type. The Onboarding test is an empty account (Brand-new customer): from the invite through the onboarding to matching the €1 in Bank, their first real money; for a limited company, also one business trip for the director (the first-trip card under Quick wins on Home; the founder is a person not on payroll yet) and its tax-free total, then Connect your Gmail to claim what they spent before the company. Every tester then makes a proposal by talking to it (the ta-da, in the test before it is finished, to see how long it takes) and presses Finish. The €1 match, the Gmail claim, the trip, the spoken proposal, the spoken Ask and the expense tick by themselves from the app's events. Finish is the reviewer's note. The Product test is demo data (Running business): the voice Ask, one question about their money out loud; for a sole trader, one expense with a receipt. Support is one step at most: one message to Mariana. The page shows only that test's steps, and the live check and the review follow it. Both tests end with four wrap-up questions, typed into the notes, and the tester pressing Stop on the camera in the app (a page change does not count). Sessions from before 29 Sep keep the old 22 steps.
Peter: "treat it as a real test and make it repeatable". A reviewer and one tester on a call for about 90 minutes, on Testing, the Tester session tab. Each test shows only its own steps; a session from before 29 Sep keeps the old 22-step script. Each step says what the tester does, what working looks like, and what Signal should receive.
- Start. The reviewer picks the tester from the Testers list (or types their email or session id), ticks Dry run or not, and presses Start before the tester opens the invite link. Only a tester can be picked, so a customer is never in a session. One session runs per tester at a time. End stops it; Reopen starts the clock again.
- The live check, by itself, every 10 seconds while the page is open. Signal reads what it holds for that tester since Start (the onboarding row and its screens and events, their recorded visits and flags, their tickets and replies, the app's named events, the camera pieces) and ticks each step: grey waiting, green arrived with the time and a link to the row, red late. A recorded visit counts only when it began at Start or in the two minutes before it, so a visit left open from earlier is not part of the session (5 Nov 2026); the review, the export and the check that the tester has finished follow the same rule. A step turns red when it has not arrived a set number of minutes after the step before it arrived (each step has its own number), and only when it is the next one due or a later one has already arrived. After End, anything that never came is red. This is code, not a model:
dashboard/js/test-session-rules.js, tested inscripts/test-test-session.js. - The clean view. Links on the session open the board, the desk and their customer page with only that tester's rows since Start, under an amber strip that says so. Nothing is deleted; Show everything takes the filter off.
- Camera and voice, tester only. After the tester says yes in the app, the app sends their face and voice in five-second pieces to
/api/replay?media=1(voice only if they say no to the camera). Signal refuses any session that is not a tester's, before a byte is stored. Each page load in the app starts a new piece of film, so about 1 second at each page change is missing (the camera opening again); the timeline shows a small orange mark there. The session page plays the pieces one after another on the session's timeline, beside the steps and the flags, with the replay at the same second one click away. Kept 90 days, then the 03:30 run deletes the files and their rows (TEST_MEDIA_KEEP_DAYSchanges the number). - Transcribe the voice (admin, after the session): each piece of film is sent to the transcriber (OpenAI whisper-1 for times, or ElevenLabs), and what they said is kept on the session with the time it was said. Clicking a line plays the video from there. It asks for English and drops what whisper makes up in silence (stock sign-offs, other languages, a lone "Thank you." with nobody talking). Pieces are about 8 MB; a retry carries on where it stopped, a piece the transcriber cannot read twice is skipped and named, and an older transcript starts again on the next press. When the fault is the voice service's own (out of credit, the key turned down, busy, or no answer in time), no piece is skipped: the voice text waits, says why in plain words, and carries on by itself from where it stopped once the service works (4 Nov 2026). Pieces skipped for that reason before 4 Nov are read again.
- Review this session (admin, after the session): the facts are counted in code first (time on each step against the time expected, pauses over 20 seconds read from the recording and the screen they were on, onboarding screens held over a minute, the flags, the chat, the questions in the Ask box, what they said around each moment, the evidence that was late or never came, the reviewer's note). Sonnet 5 only words them: per step what happened, the likely friction and a suggested fix, then the top five to fix. Any sentence with a number the facts do not hold, or a word about how the tester felt, is dropped. The model never sees the video: the EU AI Act restricts reading emotion from a face, and the science is weak. Everything it says is a suggestion for Peter to confirm.
- Notes and Send. Every step keeps the reviewer's note and mark, per session. Send the note puts it in the web front end's or Signal's inbox, as on the Tasks tab; the onboarding's notes go to the web front end, which owns it since 29 Sep. A note can run to 20,000 characters and the box grows as you type.
- Dry run. Tick it to prove the pipeline without the camera: Peter, or a Claude agent walking the script as a test tester. The camera steps are left out and everything else ticks the same way.
- The pre-flight probe, for an agent with no Signal sign-in. With the project token (
x-snapshot-token, as indocs/TASKS-INBOX.md):POST /api/test-session?probe=1with{ sessionId, dry }seats a throwaway tester ("Pre-flight probe", an example.com address, never mailed) on that browser's session id and starts its session;GET /api/test-session?id=<n>answers the facts and the live check;&media=1the camera pieces;PATCHends it, and transcribe and review run on it too, so the whole pipeline is proven.DELETEremoves the session, its camera files and the probe tester, and takes back the tester stamp on that browser's row. The token can touch nothing else. In the app,?testrec=onon any page turns the tester recorder on for that tab.
Phone tests: the Bullet app on a phone
Peter, 5 Nov 2026: "A, mobile testing first". A test of the phone app has no link to send and no screen replay. The phone tells Signal what happens while the test runs, and the phone records its own screen and the tester's voice. The tests are on Testing, Run a test, in the Mobile app group.
- Start the test. Pick the test and the tester and press Start the test. The session card shows a short code, large, and the steps to read to the tester: open Bullet on the phone, go to Settings, press and hold Version, type the code, check that Test on shows at the top, then start the phone's own screen recording with the microphone on. Advisers never see the code.
- The code. It works only while the session runs. Every post from the phone then carries it, so the phone's screens and taps belong to this session, and the line on the card goes from Waiting for the phone to Phone connected, with the taps so far. The phone is put on this session's own list, not only on the tester, so a shared office phone that was someone else's in an earlier test still shows up in the right one. A phone in a test counts against its tester, as a web tester's visits do. After End the session the phone is told the test is off, and the note under the code says the code no longer works. A code Signal did not make is refused, and Signal counts the codes each connection tries, so codes cannot be guessed.
- What the phone sends. Each tap (where, on what, and whether anything there acts), each screen, the app's own errors, and when the app opens and goes to the background, with the phone and the app's version. From these Signal works out the screens in order with the time on each, the places tapped again and again, the taps on something that did nothing, and the errors. The test page, the review and the customer page all use that one rule.
- Add a recording: it goes through Signal. After the test, Add a recording on the session card takes the phone's screen recording. The page sends it to Signal in parts, one after another, with a progress bar, and Signal puts each part in the private store; nothing goes from the browser straight to the store. A part that does not go through is sent again by itself. Stop sending asks first, then tells Signal to throw away what it has, and the video is not added. A video that was started and never finished is let go by itself after a day. Advisers never get a recording.
- The voice text. Once the video is in, Signal reads its sound in ten-minute windows and the transcriber turns each one into lines with their times. Every sound track is heard: an iPhone keeps the microphone apart from the app's own sound, and Signal mixes them. The lines are placed on the video's own clock, from the time the phone stamped on the file when that falls inside the session. A long video is read in turns, every ten minutes, until it is done, and a turn with nothing to do changes nothing. When the voice service or the store does not answer, nothing is skipped: it waits, says why in plain words, and tries again later. A window that keeps failing for about a day is left out and named, so it cannot hold the rest up; a spent credit or a refused key never counts against a window. The video is kept as long as a tester's camera pieces are, then deleted; the text is kept.
- The review waits for the recording. On the phone, the tester's words come only from the video. So an ended phone test is not reviewed while it has no video yet, or its video is still being read, for up to two days after it ended; then the review goes ahead with what there is. On Results a phone test is Test not done until its video is added (it says "No phone video yet. Add it on Run a test."), then Test done, to review.
- The test page. The phone video plays on its own clock, the taps are listed screen by screen with the trouble marked in plain words, and the counts are Signal's own. A time clicked before there is a video says how to add one. A phone test skips the web screen check: it has no web screens.
The marketing site: website visitors, before they are anyone
The marketing site records a visit with the app's own recorder, but only after a yes on its cookie banner, so the Website tab on Overview is a sample, not everyone. A visitor is anonymous: an id their browser makes once, mkt: and a random string. The replay door takes these with no customer behind them, so it has its own fences (api/_lib/marketing.js): the browser must say it is the marketing site, one visitor sends at most 400 batches a day, one visit stops being stored at 8 MB, and all website visitors together stop at 20,000 batches a day. Over a fence the recorder is told ok, so it never retries into it. The same ten flags are read from the film (overnight, or when the page opens). The page says where people leave, which parts of a page they never reach (the site sends a section event the first time a part comes into view), where they press Sign up (a signup event), and the time on each page. Website visits never count in Product health or the replay list. The Sign up links carry the visitor's id into the onboarding as vid; when it arrives, that website visit shows as Joined, linked to the new customer. Google Analytics' landing pages sit under it, read with a filter on the content group New marketing site (the property also carries the old app's pages), and since 24 Sep Google Analytics too loads only after a yes on the banner.
The marketing brief: what is going on and what to do
Every Monday at 06:30 Signal writes a short brief at the top of the Marketing tab on Overview: what is going on, what is working (keep doing it), what to fix, and what to do next. It reads 18 months of Google Ads (through Google Analytics, linked since January 2024: spend, clicks, cost a click, and key events per campaign, ad group, keyword and search term), Google Analytics, Search Console and Stripe subscriptions. Every rate and change is worked out in code first (api/_lib/marketing-pack.js), and the brief compares them with 38 published industry benchmarks, each with its source (docs/marketing-benchmarks.json; they are US or global, not Irish). A check in code drops any point whose number is not in the data or the benchmarks, and the page says how many were dropped. An admin can write a new one at any time. Google Ads key events today are the old website's forms; the new onboarding needs its own before it goes live.
The website's Ask bot: answers from the site, or a person
The marketing site has an Ask bubble. It answers ONLY from a knowledge pack Signal builds: the site's own pages, Peter's own answers (ask-knowledge.json on the site), and any Website answer on the Answers page that is active. Every hour at 15 past, Signal fetches them again and changes the pack only when a word changed. The answer is one to three sentences with a link to the page. It answers product questions only. A tax or accounting question (VAT, what to claim, a Revenue letter, returns) gets one fixed line in code, never written by the model: that is one for the chartered accountant every customer has, sign up and ask them; the bubble then shows Sign up instead of the email box. When the pack does not answer a product question, the bot says so in one line and offers a person; it never guesses. A second check runs in code on every answer: each euro figure and each link must be in the pack, or the answer is thrown away and the person is offered instead. The model is a setting (Haiku 4.5 unless an admin changes it), and an admin can run a test set through any model and see pass or fail per question. Every question is logged with the page and whether it was answered. A visitor who leaves an email becomes a ticket under the Website chip on the desk; the reply goes to that email and their answer comes back onto the ticket. Every Monday at 07:00, last week's unanswered questions land on the Answers page as Website drafts, one per topic. Nothing publishes by itself: someone writes the answer and presses Make active, and the bot knows it within the hour. Caps: 20 questions an hour and 80 a day per visitor (a visitor with no cookie id is counted by a daily hash of their address, never stored raw), 3,000 a day in all, 5 hand-offs a day each.
Product health: what is broken, what is loved, what is ignored
The report at Product (left menu, near the top), six tabs: Onboarding, Habit, Friction, Features, Power users, Asks and support. Peter's rule for it: every number is a sentence with a "so what", every number opens the evidence (the replays, the tickets or the sessions behind it), trend not snapshot, split by the shape of the company, and nothing about a person. The words below are the definitions the code uses; the thresholds sit at the top of api/_lib/product-rules.js.
- What the app sends. With every replay batch, the company profile (entity, sector, VAT, employer, size, age, source, device, plan: never a name or an email) and a few named events inside the recording itself: the screen opened, a feature started, done or abandoned, an Ask bar question and whether it was answered, a search and how many hits, an invoice reminder sent or an invoice paid, a proposal sent, won or lost, and the second the customer pressed Record. Signal pulls them out the moment the batch lands (the
track_eventstable) and keeps the latest profile on the customer. The phone app's taps, screen changes and errors (see Phone tests) are left out of what this report reads, so a busy phone never pushes the features, questions and searches it counts out of the picture; the Monday digest reads the report the same way. - The words. A session is one recorded tab visit. A customer is one session id (one company). Active in a week means at least one session that week. A week is the last seven days, then the seven before, and so on; the trend under a line is this week against the average of the weeks before it (up to four; a four-week window has three), and it is flat inside 10% either way.
- Onboarding. The funnel: opened the link, signed up, connected a bank, categorised the first transaction, matched the first receipt, sent the first invoice, ran the first payroll, filed the first return, came back in week two (a session 7 to 14 days after sign-up). Each step shows the share of sign-ups that reached it and the median hours from sign-up; the number opens the ones who got to the step before and stopped. The story names the biggest drop.
- Habit. Retention by sign-up month (still active in week 1, 4 and 12 after sign-up; a week is shown once everyone in that month has aged that far), sessions a week per active customer, and the habit line: the share of customers who had three sessions in one calendar week inside their first 28 days. Over 40% is a product that stuck.
- Friction. The replay flags (the ten kinds in the Session replay section above, Pressed Record among them), grouped by what they were about (the button for a rage, dead or hesitant click, the message for a page error, the screen for the rest), ranked by customers hit times how often, never by raw count. Each line opens the newest replay at the second it happened, with the others listed under it. Under that: the words people searched for and found nothing (the vocabulary we are missing) and the features left half-done. Every screen is named in English here and everywhere else (Bank transactions, Payroll pay employees, Profit and loss report), never the id the app files it under, and a screen Signal has never seen still reads tidily rather than wrongly.
- Features. One line per feature: the share who ever used it, the share of this week's active customers who used it this week, the median days from sign-up to first use, the abandon rate. Three bands: power (40% or more weekly), quiet (used before, by nobody in four weeks), ignored (under 10% ever); the rest in between. "Proposals that win" stays on one grey line until twenty proposals have a won or lost outcome.
- Power users. A score per customer: distinct features used times sessions a week. The top 10% and the bottom 10% (nothing is said under five scored customers), the features that separate them, and for the bottom what stopped them: their flags and their tickets. A line opens the pseudonymous session list, never a name.
- Asks and support. Ask bar questions asked, answered and not, with the top unanswered texts; tickets per 100 customers a week by screen; the median first reply; the share of tickets that arrived with a replay or a video; the top topics from Monday's Patterns; and reminded invoices paid inside seven days against those paid with no reminder, the first "Bullet made you money" line.
- What runs by itself. Ingest on every batch. The 03:30 replay run works out the flags for every recording that has none yet (oldest first, inside 50 seconds), so the report has flags without anyone watching. Each tab's answer is kept ten minutes; Refresh on the page recomputes it. The Monday digest carries three product lines: the biggest onboarding drop, the top friction row, the feature that grew most, or one line saying there is not enough data yet.
- Not yet. Where a source does not exist the line says so in words instead of showing zero: sign-up dates for everyone, the plan, and invoice paid from the ledger come from John's backend later (
docs/john-prompts/product-health.md). Heat maps, A/B tests and revenue wait.
Demo rows are not counted
Signal's contract says a session id is the company UUID, one company one row. Authenticated clients now use that UUID; the static demo profile switcher can still make connor, boing or personal rows when no real login is present. Signal keeps those demo rows out of every number: the six product tabs, the board's own counts and its live banner, the formations funnel, the Testers funnel, the Ask pack, the roster count on Messages and the Monday digest. The same goes for an id that starts test-, mock-, anon-, id-, demo- or probe-, for journal-demo, and for a proposal: reader, who is somebody reading one proposal and never a customer. One list decides all of it, dashboard/js/demo.js, and the same list decides who is never emailed. Nothing is hidden from you: a demo row is still on the board, on the desk, on the replay list and on its own page, it still opens and plays, and the search still finds it. It carries one grey word, Demo, and where a count left some out the page says how many and why in one grey line. Since 24 Sep you can also look at them on purpose: the Showing menu in the top bar switches the whole of Signal between Real only, Demo only and Everything, the bar changes colour so you always know which one you are in, and nothing is deleted to do it (see Demo data).
Demo data: one mode for the whole of Signal, and the seed
Look at the colour of the bar. That is the whole of it. Signal is in one of three modes, the same mode on every page, and the bar tells you which without being asked: Real only is the bar as it has always looked, Demo only turns the whole bar the yellow highlighter and says Demo data, and Demo and real together turns it amber and says so, because a view with both in it is the one most easily misread. A page with no top bar of its own, a customer, a replay, Testers, wears the same colour as a thin strip along the top. If the bar is white, every number on the screen is real people.
The control is one Showing menu in the top bar, beside the dark mode button, and it sets the mode for the whole tool: the board, the support desk, the accountant view, the Product report, the formations funnel, Testers, the replay list, the customer page, Messages, Answers and Tools. Set it on the desk and the board is already in it. Change it in one tab and your other tabs follow. It is one setting on purpose: a picker per page is how somebody ends up in demo on one screen and real on the next and reads a made up number as people. Real only is the default, a browser that has never been told otherwise is in it, and the API behind every page takes the same word, &data=real|demo|all. The Ask bar answers in the mode you are in and says which one when it is not the default.
Demo only is the sample world (28 Sep). In Demo only every page shows the same linked set of made-up customers, the one the prototype has always had: a name on the board is the same person on the support desk, the accountant view and their customer page, and every link between them lands. Their billing uses the real price (€49.99 a month, a 14-day trial) and anyone who has finished onboarding is an accountant client with Revenue details. Nothing in Demo only reads from or writes to the live store, except a note preview on the accountant view, which only reads. A reply, a nudge or a snooze made in Demo only stays in that browser. Use it as the benchmark for how Signal should behave; Real only is the live data held to it.
Nothing is ever deleted to change the mode, and nothing is hidden from you when you go looking. A row out of the mode is out of the list, not out of the database. A ticket, a client, a replay or a customer opened by its own link still opens and still plays, with the grey Demo word it already carries, and the customer search finds a demo person in any mode. On localhost the mode colours the bar but leaves the rows alone: every row in the local prototype is the presentation mock and carries the same test ids, so filtering it there would empty the screen and tell you nothing true.
Adding demo data. Settings, at the bottom, admin only. Add demo data writes one batch: about nine customers spread across the onboarding steps, one of them on the formations flow, three tickets, two replays with a flag or two on them, and one tester. Every row is stamped with that run, demo-<run>-<name>, every address is an example.com one, and nothing in a batch is ever emailed or ever counted as a person.
Taking one back. The same block lists every seed run with the day, who ran it and how many rows, and offers Undo this seeding on each. That is the only removal in Signal, and it can only take back the rows that one run wrote. There is no clear-everything button anywhere, on purpose: a delete cannot be undone and keeping the rows costs nothing. An undo is filtered in SQL by two things at once, the demo rule above and that run's own stamp, it reads back every row it would take and checks each one again before it deletes anything, and if a single row it matched is real or belongs to a different run it stops dead and removes nothing. A real client's proposal reading is outside all of it and can never be taken by an undo.
One customer, one page
Everything Signal knows about one person, in one timeline, newest first: /dashboard/customer?session=<id> (also ?tester= and ?email=). The desk's customer panel, the Testers name, the replay header and the Product sessions list all open it. Staff only, and read live every time, because you open it to do something about what it says.
What happens at sign-in. Before an account exists, the browser has an anonymous id. The authenticated app then receives the company's permanent UUID and calls POST /api/customers?merge=1. The call must carry a signed proof from Bullet's accounts system that the company is the one signing in, or come from the team; without one Signal refuses it and nothing moves (5 Nov 2026). Signal moves the anonymous tickets, onboarding, recordings, messages and chase history onto that UUID in one database transaction and remembers the old id as an alias. A late event from the page before sign-in is therefore filed on the same customer instead of recreating a second row.
- The header. The name and the business if a record holds one, the email, the session id, the tester they came in as, the day they signed up and when they last did anything. Then the nine profile fields the app sends. Anything Signal does not hold says "not known"; it never shows a guess and it never shows a zero that looks like a fact.
- The three lines. What they have done (how many of the fourteen watched features they have used, named), where they got stuck (their commonest replay flag, with a count and how many visits it is across), and what they asked (their tickets, how many open, and their Ask bar questions, how many unanswered). Each one is computed from rows; when there is nothing behind it, it says which feed has sent nothing rather than showing a nought.
- The timeline. One row per thing, each a sentence with its time and a link to the evidence: the onboarding steps from the web front end's onboarding, every recorded visit with its screens, how long it ran and what it was flagged for in grey (one visit is one row, never a row per flag), its link opening the replay at the first flag, every ticket with its number and status (to the desk), the named events that matter (a feature finished or left half done, a question asked, a search that found nothing, an invoice reminder, the second they pressed Record), every message we sent and whether they opened or clicked it, every journal posted against them, and the chase agent's ladder state with how many notes went out.
- The Mobile app card (5 Nov 2026), above the timeline, when the phone app has sent anything for them: the platform, the phone and the app's version (from what the app sends with every post), when they last used it, the screens they opened most, the places they tapped again and again (the same rule as a phone test's review, so the two never give different numbers) and the taps on something that did nothing. Advisers see it too: it is counts and screen names, not a recording. The phone's taps are counted on their own, so they never push anything else out of the timeline. In a tester's clean view the card says its counts cover all of the phone's use, not only since Start, because a phone keeps its id from one test to the next.
- Bounds. Every query is bound to that one customer with a limit, and the newest 500 things are drawn. When there are more, the page says the rest are older, not missing. Nothing is cached: this page is always current.
- It adds no table. It reads what the rest of Signal already stores. If a feed has sent nothing for someone, their page says so in that many words.
With nobody named, the page is a search. Open /dashboard/customer on its own and you get a heading and one box: a name, an email or a session id. It searches as you type, over the roster, the testers and the customer on every ticket, and shows at most twenty people, one line each, best match first: an exact email, then a name that starts with what you typed, then a name that contains it. Click one to open them. Nobody matching says "Nobody by that name yet." rather than an error. Once somebody is open the box stays at the top, so the next person is one search away. The Ask bar does the same lookup from any page: type an email address, or "who is", "show me", "find", "open" or "everything about" and a name, and it answers straight away with one sentence and a link to their page. Two to five people come back as a short list; more than five asks you for their email instead; nobody says so plainly. Every clause of that sentence is a field that is set on their record, and a field that is not set is left out rather than guessed. Under two characters nothing is read at all.
Ask reads everything
The Ask bar at the top of every page used to see only the onboarding board, the tickets and ROS, so it could not answer "where are people getting stuck". Now it reads a pack built by /api/ask-pack: the whole product picture, computed server-side from real rows and kept five minutes.
- What is in the pack. The six product tabs exactly as the report computes them (the same builder, never a second copy of a definition), the friction list with its screens and counts, the feature bands, the funnel step by step with the biggest drop, the flags by kind, the ticket counts by status, channel and source, the Patterns topics, the Answers library titles, the tester funnel, the segment counts and the power score. Session ids only: no name and no email ever enters it.
- The wider picture (9 Oct 2026). A test of 383 questions found most of the ones Signal could answer were missing from the pack, so it now also carries, each block counted in code with a link to the page that lists its rows:
- The desk: the counts the desk's chips show (Open is open and not snoozed, the queue; All is every ticket in the view) and, named apart, every ticket row Signal holds, demo ones too. Until 9 Oct the pack only had that last one, so Ask said 48 where the desk said 17. Then tickets by week for 12 weeks, first reply time (the first staff reply after the customer's first line) and time to close as a median and a 90th percentile for the last 7 days, 30 days and all time, how old the open ones are, the backlog on each of the last 30 days, waiting on us and on them, flagged, reopened, opened by staff, tickets a customer, tickets in a customer's first week, hand-offs to the accountant and a developer with how long they waited, and the saved answers used on replies. Under three values a median says too few to tell. Desk tickets are test data until the new web front end is live, and the pack says so. Ticket by ticket (since the fourth run, 9 Oct), each row also carries its topic (the Patterns group, else the desk's own keyword theme, one list shared with the desk page), who replied and closed (a Bullet address not on the team list reads by its first name), and the customer as a pseudonym with their Stripe plan and how far they got in the onboarding, never their name.
- Stripe: the last 12 months from the monthly report's own rules, with the MRR moves (new, back, more, less, lost) worked out from the same MRR at each month's start and end so they add up to the change; customers by the month they first paid and how many still pay 1, 3, 6 and 12 months on; the prices and intervals the paying customers are on; failed charges by Stripe's reason, and cards running out.
- Marketing: the marketing pack's spend, cost to win a customer, revenue a customer and lifetime value by month, the campaigns, and why people cancelled (counts only, no comments). Also Search Console (since 9 Oct): unpaid Google clicks, impressions and average position by month, and the top searches over 90 days, which Ask picks for any question about search, SEO, keywords, impressions or ranking.
- Product use: customers active each day for 28 days, each week and each 30 days, and visits and active days per customer. Test data until the new web front end is live.
- The chase ladder: failing checks by kind and step, flagged, snoozed, notes recorded, days failing. Sample data: the checks run on sample books until real client books arrive, and the pack says so.
- Google Analytics: sessions, new users and engaged share by week, and the last 28 days by channel, device, country and landing page, with sign-up key events left out from the day the tracking broke.
- The desk, ticket by ticket (9 Oct, the second test): one short row for every ticket opened in the last 90 days: its number, when it opened, minutes to the first reply and the first name of the staff member who sent it, when it closed and who closed it, open, snoozed or closed, the channel, the Patterns topic if the run read it, how many replies, whether it went to the accountant or a developer, and how long it has waited. Also per staff member, today and the last 7 days, and the longest waits. Signal has no owner field on a ticket, so "who replied" is who answered, not who owns it. Staff are named by first name; customers never.
- Stripe in detail: free trials started and ended each month and how many of those went on to pay; every failed card payment by invoice and whether a later try took the money, with the euro asked for, recovered and not; what each month's new customers have paid since and how much of the MRR they started on is still coming in (net and gross revenue retention). The MRR they started on is each customer's first full billing period: the first invoice that is not introductory (Stripe discounted it, or it was under half the list price and a later invoice reached the list price), or the next one when that is larger (a part first month). Until 9 Oct it was the MRR at the end of the first month, and a EUR 5 first invoice made the Dec 2025 to Apr 2026 cohorts read 333% to 1000%; the first fix looked only one invoice ahead, and those customers had paid several EUR 5 invoices, so it still read EUR 5 until the fourth run. An upgrade (EUR 49 to EUR 89) is still growth. Upgrades and downgrades by month.
- Stripe customer by customer (9 Oct, the fourth run): one row per customer paying now or who stopped in the last 12 months, with their plan, MRR now, first paid month, months paid, failed payments and whether they still pay. The customer is a pseudonym, never a name, an address or a Stripe id, and the same customer reads the same on the desk's ticket rows. Built when a question asks about customers one by one. Every rule is the monthly report's own.
- The sums worked out: the ratios Ask would not work out itself, each with its definition and the counts behind it: the SaaS quick ratio, revenue churn (MRR lost) named apart from customer churn (customers lost), sign-up to paid, website visits per sign-up and visit to paying customer, DAU/MAU and WAU/MAU, and tickets opened and closed today and in the last 7 days.
- The two ticket counts: the page sends its own figures too, which count every ticket row (demo ones included, open meaning status open, snoozed too); the desk's chips count the tickets in your view, open meaning open and not snoozed. The pack names both, so 25 open of 48 and 6 open of 10 can both be true and Ask says which it means.
- Built for the question (9 Oct). The whole pack had grown past its cap and was cutting rows a question needed (the oldest Stripe months, the friction row behind "Failed to fetch"). Now the pack is built per question: a small core every time (the counts, the window, the worked-out sums, the page links), then the blocks the question's own words, the page and the funnel point at, each with all its rows, then whatever else still fits. A block left out for room is named in the pack, so it is never read as empty. Every block is still worked out once and kept five minutes.
- Bounded. About 46 KB (40 KB until 9 Oct), under the 48,000 characters the model is sent. When it would be bigger the longest tails go first and the pack carries a list of what it dropped, so a cut tail is never read as an empty one. A caller with no question gets the old pack, without the two row tables.
- No names, checked word by word (9 Oct). What a customer typed (empty help searches, questions in the app's Ask bar, anything quoted from them) goes through a scrubber before it leaves: email addresses, phone numbers, bank accounts and PPSNs, the first names and surnames of customers and testers, a Stripe name only when it reads as a person (never a business name like "Real Builders Ltd"), a list of common Irish first names, and any name-like word standing next to one of those or next to a name already taken out. Signal's own words (the view, statuses, column headings, definitions) are never touched: the first version blanked "Real" and the desk's status column, so every answer opened with a garbled line. Short words and everyday words are never treated as names, nor are Bullet's own names and products or the accounting brands people search beside it (since the fourth run a Search Console search for a brand came out as "[a name] software"). Staff first names stay.
- Ten questions answer straight from it, with no model call at all: where are people getting stuck, what is the biggest drop in onboarding, which features are ignored, which features are loved, how many rage clicks this week, what are people searching for, what are the top unanswered questions, how many replays this week, who are the power users, what changed since last week. Each answer links to the page that proves it.
- Anything else goes to the fenced model in
api/answer.js, which may read the pack and nothing else. It is told to say "I do not have that" when the pack does not hold the answer, never to invent a number, a trend or a cause, and to finish by naming the page you should open, which becomes the show-me link. - When the pack cannot be read (you are signed out, or the door is down) the answer says exactly that. It never fills the gap with a number.
Stripe, once a night and on every change
Signal reads all of Stripe itself once a night, from 03:00 UTC: the customers, the subscriptions, the invoices, the refunds and the charges (every ten minutes until 8 Oct, when the webhook below had proved itself). Bullet has been on Stripe since 2013, so the first read of the history took a while; each run works through what is left and then costs seconds a time. New customers and new invoices only, but every subscription every time, because a subscription goes past due or cancels without anything new being created. A run does 50 seconds of work and remembers where it stopped, so the cron fires four times in the night window (03:00, 03:15, 03:30, 03:45 UTC): a read that did not finish carries on, and once the night's read is done the later runs do nothing. Stripe also pushes: when a subscription changes or an invoice is paid or fails, Signal is told inside the minute and updates that one person, so the numbers do not wait for the night. The Overview's header says when the last full read finished and when the next one runs.
Since 28 Sep Signal also reads every charge (paid or failed, the last two years, then what is new): the card it used and, when it failed, Stripe's own reason. The support desk's Billing tab now shows the customer's real Stripe record, found by their email: plan, status, the card on file and why the last payment failed. It used to be made-up data. The Monday failed-cards check lists the reasons too.
Stripe also tells Signal the moment a subscription, an invoice or a customer changes (the webhook). Stripe sends it in the account's old 2013 format, so Signal uses it only to know which one changed and reads that one again from Stripe in the current format. If that read fails, nothing is written and the next pull brings it in. The webhook needs the signing secret in the Vercel setting STRIPE_WEBHOOK_SECRET; without it Signal refuses every event.
When a card fails (John, 5 Oct 2026). Stripe's own recovery is on for the live Bullet account, and Signal only reads the result:
- Smart Retries: a failed card payment is tried again up to 8 times over 2 weeks, at times Stripe's model picks. This replaced the old fixed retries 3, 5 and 7 days apart.
- An email to the customer when a card payment fails, from Stripe.
- A Stripe link to confirm a payment when the customer's bank asks for it.
- The card updater: Stripe keeps trying to swap in a replacement card from the bank when one is issued.
- If every retry fails, Stripe still cancels the subscription and the invoice stays overdue. That is a "failed card" in the monthly report, never "chose to leave".
- It works from 5 Oct on. It does not bring back subscriptions that had already ended. So in the monthly report, compare cards failing for good after October with the months before.
A Stripe customer is matched to a Bullet customer by email address, lower cased, against the customer list and the testers. If the email is blank, or nobody here has it, no match is made and the row says which of the two it was. Signal never guesses whose money it is. On a match, the billing block (status, plan, price, started, next renewal, trial end, lifetime paid) is written onto that customer, so every page that already shows a customer can show whether they pay. Nothing is written back to Stripe, ever: the key is read only, and Stripe never gives out a full card number to anyone.
Google counted (28 Sep). Under each month of the monthly report a grey line gives what Google Analytics counted beside Stripe: its key events Hub Signup Form (beside signups), Start Subscription Form (beside paid) and Create 1st Company Form. Google counts forms sent in a browser that allows it; Stripe counts customers and money, so the report stays on Stripe and a card above the table says why the two never match. The event counts come from the hourly Google pull (the event dimension, table ga_daily).
Check this out: the Monday email, and what I checked
Every Monday at 06:45 UTC Signal looks across Stripe, the desk, the website and the ads for anything that moved more than usual or sits on the wrong side of the industry, and emails the admins and Andrew, the marketing consultant, at most five points (more people can be added on Vercel with CHECK_EXTRA). A trial in it is a Stripe trial, someone who started a subscription with a free period in Stripe; the Overview row says how many of those trials have a card saved in Stripe today (Stripe does not keep whether one was there when the trial began), and it is not sign-ups to paid, which is Conversion in the monthly report. Since 28 Sep each point is investigated first, in code, the way an analyst would: the question it raises, the standard checks for that question run on our own data (who left and did they choose to or did the card fail, how long had they paid, did they give a reason, did they write to us first; which tickets are tests and which are customers; which cards are failing now; which questions the website bot missed; who stopped in onboarding), what the answers add up to, and what to do. The email says what is going on; its link opens that point's own page, /dashboard/check?id=, with the checks and the customers or tickets behind it, each a link. The model only writes the words and may only repeat numbers the checks counted (api/_lib/investigate.js, api/_lib/weekly-check.js).
Live or test (Peter, 28 Sep): the new Bullet web front end is not live yet, so what Signal reads from it is people testing it: support desk tickets and their patterns, the website Ask bot's questions and the new onboarding. Those points carry the label Test data, on the point's page, on the Overview's card (a grey pill) and in the Monday email after the title. The model is told to describe them as test data from the new app, never as customer behaviour, and their number is shown but not tracked week by week. Stripe and Google Ads are the current product and real money, so they are live and keep their weekly tracking. The rule lives in one place, dataIsTest() in api/_lib/investigate.js, and it turns off by itself: on go-live day set WEB_FRONTEND_LIVE_SINCE on Vercel to that date (YYYY-MM-DD). Unset, or a date still ahead, means test; from midnight UTC on that date those points count as live, and their tracking starts from that week, never from the test weeks.
The six things every report does (Peter, 28 Sep): real data counted from rows, never a guess; the industry benchmark with its source, or plainly none; what to do; the one number to track, kept every week in check_history and shown against last time (the email says it too, and Past weeks sits under Check this out); how sure, from what it was counted; and every number opening the rows behind it.
The monthly report, and the email on the 1st
The monthly report (api/_lib/stripe-metrics.js) works out every month from Stripe, Dublin time, from the first month any customer was created, unless an admin picks a later start. Revenue is before VAT and net of refunds, the SaaS standard: what was kept, not what was charged. Signal reads every refund from Stripe with the rest of the pull (a refund on the payment and a credit note on the invoice are joined to the invoice, and the larger of the two comes off, never both). Refunds come off by default; an admin can switch the setting refundsNet off to put them back. The price of the product is set in Stripe, in the product catalogue; Signal only reads it and lists the prices paying customers are on.
At 07:00 UTC on the 1st of each month Signal emails the month just ended to Peter, John, Mariana and Andrew: sign-ups, new paid customers, conversion, cancelled by choice, failed cards, paying at the end of the month, net change, MRR before VAT and CAC, with a line against the month before and a link to the report. Since 2 Oct 2026 each line also carries the industry figure with its source (the Overview scorecard's published figures) and an arrow: green up for good, red down for bad, grey for about the same (within a tenth). A count with no fair industry figure is judged against last month instead. The email says plainly that the marketing spend behind CAC is typed in, not from Stripe. It replaces the monthly email Mariana wrote by hand. It goes once a month at most: the month is recorded before the mail goes, and a failed send clears it so it can be tried again. The email also says why net change is not new paid minus those lost: a cancel counts in the month they press cancel, but they pay to the end of their time. A change from the change log shows under its month on the report, and the email lists that month's changes with the number to watch. An admin can send a preview to themselves at any time.
The customer read: who stays, who leaves, and why (Overview, Customers)
Peter, 30 Sep 2026: who our customer is, what is working, what causes churn, and what the customers who pay do differently. The Customers tab on Overview counts it all in code (dashboard/js/insight-rules.js) from three sources, joined by the Stripe customer id where the old app's export has it and by email where it does not: Stripe (real, all history), the old app's monthly usage (real, once John's export runs: docs/john-prompts/usage-export.md, sent to /api/usage or uploaded on the tab; John's job sends his own Signal key, the one made in Settings, so no shared key is emailed; up to 5,000 rows a call, and a row for the same company and month replaces the one before), and the new app's answers (how they heard, what made them look, the link that brought them, the product-market fit question, the cancel reason: test data until the web front end goes live). The answer lists are dashboard/js/customer-questions.js, the one copy both sides use.
What it counts: payers still paying 1, 3, 6 and 12 months after their first payment, by the month they started, against ChartMogul's published retention; growth in one line (started paying against stopped, the last 12 months); who left by choice against a failed card, at which stage and the share who left from it, and why; the same month last year for the tax season; what they are (limited company or sole trader, VAT) and how often they invoice, with the share who stay; and what the ones who stay did in their first two months. A group under 20 people says too few to tell: that line is Signal's own, no source gives one. The fit question needs about 40 answers (First Round).
At 07:30 UTC on the 2nd of each month a model reads the counted numbers and writes what they add up to: at most six findings, each with the evidence, what it means, what to do and how sure. The fence is the marketing brief's: a finding with a number that is not in the counts or the benchmarks is dropped and counted. It also groups customers' own words into themes, but only lists which words go where; the page counts them. A finding that leans on a group with too few people is a sign at most, and a part that is still test data is never a finding: it goes under What Signal cannot see yet. It also reads the change log: when a number moved after a change it names the change by its date, as timing, never as proof of the cause. It never sees an email, a name or a Stripe id. An admin can write it again at any time. The last box, What Signal cannot see yet, is worked out from what is missing and goes away by itself when the data arrives.
The onboarding sweep: nothing new is dropped in silence
Peter, 1 Oct 2026: the onboarding lives in the web front end and changes without Signal being told. Every screen the onboarding posts is checked against Signal's step map (dashboard/js/onboarding-steps.js), and every answer against the ones Signal keeps (api/onboarding.js, KNOWN_STATE). A formations post is checked against dashboard/js/formations-steps.js the same way. A post from the phone app (it says iPhone or Android) is not checked against the step map: the app's screens are places in the app, not onboarding steps, so the sweep never files them as new. What the phone sends as answers is still checked. A new screen shares the step either side of it, so no lane number moves. Anything new is remembered once. At most once a day, everything new goes into one task, naming each screen or answer, so the board, the customer page and the reports are brought up to date. Since 5 Nov the task is held for the team: it waits on Tools, Tasks under Held for you, and reaches the Signal chat only when a person presses Send (Cancel drops it). The names come in through an open door, so read the task before you send it. The list of new names keeps to a set size and makes room by itself, dropping names already filed and older ones first; if it is still full, the day's task says so in its title. The customer page and the board's drawer show how each person found Bullet: their answers on the heard screen and the link that brought them.
Peter, 9 Oct 2026: "Signal should know everything that's happening on onboarding." Since then Signal also keeps the formation answer (formed, being formed or not started, the date and the CRO number), the name picked on the CRO search, whether they sell stock, the RCT answer (sector and role), the business address and the bank they picked. Each answer is checked before it is kept: a wrong value is dropped, never half kept. They show on the board's drawer and on the customer page, under What they told the onboarding. The home address is private. It is kept apart from everything else, so it never reaches the roster, Ask, an export or an email; only the customer page shows it, behind the staff login. The app's setup answer (confirmed, changed or locked, with the entity and VAT) is kept as well: until 9 Oct it was dropped on arrival, so the customer list could not read it.
Peter, 9 Oct 2026: two screens follow the Scope check (a small team is now 1 to 5 people). What we are and are not says Bullet is a compliance accountant (the software plus every return filed, a monthly mini year-end) and not one you meet, call for a chat or ask for financial advice; they pick one. Anyone who wants an accountant to meet and talk to goes to Not the right fit, with a way back. Both screens share the Scope check's step, so no lane number moves. Signal keeps the answer as Fit (Compliance accountant, or Wanted to meet an accountant), shows it on the board's drawer and the customer page, and the board's Not a fit (wanted to meet) chip keeps only the people told we are not a fit.
The heard screen also sends its answer to Google Analytics (the heard_about event, two custom dimensions on the property). Every hour Signal reads it back by answer and by the channel GA4 put the visit under, and the Customers tab shows the two side by side: what people said against what GA4 tracked. GA4 only has the answers of visitors who said yes to cookies.
The first payment certificate
Peter, 1 Oct 2026. When someone reaches the onboarding's paid screen, Signal emails them the first payment certificate by itself: the web front end's own certificate page, drawn as a picture in the email with an A4 PDF attached. Only one ever goes per person, even when the paid screen posts twice (a lock row of its own, 3 Oct, after Mariana got two). It is dated the day they reached the paid screen, or a date the team gives, never the day it is sent. Their customer page says whether it went, with Send the certificate (it asks first, then sends it again) and a link to see it. A staff member can have the same email sent to themselves first as a preview; nothing is recorded on the customer. While the web front end is not live, the people reaching the paid screen are testers.
Who it goes to by itself (7 Oct and 5 Nov reviews). Only someone who signed up in an earlier step (a post that already carried the sign-up), once per email address ever (from any device), never to demo, test or Bullet addresses, and with a cap on how many go by themselves each day, across everyone and from one computer. While the web front end is not live, it goes by itself only to a tester, and only to the address that tester signed up with. Anything else is held, and their customer row says why (for example "held for the team: not the address the tester signed up with"). The team's Send the certificate still works for anyone.
Google, every hour at 40 past
Signal reads Google Analytics and Google Search Console itself: how many people came to the site, which channel brought them, which page they landed on, and what they typed into Google. The first run pulls ninety days, every run after pulls the last seven, because both services revise recent days as their numbers settle. Search Console is about two days behind, so its window stops three days ago and its numbers are always final. It all lands as one row per day per thing counted, so a day read twice is a day overwritten, never doubled.
Nothing here is a person. Google knows an anonymous browser, Signal knows a customer, and nothing joins the two until the app tells Google the session id at sign-up. The Onboarding tab of the product report says so on its first row.
The email addresses
| Address | What it is |
|---|---|
support@bullethq.com | The customer-facing address. Everything Signal sends comes from here ("Bullet Support" for service mail, "Pete from Bullet" for news). It is a Google group with one member: inbound@reply.bullethq.com. So mail sent here goes straight into Signal as a ticket, and into nobody's inbox. |
reply+<ticket>@reply.bullethq.com | The hidden reply-to on every email about a ticket. A customer hitting reply lands on that exact ticket. They never see or type this. |
reply.bullethq.com | Not an address, a subdomain. Its MX record (in Route 53) points at Resend, so Resend receives any mail to it and pings Signal. |
links.bullethq.com | Not an address. It is what email links redirect through, so opens and clicks can be counted. |
signal@bullethq.com | Retired as a sender. Nothing goes from here any more. |
Your main domain's mail belongs to Google, so Resend cannot receive at @bullethq.com directly. That is why the reply. subdomain exists as a side door, and the support group forwards into it.
One email thread per ticket
Every email Signal sends the team about a ticket lands in the same conversation in your mail app. A new ticket starts a new thread. A second message from the customer, a question to John, and any answer, are all replies in it. A question to Deirdre waits for her daily email instead (see Journal entries, the accountant's queue). The subject is always the ticket's own, like "[#54] Not sure what this transaction is", so you can search on the number.
The Monday digest
Every Monday at 7:30am the admins get five lines by email: what the chase agent did this week (sent, fixed themselves, reached a human, locked), the desk (new tickets, replies, the share built on a known answer), the library (size, learned this week), the pattern of the week, and what is open, then three Product lines (the biggest onboarding drop, the top friction row, the feature that grew most, each with its link, or one line saying there is not enough data yet). Counts from the store, no model. Since 22 Oct it is the ONE Monday email: it also carries the week's Check this out points and the AI visibility score (Bullet named in how many answers from ChatGPT and Claude, against last week). The separate Check this out email now goes only to the marketing consultant outside the team. An admin can send it now from Everything running.
Preview the note. On a flagged client, or the Note button on any row in Everything running, shows the exact note the chase agent would send that client today, subject and all. Read a few before flipping the switch.
Tasks: notes and the inbox
The Tasks tab in Tools is every line of every project's TODO.md. Each task has Show prompt: the prompt file the line links, or, when it links none, the line itself with where it sits. Each task has Notes: a box anyone on the team can write in to change the task or say how to do it. Notes are kept in Signal and shared, and a note goes with the prompt wherever the prompt goes, under "Peter's notes for this task". Only a person signed in writes that heading; a note from the living-docs agent sits under its own. A note stays with its task when the line moves in the file.
Send, not copy. The main button puts the task in that project's inbox in Signal ("Send to the Signal chat"; John's tasks go to John's Codex). Send all puts a project's open tasks in as one message, in order. Nothing runs by itself: the project's chat picks its inbox up when someone says "check the inbox" there, marks each item picked up and then done, and the task shows "Sent 14:02", "Picked up by the chat" or "Done". Cancel takes a task back until it is picked up. A task waiting on Peter cannot be sent. Copy is still there for when sending is not wanted. The contract for the chats is docs/TASKS-INBOX.md.
Held for you (5 Nov 2026). What Signal files by itself from the open onboarding door (the onboarding sweep, at most one a day) is held, not sent, so no chat acts on it. It sits in its own card at the top of the Tasks tab, with Show prompt, Send to the Signal chat and Cancel. Send puts it in the Signal chat's inbox as sent by you, now; Cancel drops it for good. Only a person signed in can do either, and an adviser cannot.
Tasks that arrive by themselves. Since 5 Oct 2026 a Claude chat can put a task on John's list without anyone signed in: POST /api/tasks takes the Mac publisher's token as well as the staff cookie, and such a task shows claude@bullethq.com as who made it (Peter: "any tasks for John, put them in the prompt and into John's to-do list"). Nothing else on tasks opens to the token: reading, moving and deleting still need a person signed in.
Where to look
| Question | Where |
|---|---|
| Why did this customer get a note? | Accountant view, Everything running, search their name. The row shows the check, the day it first failed, the step and what was sent. |
| Why is this customer flagged? | Accountant view, the Flagged tab. The line under their name says which. |
| Why did the desk draft this reply? | The draft's label names the known answer. "Open in Answers" shows it. |
| Is the chase agent live or dry? | Top of this page, and the line above Flagged by the chase agent. |
| What did the last run do? | Everything running, the line under the heading: recaps, countdowns, locks, flagged. |
| What was this tester doing when they wrote in? | The ticket's header line, "Watch the replay". Or the Testers page, the Replays count on their row. |
| Change the days or the kinds | Days: LADDER_DAYS on the server. Kinds: dashboard/js/ladder-rules.js. Ask the developer. |