DEV Community

Dhairya
Dhairya

Posted on

DealClaw - Autonomous Sales Stratergist Agent

How We Built an Autonomous Sales AI Agent with Groq, Gemini Embeddings, and Gmail in 12 Hours

We built DealClaw in a 12-hour hackathon — a sales intelligence platform where the AI doesn't just advise, it acts. Here's the full technical breakdown.


The Problem

CRMs like Salesforce and HubSpot store everything but surface nothing. A rep closing a $200k deal has no idea that 3 months ago, a different rep handled the exact same "ROI projections are too optimistic" objection and won by sharing a Stripe case study. That institutional knowledge lives nowhere useful.

We wanted to build the intelligence layer that mines past deals and makes every rep smarter in real time.


Architecture Overview

Gmail / Slack / CRM export
        ↓
  Extraction (Groq)          raw text → structured JSON
        ↓
  Embedding (Gemini)         objections → 3072-dim vectors → Supabase pgvector
        ↓
  Agent (Groq)               deal context + retrieved memory → recommendations
        ↓
  Task execution (Groq + Gmail API)   drafts + sends emails autonomously
Enter fullscreen mode Exit fullscreen mode

LLM: Groq with 3-Model Waterfall Fallback

We use Groq for all inference — latency matters a lot when the agent is running mid-conversation. The fallback chain is a single file:

const MODELS = [
  'llama-3.3-70b-versatile',  // primary — best quality
  'llama-3.1-8b-instant',     // first fallback — fast
  'gemma2-9b-it',             // last resort
];

export async function groqComplete({ messages, system, max_tokens = 1024 }) {
  let lastError;
  for (const model of MODELS) {
    try {
      const response = await client.chat.completions.create({
        model,
        messages: system ? [{ role: 'system', content: system }, ...messages] : messages,
        max_tokens,
      });
      return response.choices[0].message.content;
    } catch (err) {
      lastError = err;
    }
  }
  throw new Error(`All Groq models failed: ${lastError?.message}`);
}
Enter fullscreen mode Exit fullscreen mode

If the 70B model is rate-limited or down, requests fall through silently. Callers never see the model selection logic.


Retrieval-Augmented Agent Memory

Every objection gets embedded when it's created and stored in Supabase with pgvector:

// Gemini text-embedding-004 via REST (v1beta, 3072 dims)
async function getEmbedding(text) {
  const res = await fetch(`${GEMINI_EMBED_URL}?key=${process.env.GOOGLE_AI_API_KEY}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ content: { parts: [{ text }] } }),
  });
  return (await res.json()).embedding.values;
}
Enter fullscreen mode Exit fullscreen mode

At query time, the agent embeds the current objection, runs cosine search, and injects the top matches into its system prompt:

SIMILAR PAST OBJECTIONS RETRIEVED:
[1] Objection: ROI projections not substantiated.
    Response: Shared Stripe/Shopify case studies showing 3.2x ROI. Added 90-day success clause.
    → Deal outcome: won | Tags: roi, SaaS, won
Enter fullscreen mode Exit fullscreen mode

This is what makes the recommendations specific instead of generic. The agent literally says "in the Nexus Analytics deal, this exact objection was resolved by..."


Extraction Pipeline

Raw email or call transcript → structured JSON in one shot:

const EXTRACTION_SYSTEM_PROMPT = `You are a sales intelligence extraction engine.
Return ONLY valid JSON matching this schema:
{
  "summary": "...",
  "stakeholders": [{ "name", "role", "seniority", "sentiment", "primary_concern" }],
  "objections": [{ "text", "category", "response_used" }],
  "commitments": ["..."]
}
Category must be one of: pricing | roi | timing | competitor | champion | technical | procurement`;
Enter fullscreen mode Exit fullscreen mode

We added post-parse validation — open-source models occasionally drift on enum fields:

function validateCategories(parsed) {
  parsed.objections = parsed.objections.map(obj => ({
    ...obj,
    category: VALID_CATEGORIES.includes(obj.category) ? obj.category : 'technical',
  }));
  return parsed;
}
Enter fullscreen mode Exit fullscreen mode

Autonomous Task Execution

The most interesting feature. When you open a deal, the agent analyzes context and generates specific next actions:

{
  "title": "Send ROI case study to Linda Cho",
  "description": "Linda raised ROI concerns in the last call. Past won deals show 100% resolution rate when sharing auditable case studies within 48 hours.",
  "type": "email_client",
  "priority": "high",
  "payload": {
    "to_name": "Linda Cho",
    "to_role": "CFO",
    "subject": "ROI data from similar deployments",
    "context": "Address board approval concern with Stripe/Shopify examples"
  }
}
Enter fullscreen mode Exit fullscreen mode

When approved, the agent drafts a personalized email via Groq, sends it via Gmail API, and logs it as an interaction — all without the rep touching a keyboard.

The Gmail send uses RFC 2822 encoding:

export async function sendEmail({ to, subject, body }) {
  const raw = [`To: ${to}`, `Subject: ${subject}`, `MIME-Version: 1.0`, '', body].join('\n');
  const encoded = Buffer.from(raw)
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
  return gmail.users.messages.send({ userId: 'me', requestBody: { raw: encoded } });
}
Enter fullscreen mode Exit fullscreen mode

CRM Import — Flexible Field Mapping

Sales teams export from different CRMs with different column names. We built a normaliser that maps any column name variation to our schema:

function findField(row, candidates) {
  const keys = Object.keys(row);
  for (const candidate of candidates) {
    const match = keys.find(k => k.toLowerCase().trim() === candidate.toLowerCase());
    if (match) return row[match];
  }
  return null;
}

const company = findField(row, ['company', 'account', 'account name', 'organization']);
const stage   = findField(row, ['lead status', 'stage', 'deal stage', 'pipeline stage']);
Enter fullscreen mode Exit fullscreen mode

Supports CSV, Excel (.xlsx), JSON, XML, and PDF. A HubSpot export and a Salesforce export both work without configuration.


What We'd Do Differently

  • Streaming responses — Groq supports streaming but we used blocking calls. For the agent chat, streaming would make the UX feel much more alive.
  • Smarter deduplication — we deduplicate Gmail by message ID, but if the same email arrives via a forwarded thread, it gets re-imported. Content hashing would be more robust.
  • Async task execution — currently tasks execute synchronously on the server. For long-running tasks (multiple emails), a queue (BullMQ or Supabase edge functions) would be cleaner.

Stack

Layer Tech
LLM Groq (llama-3.3-70b, fallback chain)
Embeddings Gemini text-embedding-004 (3072 dims)
Vector search Supabase pgvector
Backend Node.js + Express
Frontend React 19 + Vite + Tailwind
Email Gmail API (OAuth2)
Hosting Render

GitHub: github.com/Dhairya1890/deals_agent

Happy to answer questions about any part of the architecture.

Top comments (0)