← ALL PATCH NOTES

API v1, an MCP server, and sequences that behave

v0.9.7 NewBuff
  • API v1 is complete. Mint a scoped bearer token in Settings and drive prospects, deals, tasks, sequences and webhooks from your own code.
  • A 19-tool MCP server ships alongside it, so Claude can run your desk directly instead of through a browser.
  • One compliant sender for sequences: unsubscribe handling, a suppression list, send windows, timezones and per-mailbox throttling.
  • Notifications split into guild and personal streams, with click, reply and bounce tracking behind them.
  • The leaderboard splits three ways: Bounties, Guilds and The Grind, on both arena surfaces.
THE BUILD LOG

A linter suppression comment was the only evidence the feature was broken

An enrollment with no working mailbox walked the entire sequence in seconds and finished successfully, having sent nothing to anyone.

Here is the bug, and I think it is the best one this project has produced.

The sequence executor walks a prospect through a campaign. Send the first email, wait, check for a reply, branch, send the second, and so on over days. When a step could not send, the executor logged the failure and then advanced the enrollment exactly as though it had succeeded, setting the next step's due time to right now.

So an enrollment belonging to a user with no working mailbox would fail its first step, immediately become due for its second, fail that, immediately become due for its third, and walk the entire graph in a few seconds. At the end, it was marked completed. A campaign that reported success, in the ordinary way, having contacted nobody at all.

The variable holding the failure was never read. It had a linter suppression comment on it, the little annotation you add to tell your tooling that yes, this unused variable is deliberate. That comment was the only artefact anywhere in the system indicating that anything had gone wrong.

The fix gives failures a retry ladder of five minutes, fifteen minutes, an hour, four hours, twelve hours, and makes unknown errors pause rather than advance. The reasoning is stated plainly: advancing on an unrecognised failure is what caused the silent completions in the first place.

A sibling bug landed in the same session. Because every advance set the next due time to now, a sequence with no explicit wait steps fired all of its emails inside a single tick, at whatever hour it happened to be, as fast as the mail provider would accept them. The feature that was supposed to pace outreach over a fortnight was capable of sending the entire thing at 3am on a Sunday.

There were two senders and only one of them was real

The other structural finding that week is that the codebase contained two separate implementations of "send an email."

One lived in the main application. It had the open-tracking pixel, the tracking token, the company association and the activity logging. The other lived inside the sequence executor, and it had none of those things.

Which means every email ever sent by a sequence was untracked, missing from the prospect's timeline, and invisible to every report. The feature whose entire purpose is automated outreach was the one path where outreach left no trace.

And this is the part that makes the timing lucky rather than clever: the compliance work was queued up right behind it. Had that landed before the merge, it would have been built against one sender while a second, older copy carried on beside it, and the copy nobody remembered is always the one running unattended at two in the morning.

The consolidation happened first, by luck of ordering, and the executor's copy shrank by 123 lines as it moved to the shared service. One sender to improve, not two.

There is a good provider detail in there too. One mail API only accepts custom headers whose names begin with a particular prefix, which means the standard one-click unsubscribe header could not have been sent through it at all. The fix was to switch that provider to a different message format entirely. Compliance requirements meeting vendor implementation quirks is where a lot of well-intentioned email code quietly fails.

The reversal, two days later

On 2026-07-22 the unsubscribe footer became mandatory on every send, and sequences refused to run without a postal address on file. That is textbook correct for bulk marketing mail.

On 2026-07-24 it was undone, and the reasoning is the most interesting product judgement in this entry: that is the right treatment for a marketing blast and the wrong one for a one-to-one note to a client you already work with. Those are relationship messages, and stapling an opt-out to them is both unnecessary and odd to receive.

The default flipped to off. But the same commit is emphatic about what did not change: the suppression list stays unconditional on every send path. Turning the footer off never means contacting someone who asked you to stop.

That distinction is the whole thing. The footer is a disclosure. The suppression list is a promise. Making the disclosure configurable while keeping the promise absolute is the correct decomposition, and it took shipping the strict version and living with it for two days to see it.

That commit also carries a confession. A previous commit two days earlier had added a call to a function whose definition had never been committed. Production would have raised an error on the first send. The only reason it did not is that no mailbox was connected in production yet, so nothing had tried.

Eight minutes after that, a further fix: the new opt-in checkbox had shipped as a plain unstyled input, inherited the page's full-width field styling, and rendered as an enormous box running off the edge of the panel.

An afternoon of being wrong

The best sequence of that week is the 2026-07-24 afternoon, in four acts.

At 14:54, mail connection is failing in production, so add logging to find out why. At 15:12, a fix routing the whole mail path through a fallback resolver. At 15:32, a 190-line handoff document admitting that the logs cannot distinguish between the fallback not being deployed yet and the fallback running and failing silently.

At 17:53, the actual cause: a concurrency library, loaded at import time, had replaced the DNS resolver's internals with its own version, breaking every outbound lookup in the process. Not a network problem. A library monkey-patching another library at startup.

The honest coda is my favourite part. Removing that library fixes DNS at the source, so the fallback added at 15:12 is left in place, solving a problem it never solved, as belt and braces. Three hours of work kept in the tree with an accurate note explaining that it was aimed at the wrong thing.

One more, because it is perfect. Also that week, a one-character commit. A changelog header from May had been written with a middle dot instead of an em dash, and the parser only recognised two dash characters, so it silently skipped that entry and the feed served twenty-five items instead of twenty-six.

The entry it had been skipping was the one announcing that the sequence executor had gone live. It was fixed two minutes before the rebuild of that same executor began.

— DealArena Team