DEV Community

Cover image for We open-sourced our chatbot widget. 3 lines, drops into any site.
ithiria894
ithiria894

Posted on • Originally published at github.com

We open-sourced our chatbot widget. 3 lines, drops into any site.

Last night the bot told a fake plumbing customer "your leak inspection is $95" and then rendered a payment card with Interac and credit card options inline. No backend OAuth, no Stripe webhook setup. Just a payment link URL pasted into config.

I've been rebuilding the same chatbot widget for every demo I ship. Different brand color, same wiring around it. So I extracted it onto npm as chatbotlite. Apache 2.0, version 0.7.24 went out today.

This isn't a product launch post. It's a writeup of the three design decisions that took the most thinking. If you're building anything in the chat-widget category, the tradeoffs probably matter to you.

1. 10-provider failover (the kind that doesn't lie)

The widget posts to /api/chat on your server. Your server has a ChatBot class:

import { ChatBot } from "chatbotlite/client";
import { knowledgeFromFile } from "chatbotlite/node";

const bot = new ChatBot({
  knowledge: knowledgeFromFile("./knowledge.md"),
  providers: {
    keys: {
      openai: process.env.OPENAI_API_KEY,
      groq:   process.env.GROQ_API_KEY,
      deepseek: process.env.DEEPSEEK_API_KEY,
    },
    chain: [
      { provider: "openai",   model: "gpt-4o-mini" },
      { provider: "groq",     model: "llama-3.3-70b-versatile" },
      { provider: "deepseek", model: "deepseek-chat" },
    ]
  }
});
Enter fullscreen mode Exit fullscreen mode

If a stream errors mid-token (which OpenAI does roughly every 200 requests under load), the next provider in the chain takes over and the assistant restarts the reply.

The honest catch: tokens already streamed to the client stay visible during the switch. The user sees Our sink leak insp from OpenAI, then a brief flicker, then the full restart from Groq. True token-level replay is on the 0.8 roadmap. That one is hard because LLMs don't accept "resume from token N" semantics natively, so we need to re-inject the partial assistant message as conversation context. i'd rather ship "honest restart" today than promise something we don't have yet.

10 providers in the box: OpenAI, Groq, DeepSeek, Gemini, Mistral, Fireworks, Cerebras, SambaNova, OpenRouter, Moonshot. All through OpenAI-compat chat/completions. Anthropic isn't in the chain yet because their native API uses /v1/messages with a different request shape. Native Anthropic adapter is a 0.8 item.

2. SKILL markers instead of native tool_use

The chatbot can trigger UI cards by emitting markers inline in its reply text:

[SKILL:requestPayment amount=4250 currency="cad" reason="initial deposit"]
[SKILL:scheduleCallback durationMin=15 timezone="America/Vancouver"]
[SKILL:uploadForReview purpose="T4 slip" accept="image/*,application/pdf"]
[SKILL:pickerMessage prompt="Service type?" options="Inspection,Repair,Emergency"]
Enter fullscreen mode Exit fullscreen mode

The widget parses these out of the stream as the tokens arrive, strips them from displayed text, and renders an interactive card matching the tool name. User completes the card. Result posts back as system context on the next turn.

Why markers and not native tool_use:

Native tool_use is a different JSON shape per provider. OpenAI gives you tool_calls arrays, Anthropic gives you content blocks with type: "tool_use". If your chain spans both, you're translating between formats on every turn. Markers don't care which provider produced them. Same grammar from gpt-4o or claude-haiku.

The regex, since you'll want to see it:

const MARKER_RE = /\[SKILL:(\w+)((?:\s+\w+=(?:"[^"]*"|[\w./@*+,:-]+))*)\s*\]/g;
Enter fullscreen mode Exit fullscreen mode

There's a public spec in the repo. The protocol is portable, so if you're building your own widget you can use the same markers without coupling to this library.

3. URL-only adapter SDK

This is the one I keep recommending to people who ask "what's actually new here."

Most chat widgets handle Stripe by making you write a backend webhook handler, then OAuth into Stripe, then ship a function that handles the marker. Three days of integration work for a $95 plumbing deposit.

ChatbotLite has 13 adapters where the customer pastes a URL into config. Done.

import { stripeLink, calendlyUrl } from "chatbotlite/adapters";

<ChatWidget
  endpoint="/api/chat"
  tools={{
    requestPayment: stripeLink("https://buy.stripe.com/your-link"),
    scheduleCallback: calendlyUrl("https://calendly.com/you/30min"),
  }}
/>
Enter fullscreen mode Exit fullscreen mode

The implementation per adapter is about 5 lines. stripeLink returns { onPick: ({ amount, currency }) => ({ status: "opened", method: "stripe" }) } and opens the payment link in a new tab. Stripe handles PCI compliance, receipts, refunds.

Trade-off: you don't get programmatic confirmation that payment completed. You only know the user clicked through. For an SMB plumbing site that's fine. If you need actual completion webhooks, 0.8 will have server-side adapters that take an API key and do the full Checkout Session flow.

13 in the box right now: Stripe Payment Link, PayPal, Square, Lemon Squeezy, Gumroad (payment). Calendly, Cal.com, SavvyCal, Acuity, Microsoft Bookings, Google Calendar appointment URL (scheduling). Formspree, Tally (lead capture).

What I learned shipping it

The single dumbest bug I shipped a fix for today: the system prompt told the LLM "emit the marker INLINE in your reply" and the model started literally typing the word INLINE at the end of its message. The screenshot of the widget answering "Our sink leak inspection is $95, I can request payment for that now. INLINE" was sitting in the README hero image for about an hour before I noticed.

Lesson: when you write a system prompt instruction, treat every all-caps adverb as a potential token the model might type back at you. The fix was rewording to "Write a short message, then place the marker at the end" with an explicit "Do NOT include the word INLINE next to the marker." Caught it with a regression test. Moved on.

The CHANGELOG for 0.7.24 has the full diff.

What's on 0.8

Real work that's intended, not promised:

  • Native Anthropic /v1/messages adapter (right now you'd route Claude via OpenRouter)
  • Mid-stream token replay so failover stops showing the partial-then-restart flicker
  • A providers.router callback for per-message routing. A few people on r/reactjs asked, but I'm honestly not sure the cost math works for SMB volumes. Will reconsider with concrete data.

Try it

6 live demos at https://chatbotlite-demos.vercel.app. Plumber, restaurant, coffee shop, dentist, tax prep, yoga studio. Same npm package, different config. Each demo's bot has its own knowledge file.

Star ChatbotLite on GitHub if you'd rather not write another chat widget yourself this year.

GitHub logo agents-io / chatbotlite

Drop-in AI chatbot SDK + React widget. 3 lines to integrate. 11 LLM providers with auto-failover. 13 URL-only adapters (Stripe, Calendly, PayPal...). Markdown knowledge. Apache 2.0.

ChatbotLite

ChatbotLite

AI chatbot in 3 lines of code.
Stop burning tokens building chatbots from scratch. We did it for you.
Built for indie hackers and SMB sites that need a working chatbot today, not after a 3-week integration.

ChatbotLite widget on a real Acme Plumbing site — user asks 'how much is a leak inspection?', bot replies $95 and renders an inline payment card with Interac e-Transfer + card options.

▶ Try it live — landing page with 6 demos, install code, and comparison

GitHub stars npm version npm downloads bundle size CI license

Live demos · API Reference · Roadmap · SKILL Protocol


3 lines to chatbot

import { ChatWidget } from "chatbotlite/react";

<ChatWidget endpoint="/api/chat" title="Acme Plumbing" theme={{ primary: "#1e3a8a" }} />
Enter fullscreen mode Exit fullscreen mode

Or plain HTML (Shopify, WordPress, Webflow, anywhere):

<script src="https://unpkg.com/chatbotlite/dist/embed.global.js"></script>
<script>
  chatbotlite.mount({ endpoint: "/api/chat", title: "Acme Plumbing" });
</script>
Enter fullscreen mode Exit fullscreen mode

Server (/api/chat):

import { ChatBot } from "chatbotlite/client";
import { knowledgeFromFile } from "chatbotlite/node";
const
Enter fullscreen mode Exit fullscreen mode

I'm Nicole. Same agents-io org that ships PokeClaw (875⭐, Android automation) and Cross-Code Organizer (328⭐, cross-harness config dashboard for Claude Code, Codex CLI, MCP servers).

Top comments (0)