Job board engine
Scans for property management companies advertising for a bookkeeper or accountant. The ad is the signal: they have enough work to justify a salary, and they are about to commit to the expensive option.
Case study · Client work · Outbound
Built for a company selling outsourced bookkeeping to property managers in the US. It finds firms with a live need, drafts the outreach, and sends nothing until a person approves it. One Rust service, rewritten off a no-code platform that had run out of room.
Problem
The client sells outsourced bookkeeping and back-office work to residential property management companies. Their buyer almost never searches for "outsourced bookkeeping". What that buyer does instead is post a job ad for an in-house bookkeeper, or complain in an industry forum about being buried in reconciliations.
Both are the same moment: this company has the work and is about to solve it the expensive way. That moment is short, and no CRM report will ever surface it, because it happens entirely outside the CRM.
Scans for property management companies advertising for a bookkeeper or accountant. The ad is the signal: they have enough work to justify a salary, and they are about to commit to the expensive option.
Watches industry forums and groups for operators describing the problem themselves. These are worked as a public, genuinely useful reply rather than a pitch, because that is what those spaces tolerate.
The rule the whole thing is built around
Every public reply and every email is drafted by the system and approved by a person before it goes anywhere. No auto-posting, no auto-sending, no exceptions in the code for the cases where it would be convenient.
That is a product decision, not a timidity one. The community engine posts under a real person's name in a space where a bad automated reply is remembered for years. The value of being welcome in that forum is worth more than the hours saved by skipping review.
The three message types and which signal gets which one are documented in the repo as their own reference, because that mapping is the thing everyone keeps re-deriving and getting slightly wrong.
Rebuild
The first version was built entirely on a no-code app platform. It worked, and then it stopped being able to grow. Blocks timed out after two minutes. Its code mode was not standard HTTP, so integrations needed a browser extension acting as a shim. Access to the database and to auth was gated by the platform.
I rewrote it as a single Rust service: server-rendered pages, a small database, scheduled scans, and the external integrations as ordinary HTTP clients. It ships as one container.
The important part is the porting method. The old logic was moved across piece by piece, with tests written against the original behaviour, so the rewrite was checkable rather than a rebuild from memory. That is what makes a migration finishable.
What it proves
Intent data you buy tells you a company visited a page. A job ad for the role your service replaces tells you what they are about to spend money on, in their own words, with a date on it. It is public, and almost nobody is systematically reading it.
Building the system to catch that is straightforward. Building it so a person still reads every message before it goes out is what makes it something the client can use for years.