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.