Selling the Decision, Not the Data: The Architecture Behind Gallus Resolve
Debt collection is a data-hoarding industry. The instinct, when recovery rates sag, is to buy more data — more phone numbers, more addresses, more append files — and throw them at the same accounts. I built Gallus Resolve on the opposite premise: more data doesn't move the needle, and in a regulated workflow it can quietly raise your exposure. The thing an agency actually needs is a decision. So that's what the system produces, and that's the unit it's metered on.
This is a walkthrough of how that inversion shapes the engineering. It's written for people who build systems, not for people who buy them, so I'll stay on the architecture and leave the sales math out of it.
The inversion: meter the decision, not the record
Most collections tooling is priced and modeled around data. You pay per record enriched, per lookup, per append. The software's job is to hand you a bigger pile of information and let your team figure out what to do with it. Two problems fall out of that.
First, it optimizes the wrong thing. A larger data pile is not a better outcome; a correct action is. An engine tuned to maximize records surfaced will happily surface a thousand accounts that were never going to resolve, and your collectors pay for that in wasted dials.
Second — and this is the one that shaped the whole design — the moment your product's job is to furnish data, you've walked into the part of the regulatory landscape where the heaviest obligations live. I'm not going to make a compliance claim in a blog post; that's a question for lawyers, and it stays a question for lawyers. But as an architectural principle, there's a meaningful difference between a system that hands over data and a system that renders a decision and meters that. Gallus Resolve is built around rendering the decision. The data is an input that gets consumed and gated behind the decision, never the deliverable.
So the core object in the system isn't a record. It's a decision: for a given account, one of a small, fixed set of routing outcomes, plus the evidence trail that produced it.
Five-state routing instead of a confidence dump
The naive version of a decision engine outputs a score — 0.0 to 1.0, "how likely is this to resolve" — and dumps it back on the operator. That just relocates the hard problem. Someone still has to decide what a 0.62 means on a Tuesday.
Gallus Resolve collapses the score into a small routing model with a handful of terminal states. The exact set is configurable per portfolio, but the shape is always the same: a few actionable states, and at least one state that means not enough to act on yet. Representative set:
WORK — the evidence clears the confidence floor and the account is safe and worth actioning now.
HOLD — actionable later, but something (timing, a soft signal, a recent change) says wait. It re-enters the pipeline on a schedule.
REVIEW — the engine is not confident enough to call it, so it routes to a human instead of guessing. This is the important one; more below.
SUPPRESS — a hard signal says do not contact / do not act. Terminal until new input overrides it.
RESOLVE — closed out; no further routing.
The value of a fixed, small state set is that everything downstream — dashboards, work queues, audit, billing — keys off a stable enum instead of a floating-point number that means something different to every user. State transitions are the API. A score is an implementation detail behind them.
enum Decision {
WORK, HOLD, REVIEW, SUPPRESS, RESOLVE
}
// The engine never returns a bare score to the caller.
// It returns a Decision plus the evidence that justified it.
type DecisionResult = {
account_id: string
decision: Decision
confidence: number // retained for audit, not for the operator to interpret
evidence: EvidenceRef[] // what justified the state
decided_at: string
}
Ambiguity is a first-class output, not a failure
The single most consequential design choice: REVIEW is a valid, honest answer, and the system is proud to emit it.
Most scoring systems are built to always produce a confident-looking number, because a blank looks like the model failed. That's backwards for this domain. In collections, a wrong confident action is far more expensive than an admitted "I don't know" — it costs dials, it annoys the right people at the wrong time, and it manufactures exactly the kind of action you least want to have to defend later. Forcing false confidence is the failure mode.
So the engine is precision-first. There's a configurable confidence floor, and anything below it does not get quietly rounded up into WORK. It routes to REVIEW. A lone signal with no independent corroboration can't win an actionable state on its own, no matter how strong that single signal looks in isolation — corroboration across independent sources is a gate, not a tiebreaker. The result is a system that says "act" only when it can stand behind it, and says "look at this one yourself" the rest of the time, out loud.
That honesty is also what makes the metering fair. You're paying for decisions, and an admitted REVIEW is a real decision — it's the system doing its job of not wasting your collector on an account it can't justify.
Batch compression and idempotency
Agencies don't feed you one account. They feed you portfolios — tens or hundreds of thousands of rows, re-uploaded on cycles, overlapping with last month's file. Two engineering problems come with that: redundant work and replay safety.
Batch compression handles the redundant work. Within and across batches, many accounts collapse to the same decision from the same evidence; recomputing each in isolation is wasted compute and, worse, wasted paid-provider calls on inputs you already resolved. The engine deduplicates at the decision layer — if an account's inputs are unchanged since its last terminal decision, it short-circuits to the cached decision rather than re-deriving it. New or changed inputs are what trigger a fresh derivation.
Idempotency handles replay safety, and this is where I spent real time. Ingestion is webhook- and batch-driven, and anything webhook-driven will eventually deliver the same payload twice — network stutter, provider retries, a re-upload of yesterday's file. If "make a decision" is not idempotent, a duplicate delivery double-acts: two work orders, two contacts, two billable events. Unacceptable in a regulated workflow.
The fix is a database-enforced idempotency key on every decision event, not application-level dedup that races under load:
CREATE TABLE decision_ledger (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency_key text UNIQUE NOT NULL, -- account_id + input_hash + cycle
account_id text NOT NULL,
decision text NOT NULL,
confidence numeric NOT NULL,
evidence jsonb NOT NULL,
decided_at timestamptz NOT NULL DEFAULT now()
);
The UNIQUE constraint is the gate. A second insert with a matching key fails at the database engine, not somewhere up in application code where two concurrent workers can both think they're first. The decision is computed once, recorded once, billed once. Every state transition appends to this ledger; nothing is updated in place.
The ledger is the point
That append-only ledger isn't just plumbing for idempotency — it's the deliverable's spine. Every decision the system ever made, the inputs that justified it, the confidence at the time, and when it was made, are all recorded and immutable. Nothing about how an account got routed is reconstructed after the fact or inferred from a mutable current-state row; it's written down as it happens.
For an operator that means every action their team took traces back to a specific, timestamped, evidence-backed decision. For me as the builder it means the system can answer "why did this account get worked on March 3rd" with a row, not a shrug. In a domain where you may someday have to defend an action, a decision you can't reconstruct is a liability, and the architecture treats it that way.
The constraint that shaped everything
I'll close where the design started. The reason the product is the decision and not the data isn't a marketing line — it's the constraint the whole architecture is organized around. Keeping the data as a consumed, gated input rather than the thing you hand over is what lets the system meter decisions cleanly, and it's a deliberately different posture from a data-furnishing pipeline.
I want to be careful and precise here: that's an engineering and product design principle, not a settled legal conclusion, and I'd never present it as one. Where the regulatory lines fall for any given agency's use is a question for their counsel, full stop. What I can say is that the system was built from the first commit around that distinction — decisions as the product, ambiguity as a valid answer, and an immutable trail behind every call — rather than having those properties bolted on after the fact.
That's the whole thesis in one line: in a data-hoarding industry, the useful thing to sell isn't more data. It's the decision, made honestly, and written down.
Matt Rose is the founder of Gallus Resolve, based in St. Petersburg, FL.