Back to The Times of Claw

Build a Revenue Calculator App in DenchClaw

Build a revenue calculator app in DenchClaw that models ARR growth, churn, expansion, and monthly targets using your real CRM data and configurable assumptions.

Mark Rachapoom
Mark Rachapoom
·7 min read
Build a Revenue Calculator App in DenchClaw

Build a Revenue Calculator App in DenchClaw

Revenue planning at early-stage companies usually happens in Google Sheets, disconnected from the CRM. The result: your actuals and your model drift apart, and nobody knows which number is right. A revenue calculator app inside DenchClaw solves this — it pulls actuals from DuckDB and lets you model forward from there.

This guide builds a revenue calculator that reads your real closed deals, calculates current ARR, and lets you model growth scenarios interactively.

What You're Building#

  • Current ARR/MRR calculated from closed deals in DuckDB
  • Interactive sliders for growth rate, churn rate, and expansion revenue
  • 12-month forward model with monthly breakdown
  • Goal tracker: projected vs. target
  • Export model to CSV

Step 1: App Setup#

mkdir -p ~/.openclaw-dench/workspace/apps/revenue-calculator.dench.app

.dench.yaml:

name: Revenue Calculator
description: ARR modeling from real CRM data with growth scenario planning
icon: dollar-sign
version: 1.0.0
permissions:
  - read:crm
display: tab

