Back to The Times of Claw

Build a Competitive Intelligence Tracker App

Build a competitive intelligence tracker app in DenchClaw that monitors competitors, tracks where you're winning and losing, and surfaces deal insights based on competitor mentions.

Mark Rachapoom
Mark Rachapoom
·6 min read
Build a Competitive Intelligence Tracker App

Build a Competitive Intelligence Tracker App

Knowing why you lose deals is as important as knowing how to win them. If you're losing to the same competitor every time a certain company type is in play, that pattern should surface automatically — not require a quarterly postmortem.

This guide builds a competitive intelligence tracker as a Dench App: track competitors, log win/loss reasons, and surface competitive patterns from your deal data.

What You're Building#

  • Competitor roster with win rate per competitor
  • Deal-level competitor tracking (which competitors were in each deal)
  • Win/loss analysis by competitor, company size, deal size, and rep
  • Competitive battlecards (AI-generated talking points)
  • Alert when a competitor is mentioned in a new deal

Step 1: App Setup#

mkdir -p ~/.openclaw-dench/workspace/apps/competitive-tracker.dench.app

.dench.yaml:

name: Competitive Intel
description: Track competitors, win rates, and deal patterns
icon: target
version: 1.0.0
permissions:
  - read:crm
  - write:crm
  - chat:create
display: tab

You'll also need a Competitor field (or tags field) on your deals object. If you don't have one, ask DenchClaw: "Add a Competitor field to my deals object".

