<?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: Doug silkstone</title>
    <description>The latest articles on DEV Community by Doug silkstone (@dougwithseismic).</description>
    <link>https://dev.to/dougwithseismic</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%2F2849980%2Fc0fa5143-c3de-4145-9fdc-6eafdf97325b.png</url>
      <title>DEV Community: Doug silkstone</title>
      <link>https://dev.to/dougwithseismic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dougwithseismic"/>
    <language>en</language>
    <item>
      <title>A Practical Holdout Pattern for Coupon Extension Rules</title>
      <dc:creator>Doug silkstone</dc:creator>
      <pubDate>Mon, 05 Oct 2026 16:35:23 +0000</pubDate>
      <link>https://dev.to/dougwithseismic/a-practical-holdout-pattern-for-coupon-extension-rules-2kbd</link>
      <guid>https://dev.to/dougwithseismic/a-practical-holdout-pattern-for-coupon-extension-rules-2kbd</guid>
      <description>&lt;p&gt;Coupon extensions can change a checkout experience and the way a discount is attributed. A detected extension is not proof that it caused a bad order, though. Before applying a global block, a merchant needs a comparison they can explain.&lt;/p&gt;

&lt;p&gt;I use a simple control loop when building Benson: observe the session, assign a stable holdout, measure the same outcomes on both sides, then choose the smallest rule the evidence supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with an event contract
&lt;/h2&gt;

&lt;p&gt;Keep detection, treatment, and outcome data separate. A useful session record can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a first-party scoped session identifier;&lt;/li&gt;
&lt;li&gt;the detected surface, such as a coupon extension or shopping agent;&lt;/li&gt;
&lt;li&gt;the rule state: observe, treatment, or holdout;&lt;/li&gt;
&lt;li&gt;code attempts and the response from the storefront;&lt;/li&gt;
&lt;li&gt;order, conversion, discount, and order value fields when they exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This contract lets a team answer two different questions: “What appeared in the session?” and “What changed after we applied a rule?” Combining those questions into one event makes later analysis hard to audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep detection separate from the decision
&lt;/h2&gt;

&lt;p&gt;An extension can try a code without changing the final order. An AI shopping agent can request several offers without telling you whether the shopper would have paid full price, used a first-party offer, or abandoned checkout. Detection is the beginning of the investigation.&lt;/p&gt;

&lt;p&gt;In observe mode, record what a proposed rule would match while leaving the existing experience in place. This is a safe way to find an overly broad selector, a valuable partner, or a code that the merchant intended to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Assign a stable holdout
&lt;/h2&gt;

&lt;p&gt;Use a stable first-party identifier so the same kind of visit does not move between groups on every reload. The exact split depends on the store and the experiment, but the assignment must be decided before reading the outcome.&lt;/p&gt;

&lt;p&gt;An illustrative assignment looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bucket = hash(experiment_id + stable_session_id) % 100
rule_state = "treatment" if bucket &amp;lt; 50 else "holdout"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The holdout keeps the current experience. The treatment applies the proposed block, cap, or replacement. Keep the time window, offer conditions, and traffic definition visible beside the result; otherwise a clean-looking comparison can hide a campaign or merchandising change.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Compare outcomes side by side
&lt;/h2&gt;

&lt;p&gt;Read the treatment and holdout together. Useful measures include conversion rate, order value, discount cost, commission or attribution cost, and code attempts per session. A treatment group with fewer discounts is not automatically better if it also loses valuable orders.&lt;/p&gt;

&lt;p&gt;Avoid turning an illustrative dashboard or a before-and-after estimate into a customer result report. A concurrent holdout is stronger evidence because both groups experience the same period and store conditions. If the sample is small or traffic changes halfway through, label the result as a signal and keep the test running.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Put guardrails around the rule
&lt;/h2&gt;

&lt;p&gt;A rule should have a narrow match, a clear rollback, and a visible holdout. Check the rule against the sessions it would affect before enabling it. Watch for changes in traffic mix, first-party promotions, inventory, and campaign terms that could explain an outcome difference.&lt;/p&gt;

&lt;p&gt;If conversion stays comparable while discount or attribution cost moves in the desired direction, the merchant has evidence for a controlled rollout. If conversion falls, keep the holdout and investigate the trade-off instead of expanding the block.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Make the decision reproducible
&lt;/h2&gt;

&lt;p&gt;Store the experiment name, assignment rule, time window, storefront conditions, and the version of the control in the readout. That record turns “the extension looked expensive” into a decision another operator can inspect and repeat.&lt;/p&gt;

&lt;p&gt;This is the gap Benson is built for: showing online stores which coupon extensions and AI shopping agents appear in their sessions and what they cost, then supporting hide, cap, or replacement rules with a holdout behind each change. I am the founder of Benson. This article describes a measurement method, not a customer result report. Learn more at &lt;a href="https://trybenson.com" rel="noopener noreferrer"&gt;https://trybenson.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Spin up a License Key API on new Railway Functions In Less Than Two Minutes!</title>
      <dc:creator>Doug silkstone</dc:creator>
      <pubDate>Tue, 11 Feb 2025 23:07:26 +0000</pubDate>
      <link>https://dev.to/dougwithseismic/spin-up-a-license-key-api-on-new-railway-functions-in-less-than-two-minutes-4m5i</link>
      <guid>https://dev.to/dougwithseismic/spin-up-a-license-key-api-on-new-railway-functions-in-less-than-two-minutes-4m5i</guid>
      <description>&lt;p&gt;Railway's new &lt;a href="https://docs.railway.com/reference/functions" rel="noopener noreferrer"&gt;Functions&lt;/a&gt; feature makes it incredibly easy to deploy single-file TypeScript code without the overhead of managing infrastructure or repositories. &lt;/p&gt;