Step 2: HTML Layout#

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Revenue Calculator</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 20px; display: grid; grid-template-columns: 320px 1fr; gap: 20px; min-height: 100vh; }
    .controls { display: flex; flex-direction: column; gap: 16px; }
    .card { background: #1e293b; border-radius: 12px; padding: 16px; }
    h3 { font-size: 12px; color: #64748b; text-transform: uppercase; margin: 0 0 12px; }
    .metric-big { font-size: 32px; font-weight: 800; line-height: 1; margin-bottom: 4px; }
    .metric-label { font-size: 12px; color: #64748b; }
    .slider-group { margin-bottom: 14px; }
    .slider-header { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }
    .slider-label { color: #94a3b8; }
    .slider-value { font-weight: 700; color: #6366f1; }
    input[type="range"] { width: 100%; accent-color: #6366f1; }
    .model-table { width: 100%; border-collapse: collapse; }
    .model-table th { font-size: 11px; color: #64748b; text-transform: uppercase; padding: 8px 12px; text-align: right; }
    .model-table th:first-child { text-align: left; }
    .model-table td { padding: 8px 12px; border-top: 1px solid #334155; font-size: 13px; text-align: right; }
    .model-table td:first-child { text-align: left; color: #94a3b8; }
    .model-table tr.highlight { background: #6366f110; }
    .model-table tr.highlight td { font-weight: 700; }
    .chart-container { width: 100%; height: 200px; position: relative; margin-top: 16px; }
    canvas { width: 100% !important; height: 200px !important; }
    button { padding: 10px 16px; background: #334155; color: #94a3b8; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; width: 100%; }
    .arr-context { font-size: 12px; color: #64748b; margin-top: 8px; }
  </style>
</head>
<body>
  <div class="controls">
    <div class="card">
      <h3>Current State (from CRM)</h3>
      <div class="metric-big" id="currentARR" style="color:#10b981">—</div>
      <div class="metric-label">Estimated ARR</div>
      <div class="arr-context" id="arrContext"></div>
    </div>
    <div class="card">
      <h3>Growth Assumptions</h3>
      <div class="slider-group">
        <div class="slider-header"><span class="slider-label">New MRR / month</span><span class="slider-value" id="v-new-mrr">$0</span></div>
        <input type="range" id="s-new-mrr" min="0" max="50000" step="500" value="5000" oninput="updateModel()">
      </div>
      <div class="slider-group">
        <div class="slider-header"><span class="slider-label">Monthly churn rate</span><span class="slider-value" id="v-churn">2%</span></div>
        <input type="range" id="s-churn" min="0" max="15" step="0.5" value="2" oninput="updateModel()">
      </div>
      <div class="slider-group">
        <div class="slider-header"><span class="slider-label">Expansion revenue (% of base)</span><span class="slider-value" id="v-expansion">0%</span></div>
        <input type="range" id="s-expansion" min="0" max="20" step="0.5" value="0" oninput="updateModel()">
      </div>
      <div class="slider-group">
        <div class="slider-header"><span class="slider-label">Annual target ARR</span><span class="slider-value" id="v-target">$1M</span></div>
        <input type="range" id="s-target" min="100000" max="10000000" step="50000" value="1000000" oninput="updateModel()">
      </div>
    </div>
    <button onclick="exportCSV()">Export to CSV</button>
  </div>
  <div>
    <div class="card" style="margin-bottom:16px">
      <h3>12-Month Revenue Model</h3>
      <div class="chart-container">
        <canvas id="revenueChart"></canvas>
      </div>
    </div>
    <div class="card">
      <h3>Monthly Breakdown</h3>
      <table class="model-table">
        <thead><tr><th>Month</th><th>MRR</th><th>ARR Run Rate</th><th>vs. Target</th></tr></thead>
        <tbody id="modelTable"></tbody>
      </table>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  <script src="calculator.js"></script>
</body>
</html>

Step 3: Calculator Logic#

calculator.js:

let currentMRR = 0;
let chart = null;
 
function fmt(n) {
  if (n >= 1000000) return '$' + (n / 1000000).toFixed(2) + 'M';
  if (n >= 1000) return '$' + (n / 1000).toFixed(0) + 'K';
  return '$' + Math.round(n).toLocaleString();
}
 
function pct(actual, target) {
  const p = Math.round(actual / target * 100);
  const color = p >= 100 ? '#10b981' : p >= 75 ? '#f59e0b' : '#ef4444';
  return `<span style="color:${color}">${p}%</span>`;
}
 
async function loadCurrentARR() {
  // Calculate MRR from deals closed in the last 30 days
  const recentRevenue = await dench.db.query(`
    SELECT SUM(CAST("Value" AS DOUBLE)) AS total
    FROM v_deals
    WHERE "Stage" = 'Closed Won'
      AND "Close Date" >= CURRENT_DATE - INTERVAL '30 days'
  `);
  
  // Also get all-time customer count
  const customerCount = await dench.db.query(`
    SELECT COUNT(*) AS count FROM v_people WHERE "Status" = 'Customer'
  `);
 
  const monthlyRevenue = recentRevenue[0]?.total || 0;
  currentMRR = monthlyRevenue;
  
  document.getElementById('currentARR').textContent = fmt(currentMRR * 12);
  document.getElementById('arrContext').innerHTML = `
    Based on ${fmt(currentMRR)} MRR (deals closed last 30 days)<br>
    ${customerCount[0]?.count || 0} active customers in CRM
  `;
 
  // Pre-fill new MRR slider with current trend
  const slider = document.getElementById('s-new-mrr');
  slider.value = Math.max(500, Math.min(monthlyRevenue, 50000));
  
  updateModel();
}
 
function updateModel() {
  const newMRR = Number(document.getElementById('s-new-mrr').value);
  const churnPct = Number(document.getElementById('s-churn').value) / 100;
  const expansionPct = Number(document.getElementById('s-expansion').value) / 100;
  const target = Number(document.getElementById('s-target').value);
 
  // Update slider display values
  document.getElementById('v-new-mrr').textContent = fmt(newMRR);
  document.getElementById('v-churn').textContent = (churnPct * 100).toFixed(1) + '%';
  document.getElementById('v-expansion').textContent = (expansionPct * 100).toFixed(1) + '%';
  document.getElementById('v-target').textContent = fmt(target);
 
  // Calculate 12-month model
  const months = [];
  let mrr = currentMRR;
  const now = new Date();
 
  for (let i = 1; i <= 12; i++) {
    const date = new Date(now);
    date.setMonth(date.getMonth() + i);
    const monthLabel = date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
 
    const churnAmount = mrr * churnPct;
    const expansionAmount = mrr * expansionPct;
    mrr = mrr - churnAmount + expansionAmount + newMRR;
    
    months.push({
      label: monthLabel,
      mrr: Math.max(0, mrr),
      arr: Math.max(0, mrr * 12)
    });
  }
 
  // Render table
  const targetARR = target;
  document.getElementById('modelTable').innerHTML = months.map((m, i) => `
    <tr ${i === 11 ? 'class="highlight"' : ''}>
      <td>${m.label}${i === 11 ? ' (EOY)' : ''}</td>
      <td>${fmt(m.mrr)}</td>
      <td>${fmt(m.arr)}</td>
      <td>${pct(m.arr, targetARR)}</td>
    </tr>
  `).join('');
 
  // Render chart
  const labels = months.map(m => m.label);
  const arrValues = months.map(m => m.arr);
  const targetLine = months.map(() => target);
 
  if (chart) chart.destroy();
  chart = new Chart(document.getElementById('revenueChart'), {
    type: 'line',
    data: {
      labels,
      datasets: [
        {
          label: 'Projected ARR',
          data: arrValues,
          borderColor: '#6366f1',
          backgroundColor: 'rgba(99,102,241,0.1)',
          fill: true,
          tension: 0.4
        },
        {
          label: 'Target',
          data: targetLine,
          borderColor: '#f59e0b',
          borderDash: [5, 5],
          fill: false,
          pointRadius: 0
        }
      ]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: { legend: { labels: { color: '#94a3b8', font: { size: 11 } } } },
      scales: {
        x: { ticks: { color: '#64748b', font: { size: 10 } }, grid: { color: '#1e293b' } },
        y: {
          ticks: { color: '#64748b', font: { size: 10 }, callback: v => fmt(v) },
          grid: { color: '#334155' }
        }
      }
    }
  });
}
 
function exportCSV() {
  const rows = document.querySelectorAll('#modelTable tr');
  const csv = ['Month,MRR,ARR Run Rate,vs. Target']
    .concat([...rows].map(r => [...r.querySelectorAll('td')].map(td => td.textContent).join(',')))
    .join('\n');
  const blob = new Blob([csv], { type: 'text/csv' });
  const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: 'revenue-model.csv' });
  a.click();
}
 
loadCurrentARR();

Frequently Asked Questions#

How does it calculate current MRR?#

It sums deal values closed in the last 30 days from your v_deals view. For more accuracy, add a Monthly Value field to your deals for subscription deals where the deal value is the total contract, not monthly.

Can I add multiple revenue scenarios?#

Add scenario tabs (conservative/base/optimistic) with different slider presets. Save scenarios to dench.store and switch between them.

How do I add actual revenue tracking month by month?#

Query your closed deals grouped by month and overlay them on the chart as an "Actuals" line. This gives you model vs. actual comparison as the year progresses.

Does this work for non-SaaS businesses?#

Yes. For project-based businesses, adjust the model to show total bookings rather than ARR. Remove the churn slider (or set it to 0%) and focus on the new business pipeline.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA