How to stop bot spam in your AI Chatbot

Tired of bots spamming your AI Chatbot and wasting your message limit? While the widget doesn’t have a built-in feature for this, our devs came up with a simple and effective solution — adding a Honeypot to filter out bot submissions :honey_pot:

How it works?


The Honeypot solution adds an invisible input field that real users never see. Bots, however, tend to fill in every field they find.

If this hidden field is filled out, the message is automatically blocked and won’t be sent :sparkles:


How to set it up ?


Add the custom script

Go to the Settings tab in the widget editor, find the Custom JS section, and add the following script:

const CHAT_PORTAL_SELECTOR = ".es-portal-root[class*='eapps-ai-chatbot']";

const HONEYPOT_CLASS = 'es-honeypot';
const HONEYPOT_NAME = 'message';

const waitForElement = (selector, interval = 250) => {
  return new Promise((resolve) => {
    const timer = setInterval(() => {
      const element =
        util.findElement(selector) ?? document.body.querySelector(selector);
      if (element) {
        clearInterval(timer);
        resolve(element);
      }
    }, interval);
  });
};

const createHoneyPotField = (sourceField) => {
  const fieldClone = sourceField.cloneNode(true);

  const input = fieldClone.querySelector('input');

  const placeholder = fieldClone.querySelector(
    '[class*="TextControlBasePlaceholder-sc"]'
  );

  if (!input) {
    return null;
  }

  fieldClone.classList.add(HONEYPOT_CLASS);
  fieldClone.setAttribute('aria-hidden', 'true');

  input.type = 'text';
  input.value = '';

  input.name = HONEYPOT_NAME;
  input.id = HONEYPOT_NAME;
  input.placeholder = HONEYPOT_NAME;

  input.tabIndex = -1;
  input.autocomplete = 'off';
  input.setAttribute('aria-hidden', 'true');

  if (placeholder) {
    placeholder.textContent = 'Message';
  }

  return fieldClone;
};

const insertHoneyPot = (form) => {
  const submitButton = form.querySelector('[type="submit"]');

  const sourceField = form.querySelector('.es-form-field-shortText');

  if (!submitButton || !sourceField) {
    return;
  }

  const existingHoneyPot = form.querySelector(`.${HONEYPOT_CLASS}`);

  if (existingHoneyPot) {
    return;
  }

  const honeyPotField = createHoneyPotField(sourceField);

  if (!honeyPotField) {
    return;
  }

  form.insertBefore(honeyPotField, submitButton);
};

const handleFormSubmit = (e) => {
  const form = e.target;

  if (!(form instanceof HTMLFormElement)) {
    return;
  }

  const honeyPotInput = form.querySelector(`.${HONEYPOT_CLASS} input`);

  if (honeyPotInput?.value?.trim()) {
    e.preventDefault();
    e.stopPropagation();

    console.warn('Spam blocked by honeypot');
  }
};

const patchedForms = new WeakSet();

const observePortal = (portal) => {
  const observer = new MutationObserver(() => {
    const form = portal.querySelector('form');

    if (!form || patchedForms.has(form)) {
      return;
    }

    insertHoneyPot(form);

    form.addEventListener('submit', handleFormSubmit, true);

    patchedForms.add(form);
  });

  observer.observe(portal, {
    childList: true,
    subtree: true,
  });
};

const initHoneyPot = async () => {
  const portal = await waitForElement(CHAT_PORTAL_SELECTOR);

  observePortal(portal);
};

initHoneyPot();

Note: Custom JS only works on the live site after the widget is installed —it won’t run inside the widget editor or preview.


Add the CSS code

Then, open the Custom CSS section, paste the code below and publish changes:

.es-honeypot {
  position: absolute !important;
  left: -9999px !important;
  top: 0 !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: 0 !important;
  border: 0 !important;
  overflow: hidden !important;
  opacity: 0 !important;
  pointer-events: none !important;
}


That’s it! Your AI Chatbot is now protected from bot spam and unnecessary message usage.

Guys, was this solution helpful for you? We’d love to hear your thoughts — feel free to share them in the comments :slightly_smiling_face:

Howdy!

