FinalOffer Services API
Cloudflare Workers + Hono service that powers our property and contact intelligence features.
FinalOffer AI relies on a dedicated Hono service that runs on Cloudflare Workers to source property intelligence, normalize contact data, and expose a stable REST interface to our Convex backend and frontend clients. This page documents how that service is architected so integrators and partners understand the guarantees behind the data we surface.
Overview
The FinalOffer Services API is a Cloudflare Workers backend that orchestrates Firecrawl for crawling + scraping and Workers AI for fallback extraction behind a typed REST API powered by Hono and @hono/zod-openapi. Results are cached in Cloudflare D1 to minimize external API usage, so FinalOffer clients can query property and contact data at low latency without managing scrapers or vendor integrations.
Our Convex backend communicates with this service through its documented endpoints; this doc is a public reference for how we treat data before it reaches the rest of the FinalOffer platform.
Core Endpoints
POST /api/property-info– Retrieve listing agent, brokerage, MLS ID, and price history for a property address.POST /api/contact-info– Fetch multi-channel contact info for a real estate professional or brokerage.GET /docs– Scalar UI backed by the auto-generated OpenAPI 3.1 specification.
Architecture
This service follows a cache-first orchestration pattern:
- Check D1 for a fresh cached result (configurable TTL per endpoint).
- On cache miss, orchestrate Firecrawl (search + scrape) and Workers AI (fallback extraction).
- Normalize and validate the response using shared Zod schemas.
- Persist the payload back into D1 before returning it to the caller.
flowchart TB
Client[FinalOffer Backend Clients]
subgraph CF[Cloudflare Workers Runtime]
direction TB
Router[Hono Router + OpenAPI]
subgraph Routes
PropertyRoute["/api/property-info"]
ContactRoute["/api/contact-info"]
end
subgraph Services
PropertyExtractor["Property Extractor<br/>- Normalize address<br/>- Hash for cache key<br/>- Extract listing details<br/>- Parse price history"]
ContactExtractor["Contact Extractor<br/>- Normalize criteria<br/>- Hash for cache key<br/>- Map contact fields<br/>- Flatten emails/phones"]
end
D1[(Cloudflare D1<br/>property_cache<br/>contact_cache)]
Router --> PropertyRoute
Router --> ContactRoute
PropertyRoute --> PropertyExtractor
ContactRoute --> ContactExtractor
PropertyExtractor <--> D1
ContactExtractor <--> D1
end
subgraph External
Firecrawl[Firecrawl API<br/>Search + Scrape]
WorkersAI[Workers AI<br/>Llama 3.3 70B<br/>Fallback Extraction]
end
Client -->|POST /api/property-info| Router
Client -->|POST /api/contact-info| Router
PropertyExtractor -->|Search → Scrape| Firecrawl
PropertyExtractor -.->|Fallback if needed| WorkersAI
ContactExtractor -->|Structured JSON| Firecrawl
style CF fill:#f4f4f4,stroke:#333,stroke-width:2px
style D1 fill:#ffa500,stroke:#333,stroke-width:2px
style Firecrawl fill:#ff6b35,stroke:#333,stroke-width:2px
style WorkersAI fill:#3178c6,stroke:#333,stroke-width:2pxWhy this pattern?
- Cost efficiency – Cache hits avoid Firecrawl calls; Workers AI only runs when regex-based parsing fails.
- Resilience – When
forceRefresh=truebut Firecrawl errors, we serve a stale cache entry rather than failing the pipeline. - Type safety – Shared Zod schemas generate OpenAPI docs and enforce request/response contracts at runtime.
- Edge performance – Cloudflare Workers run close to users while D1 replicates data globally.
- Observability – Pino structured logging plus Sentry error tracking keep us informed about unusual traffic.
Key Design Choices
Cache-first data sourcing
Every request hashes normalized inputs (address or agent metadata) into deterministic cache keys so repeated queries return instantly when within TTL. This is what lets FinalOffer refresh property details frequently without being rate-limited.
Graceful degradation
If Firecrawl fails while forceRefresh=true, the previous cache entry is returned when available—even if stale—keeping user experiences uninterrupted while investigations happen.
Hybrid extraction
- Property info: Regex-first extraction with Workers AI (Llama 3.3 70B) fallback for edge cases such as non-standard listing templates.
- Contact info: Firecrawl’s structured JSON extraction is sufficient, so no AI fallback is needed.
Schema-first migrations
We rely on Drizzle ORM to manage D1 schema changes; migration SQL is generated from src/db/schema.ts, keeping local and production environments consistent with a single workflow.
Working with the Service
To collaborate on this service locally:
- Clone the repo and install dependencies with
pnpm install. - Copy
.env.exampleto.envand populate the keys referenced insrc/env.ts. - Run
pnpm db:migrate:localto sync the D1 schema followed bypnpm devto start the worker.
That flow is all most contributors need. Advanced tasks (lint, tests, remote migrations) follow the same naming pattern and are available in the package scripts if required.
Database Tooling
Drizzle + Cloudflare D1 handle persistence:
pnpm db:generate– create SQL from schema edits.pnpm db:migrate:local– apply migrations locally.pnpm db:migrate:remote– push migrations to production.pnpm db:execute:*– run ad-hoc SQL (local or remote mode).
Typical workflow:
- Edit
src/db/schema.ts. pnpm db:generate.pnpm db:migrate:local.- Validate via tests or manual checks.
pnpm db:migrate:remotefor production.
Project Structure Highlights
src/app.ts– Registers middleware, auth, and route modules.src/routes/property-info.ts– Implements Firecrawl + Workers AI orchestration for property metadata.src/routes/contact-info.ts– Structured contact info lookups.src/services/*– Cache helpers and data normalization utilities.src/db/*– Drizzle schema + migration helpers.scripts/verify-access.ts– Bun script to validate Cloudflare Access credentials end-to-end.
Endpoint Summaries
POST /api/property-info
- Purpose: Provide structured property listing data (agent, brokerage, MLS, price history).
- How: Normalize the address, search & scrape via Firecrawl, parse via regex, fall back to Workers AI, then cache in D1 (default TTL 5 days, max 30).
- Sample request:
{
"address": "2308 La Casa Dr, Austin, TX 78704",
"cacheTtlDays": 7,
"forceRefresh": false
}POST /api/contact-info
- Purpose: Retrieve multi-channel contact details for agents or brokerages.
- How: Build targeted Firecrawl search queries, run structured JSON extraction, normalize phones/emails, then cache (default TTL 30 days, max 60).
- Sample request:
{
"agentName": "Sarah Johnson",
"companyName": "Keller Williams Realty",
"state": "TX",
"cacheTtlDays": 30
}GET /docs
Scalar-powered docs generated from the shared OpenAPI 3.1 schema, useful for partners who need to call the service directly.
Production Deployment & Access
- Base URL:
https://finaloffer-services-hono.dimethyl.workers.dev - Primary routes:
POST /api/property-info,POST /api/contact-info, plus/docs. - Authentication: Cloudflare Access service tokens via
CF-Access-Client-IdandCF-Access-Client-Secretheaders. These secrets live in environment variables (CF_ACCESS_CLIENT_ID,CF_ACCESS_CLIENT_SECRET).
Special Thanks
Huge thanks to CJ from the Syntax YouTube channel for the Stoker package—it's been instrumental in keeping this service pleasant to work with.