Planned2026-08 – nowSolo full-stack developer

Associado

A referral's commission depends on a handshake off-platform, and one side profits from denying it happened.

  • fastapi
  • postgresql
  • pgvector
  • claude
  • sqlalchemy

The problem and constraints

Associado exists to formalize something small business associations already do informally and inconsistently: a member asks around for a plumber, a bookkeeper, a photographer, someone in the group vouches for a specific other member, the two connect, a deal happens, and, in theory, the association was owed a small commission for having made the introduction in the first place. In practice, that last part almost never happens, because nobody is tracking who referred whom, whether it led anywhere, or what it was worth.

The first constraint is that the one fact the entire business model depends on (did the deal actually close, and for how much) happens entirely outside the software, in a conversation and a handshake between two people. The platform can ask about it, but it cannot observe it directly, and it’s asking two people who don’t have symmetric incentives to answer honestly: the person who’d have to pay a commission has every reason to say less than the truth, while the person who wouldn’t pay anything has no particular reason to correct them.

The second constraint is the shape of the request itself. Members don’t type category names. They type “who fixes a leak,” and a system that only understands an exact catalog term like “plumber” will silently fail the moment someone’s vocabulary doesn’t match what got typed into the category list. But a system that answers purely on vague semantic similarity produces a result nobody can audit, and the single most predictable complaint this platform will ever receive is a provider asking why they, specifically, weren’t the one referred. Whatever resolves a request has to be both flexible enough to understand real language and precise enough to explain itself in one sentence.

The third constraint is money handling in a system whose core mechanism is an AI agent reading unstructured WhatsApp messages. Text from a member is exactly the kind of input a language model can be maneuvered by, deliberately or not, and the one part of this system that absolutely cannot be maneuvered is what gets charged to whom. Whatever architecture got built had to make it structurally impossible for anything the model touches to directly decide a commission, a guarantee no policy alone could give.

The fourth constraint was avoiding a specific, already-lived mistake. A sibling platform (a WhatsApp CRM already in production) solves a similarly-shaped problem with a visual workflow tool orchestrating dozens of integration steps, and that tool has a real, ongoing cost: credentials that don’t transfer between environments, execution order that depends on where a box sits on a canvas rather than anything committed to version control, and a saved workflow file that drifts from what’s actually running. This project’s expected volume is a few hundred requests a month for one association, well short of the multi-tenant SaaS scale that justified that other system’s complexity. Carrying the same architecture over here would mean paying operational costs this project’s actual size doesn’t call for.

The last constraint was time itself. A referral that never expires turns into a commission owed on a business relationship the association stopped actually being part of the moment introductions turned into an ongoing arrangement between two people. A plumber introduced once who ends up doing a customer’s monthly maintenance for years is a real outcome this platform needs to not misread as thirty-six separate commission-worthy events.

Architecture

A member sends a WhatsApp message describing what they need. An AI agent (Claude, using tool use rather than free-form generation) reads it and tries to resolve it to a category in the service catalog, first by exact and trigram matching against known terms and synonyms, then, only if that doesn’t land with confidence, by semantic vector search over category embeddings. If neither path resolves cleanly, the agent asks a clarifying question instead of guessing: an unresolved request is logged as a gap in the catalog instead of silently dropped, which is what eventually tells the association where a synonym or a whole category is missing.

Once a category resolves, the same agent queries for providers offering that service (filtered, structurally, to only those who’ve turned on referral consent) and returns a short list to the member, notifying each matched provider that they were referred. Everything up to this point is the agent’s job: understanding language, matching intent to a catalog entry, formatting a helpful answer. None of it touches money.

Money lives entirely in a separate, deterministic path the agent has no access to. Days after a referral goes out, a scheduling worker (running as part of the same process, on a plain database table rather than a task queue) sends a follow-up to both the requester and the provider, independently, asking whether the deal closed and for how much. Only when both answers describe the same deal, within a small tolerance for rounding, does the backend generate a commission automatically. A disagreement in value, one-sided silence, or any ambiguity doesn’t get resolved by more automation: it becomes a queued item in the admin panel for a person to look at, on the theory that a wrongfully generated charge costs the association more trust than a missed one costs it money.

That commission, once generated, is bounded in time on both ends. A request that goes 30 days without a confirmed close simply expires. There’s no such thing as a referral sitting open forever. And if the same requester and provider close another deal in the same category within 90 days of the last one, the system still logs the new referral and still notifies everyone, but records the resulting deal as exempt rather than commission-generating, visibly, as a zero-value, explicitly-exempt record rather than an entry that quietly doesn’t exist. Both windows are configuration values rather than constants baked into logic, and both are explicitly first-guess numbers the design expects to revisit once real conversion data exists to revisit them with.

Architecture

Associado: referral and commission flow A member asks for a service on WhatsApp. An AI agent resolves the request to a catalog category, first by taxonomy, then by semantic search only if taxonomy is not confident. The agent returns a list of matching providers and notifies them, but every write to the commission table happens only in deterministic backend code the AI cannot reach. Days later, both the requester and the provider are asked independently whether the deal closed; a commission is generated only when both answers agree. Member asks on WhatsApp AI agent (Claude) taxonomy, then semantic fallback Providers matched notified of the referral Backend (deterministic) asks both sides later, only writer of commissions the AI never writes to the commission table

Decisions

DECISION 01/04 · Matching a free-text request

ChosenResolve every request to a catalog category first by exact and fuzzy matching against known terms, and only fall back to semantic vector search when that doesn't resolve with confidence, ambiguity becomes a clarifying question to the member instead of a guess