&lt;p&gt;In this article, we'll walk through building a license API using &lt;a href="https://hono.dev/" rel="noopener noreferrer"&gt;Hono&lt;/a&gt;, Redis, and the Bun runtime—all in a single file. No more excuses, it's time to build software!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Railway Functions are perfect for small tasks like handling webhooks, cron jobs, or simple APIs. What makes them even better is how they interact with the rest of the Railway ecosystem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Customizing Your Prefixes
&lt;/h2&gt;

&lt;p&gt;Before diving in, make sure to edit the prefix values in the code to suit your needs. The default prefixes, such as &lt;code&gt;WTHSEISMIC_&lt;/code&gt;, are placeholders and should be replaced with values relevant to your project to ensure uniqueness and avoid conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Railway account.&lt;/li&gt;
&lt;li&gt;Basic knowledge of TypeScript.&lt;/li&gt;
&lt;li&gt;Familiarity with serverless concepts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up a New Project
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a New Project:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Head over to your Railway dashboard and create a new project. Choose the &lt;strong&gt;Functions&lt;/strong&gt; service to get started.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Select the Functions Environment:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Railway will automatically set up the environment using the Bun runtime for your function. No need for additional configuration!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: Provision Redis
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add a Redis Service:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In your Railway project, provision a Redis service. This will be used to store license data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Share Environment Variables:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
After provisioning Redis, click on the Redis service, then head to &lt;strong&gt;Variables&lt;/strong&gt;. Use the "Share Variables" feature to expose the following environment variables to your function:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;REDISHOST&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REDISPORT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REDISUSER&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REDISPASSWORD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;(Optional) &lt;code&gt;REDIS_TLS&lt;/code&gt; if your Redis requires TLS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Create Your Function
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a New Function:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In the Railway dashboard, click &lt;strong&gt;New Function&lt;/strong&gt;. This opens the built-in editor which supports TypeScript using Bun.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set Up Additional Environment Variables:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Add any additional variables your API requires. For example, generate unique values (e.g., using a UUID generator) for:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ADMIN_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HMAC_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PAYMENT_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Optionally, set &lt;code&gt;PORT&lt;/code&gt; (defaults to 3000 if not provided).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Paste and Save the Code
&lt;/h2&gt;

&lt;p&gt;Copy and paste the following complete code into the &lt;strong&gt;Source Code&lt;/strong&gt; tab of your Railway Function. Save your changes using &lt;code&gt;⌘+S&lt;/code&gt; (or &lt;code&gt;Ctrl+S&lt;/code&gt; on Windows). Railway will automatically deploy your changes!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono/cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rateLimiter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono-rate-limiter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Custom prefix configuration&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LICENSE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WTHSEISMIC_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LICENSE_SET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WTHSEISMIC_LICENSES&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEBHOOK_SET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WTHSEISMIC_WEBHOOKS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Updated LicenseTier type to include "unlimited"&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LicenseTier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;basic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enterprise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unlimited&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="nx"&gt;here&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Test Your Deployment
&lt;/h2&gt;

&lt;p&gt;Once deployed, Railway provides a URL for your function. Use the following &lt;code&gt;curl&lt;/code&gt; commands to test each endpoint:&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1: Test the Root Endpoint
&lt;/h3&gt;

&lt;p&gt;Returns the current timestamp.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET https://&amp;lt;your-function-url&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.2: Create a New License
&lt;/h3&gt;

&lt;p&gt;Creates a new license key for a user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://&amp;lt;your-function-url&amp;gt;/api/licenses &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "user@example.com", "tier": "pro", "durationDays": 90}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3: Validate a License
&lt;/h3&gt;

&lt;p&gt;Validates an existing license key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET https://&amp;lt;your-function-url&amp;gt;/api/licenses/WTHSEISMIC_xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4: Handle Payment Webhook
&lt;/h3&gt;

&lt;p&gt;Simulates a payment webhook to extend a license's duration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://&amp;lt;your-function-url&amp;gt;/webhooks/payment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Payment-Signature: &amp;lt;signature&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"licenseKey": "WTHSEISMIC_xxx", "durationDays": 30}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.5: Extend License via Admin Endpoint
&lt;/h3&gt;

&lt;p&gt;Extends the license duration using an admin key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://&amp;lt;your-function-url&amp;gt;/admin/licenses/WTHSEISMIC_xxx/extend &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Admin-Key: your-secret-key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"durationDays": 30}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use these examples to verify that your function is working as expected. &lt;/p&gt;

&lt;p&gt;So there you have it, in less than two minutes, we've put together a license key server on Railway using Redis, Hono and Bun - Demonstrating how headache-free the whole process is, and how (not so) small snippets of code can live within your Railway ecoystem within minutes.&lt;/p&gt;

&lt;p&gt;Great feature, great platform.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>javascript</category>
      <category>bunjs</category>
      <category>railway</category>
    </item>
  </channel>
</rss>