For an important topic such as this one, what are Elfsight’s plans to incorporate the above into the AI chatbot’s native “defense mechanism” to prevent bots from consuming message tokens and endangering the affected website and/or business? Shouldn’t this topic be classified as “Security and Safety” and acted upon? Adding a “Honeypot” (ON/OFF) toggle to the configurator would be ideal (see below for example).

Please note that, recently, many of our message tokens were used up but never received any incoming notifications (i.e., emails) telling us what or who used our message tokens. I suspect the rapid usage was caused by a bot and/or Elfsight developers/team members testing changes made to the configurator or internal chatbot code.

Thank you!


Hi there, @Petar_Dietrich :waving_hand:

I completely understand your concern about the lack of a spam protection mechanism, and I’m really sorry it’s not supported yet.

At the moment, this feature isn’t on our roadmap, but we have received a request for a CAPTCHA option. Hopefully, our developers will be able to consider this improvement soon. We’ll keep you updated in the Wishlist thread if there are any changes :slightly_smiling_face:

Regarding the messages used, our devs and the Support Team don’t test new features on customers’ widgets since they use their own widgets for testing.

We can use some of the messages from your limit, but only when a user reports an issue with the chatbot’s response or needs a custom behavior setup and the number of messages we can use isn’t high (usually, just a few messages).

Additionally, I’d like to note that such tests or setups are performed through the configurator (except for the script testing, which should be live), so the messages used will be deducted from the test message limit, not from the overall message limit given on your plan.

Looking into your case, I don’t see any recent requests from you on the forum or in our support folder that would require the dev team to use your messages.

To confirm this, here’s a quick breakdown of messages used for the last 2 weeks:

May 5-6

Only 15 messages were used, all coming from your website, meaning they were sent by you, your team, or your clients:


April 28 - May 4

No conversations during this period.


April 24-27

12 messages were used. Most conversations came from your website, with the exception of one message sent by us to test the script for opening the email service in a new tab:


In total, 27 messages were used over the last two weeks, with 26 sent from your website. The messages appear to be live and not generated by bots.

I hope this clears things up.

If you’ve noticed a spike in message usage during a different period, please let me know, and I’ll be happy to check things.

Hi @Max,

As always, thank you for your quick and thorough support!

Well, there must be something going on. Bots, most likely. However, since our team burned through our test messages early on, I can only infer that any testing performed by Eflsight using our configurator indeed deducted tokens from our live messages. Not a biggie, but I would like to recommend Elfsight implement a method by which any form of testing conducted by Elfsight team members via a customer’s configurator should never deduct any type of message tokens.

Last, can you kindly delete the company information you posted (i.e., stats) and send them to me via DM? By the way, those stats are great. Customers should have access to them. Make them part of an AI Chatbot Debug Log available to the customer.

Thank you, amigo!

Also: (almost forgot)

Can you re-check your above JS and CSS codes and provide an alternate solution? I entered the above JS code into our configurator and the validator rejected it since we’re already using waitForElement.

Details:

Update:

In addition to the above, it appears your JS code has an additional issue/error. See below for details and fix.


The error is happening because you have defined the waitForElement function twice in the same script scope.

In JavaScript, you cannot declare a const with the same name more than once. Since you’ve combined several different features (Form Validation, Button Animations, and the Honeypot) into one file, the second declaration of waitForElement is crashing the script.

How to Fix It

You only need one version of that function. Here is the streamlined fix:

  1. Delete the definition of waitForElement from the bottom of your script (around line 457).
  2. Ensure there is one global version at the top of your script.
  3. Crucial Logic Fix: In your Honeypot section, your handlePreSubmit function is still trying to read .value from a div (the container), which will return undefined and allow spam through.

The Corrected “Prevent Spam” Section

Replace the bottom section of your code (from /* Prevent Spam via Honeypot */ downwards) with this version. I have removed the duplicate function and fixed the selector logic:

/* Prevent Spam via Honeypot */

// Note: waitForElement is already declared at the top of the script, 
// so we do not re-declare it here.

const CHAT_PORTAL_SELECTOR = ".es-portal-root[class*='eapps-ai-chatbot']";
const HONEYPOT_CONTAINER_CLASS = "es-field"; 
const HONEYPOT_NAME_VAL = "message_internal";

const insertHoneyPot = (form) => {
  const submitButton = form.querySelector('[type="submit"]');
  const template = form.querySelector('.es-form-field-shortText');
  
  if (!template || !submitButton) return;

  const fieldClone = template.cloneNode(true);
  const input = fieldClone.querySelector('input');
  const placeHolder = fieldClone.querySelector('[class*="TextControlBasePlaceholder-sc"]');
  
  input.type = "text";
  input.value = "";
  input.id = HONEYPOT_NAME_VAL;
  input.name = HONEYPOT_NAME_VAL;
  input.placeholder = HONEYPOT_NAME_VAL;
  input.autocomplete = "off";
  input.tabIndex = -1; // Hide from keyboard users

  if (placeHolder) {
    placeHolder.textContent = "Message";
  }
  
  // Apply class and hide it from human view
  fieldClone.classList.add(HONEYPOT_CONTAINER_CLASS);
  fieldClone.style.position = "absolute";
  fieldClone.style.opacity = "0";
  fieldClone.style.height = "0";
  fieldClone.style.zIndex = "-1";

  form.insertBefore(fieldClone, submitButton);
};

const handleHoneyPotSubmit = (e) => {
  const form = e.target.closest('form');
  // Target the actual INPUT inside our honeypot container
  const honeyInput = form.querySelector(`.${HONEYPOT_CONTAINER_CLASS} input`);
  
  if (honeyInput && honeyInput.value.trim() !== "") {
    console.warn("Spam detected.");
    e.preventDefault();
    e.stopPropagation();
  }
};

const portalObserve = (portal) => {
  const observer = new MutationObserver(() => {
    const chat = portal.querySelector(".es-window-container");
    if (!chat) return;

    const form = chat.querySelector("form");
    const existingHoneypot = form?.querySelector(`.${HONEYPOT_CONTAINER_CLASS}`);
    
    if (form && !existingHoneypot) {
      insertHoneyPot(form);
      const submitButton = form.querySelector('[type="submit"]');
      if (submitButton) {
        submitButton.addEventListener("click", handleHoneyPotSubmit);
      }
    }
  });

  observer.observe(portal, { childList: true, subtree: true });
};

const initHoneyPot = async () => {
  try {
    const portal = await waitForElement(CHAT_PORTAL_SELECTOR);
    portalObserve(portal);
  } catch (err) {
    // Silently fail if portal doesn't load
  }
};

initHoneyPot();

Why this works:

  • No Redeclaration: By removing the const waitForElement line from the bottom, the script will use the version already defined at the top of your file.
  • Correct Selector: Instead of honeyPot.value (which was looking at a div), it now uses form.querySelector('.es-field input').value.
  • Better Hiding: Added tabIndex = -1 so real users don’t accidentally tab into the “invisible” field while filling out the form.

Sure, I’ve removed the info with the message stats from the post!

Regarding your suggestion to exclude messages from our team when helping with the widget setup: the thing is, all messages (including test messages) are not free for us and require additional resources and costs.

At the same time, we understand that precise widget configuration is crucial for accurate bot responses. That’s why we’ve added 100 free messages for testing, which are intended for both your own work and for cases when you reach out to us for help.

Anyway, we greatly appreciate your feedback, and we’ve passed it along to the development team

As for the Honeypot script, the solution provided in the post is correct. We add scripts to the Pro Tips category assuming that no other scripts are already in the widget.

If other scripts are already in use (like in your case), adjustments will need to be made depending on the specific situation. Since it’s impossible to account for all possible scenarios, we publish the default version of the script.

If you’re using other scripts and want to add one more, just let us know, and we’ll be happy to assist :slightly_smiling_face:

Thank you, Max.

Did you catch these details (see above as well)? It appears your original JS code needs to be reviewed and corrected.

Crucial Logic Fix: In your Honeypot section, your handlePreSubmit function is still trying to read .value from a div (the container), which will return undefined and allow spam through.

and

  • Better Hiding: Added tabIndex = -1 so real users don’t accidentally tab into the “invisible” field while filling out the form.

Cheers!

My bad, sorry for missing this!

Our devs reviewed this solution and you’re absolutely right! We’ve adjsuted both the script and CSS code in the post and now it should be working fine :slightly_smiling_face:

A huge thank you for your help!

Awesome! You’re welcome. Here to help too! :+1: