FinalOffer AI

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.

Thanks CJ (Syntax) for Stoker

TypeScript Cloudflare Workers pnpm Sentry CodeRabbit Firecrawl Company shpit.dev Project finaloffer.dev

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:

  1. Check D1 for a fresh cached result (configurable TTL per endpoint).
  2. On cache miss, orchestrate Firecrawl (search + scrape) and Workers AI (fallback extraction).
  3. Normalize and validate the response using shared Zod schemas.
  4. 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:2px

Why this pattern?

  1. Cost efficiency – Cache hits avoid Firecrawl calls; Workers AI only runs when regex-based parsing fails.
  2. Resilience – When forceRefresh=true but Firecrawl errors, we serve a stale cache entry rather than failing the pipeline.
  3. Type safety – Shared Zod schemas generate OpenAPI docs and enforce request/response contracts at runtime.
  4. Edge performance – Cloudflare Workers run close to users while D1 replicates data globally.
  5. 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:

  1. Clone the repo and install dependencies with pnpm install.
  2. Copy .env.example to .env and populate the keys referenced in src/env.ts.
  3. Run pnpm db:migrate:local to sync the D1 schema followed by pnpm dev to 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:

  1. Edit src/db/schema.ts.
  2. pnpm db:generate.
  3. pnpm db:migrate:local.
  4. Validate via tests or manual checks.
  5. pnpm db:migrate:remote for 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-Id and CF-Access-Client-Secret headers. 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.