Key flows
1. Lead creation → classification → Pipedrive → email
POST /api/leads validates and classifies every submission, persists it, syncs to Pipedrive, and fans out side effects in the background.
sequenceDiagram
participant Client
participant RL as Rate-limit MW
participant H as create-lead.handler
participant LLM as LiteLLM
participant KV as KV (CACHE)
participant PD as Pipedrive
participant Notify as notifyLeadCreated (waitUntil)
participant CEQ as ConfirmationEmailQueue DO
participant MG as Mailgun
participant CU as ClickUp
Client->>RL: POST /api/leads
RL->>RL: if prototype-eligible, check dual-bucket quota
RL->>H: next() (+ stashed identifiers)
H->>H: validate personal/steps + reCAPTCHA (unless dev / trusted chat)
H->>LLM: classifySubmission()
LLM-->>H: { category, confidence, reasoning }
alt category = spam (filter on)
H->>KV: save lead status=spam
H-->>Client: 200 (no Pipedrive/email)
else category = vendor (filter on)
H->>KV: save lead status=vendor
H->>MG: send vendor decline email (waitUntil)
H-->>Client: 200
else lead
H->>KV: createLead → save lead
H->>PD: create lead (+ resolve CC email)
PD-->>H: pipedriveLeadId
H->>KV: save lead with pipedriveLeadId + classification
H-->>Client: 200 { leadId, previewUrl?, pipedrive }
H->>Notify: waitUntil(notifyLeadCreated)
Notify->>CEQ: enqueue service-specific confirmation email (delayed)
Notify->>PD: add service-specific note
Notify->>CU: direct immediate lead alert (<1 min)
CEQ->>MG: send confirmation email after delay
end
Lead routing in notifyLeadCreated depends on the service type (isDiagnosticLead, isStrategicAdviceLead, isDedicatedTeamLead, isTechnologyStrategyLead, isProcessAutomationLead), each mapping to its own email template, Pipedrive note, and ClickUp payload. The ClickUp payload is posted to the direct lead-alert channel in under a minute and includes the landing/source, phone, message, and a WhatsApp-first Sandler template when a phone is available. It is an alert only: /leads-seguimiento remains responsible for later outreach cadence, preventing duplicate follow-ups.
2. AI prototype generation — SSE streaming + Durable Object processing
For prototype-eligible leads, the browser opens an SSE stream that drives live generation; a Durable Object then reliably polls V0 to completion even if the browser disconnects.
sequenceDiagram
participant Browser
participant SSE as generate-stream handler
participant V0 as V0 SDK
participant KV as KV (CACHE)
participant PQ as PrototypeQueue DO
participant PROC as processLeadBackground
participant MG as Mailgun
participant CU as ClickUp
participant Cron as Hourly cron
Browser->>SSE: GET /api/leads/:id/generate-stream
SSE->>KV: load lead, check eligibility / existing state
alt already has previewUrl
SSE-->>Browser: progress(completed) + complete
else fresh / stale / failed
SSE->>V0: createStreaming / streamPrototype
V0-->>SSE: streamed progress
SSE-->>Browser: SSE progress events
SSE->>PQ: enqueue lead (finalize / on error)
end
loop alarm every ~2 min (per job, up to 5 concurrent)
PQ->>PROC: process lead
PROC->>V0: pollStatus(chatId)
alt completed + message finished
PROC->>KV: mark completed, increment rate-limit buckets
PROC->>PD as Pipedrive: add "ready" note
PROC->>CU: notify completed
PROC->>MG: send "Prototype is Ready" email
else plan mode
PROC->>V0: sendMessage("Build")
else interrupted
PROC->>V0: resumeMessage / sendContinue (≤3 attempts)
else still processing
PROC->>PQ: re-check after 2 min
else failed / stale
PROC->>KV: mark failed, clear in-progress buckets
PROC->>CU: notify failed
end
end
Cron->>KV: scan metadata for orphaned/stuck leads
Cron->>PQ: re-enqueue (DO dedups)
Notes:
- Rate-limit counters increment only on successful completion; failures clear the in-progress hold without consuming quota.
- A prototype generating longer than 15 minutes is treated as stale: the processor first tries up to
MAX_CONTINUE_ATTEMPTS (3) recoveries, then marks it failed.
- The hourly cron (
prototype-checker.ts) is a safety net only — the normal path enqueues directly from the stream/finalize step.
Trusted chat (Agno) prototype path
When POST /api/leads carries a valid X-Chat-Api-Key, the V0 chat is created synchronously (triggerV0Prototype) so the HTTP response reflects the real outcome (the Agno tool then tells the LLM the truth). The rate-limit buckets are marked in-progress synchronously to block parallel requests, and the post-creation signals (Pipedrive note, ClickUp, prototype-queue enqueue, email) fire in the background.
3. Sales chat streaming
sequenceDiagram
participant Browser
participant Chat as chat/stream handler
participant Agno as Agno (WebSocket)
participant KV as KV (CACHE)
Browser->>Chat: POST /api/chat/stream (X-Chat-Api-Key)
Chat->>Chat: validate body, resolve trusted client IP
Chat->>KV: persist user message + session→IP meta
Chat->>Agno: WS upgrade → send { query, session_id, user_ip }
loop streaming
Agno-->>Chat: token events
Chat-->>Browser: SSE token
end
Agno-->>Chat: done (final content + metadata)
Chat->>KV: persist assistant message
Chat-->>Browser: SSE done
The session→IP mapping lets GET /api/leads/lookup attribute the IP bucket of the dual-bucket rate limit even when the Agno tool omits X-User-IP.
4. Blog content delivery
sequenceDiagram
participant Client
participant Posts as posts handler
participant Cached as CachedWordPressService
participant KV as KV (posts_cache:)
participant WP as WordPress REST API
Client->>Posts: GET /api/posts?...
Posts->>Cached: listPosts(params)
alt cache hit (and no ?cache=false)
Cached->>KV: get cached payload
KV-->>Cached: posts
else miss / bypass
Cached->>WP: GET /wp-json/wp/v2/posts?...
WP-->>Cached: posts JSON
Cached->>KV: store with TTL
end
Cached-->>Posts: posts + pagination
Posts-->>Client: JSON (+ per-post detail URLs)
A WordPress publish webhook can purge the cache via the authenticated DELETE /api/posts/cache / POST /api/posts/cache/clear endpoints (X-Cache-Api-Key), which paginate through and delete every posts_cache: key.