Project · Sulfur
Agent Arena
Sealed-bid combustion — the market decides which agent represents the home.
Essence
For the people who use it
Picking a real-estate agent is the most consequential decision a homeowner makes in a sale, and it's the decision with the worst information. Three agents pitch, the homeowner picks the one with the warmest handshake, and the market never gets a vote. Agent Arena flips the model. Verified agents bid for exclusive listing rights on a property — sealed bids, real time, with the homeowner watching. The market sets the agent's value.
The flow is direct. A homeowner lists their property. The platform identifies qualified agents in the ZIP code, ranked by data (sold-price ratio, days on market, recent reviews). Those agents are invited to a real-time auction with a countdown. The highest bidder wins exclusive 90-day representation, pays the commission upfront via Authorize.Net, and the listing begins.
Built for the U.S. luxury segment ($1M+) where the commission is worth competing for and the homeowner has the leverage. Two sides, two pains solved: homeowners get vetted, ranked agents competing on their property; agents get leads against actively engaged sellers instead of cold-prospecting.
- Real-time live bidding via SignalR, with bid floors and countdown timers.
- Agent ranking system — sold-price ratio, days on market, reviews, unified.
- Sealed-bid transparency — all proposals visible side-by-side on one document.
- Automated agent qualification — only agents meeting performance thresholds in a ZIP are invited.
- Tokenized Authorize.Net commission upfront — no payment processor holds raw credentials.
- Email + SMS notifications via SendGrid and Twilio for invitations, updates and results.
- Data-driven lead generation — Redfin scraper identifies expired listings; monthly mailers go out to those owners.
- Mobile-first Blazor UI — custom CSS, no frontend framework, 512 MB WASM heap tuned for mobile bidders.
Construction
For the engineers
Stack
- API
- ASP.NET Core 10 on .NET 10 · MediatR 14 (CQRS) · JWT Bearer · FluentValidation 12 · Swagger 3.3.1
- Public site
- Blazor WebAssembly 10 — custom CSS only, no frontend framework
- Admin
- Blazor WebAssembly 10 with Bootstrap
- Real-time
- SignalR 10 — dedicated auction hub in a separate process
- Database
- SQL Server / Azure SQL · EF Core 10 in database-first mode, no migrations
- Payments
- Authorize.Net 8.0.1 — tokenized, PCI-DSS by design
- Messaging
- SendGrid 9.29.3 (email) · Twilio 7.14.3 (SMS)
- Storage
- Azure Blob Storage 12.27.0
- Observability
- Application Insights · OpenTelemetry 1.15.3 · Raygun 2.0.1
- Testing
- NUnit 4 · Moq · Playwright 1.58 · NBomber 6.2.0 (50–100 concurrent agents)
- Hosting
- Azure App Services on Linux · Cloudflare DNS · Azure DevOps pipelines
Architecture
Twelve-project solution.
Core (~28 repository interfaces, ~44 service interfaces, every domain entity),
Infrastructure (EF Core repositories + Authorize.Net + SendGrid + Twilio + Blob),
Common (shared DTOs and contracts), Api, Site (public Blazor
WASM, pre-rendered for SEO), Admin, Auction (the SignalR hub process),
Tests, LoadTests, RedFin (console scraper),
plus TheLedger.Api + TheLedger.Site — a sibling Azure-Functions /
static-site lead-generation funnel that drives luxury sellers into the auction platform.
The auction hub runs in its own process. SignalR lives in
AgentArena.Auction with its own deployment and its own scaling story. Auction state
is a static ConcurrentDictionary protected by per-room semaphores; messaging is
group-based per auction; JWT is passed via query string for the WebSocket upgrade. The API can
scale horizontally without contending for auction state, and a misbehaving auction can't take
the API down with it.
Payment tokenization end-to-end. Authorize.Net tokens are the only payment
identifier the platform ever sees — raw PCI data never enters a log, never lands on disk. Silent
post webhooks reconcile transactions out-of-band. The full threat model is documented in
payment-security-hardening-2026-02-10.md.
Three-layer caching with explicit coordination. API memory cache with sliding
expiry (30 min–24 h) + a CacheCoordinationMiddleware that auto-invalidates on
mutations + client-side localStorage / IndexedDB with monthly refresh against ETags. Stale data
across services is solved by coordination, not hope.
The RedFin mailing-list pipeline. A standalone console app pulls Redfin
gis-csv across twenty South Florida ZIPs, deduplicates by MLS#, and bulk-inserts
~13K rows per run via SqlBulkCopy into a RedfinRaw staging table. The
API later promotes those rows into PublicListing records on a lifecycle —
Sequence A1–A8 for stale-but-active listings bucketed by days-on-market, Sequence B1–B4 for
the four months after expiration — and targeted mailers go out per bucket. Address
normalisation is multi-step (uppercase, strip punctuation, standardise suffixes / directionals /
units, append city / state / ZIP), critical because 93% of ATTOM records carry unit numbers and
unit-level precision is the only way to avoid false-positive matches.
Notable details
- Database-first SQL — idempotent scripts in
dbSchema.sql/dbUpdate.sqlfor zero-downtime deploys. IsolatedImportModeflag disables background services during local data imports so dev runs don't conflict with deployed environments.- Custom CSS only on the public site — no npm CVEs in the client, no surprise bundle size.
- WASM heap explicitly capped at 512 MB for mobile bidders; pre-rendering adds static HTML for SEO.
- NBomber load tests simulate full agent journeys against the auction hub.
- Azure DevOps pipelines for
devandmain, with manual trigger for the RedFin scraper and load tests. - Cloudflare DNS routes every domain (
agentarena.com,auction.agentarena.com,theledger.estate) to its respective Azure service.