<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stoney Epling</title>
    <description>The latest articles on DEV Community by Stoney Epling (@stoney_epling_da267a9f72a).</description>
    <link>https://dev.to/stoney_epling_da267a9f72a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3663988%2F35c34c4f-9a73-4953-a09d-132285231a06.png</url>
      <title>DEV Community: Stoney Epling</title>
      <link>https://dev.to/stoney_epling_da267a9f72a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stoney_epling_da267a9f72a"/>
    <language>en</language>
    <item>
      <title>Why We Built an AI Gateway in Go: Failover, PII Redaction, and Sub-Millisecond Caching</title>
      <dc:creator>Stoney Epling</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:06:31 +0000</pubDate>
      <link>https://dev.to/stoney_epling_da267a9f72a/why-we-built-an-ai-gateway-in-go-failover-pii-redaction-and-sub-millisecond-caching-26lj</link>
      <guid>https://dev.to/stoney_epling_da267a9f72a/why-we-built-an-ai-gateway-in-go-failover-pii-redaction-and-sub-millisecond-caching-26lj</guid>
      <description>&lt;p&gt;Every team shipping LLMs to production hits the same three bottlenecks sooner or later:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upstream Downtime &amp;amp; Spikes:&lt;/strong&gt; An OpenAI 503 or an Anthropic 529 "Overloaded" error takes down your user-facing app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance &amp;amp; Data Leaks:&lt;/strong&gt; Sensitive customer data (emails, credit cards, SSNs) gets sent to external foundation model providers unredacted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runaway Token Costs:&lt;/strong&gt; Repetitive, near-identical prompts run through high-cost frontier models instead of hitting an in-memory cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To solve this, many teams turn to proxies like LiteLLM, Portkey, or Helicone. While these tools have paved the way, we found recurring operational friction around multi-tenant quota desyncs, high proxy overhead, and complex self-hosting setups.&lt;/p&gt;

&lt;p&gt;We built &lt;strong&gt;SentinelGateway&lt;/strong&gt; as a zero-dependency, high-throughput Go proxy that acts as an OpenAI-compatible drop-in layer for production apps.&lt;/p&gt;

&lt;p&gt;Here is an architectural breakdown of how it works under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Zero Codebase Refactoring (The 1-Line Drop-In)
&lt;/h2&gt;

&lt;p&gt;Developers should not have to learn a proprietary SDK or rewrite orchestration chains. SentinelGateway implements the complete &lt;code&gt;/v1/chat/completions&lt;/code&gt; schema.&lt;/p&gt;