Discarded insteadPure semantic search over free-text descriptions, or letting the model itself pick which providers to suggest

The first complaint this system will predictably get is a provider asking why they weren't referred; a category-first match keeps every referral explainable in one sentence, and gives commission rules a stable key to attach to. Letting the model choose providers directly would put the entire member list in its context and make the result unauditable

Cost acceptedTwo code paths exist for answering the same question, with confidence thresholds that start as an informed guess rather than a measured result, and an external embeddings provider enters the stack purely to cover the vocabulary gap taxonomy alone can't

DECISION 02/04 · Confirming a closed deal

ChosenAsk the requester and the provider independently whether the referred deal closed, and generate commission automatically only when both answers agree: any disagreement, silence, or one-sided confirmation becomes a case for a human to resolve instead of an automatic decision

Discarded insteadTrust the provider's own report and let the association spot-check it later, or only ask the requester

The side that pays the commission is also the only side with an incentive to underreport or deny the deal happened: a single-sided report turns commission calculation into self-declaration by the party least motivated to be accurate about it

Cost acceptedDoubles the messages sent per referral, which raises both perceived annoyance and the account risk with the WhatsApp provider, and unresolved silence, the most common real-world outcome, becomes recurring human work instead of an automated close

DECISION 03/04 · How long a referral counts

ChosenA closed deal only generates commission if confirmed within 30 days of the referral; the same pair closing again in the same category within 90 days is exempt from a second commission

Discarded insteadNo time limit: any future deal between two people ever introduced by the platform owes commission indefinitely

Without a window, a referral from January would in theory owe commission on a deal closed in November, and a recurring relationship (an accountant hired every month) would owe commission every single month from one original introduction, both are commercial relationships the association stopped actually intermediating

Cost acceptedThe association loses commission on legitimately slow-closing deals (a large renovation, a long quote-and-decide cycle), and the anti-duplication rule can be gamed by simply waiting out 90 days between hires, both accepted because the alternative, charging forever, causes far more friction with members than the revenue it would protect

DECISION 04/04 · Single service

ChosenA single FastAPI service does everything (webhook, AI agent, matching, reconciliation, commission, admin panel, and the scheduling worker) with no n8n, no Redis, and no separately built frontend

Discarded insteadThe same pattern as a sibling platform already in production: FastAPI plus n8n for orchestration, Redis for state, a separate frontend build

That sibling platform works, but its operational cost concentrates in specific, recurring places: a 99-node visual workflow whose credentials don't transfer between environments and whose real execution order depends on node position on a canvas rather than anything version-controlled. At this project's expected volume (hundreds of requests a month, business logic that's mostly deterministic rules), that cost has no matching benefit

Cost acceptedThere's no visual editor to tweak the flow without a deploy: any behavior change is a commit and a rebuild. The scheduling worker runs in the same process as the API, so an HTTP traffic spike can delay a follow-up message; separating it is the first change to make if volume grows enough to matter

Invariants

  • A member without active referral consent never appears in a match result, under any circumstance

    Guaranteed bythe query that resolves a category to candidate providers filters on consentimento_indicacao = true before anything else runs

  • The AI agent can never write to the commissions table or change a deal's status

    Guaranteed byno tool exposed to the model has write access to those tables; every write to money or deal status happens exclusively in deterministic backend code the model cannot reach

  • A monetary value is never represented as a float

    Guaranteed byevery amount is a BIGINT in cents, BRL only, a schema-level constraint rather than a code convention someone has to remember

  • The same inbound WhatsApp message is never processed twice, even if the provider delivers it more than once

    Guaranteed byevery inbound message is stored keyed by the provider's own message ID under a UNIQUE index; a conflict on insert is treated as a duplicate and discarded

  • A category's stable identifier never changes once created

    Guaranteed bycategorias_servico.slug is immutable by convention and by the fact that commission history and reporting key off it; only the display label is editable

Risk identified

Risk identified · mitigated at design time

This project has no real incident yet because it never ran in production. Here is a risk the design already accounts for.

Symptom
If commission were generated from either side's word alone, the side that pays it has a direct incentive to underreport the deal's value or deny it closed at all, turning the commission into a self-declaration made by the party with the least reason to be honest about it
Root cause
The fact that a deal happened lives entirely outside the system, in a WhatsApp conversation and a handshake between two people; the platform's only signal is whatever either side chooses to type back, days later, when asked
Fix
Ask both sides independently, and generate a commission automatically only when the requester's and the provider's answers agree on the same deal, within a 20% value tolerance, so ordinary rounding doesn't turn into manual work. Anything short of that, including silence from either side, becomes a pending case queued for a human to resolve
Prevention
The design commits to running conservative by default: a missed commission costs the association less than a wrongful charge would, and every disagreement is treated as a data point to review rather than a threshold to quietly tune away until the complaints stop

Results

8functional specs written before any codecount of files in docs/specs/, 2026-08-29
11architecture decisions recordedcount of files in docs/decisions/, 2026-08-29
30 dayswindow for a referral to still generate commissionADR-007
90 daysanti-duplication window for the same pair and categoryADR-007
20%value disagreement tolerated before a human has to lookADR-006

Full stack

Backend

  • FastAPI (Python 3.12)
  • SQLAlchemy 2 + Alembic
  • Jinja2 + HTMX (admin panel)

Data

  • PostgreSQL 16 + pgvector

Infra

  • Evolution API (self-hosted WhatsApp)
  • EasyPanel

AI

  • Claude (Anthropic, tool use)
  • external embeddings provider

Interested in a project like this? Get in touch.

Get in touch