GetSellers
- Year
- 2025–2026
- Type
- Work project · Tres Astronautas
- Stack
- Node.js
- Express
- TypeScript
- MongoDB and Mongoose
- React and Vite
- TanStack Query
- Zustand
- Tailwind CSS and Radix UI
- Stripe
- Socket.IO
- Twilio
- Turborepo
Summary
GetSellers is a marketplace for US real-estate investors. Homeowners who want to sell fill in a form in a marketing campaign, their data arrives as a lead, and each lead goes automatically to one investor subscribed to that county, which the product calls a market.
Investors pick their markets, load a prepaid wallet and receive leads in the app, by email and by SMS. They work each lead through a pipeline, from new lead to closed or cold, and can ask for a refund on a bad one. The company's own team runs everything else from an admin site: users, markets, sources and prices, refunds, broadcasts and reports.
The product is made of three parts:
- An investor app for the people who buy leads.
- An admin site for the team that runs the marketplace, built from scratch during the project.
- An API behind both, which also receives the leads from the campaigns.
The engagement was the handover and modernization of a product that was already live. I worked on it at Tres Astronautas for a client, as a Senior Software Engineer, in charge of the whole backend. This case study shows the product through its flows and diagrams.
- 17
- capabilities
- 45
- features
- 289
- backend test files
Context & role
- Client: A US real-estate lead marketplace
- Company: Tres Astronautas
- Product: An investor app, an admin site and the API behind both
- My role: Senior Software Engineer
- Team: 1 product owner, 2 software engineers, 1 product designer and 1 QA
- What I built: The whole backend: the restructure of the API, lead intake and assignment, the wallet and its payments, refunds, referrals and the scheduled work
- Engagement: Handover and modernization of a live product, from 2025 to 2026
Problem
The marketplace was already running when the project started, and the outgoing team had left a technical document behind. Taking it over safely meant changing nothing until its state was understood, so the first sprint was spent reviewing that document, running a gap analysis and measuring how the live product performed. That review named the critical areas: the lead feed, the counties, billing and the setup wizard.
The pains it found were of three kinds:
- For investors, lead delivery depended on billing. A failed charge could stop leads from arriving, even for an investor with another card saved.
- For the team that runs the marketplace, there were no tools of their own for refunds, broadcasts, reports, source prices or referrals.
- For the product itself, the backend's structure made each use case hard to test on its own, and lead data carried sensitive and duplicated information that should never reach the browser.
The investor carries most of that weight. Leads are what they pay for, and every problem in billing or delivery reaches them as a lead that did not arrive, so most of the backend work was designed around keeping leads flowing.
Users & context of use
Four profiles shape the product, but only two of them sign in. The homeowner is the reason the leads exist and never opens the app. The sales representative is a role that admins manage, not a user with screens of its own.
Real-estate investor
Subscribes to counties, receives leads, works them through the pipeline and manages the wallet, the cards, the referral code and refunds.
Investor app
Homeowner
Someone who wants to sell. Fills in a form in a marketing campaign, with their consent. Their personal data stays hidden until the lead belongs to an investor.
Marketing campaigns
Platform administrator
Manages users, partners, sources and prices, markets, refunds, broadcasts, reports and webhooks, and gets an alert when a lead cannot be assigned.
Admin site
Sales representative
A role that admins assign and list under users. Referral commissions belong to the investor who referred, not to this role.
Admin site, as a record
The moments that shaped the design
- A lead arrives from a campaign at any hour. It has to reach the right investor in seconds, without anyone on the team stepping in.
- An investor's card is declined in the middle of the night. Their leads should keep coming if another card can pay.
- An investor opens the feed and sees leads that are not theirs yet. They should see enough to decide, but never the homeowner's personal data.
- An admin reviews a refund and needs to know that completing it credits the wallet and sends the lead to someone else, in one step.
Goals, success metrics & constraints
Goals
- Keep the live product working while it is modernized.
- Never let billing block the delivery of a lead.
- Give the team that runs the marketplace its own tools.
- Keep personal data inside the backend until an investor owns the lead.
- Make each use case testable on its own.
How success would be measured
No success metrics were agreed with the client during the engagement. These are the ones I would propose, each tied to one of the goals:
| Goal | Metric | What it tells us |
|---|---|---|
| Delivery never blocked by billing | Leads left unassigned for lack of funds, and recharges saved by a backup card | Whether the wallet keeps leads flowing. |
| Fast routing | Time from the webhook to an assigned lead | Whether a lead reaches an investor while it is still fresh. |
| Tools for operations | Refunds resolved per week, and time to resolve one | Whether the admin site replaced the manual work. |
| Fair distribution | Spread of leads among the subscribers of each market | Whether taking turns works as intended. |
| Testable backend | Coverage of the use cases, and defects found after release | Whether the restructure paid off. |
Constraints
- A live product. Every change had to land without stopping lead delivery for the investors already using it.
- Two engineers. I owned the backend and a teammate owned the web apps, so the API contract between us had to stay clear and stable.
- An integration that never closed. The refund flow was planned against the client's own backend, but it could not be completed because of connectivity issues, which is why operations got an admin site of their own.
- Many lead sources. Each campaign names the same fields differently, and the intake had to accept all of them.
Process & prioritization
From problem to scope
The work was organized in sprints 0 to 6. Sprint 0 was the review of the live product, Sprint 1 restructured the backend and set up the monorepo, and the following sprints moved payments to a new provider, built the admin site and reworked the language of the product. The whole scope was consolidated into 17 capabilities and 45 features.
How the team worked
The two engineers split the product along the API: I owned the backend, and my teammate owned the investor app and the admin site.
What the platform covers
| Area | What it enables |
|---|---|
| Lead intake | A webhook for every campaign, one adapter that normalizes the fields, and enrichment with property data and a price. |
| Assignment | County, then state, then nationwide, taking turns inside each market, with budget and balance checks. |
| Lead feed and pipeline | A live feed, lead detail and a pipeline from new lead to closed or cold, with personal data masked until the lead is assigned. |
| Markets | Subscriptions to US counties, each one open, secured, held, full or released. |
| Wallet and payments | Saved cards, a backup card, auto-recharge, a monthly budget with a monthly reset, receipts and history. |
| Refunds | Up to two requests per lead, one open at a time, with the lead reassigned when a refund is completed. |
| Notifications | In the app in real time, by email and by SMS, according to each investor's settings. |
| Referrals | Referral codes, settings and commissions, processed every day. |
| Admin site | Users, partners, sources and prices, markets, refunds, broadcasts, reports and webhooks. |
What waited
- The refund integration with the client's own backend was never completed, so refunds live in the admin site instead.
- Part of the old payment provider is still in the repository, from the migration.
Design decisions
The decisions that most shaped the product, made by the team and designed by the product designer of the project, part of the Tres Astronautas team. Each one is shown as a flow or a diagram.
Ready to receive leads in one sitting
Problem. An investor who signs up but never adds a card or a budget is an investor who never receives a lead.
Decision. A setup wizard asks for the three things a lead needs: the markets, a card to pay with and a budget.
Trade-off. A longer first session.
In the product. Each sign-in is confirmed with a one-time code by email, and a second card is encouraged with a one-time discount on the next lead.
One lead, one investor, taking turns
Problem. Leads are exclusive. Sending each one to every subscriber would make it worthless, and always picking the same investor would be unfair.
Decision. A lead looks for subscribers in its county, then its state, then nationwide. Inside each scope, the investor who waited longest gets it, as long as they are under their monthly budget and have the balance to pay.
Trade-off. A lead can end up unassigned when nobody in its scope can pay.
In the product. An investor short on balance has their subscription held and the next one is tried, and when nobody qualifies the admins get an alert right away.
Billing never blocks a lead
Problem. Charging a card at the moment of each assignment meant that a declined card stopped the lead, even when the investor had another card saved.
Decision. Leads are paid from a prepaid balance. When the balance drops under the investor's threshold, recharge events are created and charged in the background, trying the other saved cards when one is declined.
Trade-off. More billing states to model and test, and an investor with no funds is skipped until the wallet is recharged.
In the product. Each charge has its own key, so a retry never charges twice, and the investor gets an email only when every card has failed.
A refund that puts the lead back to work
Problem. A refund that only returns money leaves the lead idle, even though another investor in the market could still use it.
Decision. Completing a refund credits the wallet, unassigns the lead and offers it to the next investor in the same market, in one step.
Trade-off. Refunds touch billing, assignment and notifications at once.
In the product. Each lead allows two requests at most and only one open at a time, a rejected request can be sent again with the form already filled in, and either side can cancel while it is in progress.
Personal data stays in the backend
Problem. The feed shows leads to investors before they own them. Sending the homeowner's full data to the browser and hiding it there would leave it one inspection away.
Decision. The API masks the last name, email, phone, address and location of every lead in the feed, and returns the full data only for leads the investor owns.
Trade-off. An extra mapping on every lead response.
In the product. The map shows an approximate location by design, and sensitive or duplicated fields were removed from the data model altogether.
Design system
The investor app and the admin site share one design system, created by the product designer assigned to the project at Tres Astronautas.
Foundations
- Two typefaces. Poppins for titles and Mona Sans for the interface, tables and data.
- A dark interface. A deep navy page with darker cards, and soft blue-gray tones for text and icons.
- One violet for action. A violet ramp where one tone marks the main action, with fixed tones for hover, selected and disabled.
- Status colors with fixed meanings. Success, danger and warning, each with its own ramp.
- Radius and spacing as tokens. A short scale of radius and spacing values shared by every component.
From design to code
As a Senior Software Engineer, I owned the backend: its restructure, its use cases and everything that runs behind the two web apps. This section explains how it is organized, in terms of what each part does.
Architecture
The repository is a monorepo with the API, the investor app, the admin site and shared packages. I moved the backend from a layered structure to a hexagonal one: a request goes from a thin route and controller to a use case that does one thing, the domain holds the interfaces and rules, and the adapters talk to the database and the outside services.
The dependencies are wired in one place, so the tests replace them with mocks and each use case can be tested on its own. Scheduled jobs and the real-time notifications run in the same process as the API.
Leads, refunds and markets
A lead in the investor's pipeline can move to any status, from new lead to closed or cold. A refund starts in progress and ends completed, rejected or cancelled. A market shows each investor where they stand: open, secured, held when the balance runs short, full, or released.
Scheduled work
Four jobs run in the background: one cleans expired one-time codes every day, one charges the pending recharge events every two minutes, one processes referral commissions every morning, and one resets the monthly budgets on the first day of the month.
Quality
The backend has 289 test files, 270 unit and 19 integration, with an in-memory database and a coverage threshold of 60%. Because each use case can be tested on its own, most of that suite is unit tests.
Delivery
Releases are tag-driven and per app: the tag says which part ships and to which environment, so the API, the investor app and the admin site can be released on their own.
Technical decisions
| Decision | Why | Trade-off |
|---|---|---|
| Layered to hexagonal backend | Each use case can be tested alone, with less coupling. | A large restructure up front, paid back by the test suite. |
| One payment provider | The wallet and the investor app share one provider and one model of cards and charges. | A migration, with part of the old provider still in the repository. |
| Prepaid wallet with auto-recharge | A lead is paid from the balance, and the wallet is refilled later with retries. | More billing states to model and test. |
| Taking turns inside each market | Leads are exclusive, and turns spread them fairly. | A lead can stay unassigned when nobody can pay. |
| One adapter for the lead webhook | Sources name fields differently, so one adapter maps them all. | A source with a new shape means extending the adapter. |
| Masking lead data in the API | Personal data does not leave the backend until it is needed. | Extra mapping on every lead response. |
| Tag-driven, per-app releases | Each part of the product ships on its own. | Releases depend on tagging discipline. |
Results
The modernized product is in production, and the client kept a live marketplace the whole way through. Investors sign up, pick their markets, load a wallet that refills itself and receive leads in the app, by email and by SMS, then work each lead through the pipeline and ask for a refund on a bad one. The team that runs the marketplace handles refunds, broadcasts, reports, source prices and referrals from its own admin site.
The refund integration with the client's own backend was never completed, so that part still has to be closed. No revenue or conversion figures were shared for this engagement.
Learnings & next steps
Next steps
- Close the refund integration with the client's own backend once the connection is possible.
- Remove what is left of the old payment provider.
- Measure the time from webhook to assignment, and the leads saved by backup cards, as the first baseline.
What I learned
- Read before you touch. Spending the first sprint only reviewing a live product felt slow. It is what let the rest of the project change billing and assignment without breaking delivery for the investors already using it.
- Money flows deserve their own states. Moving from "charge now" to a wallet with recharge events added states, but each one made a failure visible and recoverable instead of silent.
- The API is where a two-person team meets. With one engineer on each side of it, keeping the API stable and predictable mattered more than any shared code.