&lt;p&gt;Switching from direct OpenAI calls to multi-provider proxying requires updating only the base configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="c1"&gt;# Drop-in SentinelGateway proxy
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[https://sentinelgateway.ai/v1](https://sentinelgateway.ai/v1)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SENTINEL_GATEWAY_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4-failover&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Process this document...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Multi-Provider Capacity-Aware Failover
&lt;/h2&gt;

&lt;p&gt;When routing across OpenAI, Anthropic, Groq, and Google Gemini, raw network retries are not enough. If OpenAI returns HTTP 429 (rate-limited) or HTTP 503 (service unavailable), a naive retry to the same endpoint only amplifies cascading failures.&lt;/p&gt;

&lt;p&gt;SentinelGateway isolates upstream adapters and runs a capacity-aware failover loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The gateway checks upstream health and rate allocations in Redis.&lt;/li&gt;
&lt;li&gt;If the primary provider fails or returns a recoverable 5xx or 429 status, the proxy catches the error before the client socket drops.&lt;/li&gt;
&lt;li&gt;The prompt is automatically translated into the target provider's native format (e.g., Anthropic Messages API or Gemini REST payload) and dispatched in sub-25ms.&lt;/li&gt;
&lt;li&gt;If upstream token consumption fails midway, reservation refunds execute atomically to prevent phantom billing.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. In-Flight, Zero-Retention PII Scrubbing
&lt;/h2&gt;

&lt;p&gt;Sending raw prompts containing user identifiers to upstream providers creates serious compliance liabilities under GDPR and SOC2.&lt;/p&gt;

&lt;p&gt;SentinelGateway intercepts payloads in-flight before they touch the wire:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-speed token scanners detect SSNs, credit card numbers, API keys, and email addresses.&lt;/li&gt;
&lt;li&gt;Sensitive entities are redacted or pseudonymized in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero retention:&lt;/strong&gt; The gateway does not write prompt bodies to permanent disk logs, eliminating downstream breach exposure.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Sub-Millisecond Semantic Caching in Redis
&lt;/h2&gt;

&lt;p&gt;Many production workloads—such as customer support bots, classification pipelines, and documentation copilots—process semantically identical queries repeatedly.&lt;/p&gt;

&lt;p&gt;SentinelGateway runs an atomic Redis Lua evaluation pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exact and normalized prompt fingerprints are verified against hot cache tiers.&lt;/li&gt;
&lt;li&gt;Cache hits return in sub-15ms with zero upstream token consumption.&lt;/li&gt;
&lt;li&gt;Monthly quotas are decremented using atomic Lua counters, ensuring strict concurrency isolation across distributed nodes without race conditions.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How Does It Compare?
&lt;/h2&gt;

&lt;p&gt;If you are evaluating AI gateways for your infrastructure, check out our detailed side-by-side architecture comparisons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sentinelgateway.ai/compare/sentinel-vs-litellm" rel="noopener noreferrer"&gt;Sentinel vs. LiteLLM&lt;/a&gt;: A look at Python vs. Go runtimes, throughput benchmarks, and deployment overhead.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sentinelgateway.ai/compare/sentinel-vs-portkey" rel="noopener noreferrer"&gt;Sentinel vs. Portkey&lt;/a&gt;: Comparing telemetry pipelines, rate limiting, and enterprise control planes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sentinelgateway.ai/compare/sentinel-vs-helicone" rel="noopener noreferrer"&gt;Sentinel vs. Helicone&lt;/a&gt;: Logging overhead, semantic caching efficiency, and proxy latencies.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;SentinelGateway is live in production. You can spin up a free workspace with an included monthly token quota to test failovers and caching in your staging pipeline:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://sentinelgateway.ai" rel="noopener noreferrer"&gt;Get Started Free on SentinelGateway&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are running production AI workloads and have questions about our Go routing architecture or Redis Lua quota scripts, drop a comment below!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>go</category>
      <category>devops</category>
    </item>
    <item>
      <title>Tutorial: How to Detect VPNs and Tor Users in Node.js Express</title>
      <dc:creator>Stoney Epling</dc:creator>
      <pubDate>Tue, 16 Dec 2025 03:46:21 +0000</pubDate>
      <link>https://dev.to/stoney_epling_da267a9f72a/tutorial-how-to-detect-vpns-and-tor-users-in-nodejs-express-2023</link>
      <guid>https://dev.to/stoney_epling_da267a9f72a/tutorial-how-to-detect-vpns-and-tor-users-in-nodejs-express-2023</guid>
      <description>&lt;p&gt;If you run any kind of public API, SaaS, or forum, you already know the pain: &lt;strong&gt;Bot traffic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You ban a user for spamming, and 5 seconds later they are back with a new account because they toggled their VPN. You block an IP, and they switch to a Tor exit node.&lt;/p&gt;

&lt;p&gt;In this tutorial, I'm going to show you how to detect &lt;strong&gt;Non-Residential IPs&lt;/strong&gt; (VPNs, Proxies, and Hosting Centers) in your Node.js application so you can block them—or at least challenge them with a CAPTCHA—before they touch your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;We want a middleware function in Express that looks like this:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
javascript
app.use((req, res, next) =&amp;gt; {
  if (isHighRisk(req.ip)) {
     return res.status(403).send("VPNs are not allowed.");
  }
  next();
});

Here is how to build it.

Method 1: The "Hard" Way (Self-Hosted Lists)
If you want to do this entirely for free and offline, you need to download and maintain lists of known "Bad IPs."

Step 1: Get the Data
You will need to find a text file of Tor Exit nodes and IP ranges for major cloud providers (AWS, DigitalOcean, Linode).

Tor Exit Nodes: The Tor project publishes a list of exit addresses.

Cloud Ranges: AWS and Google publish their IP ranges in massive JSON files.

Step 2: The Code
You'll need to parse these lists into memory and check every incoming request.

JavaScript

const fs = require('fs');
const ipRangeCheck = require('ip-range-check'); // You'll need this npm package

// 1. Load the massive lists into memory (Careful with RAM!)
const torNodes = fs.readFileSync('tor-exit-nodes.txt', 'utf8').split('\n');
const awsRanges = JSON.parse(fs.readFileSync('aws-ip-ranges.json', 'utf8')).prefixes.map(p =&amp;gt; p.ip_prefix);

function isHighRisk(userIp) {
    // Check if IP is in the Tor list
    if (torNodes.includes(userIp)) return true;

    // Check if IP is in a Cloud Range (CPU intensive)
    if (ipRangeCheck(userIp, awsRanges)) return true;

    return false;
}

The Problem with Method 1
Stale Data: VPN providers rotate IPs daily. If you don't update your lists every hour, you will miss attacks.

Memory Hog: Loading millions of IPs into Node.js memory can crash your server (I learned this the hard way and OOM-killed my $5 droplet).

False Positives: It's hard to distinguish between a "Good" data center IP and a "Bad" VPN.

Method 2: The "Easy" Way (Live API Lookup)
After struggling with maintaining my own lists, I built a dedicated API called CandyCornDB to handle the heavy lifting. It specifically targets Infrastructure (ASN/ISP data) rather than just "bad behavior," so it catches fresh VPNs instantly.

Here is how to implement it in 3 lines of code.

Step 1: Get a Free API Key
You can grab a free key here (no credit card required).

Step 2: The Middleware
We will query the API, which returns a trustScore (0-100).

0-50: Residential / Safe

75+: High Risk (VPN / Tor / Hosting)

JavaScript

const axios = require('axios');

async function checkRiskScore(req, res, next) {
    const userIp = req.ip; 

    try {
        const response = await axios.get('[https://candycorndb.com/api/public/ip-score](https://candycorndb.com/api/public/ip-score)', {
            params: { ip: userIp }
        });

        const { score, isTor, isVPN } = response.data;

        // BLOCK if it's a confirmed Tor node or very high risk
        if (isTor || score &amp;gt; 85) {
            return res.status(403).json({ error: 'Anonymizers not allowed.' });
        }

        // CHALLENGE if it's suspicious (e.g., DigitalOcean droplet)
        if (score &amp;gt;= 50) {
            // Logic to show a CAPTCHA goes here...
            console.log(`Suspicious traffic from ${userIp}`);
        }

        next();
    } catch (err) {
        // Fail open: If API is down, let the user in so you don't block real people
        next();
    }
}

// Apply to your sensitive routes
app.post('/api/signup', checkRiskScore, (req, res) =&amp;gt; {
    res.send("Account created!");
});

Why this is better
Just-in-Time Scanning: If the API hasn't seen the IP before, it scans open ports and ISP data in &amp;lt;500ms. You never get "Unknown."

No Maintenance: You don't need to download daily CSV dumps.

Saves RAM: Your Node server handles the logic, not the database storage.

Summary

Blocking bad IPs is an arms race. If you are building a small hobby project, Method 1 is a fun learning exercise. But if you are protecting a production app, offloading the risk detection to a dedicated API (Method 2) is usually cheaper than the time you'll spend unbanning spam accounts.

Let me know if you have questions about IP filtering logic! I've spent way too much time staring at ASN lists lately. 😅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
