<?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: Hira</title>
    <description>The latest articles on DEV Community by Hira (@hirajha).</description>
    <link>https://dev.to/hirajha</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%2F4062468%2Ffed88928-c26e-4469-b54c-8ccba7076b4c.png</url>
      <title>DEV Community: Hira</title>
      <link>https://dev.to/hirajha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hirajha"/>
    <language>en</language>
    <item>
      <title>The billing job that ran green every night and billed nothing</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:54:49 +0000</pubDate>
      <link>https://dev.to/hirajha/the-billing-job-that-ran-green-every-night-and-billed-nothing-2l0n</link>
      <guid>https://dev.to/hirajha/the-billing-job-that-ran-green-every-night-and-billed-nothing-2l0n</guid>
      <description>&lt;p&gt;We published a per-lookup overage rate on every paid plan from launch. None of it was ever charged to anyone.&lt;/p&gt;

&lt;p&gt;The reporter existed. It ran nightly at 03:20, it selected the previous day's billable events, it sent them to the payment provider, and it exited zero. Every night. For weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-minute window
&lt;/h2&gt;

&lt;p&gt;The provider rejects any usage event whose timestamp is more than an hour old:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;events[0].timestamp: Timestamp cannot be older than 1 hour
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A batch sent at 03:20 for the previous day had every single event refused. The job was not broken in any way a green checkmark could show you. It ran, it reported nothing, and it billed nothing.&lt;/p&gt;

&lt;p&gt;The fix was the schedule, not the code: every 15 minutes instead of nightly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 200 that means nothing
&lt;/h2&gt;

&lt;p&gt;The second one was worse. Reporting usage against a subscription whose product carries no meter returns &lt;strong&gt;200&lt;/strong&gt;. The events are accepted. The rows get marked reported. Nothing reaches the invoice.&lt;/p&gt;

&lt;p&gt;That is indistinguishable from success at every layer you would normally check. We only found it by reading the meter's own count back, rather than trusting the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// reporting a 200 is not proof. the meter's count is.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;meterTotal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&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;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;meterTotal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The boundary nobody had tested
&lt;/h2&gt;

&lt;p&gt;Overage is charged on the excess, not the allowance. At 20,001 lookups on a plan including 20,000, the customer owes for &lt;strong&gt;one&lt;/strong&gt; lookup.&lt;/p&gt;

&lt;p&gt;We had a function doing that arithmetic and no test executing it. The existing tests read the source as text and asserted on strings, which can prove a line exists but not that it returns the right number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20,001 charges for ONE lookup, not 20,001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="nf"&gt;projectedSpend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;starter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="nx"&gt;_001&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;49&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We injected the bug before trusting the test — replaced the excess with the full count, and it failed with &lt;code&gt;expected 49, got 129&lt;/code&gt;. An $80 error, stated as money.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A cron that exits zero is not a cron that worked&lt;/li&gt;
&lt;li&gt;Read the state back from the system you are integrating with, not the response it handed you&lt;/li&gt;
&lt;li&gt;Test the boundary where the arithmetic changes, and prove the test fails before you rely on it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are clever. All three shipped anyway.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>debugging</category>
      <category>software</category>
    </item>
    <item>
      <title>How to handle AI agents signing up to your product</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Wed, 05 Aug 2026 11:39:59 +0000</pubDate>
      <link>https://dev.to/layercall/how-to-handle-ai-agents-signing-up-to-your-product-102k</link>
      <guid>https://dev.to/layercall/how-to-handle-ai-agents-signing-up-to-your-product-102k</guid>
      <description>&lt;p&gt;Start from the uncomfortable part. Every fraud signal the industry has built is a proxy for "is a human here", and a capable AI agent genuinely has the things those proxies measure. It drives a real browser, so the fingerprint is real. It arrives on a residential connection, so the address is clean. It controls a real mailbox, so it receives the verification code. It is not defeating your checks; it is satisfying them. So detection is an arms race you lose slowly, and the useful question is a different one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop asking whether it is a bot
&lt;/h2&gt;

&lt;p&gt;"Is this automated" has become a bad question, because a growing share of legitimate traffic is somebody's assistant doing what they explicitly asked it to do. Refusing that refuses a customer who is standing right there. The question that still has an answer is: on whose authority is this acting, and can they be held to it?&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the agents that identify themselves
&lt;/h2&gt;

