DEV Community

babycat
babycat

Posted on

The Free Server Saw My API Key: A Client-Side Redaction Layer for AI Prompts

Last week I pasted a code snippet into a chat widget backed by MonkeyCode's free server, and the snippet contained a hardcoded API key. The response was helpful, the stream rendered smoothly, and then it hit me: that key had just traveled through a server I don't control. Free tiers are great for experiments, but they're also a reminder that someone else is reading your prompts.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The fix isn't to avoid free servers. It's to assume they're transparent and build a redaction layer in the browser.

The data flow you forgot about

When you type a prompt, it goes through at least three hops: your browser, the API gateway, and the model provider. On a free server, you're not paying with money — you're paying with data. Your prompt may contain:

  • Hardcoded API keys and secrets
  • Internal hostnames and IP addresses
  • Customer emails and personal data
  • Proprietary code snippets that reveal business logic

The solution is a client-side redaction layer with three steps: identify sensitive patterns, replace them with placeholders before the request leaves the browser, and restore them when the response comes back.

A 40-line redaction layer

Here's the complete implementation. It's intentionally small so you can read every line:

// redaction-layer.ts
export class RedactionLayer {
  private map = new Map<string, string>();
  private counter = 0;

  private patterns: RegExp[] = [
    /sk-[A-Za-z0-9_-]{16,}/g,                    // OpenAI-style API keys
    /AKIA[0-9A-Z]{16}/g,                          // AWS access key IDs
    /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----\s*-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
    /[\w.+-]+@[\w-]+\.[\w.-]+/g,                  // email addresses
    /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,               // IPv4 addresses
    /(?:https?:\/\/)?[\w-]+\.(?:corp|internal|local)(?:\/\d+)?/g,  // internal hostnames
  ];

  redact(text: string): { safe: string; count: number } {
    const combined = new RegExp(
      this.patterns.map((p) => p.source).join("|"),
      "g"
    );

    let count = 0;
    const safe = text.replace(combined, (match) => {
      count++;
      const placeholder = `⟦REDACTED_${this.counter++}⟧`;
      this.map.set(placeholder, match);
      return placeholder;
    });

    return { safe, count };
  }

  restore(text: string): string {
    return text.replace(/⟦REDACTED_\d+⟧/g, (ph) => this.map.get(ph) ?? ph);
  }

  clear(): void {
    this.map.clear();
    this.counter = 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

The redact method combines all patterns into one regex and replaces every match with a unique placeholder. The restore method reverses the operation. The map holds the placeholder-to-original mapping in memory.

Wiring it into a streaming client

Here's how the layer plugs into an existing chat flow:

const layer = new RedactionLayer();

async function sendChat(prompt: string) {
  const { safe, count } = layer.redact(prompt);

  if (count > 0) {
    statusRegion.textContent =
      `${count} sensitive item${count > 1 ? "s" : ""} redacted before sending`;
  }

  const res = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ prompt: safe }),
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let output = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    output += layer.restore(decoder.decode(value, { stream: true }));
    render(output);
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice what's happening: the prompt is redacted before fetch is called, and every chunk is restored before it's rendered. The model never sees the original secrets, and the user never sees the placeholders.

To restore or not to restore

This is the key design decision. There are three strategies:

Strategy What happens Best for
Always restore Every placeholder is replaced when rendering Simple, but the model may echo placeholders
Never restore Placeholders stay visible Maximum safety, but confusing output
Restore on demand User clicks "show original" to reveal Privacy with user control

I recommend restore-on-demand. The default view shows placeholders, and a button labeled "Show original values" reveals them. This gives the user agency over their data.

Tell the user what you redacted

Redaction is a state change, and state changes need to be announced. Use a role="status" region, not alert():

<div role="status" aria-live="polite" id="status-region"></div>
Enter fullscreen mode Exit fullscreen mode

When the layer redacts something, set the text content of that region. Screen reader users will hear "3 sensitive items redacted before sending" without losing focus. Also provide a way to inspect what was redacted — a simple expandable list of original values — so users can verify the layer didn't miss anything or over-redact.

When redaction makes things worse

Redaction is a heuristic, and heuristics fail. Here are the failure modes I've hit:

  1. Regex misses context. A variable named const secret = "abc123" — the regex catches abc123 only if it matches a pattern. It won't catch a customer name or a project codename.
  2. Placeholders change token semantics. The model sees ⟦REDACTED_0⟧ and doesn't know it's an API key. If you're asking "is this key hardcoded?", the answer will be useless.
  3. The mapping is memory-only. Refresh the page and the placeholders become permanent. If you persist chat history, you need to persist the mapping too.
  4. Redaction can break structured data. If you paste a JSON blob with a "password" field, the regex might replace the value but leave the key intact, producing invalid JSON.

Who should skip this

Don't use a redaction layer if:

  • Your task requires the model to analyze the sensitive data itself, like a security audit of your own code.
  • You need end-to-end encryption; redaction is not encryption, it's masking.
  • You're sending highly structured data where regex replacement can corrupt the format.

And if you're on a paid plan with a data-processing agreement, the calculus changes — but the habit of assuming your prompts are visible is still a good one.

The 40-line habit

Next time you paste a code snippet into an AI chat, ask yourself: is there anything in here I wouldn't want a stranger to read? If the answer is yes, spend 40 lines on a redaction layer. MonkeyCode's free server is a convenient place to practice — precisely because it's free, you should assume the data is being read. Build the layer, paste a fake API key, and watch it get redacted before the request leaves the browser. That's a much better feeling than watching it appear in someone else's logs.

MonkeyCode provides free models that can run this workflow.

Top comments (0)