<?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: Agent Knowledge Research</title>
    <description>The latest articles on DEV Community by Agent Knowledge Research (@agentk_6588bb59c6339).</description>
    <link>https://dev.to/agentk_6588bb59c6339</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%2F4208929%2F62ef314d-97e6-4d40-a364-992b07f1f7b4.jpeg</url>
      <title>DEV Community: Agent Knowledge Research</title>
      <link>https://dev.to/agentk_6588bb59c6339</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agentk_6588bb59c6339"/>
    <language>en</language>
    <item>
      <title>3 Stripe bugs AI coding agents keep writing (and a 60-second script to catch them)</title>
      <dc:creator>Agent Knowledge Research</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:16:09 +0000</pubDate>
      <link>https://dev.to/agentk_6588bb59c6339/3-stripe-bugs-ai-coding-agents-keep-writing-and-a-60-second-script-to-catch-them-1da4</link>
      <guid>https://dev.to/agentk_6588bb59c6339/3-stripe-bugs-ai-coding-agents-keep-writing-and-a-60-second-script-to-catch-them-1da4</guid>
      <description>&lt;p&gt;If you've had Cursor, Claude Code, Lovable or Bolt wire up Stripe for you, the happy path probably works. You click "Upgrade", pay with &lt;code&gt;4242 4242 4242 4242&lt;/code&gt;, land on &lt;code&gt;/success&lt;/code&gt;, and you're Pro. Ship it.&lt;/p&gt;

&lt;p&gt;The trouble is that the happy path is the only path most of us test. After reading a lot of agent-written Stripe integrations, I keep seeing the same three bugs. None of them shows up in a demo, and all three let people get paid features without paying.&lt;/p&gt;

&lt;p&gt;So I wrote a small, MIT-licensed bash script that checks for them in about a minute: &lt;a href="https://github.com/PayWallKit/paywall-check" rel="noopener noreferrer"&gt;PayWallKit/paywall-check&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the webhook accepts unsigned events
&lt;/h2&gt;

&lt;p&gt;Stripe signs every webhook it sends. Your handler is supposed to check that signature with &lt;code&gt;stripe.webhooks.constructEvent(rawBody, signature, secret)&lt;/code&gt; before trusting anything.&lt;/p&gt;

&lt;p&gt;Agents get this wrong in a specific way. They parse the body as JSON first (&lt;code&gt;await req.json()&lt;/code&gt;), then call &lt;code&gt;constructEvent&lt;/code&gt; on it. Verification fails, because the signature covers the &lt;em&gt;raw&lt;/em&gt; bytes. Then the agent "fixes" the error by deleting the check. Now anyone who finds your webhook URL can do this:&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://yourapp.com/api/webhooks/stripe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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;'{"type":"checkout.session.completed","data":{"object":{"payment_status":"paid"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns a 200, you have a free "payment succeeded" button on the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix (Next.js App Router):&lt;/strong&gt;&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&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;Request&lt;/span&gt;&lt;span class="p"&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;body&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// raw body, not req.json()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&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;headers&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&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;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&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;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bad signature&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// handle event...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Supabase Edge Functions (Deno), use &lt;code&gt;constructEventAsync&lt;/code&gt; with &lt;code&gt;Stripe.createSubtleCryptoProvider()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: the success page grants access
&lt;/h2&gt;

&lt;p&gt;This one is very common: the &lt;code&gt;/success&lt;/code&gt; page reads &lt;code&gt;session_id&lt;/code&gt; from the URL and flips the user to Pro. Two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Anyone can type &lt;code&gt;/success?session_id=anything&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A real buyer who closes the tab before the redirect finishes pays and gets nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The success page should only say "thanks, we're setting things up." The verified webhook is the only thing that should write paid access to your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: secret keys in the client bundle
&lt;/h2&gt;

&lt;p&gt;Anything prefixed &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; or &lt;code&gt;VITE_&lt;/code&gt; ships to every visitor's browser. Agents sometimes put the Stripe secret key, the webhook secret, or Supabase's &lt;code&gt;service_role&lt;/code&gt; key behind one of those prefixes to make an error go away.&lt;/p&gt;

&lt;p&gt;Note that Stripe secret keys start with &lt;code&gt;sk_live_&lt;/code&gt; / &lt;code&gt;sk_test_&lt;/code&gt; (underscore), and webhook secrets start with &lt;code&gt;whsec_&lt;/code&gt;. A search for &lt;code&gt;sk-&lt;/code&gt; only finds OpenAI keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/PayWallKit/paywall-check
&lt;span class="nb"&gt;cd &lt;/span&gt;paywall-check
./paywall-check.sh https://yourapp.com /api/webhooks/stripe /success ./.next/static
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arguments are your base URL, your webhook path, your success path, and (optionally) your built client directory. The script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sends &lt;strong&gt;one&lt;/strong&gt; unsigned fake &lt;code&gt;checkout.session.completed&lt;/code&gt; to your webhook and fails if it gets a 2xx,&lt;/li&gt;
&lt;li&gt;sends &lt;strong&gt;one&lt;/strong&gt; GET to your success page with a made-up &lt;code&gt;session_id&lt;/code&gt; and fails if the page says you've been upgraded,&lt;/li&gt;
&lt;li&gt;greps your client build for &lt;code&gt;sk_live_&lt;/code&gt;/&lt;code&gt;sk_test_&lt;/code&gt;, &lt;code&gt;whsec_&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It exits non-zero on any failure, so you can drop it into CI. Only run it against apps you own.&lt;/p&gt;

&lt;p&gt;It's a smoke test, not an audit. A "pass" on check 2 only means the page didn't &lt;em&gt;say&lt;/em&gt; you're upgraded, so look at your database too. It also doesn't cover refunds, cancellations, duplicate events or client-chosen prices. Those need the Stripe CLI (&lt;code&gt;stripe listen&lt;/code&gt; + &lt;code&gt;stripe trigger&lt;/code&gt;) and a look at what actually changed in your DB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell the agent up front
&lt;/h2&gt;

&lt;p&gt;Fixing these after the fact is fine. Not getting them in the first place is better. This block goes in &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;.cursor/rules&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- The webhook is the only writer of paid access. Success pages, redirects,
  client callbacks and query strings never grant access.
- Verify every webhook with stripe.webhooks.constructEvent(rawBody, signature,
  STRIPE_WEBHOOK_SECRET). Use the raw body; never parse JSON first.
  (Supabase Edge Functions / Deno: constructEventAsync + createSubtleCryptoProvider.)
- Idempotency: record event.id before handling; skip duplicates.
- The client sends a plan key, never a price, amount, quantity or user id.
- Gate on the server. UI checks are cosmetic.
- customer.subscription.deleted, charge.refunded and charge.dispute.created remove access.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idempotency line matters more than it looks. Stripe retries any webhook that doesn't get a fast 2xx, so "my webhook fired twice" is normal. Store &lt;code&gt;event.id&lt;/code&gt; with a unique constraint and skip duplicates.&lt;/p&gt;

&lt;p&gt;I sell a $19 &lt;a href="https://paywall-kit.pages.dev" rel="noopener noreferrer"&gt;Paywall Kit&lt;/a&gt; with full rules and a reference code. HN5 takes $5 off through Friday.&lt;/p&gt;

&lt;p&gt;What other failure modes have you seen in agent-written billing code? I'd like to add checks for the common ones, so issues and PRs on &lt;a href="https://github.com/PayWallKit/paywall-check" rel="noopener noreferrer"&gt;the repo&lt;/a&gt; are welcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with the help of AI and checked by me before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
