# 10xAnswers

> A drop-in React chat widget for AI answers, built and published alone: 12,493 downloads across 55 releases on npm.

- Kind: side project
- Role: Solo
- When: Dec 2024 to May 2025, maintained since
- Stack: React, Recoil, Vite, Express, Gemini
- Live: https://10x-answers.vercel.app
- 10x-answers.vercel.app: https://10x-answers.vercel.app
- npm: https://www.npmjs.com/package/10xanswers

10xAnswers is a React chat widget you drop into a site: pass it a persona prompt and a backend URL, and it answers questions about you or your product.

I built it alone and published it to npm, where it has 12,493 downloads across 55 releases. It renders markdown with highlighted code, lets you edit a question, and is themed through about 20 CSS variables, while a hosted Express proxy keeps the model key on the server.

## What I built

- **The component.** A draggable React chat widget: markdown, highlighted code, editable questions, themed with about 20 CSS variables.
- **Published on npm.** 12,493 downloads across 55 releases, as ES and UMD builds behind an exports map.
- **The hosted proxy.** The model key stays on the server; input validated, size capped, clean 400, 413, 429 and 502 answers.
- **A playground that writes the code.** A 15-field configurator that turns your choices into the snippet to paste.
- **A TypeScript port.** For Plura AI as plura-chatbot, where I'm the largest contributor.

## Kept alive

In September 2026 I found that a junk dependency had made the published package uninstallable for everyone, and shipped 1.1.17 to fix it.

The hosted backend was calling a retired model. It now uses an always-current alias and answers like an API: 400 for a malformed request, 413 over the prompt cap, 429 when the model is rate limited, 502 when it fails.

```js
// 10xAnswers-BE · api/index.js
app.post("/", async (req, res) => {
  const prompt = req.body?.contents?.[0]?.parts?.[0]?.text

  if (typeof prompt !== "string" || !prompt.trim()) {
    return res.status(400).json({ message: /* shape */ })
  }
  if (prompt.length > MAX_PROMPT_CHARS) {
    return res.status(413).json({ message: /* limit */ })
  }

  try {
    // History arrives inside the prompt from the client,
    // so this function stays stateless and conversations
    // from different sites never mix.
    const result = await model.generateContent(prompt)
    const text = result.response.text()
    res.json({
      candidates: [{ content: { parts: [{ text }] } }],
    })
  } catch (err) {
    const status = err?.status === 429 ? 429 : 502
    res.status(status).json({ message: /* try again */ })
  }
})
```

Stateless on purpose: history travels with each question, so two sites using the same backend never share a conversation.

## On npm

12,493 downloads so far, across 55 releases.

## Next

Version 2: streaming, React 19, provider adapters and a voice mode, as the interface of an agent on this site.