&lt;p&gt;Web Bot Auth is the emerging standard for this — RFC 9421 HTTP Message Signatures, with an IETF working group chartered in early 2026 and Cloudflare, Google, Amazon and OpenAI behind it. The agent signs its request with a key its operator publishes at a well-known URL, and you check the signature. This is the one check in fraud detection with no false-positive rate, because it is arithmetic rather than judgement. Increasingly agents do sign, because operators want their traffic accepted.&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 &lt;span class="s2"&gt;"https://www.layercall.com/v1/agent/authorize"&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-Api-Key: YOUR_KEY"&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;'{
    "method": "POST",
    "url": "https://yoursite.com/signup",
    "headers": { "signature": "…", "signature-input": "…", "signature-agent": "…" }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use trigger, not user agent, to tell them apart
&lt;/h2&gt;

&lt;p&gt;An operator's Signature Agent Card declares a trigger with exactly two values: fetcher, meaning a person initiated this request, and crawler, meaning nobody did. That distinction is invisible to every classical signal — both are "a bot", and both may be perfectly signed — and it is the whole game. A customer's assistant filling in a signup because they asked it to is a customer. An autonomous crawler POSTing to the same endpoint on nobody's behalf is not, and never was.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// proven is true for exactly one thing: a signature that verified.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proven&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified_agent&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetcher&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// a person asked for this&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// nobody asked for this&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Separate what you can prove from what you inferred
&lt;/h2&gt;

&lt;p&gt;A verified signature is proof. Headless detection and automation markers are inference, and inference a capable adversary patches in an afternoon. Keep those apart in your own code, because the moment they are the same field you will start treating a guess like a certainty — and acting automatically on a guess is how you refuse real customers. Branch on a single boolean that means "this was settled cryptographically", not on a vocabulary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch volume with linkage, not with identity
&lt;/h2&gt;

&lt;p&gt;Signing does not make an agent harmless. Someone can tell their assistant to open fifty trial accounts and it will honestly declare itself user-initiated every single time, because it is telling the truth. The card cannot see that and does not claim to. What gives it away is the shape: one device across twelve addresses, fifty signups from one /24 inside an hour. Score the pattern, not just the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a naive version of this gets wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocking all automation. That reflex gets more expensive every month as more real customers delegate to assistants, and it is invisible in your metrics — refused signups do not file complaints.&lt;/li&gt;
&lt;li&gt;Treating an unsigned request as malicious. Most traffic is unsigned and most of it is fine. Absence of a signature says nothing; it is a missing claim, not a false one.&lt;/li&gt;
&lt;li&gt;Trusting a user agent string. It is self-asserted plain text with no verification of any kind, which is precisely why the signature standard exists.&lt;/li&gt;
&lt;li&gt;Believing a vendor who claims to detect any AI agent. A capable agent driving a real browser on a residential connection is indistinguishable from a person by construction — anyone selling certainty there is selling you an arms race.&lt;/li&gt;
&lt;li&gt;Assuming a verified fetcher is safe. It means a person asked for this request, not that the person is honest. Pair it with velocity.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.layercall.com/guides/handle-ai-agent-signups" rel="noopener noreferrer"&gt;www.layercall.com/guides/handle-ai-agent-signups&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>How to add fraud checks without losing real customers</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:44:47 +0000</pubDate>
      <link>https://dev.to/layercall/how-to-add-fraud-checks-without-losing-real-customers-46cg</link>
      <guid>https://dev.to/layercall/how-to-add-fraud-checks-without-losing-real-customers-46cg</guid>
      <description>&lt;p&gt;Every fraud check has two error rates, and only one of them is visible. You will notice the fraudsters who get through, because they cost you money in ways that show up on a dashboard. You will never notice the real customers you turned away, because they simply leave. That asymmetry is why fraud systems drift toward being too strict over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never let a check fail closed
&lt;/h2&gt;

&lt;p&gt;If your fraud provider is slow, down, or you have hit a quota, the signup must still work. A fraud check that takes signups offline during an outage has cost you more than the fraud it was bought to stop — and unlike fraud, it hits every legitimate user at once rather than a few bad ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scoreUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fraud check unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// proceed anyway&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use the middle option
&lt;/h2&gt;

&lt;p&gt;Allow and block is a false choice, and it forces every uncertain case into one of two wrong answers. A third path — verify — resolves almost everything: a one-time code costs a real person a few seconds and costs an automated attack the whole run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log what you rejected, then read it
&lt;/h2&gt;