Step 2: HTML Layout#

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Competitive Intelligence</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 20px; }
    .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
    .comp-card { background: #1e293b; border-radius: 12px; padding: 16px; cursor: pointer; border: 2px solid transparent; transition: border-color 0.15s; }
    .comp-card:hover, .comp-card.selected { border-color: #6366f1; }
    .comp-name { font-weight: 700; font-size: 16px; margin-bottom: 8px; }
    .win-rate { font-size: 24px; font-weight: 800; }
    .win-rate.good { color: #10b981; }
    .win-rate.ok { color: #f59e0b; }
    .win-rate.bad { color: #ef4444; }
    .comp-sub { font-size: 12px; color: #64748b; margin-top: 4px; }
    .bar-bg { background: #334155; border-radius: 4px; height: 6px; margin-top: 8px; }
    .bar-fill { height: 6px; border-radius: 4px; }
    .section { background: #1e293b; border-radius: 12px; padding: 20px; margin-bottom: 20px; }
    h2 { font-size: 16px; margin: 0 0 16px; }
    table { width: 100%; border-collapse: collapse; }
    th { font-size: 11px; color: #64748b; text-transform: uppercase; padding: 8px 12px; text-align: left; }
    td { padding: 10px 12px; border-bottom: 1px solid #334155; font-size: 13px; }
    .badge { padding: 2px 8px; border-radius: 999px; font-size: 11px; }
    .won { background: #10b98120; color: #10b981; }
    .lost { background: #ef444420; color: #ef4444; }
    button { padding: 8px 16px; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; }
    .btn-primary { background: #6366f1; color: white; }
    .btn-secondary { background: #334155; color: #94a3b8; }
    .battlecard { background: #0f172a; border-radius: 8px; padding: 16px; white-space: pre-wrap; font-size: 13px; line-height: 1.6; color: #94a3b8; }
    .tabs { display: flex; gap: 4px; margin-bottom: 16px; }
    .tab { padding: 6px 14px; border-radius: 8px; border: none; background: transparent; color: #64748b; cursor: pointer; font-size: 13px; }
    .tab.active { background: #334155; color: #e2e8f0; }
  </style>
</head>
<body>
  <h1 style="font-size:20px;margin-bottom:20px">Competitive Intelligence</h1>
  <div class="grid" id="competitorGrid"></div>
  <div class="section" id="detailSection" style="display:none">
    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
      <h2 id="selectedCompName">Competitor Name</h2>
      <button class="btn-primary" onclick="generateBattlecard()">Generate Battlecard</button>
    </div>
    <div class="tabs">
      <button class="tab active" onclick="showSubTab('deals')">Deals</button>
      <button class="tab" onclick="showSubTab('patterns')">Patterns</button>
      <button class="tab" onclick="showSubTab('battlecard')">Battlecard</button>
    </div>
    <div id="subtab-deals"></div>
    <div id="subtab-patterns" style="display:none"></div>
    <div id="subtab-battlecard" style="display:none"><div class="battlecard" id="battlecardContent">Click "Generate Battlecard" to create talking points.</div></div>
  </div>
  <script src="tracker.js"></script>
</body>
</html>

Step 3: Tracker Logic#

tracker.js:

let competitors = [];
let selectedComp = null;
 
async function loadCompetitors() {
  // Get all distinct competitors from deals
  const compDeals = await dench.db.query(`
    SELECT
      "Competitor" AS name,
      COUNT(*) AS total,
      SUM(CASE WHEN "Stage" = 'Closed Won' THEN 1 ELSE 0 END) AS won,
      SUM(CASE WHEN "Stage" = 'Closed Lost' THEN 1 ELSE 0 END) AS lost,
      SUM(CASE WHEN "Stage" NOT IN ('Closed Won', 'Closed Lost') THEN 1 ELSE 0 END) AS open,
      SUM(CASE WHEN "Stage" = 'Closed Won' THEN CAST("Value" AS DOUBLE) ELSE 0 END) AS value_won,
      SUM(CASE WHEN "Stage" = 'Closed Lost' THEN CAST("Value" AS DOUBLE) ELSE 0 END) AS value_lost
    FROM v_deals
    WHERE "Competitor" IS NOT NULL AND "Competitor" != ''
    GROUP BY "Competitor"
    ORDER BY total DESC
  `);
 
  competitors = compDeals.map(c => ({
    ...c,
    winRate: c.total > 0 ? Math.round((c.won / (c.won + c.lost || 1)) * 100) : null
  }));
 
  renderGrid();
}
 
function renderGrid() {
  const grid = document.getElementById('competitorGrid');
  grid.innerHTML = competitors.map(comp => {
    const wr = comp.winRate;
    const wrClass = wr === null ? 'ok' : wr >= 60 ? 'good' : wr >= 40 ? 'ok' : 'bad';
    
    return `
      <div class="comp-card ${selectedComp?.name === comp.name ? 'selected' : ''}" onclick="selectCompetitor('${comp.name}')">
        <div class="comp-name">${comp.name}</div>
        <div class="win-rate ${wrClass}">${wr !== null ? wr + '%' : 'N/A'}</div>
        <div class="comp-sub">Win rate · ${comp.total} deals</div>
        <div class="bar-bg">
          <div class="bar-fill" style="width:${wr || 0}%;background:${wrClass === 'good' ? '#10b981' : wrClass === 'ok' ? '#f59e0b' : '#ef4444'}"></div>
        </div>
        <div class="comp-sub" style="margin-top:8px">
          ${comp.won}W · ${comp.lost}L · ${comp.open} open
        </div>
      </div>
    `;
  }).join('') || '<p style="color:#64748b">No competitor data found. Add a "Competitor" field to your deals.</p>';
}
 
async function selectCompetitor(name) {
  selectedComp = competitors.find(c => c.name === name);
  document.getElementById('selectedCompName').textContent = name;
  document.getElementById('detailSection').style.display = 'block';
  renderGrid(); // Re-render to show selection
  await loadCompetitorDeals(name);
}
 
async function loadCompetitorDeals(compName) {
  const deals = await dench.db.query(`
    SELECT id, "Deal Name", "Company", "Value", "Stage", "Owner", "Close Date",
           "Loss Reason"
    FROM v_deals
    WHERE "Competitor" = '${compName}'
    ORDER BY CAST("Value" AS DOUBLE) DESC NULLS LAST
    LIMIT 30
  `);
 
  document.getElementById('subtab-deals').innerHTML = `
    <table>
      <thead><tr><th>Deal</th><th>Value</th><th>Stage</th><th>Owner</th><th>Loss Reason</th></tr></thead>
      <tbody>
        ${deals.map(d => `
          <tr>
            <td><div style="font-weight:600">${d['Deal Name'] || '—'}</div><div style="font-size:11px;color:#64748b">${d.Company || '—'}</div></td>
            <td style="color:#10b981;font-weight:700">$${Number(d.Value || 0).toLocaleString()}</td>
            <td><span class="badge ${d.Stage === 'Closed Won' ? 'won' : d.Stage === 'Closed Lost' ? 'lost' : ''}">${d.Stage || '—'}</span></td>
            <td style="color:#64748b">${d.Owner || '—'}</td>
            <td style="color:#94a3b8">${d['Loss Reason'] || '—'}</td>
          </tr>
        `).join('')}
      </tbody>
    </table>
  `;
 
  // Load patterns
  const patterns = await dench.db.query(`
    SELECT "Loss Reason", COUNT(*) AS count
    FROM v_deals
    WHERE "Competitor" = '${compName}' AND "Stage" = 'Closed Lost' AND "Loss Reason" IS NOT NULL
    GROUP BY "Loss Reason" ORDER BY count DESC
  `);
 
  document.getElementById('subtab-patterns').innerHTML = patterns.length ? `
    <h3 style="font-size:13px;color:#64748b;margin-bottom:12px">Most common loss reasons vs. ${compName}</h3>
    ${patterns.map(p => `
      <div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #334155;font-size:13px">
        <span>${p['Loss Reason']}</span>
        <span style="color:#ef4444;font-weight:700">${p.count} deals</span>
      </div>
    `).join('')}
  ` : '<p style="color:#64748b;font-size:13px">No loss reason data available. Add a "Loss Reason" field to your deals.</p>';
}
 
function showSubTab(tab) {
  ['deals', 'patterns', 'battlecard'].forEach(t => {
    document.getElementById(`subtab-${t}`).style.display = t === tab ? 'block' : 'none';
  });
  document.querySelectorAll('.tab').forEach((btn, i) => {
    btn.classList.toggle('active', ['deals', 'patterns', 'battlecard'][i] === tab);
  });
}
 
async function generateBattlecard() {
  if (!selectedComp) return;
  document.getElementById('battlecardContent').textContent = 'Generating...';
  showSubTab('battlecard');
 
  const session = await dench.chat.createSession();
  const prompt = `Generate a sales battlecard for competing against ${selectedComp.name}.
 
Our product is DenchClaw — a local-first, open-source AI CRM.
Competitor: ${selectedComp.name}
Our win rate vs them: ${selectedComp.winRate || 'unknown'}%
Deals won: ${selectedComp.won}, deals lost: ${selectedComp.lost}
 
Create a battlecard with:
1. Competitor Overview (2 sentences)
2. Why We Win (3 bullet points with specific advantages)
3. Why We Lose (2-3 honest reasons)  
4. Winning Objection Responses (top 3 objections and responses)
5. Qualifying Questions (2-3 questions that favor us)
6. Trap-Setting Questions (2 questions that expose competitor weaknesses)
 
Keep it punchy and practical for a sales rep to use before a call.`;
 
  const result = await dench.chat.send(session.id, prompt);
  document.getElementById('battlecardContent').textContent = result;
}
 
loadCompetitors();
Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA