Dev.to WebDev 🛠 Dev 👁 0 📖 2 min read

Keep bots out of your Google Sheet lead form (honeypot + time trap)

The moment a lead form is public, bots start posting to it. In the webhook post I used a shared token, which stops drive-by scripts but not a bot that reads your page source. Here are two free additions that catch almost

The moment a lead form is public, bots start posting to it. In the webhook post I used a shared token, which stops drive-by scripts but not a bot that reads your page source. Here are two free additions that catch almost everything else: a honeypot field and a time trap. No CAPTCHA, no third-party service.

The form side

Add two things to the form from the webhook post:

<form action="YOUR_EXEC_URL" method="POST">
  <input name="name" placeholder="Your name">
  <input name="email" type="email" required placeholder="[email protected]">

  <!-- Honeypot: invisible to humans, irresistible to bots -->
  <input name="company" tabindex="-1" autocomplete="off"
         aria-hidden="true" style="position:absolute;left:-9999px">

  <input type="hidden" name="token" value="change-me">
  <input type="hidden" name="t" id="t">
  <button type="submit">Send</button>
</form>
<script>
  document.getElementById('t').value = Date.now();
</script>

Two details matter. Hide the honeypot with off-screen CSS, not type="hidden", because many bots skip hidden inputs. And give it a tempting name like company or website, since bots fill every field they can see. The aria-hidden and tabindex="-1" keep screen readers and keyboard users out of it.

The script side

Extend the doPost from the webhook post:

const MIN_SECONDS = 3;

function doPost(e) {
  const p = e.parameter;
  if (p.token !== SECRET) {
    return json({ ok: false, error: 'bad token' });
  }
  // Honeypot: a human never fills this.
  if (String(p.company || '') !== '') {
    return json({ ok: true }); // pretend it worked, save nothing
  }
  // Time trap: real people need a few seconds to type an email.
  const t = Number(p.t || 0);
  if (!t || (Date.now() - t) < MIN_SECONDS * 1000) {
    return json({ ok: true }); // same fake success
  }
  const email = String(p.email || '').trim();
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
    return json({ ok: false, error: 'bad email' });
  }
  SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Leads')
    .appendRow([new Date(), safe(p.name), safe(email), '', '']);
  return json({ ok: true });
}

Notice both bot rejections return ok: true. Telling a bot exactly why it failed is free debugging help, so give it a fake win and move on.

Why this works, and where it stops

  • Bots fill every field; humans never see the honeypot. That alone catches most form spam.
  • Bots submit instantly; humans take a few seconds. The time trap catches the ones clever enough to skip odd fields.
  • A determined human spammer still gets through. At lead-form volumes, that is a reply-to-the-weird-email problem, not a code problem.

If you want one more layer, dedupe in the sheet: before appendRow, scan the email column and skip repeats. Keeps the sheet clean when someone double-submits.

This sits on top of the earlier pieces: the webhook that catches leads, the auto-reply that answers them, and the follow-up for the quiet ones. All of it, plus editable templates and a setup guide, is in the LeadReply Kit. The kit is mock-tested sample code, so read it before pointing it at real leads.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.