&lt;p&gt;Store the score and the reasons for every signup you challenged or refused, then actually look through them weekly. This is the only way false positives become visible. If you find a category of real customer in there — a country, an email provider, a VPN — that is a rule to loosen, not a coincidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start in observe-only mode
&lt;/h2&gt;

&lt;p&gt;Run the checks and record the verdicts without acting on them for a week or two. You will see what proportion of your normal, healthy traffic would have been challenged before you inconvenience anybody. If that number surprises you, better to learn it from a log than from your support inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a naive version of this gets wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Turning the strictness up after an incident and never turning it back down. Ratchets only go one way, and nobody is measuring the cost.&lt;/li&gt;
&lt;li&gt;Never distinguishing "low risk" from "we could not check". They are completely different, and collapsing them means an outage looks like a clean bill of health.&lt;/li&gt;
&lt;li&gt;Judging accuracy by fraud caught alone. A system that blocks everyone catches all fraud. The number that matters is how many real customers it cost.&lt;/li&gt;
&lt;li&gt;Trusting a vendor who never publishes their mistakes. Every one of these systems has false positives, including ours — a provider who will not talk about theirs has not stopped having them.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.layercall.com/guides/score-signup-without-blocking-real-users" rel="noopener noreferrer"&gt;www.layercall.com/guides/score-signup-without-blocking-real-users&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>How to stop free trial abuse</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:39:36 +0000</pubDate>
      <link>https://dev.to/layercall/how-to-stop-free-trial-abuse-934</link>
      <guid>https://dev.to/layercall/how-to-stop-free-trial-abuse-934</guid>
      <description>&lt;p&gt;Trial abuse is not usually a thousand people taking one trial each. It is a handful of people taking a thousand trials, which means the whole problem is linkage: recognising that the twelfth signup is the same person as the first. That is a different job from scoring a single signup in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find what the accounts share
&lt;/h2&gt;

&lt;p&gt;An abuser varies whatever is cheap and reuses whatever is expensive. Email addresses are free, so those change every time. Device fingerprints, IP ranges and phone numbers cost money or effort, so those repeat. Look for repetition in the expensive things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch email aliasing before you catch anything else
&lt;/h2&gt;

&lt;p&gt;Gmail ignores dots and anything after a plus sign, so &lt;a href="mailto:user+1@gmail.com"&gt;user+1@gmail.com&lt;/a&gt; through &lt;a href="mailto:user+400@gmail.com"&gt;user+400@gmail.com&lt;/a&gt; are all one mailbox. Normalising the address collapses a hundred trials into one visible account, and it costs nothing to implement.&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="s2"&gt;"https://www.layercall.com/v1/verify/email?email=user%2Btrial7@gmail.com"&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-Api-Key: YOUR_KEY"&lt;/span&gt;
&lt;span class="c"&gt;# normalized_email collapses the alias back to the real mailbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add a device signal
&lt;/h2&gt;

&lt;p&gt;A browser fingerprint survives a new email address and a fresh incognito window, which is exactly what a repeat trialist changes between attempts. It is not perfect and it should not be your only check, but it links accounts that otherwise look unrelated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raise friction in proportion, not uniformly
&lt;/h2&gt;

&lt;p&gt;Requiring a card from everyone stops abuse and also stops your growth. Requiring a phone number or a card only from signups that already look linked keeps the front door open for the majority who are exactly who they say they are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scoreUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;device_id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;requireCard&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;requirePhoneVerification&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;startTrial&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What a naive version of this gets wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Requiring a card from every trial. It works, and it will cost you more in lost signups than the abuse ever cost you in compute.&lt;/li&gt;
&lt;li&gt;Blocking a whole IP range after one abuser. Shared addresses mean you will hit an office, a campus or a mobile carrier and never hear from the people you locked out.&lt;/li&gt;
&lt;li&gt;Assuming a repeat device is always one person. Shared computers, family machines and library terminals are real.&lt;/li&gt;
&lt;li&gt;Treating the second signup as abuse. People genuinely lose access to old email accounts and start again. Linkage is a reason to verify, not to accuse.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.layercall.com/guides/stop-free-trial-abuse" rel="noopener noreferrer"&gt;www.layercall.com/guides/stop-free-trial-abuse&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>How to detect VPN and proxy users at signup</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:34:25 +0000</pubDate>
      <link>https://dev.to/layercall/how-to-detect-vpn-and-proxy-users-at-signup-jpg</link>
      <guid>https://dev.to/layercall/how-to-detect-vpn-and-proxy-users-at-signup-jpg</guid>
      <description>&lt;p&gt;VPN detection is genuinely hard, and most implementations quietly fail. The common approach — check whether the IP belongs to a hosting provider's ASN — misses the majority of consumer VPN traffic, because the large consumer VPNs rent capacity from ordinary hosting companies rather than running their own networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know why the obvious method fails
&lt;/h2&gt;

&lt;p&gt;ASN lookups tell you an address belongs to a hosting company. That catches servers, and it catches VPN operators who own their own network. It misses NordVPN, Surfshark, PIA and CyberGhost exits sitting on shared hosts, because the ASN belongs to the host, not the VPN. We measured this: fifteen real NordVPN exits, and three of them looked completely clean to an ASN-only check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask the operators instead
&lt;/h2&gt;

&lt;p&gt;Several major VPN providers publish their live server lists — the same endpoints their own apps call to pick a server. NordVPN publishes roughly 9,000 exit addresses; Mullvad publishes its relays. Reading those is exact rather than inferred, and it is public data, so there is no licence question. Providers who publish nothing still need the inferential approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the categories in your response
&lt;/h2&gt;

&lt;p&gt;"Datacenter", "commercial VPN", "public proxy" and "Tor exit" are four different facts with four different meanings. A Tor exit on a banking signup is a strong signal. A commercial VPN on a consumer app is Tuesday.&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="s2"&gt;"https://www.layercall.com/v1/score/ip?ip=1.2.3.4"&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-Api-Key: YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix your geolocation while you are here
&lt;/h2&gt;

&lt;p&gt;A geolocation database maps a VPN range to where the operator registered it, not where the exit physically is. That is how a user connecting through Amsterdam gets reported as Switzerland. If you make decisions on country, a VPN silently corrupts them — so prefer the operator's declared location when the exit is known.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a naive version of this gets wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocking VPN users outright. This is the single most costly overreaction in fraud prevention. Privacy-conscious users, remote employees on corporate VPNs, travellers and entire regions route this way, and they are disproportionately the technical customers who pay.&lt;/li&gt;
&lt;li&gt;Trusting an ASN label without verifying who holds it. We had one ASN labelled as a VPN provider that actually belonged to Wake Forest University — every student looked like a VPN user.&lt;/li&gt;
&lt;li&gt;Reading a VPN's registered country as the user's location. It is frequently a different continent.&lt;/li&gt;
&lt;li&gt;Counting datacenter plus VPN plus proxy as three separate penalties. They are usually one fact about one connection; adding them up manufactures risk that is not there.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.layercall.com/guides/detect-vpn-at-signup" rel="noopener noreferrer"&gt;www.layercall.com/guides/detect-vpn-at-signup&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>How to block disposable email addresses at signup</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:28:32 +0000</pubDate>
      <link>https://dev.to/layercall/how-to-block-disposable-email-addresses-at-signup-4kl</link>
      <guid>https://dev.to/layercall/how-to-block-disposable-email-addresses-at-signup-4kl</guid>
      <description>&lt;p&gt;Disposable-address detection is the highest-return check on a signup form, and also the one most often implemented badly. The naive version — a hardcoded list of a dozen domains — stops nothing, because throwaway providers add domains faster than you will update your array.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand what you are actually detecting
&lt;/h2&gt;

&lt;p&gt;Throwaway providers operate thousands of rotating domains, not one. Public blocklists currently track around 8,000 of them, and the distribution is not what people expect: about a third sit on .com, which means blocking unusual TLDs is not a substitute for a real list. Our Trust Index publishes the current breakdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not write the regex
&lt;/h2&gt;

&lt;p&gt;Every team tries &lt;code&gt;/mailinator|guerrilla|10minute/&lt;/code&gt; first. It fails on day one, because these providers publish hundreds of unrelated-looking domains specifically so that pattern matching does not work. A maintained list is the only approach that keeps working, and it needs updating continuously rather than once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the address, get the reason
&lt;/h2&gt;

&lt;p&gt;You want more than a boolean. Whether the domain accepts mail at all, whether the mailbox exists, and whether the handle looks machine-generated are separate facts, and they justify different responses.&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="s2"&gt;"https://www.layercall.com/v1/verify/email?email=someone@example.com"&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-Api-Key: YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Decide what a hit actually means for your product
&lt;/h2&gt;

&lt;p&gt;For a paid product with no free tier, a throwaway address is close to meaningless — nobody pays with a card and then reads their receipts in a temporary inbox. For a free trial, it is the single strongest abuse signal you have. The right response depends on what abuse costs you, which is a decision only you can make.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a naive version of this gets wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocking free providers along with disposable ones. Gmail, Outlook and iCloud are where most of your real customers are. These are completely different categories and conflating them is the most expensive mistake on this page.&lt;/li&gt;
&lt;li&gt;Treating a role address as fake. admin@ or billing@ at a company domain is a real person doing their job — often the person who approves the invoice.&lt;/li&gt;
&lt;li&gt;Assuming a role handle on a consumer mailbox is a role account. &lt;a href="mailto:hello@gmail.com"&gt;hello@gmail.com&lt;/a&gt; is somebody's personal address, not a shared inbox. We flagged those for weeks before catching it.&lt;/li&gt;
&lt;li&gt;Blocking rather than challenging on a single signal. A disposable address plus nothing else is thin evidence; ask for a code.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.layercall.com/guides/block-disposable-emails" rel="noopener noreferrer"&gt;www.layercall.com/guides/block-disposable-emails&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>How to stop fake signups</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:27:33 +0000</pubDate>
      <link>https://dev.to/layercall/how-to-stop-fake-signups-kkg</link>
      <guid>https://dev.to/layercall/how-to-stop-fake-signups-kkg</guid>
      <description>&lt;p&gt;If fake accounts appeared overnight, you are almost certainly being hit by one script rather than many people. That is good news: scripted signups reuse infrastructure, and infrastructure is measurable. Here is the order to check things in, cheapest first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the email domain before you check anything else
&lt;/h2&gt;

&lt;p&gt;Disposable-address providers are the cheapest thing an attacker uses and the cheapest thing for you to detect. There are roughly 8,000 known throwaway domains, and a scripted signup run will lean on a handful of them. This single check typically removes most of the volume, and it never touches a real customer — nobody signs up for a product they intend to pay for using a ten-minute mailbox.&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="s2"&gt;"https://www.layercall.com/v1/verify/email?email=test@mailinator.com"&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-Api-Key: YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Then look at where the request came from
&lt;/h2&gt;

&lt;p&gt;A datacenter IP address means the request did not come from a phone or a home broadband connection — it came from a rented server. Real customers occasionally arrive via VPNs, so treat this as a signal rather than a verdict. But a burst of signups from one hosting provider inside an hour is not a coincidence.&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="s2"&gt;"https://www.layercall.com/v1/score/ip?ip=1.2.3.4"&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-Api-Key: YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check how old the email's domain is
&lt;/h2&gt;

&lt;p&gt;Attackers who move past disposable providers register their own domains, and they use them immediately. A domain registered three days ago that is already signing up for your product is worth a second look. Be careful here: real founders launch on week-old domains too, which is why this should raise a challenge rather than a block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Score the whole signup, not each field separately
&lt;/h2&gt;

&lt;p&gt;The signals matter together. A disposable email is worth a challenge. A disposable email from a datacenter IP on a domain registered yesterday is not ambiguous. One call scores every field and returns a single verdict, so you write one branch instead of five.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.layercall.com/v1/score/user&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Api-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LAYERCALL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendOneTimeCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createAccount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Send a code — do not slam the door
&lt;/h2&gt;

&lt;p&gt;This is the part most teams get wrong under pressure. Blocking outright turns every false positive into a lost customer who never tells you why they left. A one-time code costs a real person eight seconds and costs a script the entire attack, because the script has no mailbox to receive it. Reserve outright rejection for the cases where several strong signals agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a naive version of this gets wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocking every VPN user. Plenty of privacy-conscious people, remote workers on corporate VPNs, and entire countries browse this way. VPN use is a reason to verify, not to refuse.&lt;/li&gt;
&lt;li&gt;Treating an unmeasured signal as a negative. If a domain's registration date could not be determined, that is unknown — not "old and therefore fine". We shipped this bug ourselves and wrote it up.&lt;/li&gt;
&lt;li&gt;Blocking shared IP addresses. University networks, mobile carriers and offices put thousands of unrelated people behind one address. Punishing that address punishes all of them.&lt;/li&gt;
&lt;li&gt;Scoring the wrong device. If you fingerprint the browser, make sure you are reading the end user's user agent and not your own server's HTTP client — we made exactly this mistake and every request looked identical.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.layercall.com/guides/stop-fake-signups" rel="noopener noreferrer"&gt;www.layercall.com/guides/stop-fake-signups&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
  </channel>
</rss>
