<?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: Solo Earn</title>
    <description>The latest articles on DEV Community by Solo Earn (@soloearn).</description>
    <link>https://dev.to/soloearn</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%2F4180471%2F4e94ae74-834e-49cf-9fa3-6360f85cd252.png</url>
      <title>DEV Community: Solo Earn</title>
      <link>https://dev.to/soloearn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/soloearn"/>
    <language>en</language>
    <item>
      <title>How to test an x402 endpoint with curl</title>
      <dc:creator>Solo Earn</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:14:00 +0000</pubDate>
      <link>https://dev.to/soloearn/how-to-test-an-x402-endpoint-with-curl-4a9l</link>
      <guid>https://dev.to/soloearn/how-to-test-an-x402-endpoint-with-curl-4a9l</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; request the paid URL with &lt;code&gt;curl -i&lt;/code&gt;. You should get status &lt;code&gt;402&lt;/code&gt; and a &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; header that base64-decodes to JSON with &lt;code&gt;x402Version: 2&lt;/code&gt; and an &lt;code&gt;accepts&lt;/code&gt; list. Check that the amount is in the token’s smallest units, that the network and asset are the ones you mean, that CORS exposes the header, and that &lt;code&gt;HEAD&lt;/code&gt; returns 402 too.&lt;/p&gt;

&lt;p&gt;Every command below runs against this store’s own paywall, so you can try them right now and compare with your endpoint. Prefer a browser? The free &lt;a href="https://x402-seller-kit.soloearn.workers.dev/tools/x402-checker" rel="noopener noreferrer"&gt;x402 endpoint checker&lt;/a&gt; runs all of these checks (and a few more) in one go. Set the URL once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL=https://x402-seller-kit.soloearn.workers.dev/download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. The status is 402
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;      &lt;span class="c"&gt;# 402&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 200 means the file is unprotected. A 401 or 403 means something else is in front of it, such as an auth layer or a firewall rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The PAYMENT-REQUIRED header is there and decodes
&lt;/h2&gt;

&lt;p&gt;The x402 v2 HTTP transport spec puts the payment terms, base64-encoded, in this header. Clients read the header, not the body.&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^payment-required:'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;x402Version&lt;/code&gt;, &lt;code&gt;resource&lt;/code&gt; and an &lt;code&gt;accepts&lt;/code&gt; array. If &lt;code&gt;base64 -d&lt;/code&gt; fails, check for URL-safe characters or a missing &lt;code&gt;btoa&lt;/code&gt; on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Browsers can read it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^access-control-expose-headers'&lt;/span&gt;
&lt;span class="c"&gt;# access-control-expose-headers: PAYMENT-REQUIRED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, a browser-based x402 client on another origin gets the 402 but can’t see the terms. Expose &lt;code&gt;PAYMENT-RESPONSE&lt;/code&gt; as well on the paid response.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The amount is in the right units
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.accepts[0].amount'&lt;/span&gt;          &lt;span class="c"&gt;# 29000000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amounts are strings in the token’s smallest unit. USDC has 6 decimals, so &lt;code&gt;29000000&lt;/code&gt; is 29 USDC. A common mistake is writing &lt;code&gt;"29"&lt;/code&gt;, which asks for 0.000029 USDC.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Network, asset and payTo are what you meant
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.accepts[0] | {network, asset, payTo, maxTimeoutSeconds}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Networks use CAIP-2 IDs: Base mainnet is &lt;code&gt;eip155:8453&lt;/code&gt;, Base Sepolia is &lt;code&gt;eip155:84532&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Native USDC on Base mainnet is &lt;code&gt;0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&lt;/code&gt;. A testnet token address on mainnet, or the reverse, means nobody can pay.&lt;/li&gt;
&lt;li&gt;Paste &lt;code&gt;payTo&lt;/code&gt; into a block explorer and confirm it’s an address you control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. HEAD returns 402 as well
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;                         &lt;span class="c"&gt;# HTTP/2 402&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some link checkers and directory crawlers send &lt;code&gt;HEAD&lt;/code&gt; first. On this store, an early version answered &lt;code&gt;HEAD&lt;/code&gt; with 404 while &lt;code&gt;GET&lt;/code&gt; returned 402, and we only noticed it in our request logs. Route both methods to the same handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Discovery metadata is present
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'{desc: .resource.description, mime: .resource.mimeType, bazaar: (.extensions.bazaar != null)}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clear description and MIME type, plus Bazaar discovery metadata in &lt;code&gt;extensions&lt;/code&gt;, help directories and agents list and price your endpoint. Keep the description specific: what the buyer gets, in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  A one-shot check script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# usage: ./x402-check.sh https://your.host/paid-path&lt;/span&gt;
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;' 402'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ok   402 on GET"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL GET is not 402"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;' 402'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ok   402 on HEAD"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL HEAD is not 402"&lt;/span&gt;
&lt;span class="nv"&gt;T&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^payment-required:'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$T&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ok   PAYMENT-REQUIRED decodes"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL no PAYMENT-REQUIRED header"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qi&lt;/span&gt; &lt;span class="s1"&gt;'^access-control-expose-headers:.*payment-required'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ok   header exposed to browsers"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARN header not in Access-Control-Expose-Headers"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$T&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.accepts[] | "info \(.network) \(.asset) amount=\(.amount) payTo=\(.payTo)"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This covers what a client and a directory see before payment. To test the paid path, use an x402 client library with a test wallet, ideally on Base Sepolia first.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;Which headers does x402 v2 use?&lt;/p&gt;

&lt;p&gt;Three, all base64-encoded JSON: PAYMENT-REQUIRED (server to client, sent with the 402), PAYMENT-SIGNATURE (client to server, carrying the signed payment) and PAYMENT-RESPONSE (server to client, carrying the settlement result).&lt;/p&gt;

&lt;p&gt;Is the JSON body required?&lt;/p&gt;

&lt;p&gt;The x402 v2 HTTP transport spec says protocol information travels in the headers and that the response body is up to the server. Sending the same terms in the body as well makes the endpoint easier for people and simple crawlers to read.&lt;/p&gt;

&lt;p&gt;Why does my browser client not see PAYMENT-REQUIRED?&lt;/p&gt;

&lt;p&gt;Browsers hide non-standard response headers from cross-origin scripts unless the server lists them in Access-Control-Expose-Headers. Add PAYMENT-REQUIRED and PAYMENT-RESPONSE there.&lt;/p&gt;

&lt;p&gt;Can curl test the paid path?&lt;/p&gt;

&lt;p&gt;Not on its own. A real PAYMENT-SIGNATURE needs a wallet signature, so use an x402 client library with a funded test wallet, or a testnet network, for that part. curl is for checking everything a client and a directory see before payment.&lt;/p&gt;

&lt;p&gt;Source for header names: the x402 v2 HTTP transport spec in the &lt;a href="https://github.com/coinbase/x402" rel="noopener noreferrer"&gt;coinbase/x402 repository&lt;/a&gt;. Building the endpoint from scratch? Read &lt;a href="https://x402-seller-kit.soloearn.workers.dev/guides/sell-digital-file-usdc-x402-cloudflare-workers" rel="noopener noreferrer"&gt;how to sell a digital file for USDC with x402 on Cloudflare Workers&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://x402-seller-kit.soloearn.workers.dev/guides/test-x402-endpoint-curl" rel="noopener noreferrer"&gt;x402-seller-kit.soloearn.workers.dev&lt;/a&gt;. Free tool: the &lt;a href="https://x402-seller-kit.soloearn.workers.dev/tools/x402-checker" rel="noopener noreferrer"&gt;x402 endpoint checker&lt;/a&gt;. If you'd rather not build it yourself, the &lt;a href="https://x402-seller-kit.soloearn.workers.dev/" rel="noopener noreferrer"&gt;x402 Seller Kit&lt;/a&gt; ($29) is a tested Cloudflare Worker template.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>curl</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why can’t my browser x402 client read the PAYMENT-REQUIRED header?</title>
      <dc:creator>Solo Earn</dc:creator>
      <pubDate>Mon, 12 Oct 2026 22:14:33 +0000</pubDate>
      <link>https://dev.to/soloearn/why-cant-my-browser-x402-client-read-the-payment-required-header-24cf</link>
      <guid>https://dev.to/soloearn/why-cant-my-browser-x402-client-read-the-payment-required-header-24cf</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; browsers hide non-standard response headers from cross-origin scripts. Send &lt;code&gt;Access-Control-Expose-Headers: PAYMENT-REQUIRED, PAYMENT-RESPONSE&lt;/code&gt; on your 402 and paid responses, and answer the &lt;code&gt;OPTIONS&lt;/code&gt; preflight with &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; that includes &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the spec puts in headers
&lt;/h2&gt;

&lt;p&gt;In the x402 v2 HTTP transport, the server sends the payment terms base64-encoded in &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; with the 402. The client retries with &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt;, and the server confirms with &lt;code&gt;PAYMENT-RESPONSE&lt;/code&gt;. &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; and &lt;code&gt;PAYMENT-RESPONSE&lt;/code&gt; are response headers that aren’t CORS-safelisted, so a script on another origin sees the 402 status but gets &lt;code&gt;null&lt;/code&gt; for them unless you list them in &lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;. &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt; is a request header: it goes in &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;, not the expose list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Worker headers
&lt;/h2&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;CORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access-Control-Allow-Origin&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="s1"&gt;*&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="s1"&gt;Access-Control-Allow-Headers&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="s1"&gt;PAYMENT-SIGNATURE, X-PAYMENT, 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="s1"&gt;Access-Control-Expose-Headers&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="s1"&gt;PAYMENT-REQUIRED, PAYMENT-RESPONSE&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="c1"&gt;// OPTIONS -&amp;gt; new Response(null, { status: 204, headers: CORS })&lt;/span&gt;
&lt;span class="c1"&gt;// add CORS to the 402 and to the paid 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test it with curl
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the 402 exposes the terms&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^access-control-expose-headers'&lt;/span&gt;
&lt;span class="c"&gt;# the preflight allows the payment header&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-X&lt;/span&gt; OPTIONS &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Origin: https://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="s1"&gt;'Access-Control-Request-Method: GET'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Access-Control-Request-Headers: payment-signature'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^access-control-allow-headers'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or paste the URL into the free &lt;a href="https://x402-seller-kit.soloearn.workers.dev/tools/x402-checker" rel="noopener noreferrer"&gt;x402 endpoint checker&lt;/a&gt;, which warns when the 402 has no CORS headers or doesn’t expose &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt;. It doesn’t send a preflight, so run the second curl command for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;Which headers does x402 v2 use over HTTP?&lt;/p&gt;

&lt;p&gt;PAYMENT-REQUIRED (server to client, with the 402), PAYMENT-SIGNATURE (client to server) and PAYMENT-RESPONSE (server to client, with the settlement result). All three carry base64-encoded JSON.&lt;/p&gt;

&lt;p&gt;What about X-PAYMENT?&lt;/p&gt;

&lt;p&gt;X-PAYMENT is the older v1 request header. Some servers accept it alongside PAYMENT-SIGNATURE for older clients, so allow both in Access-Control-Allow-Headers if you support both.&lt;/p&gt;

&lt;p&gt;Do server-side agents need CORS?&lt;/p&gt;

&lt;p&gt;No. CORS only applies to scripts running in a browser. Server-side clients and agents read every header regardless.&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md" rel="noopener noreferrer"&gt;x402 v2 HTTP transport spec&lt;/a&gt;, &lt;a href="https://developers.cloudflare.com/agents/tools/payments/x402/" rel="noopener noreferrer"&gt;Cloudflare Agents x402 docs&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers" rel="noopener noreferrer"&gt;MDN on Access-Control-Expose-Headers&lt;/a&gt;. More checks: &lt;a href="https://x402-seller-kit.soloearn.workers.dev/guides/test-x402-endpoint-curl" rel="noopener noreferrer"&gt;how to test an x402 endpoint with curl&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://x402-seller-kit.soloearn.workers.dev/guides/x402-payment-required-header-cors" rel="noopener noreferrer"&gt;x402-seller-kit.soloearn.workers.dev&lt;/a&gt;. Free tool: the &lt;a href="https://x402-seller-kit.soloearn.workers.dev/tools/x402-checker" rel="noopener noreferrer"&gt;x402 endpoint checker&lt;/a&gt;. If you'd rather not build it yourself, the &lt;a href="https://x402-seller-kit.soloearn.workers.dev/" rel="noopener noreferrer"&gt;x402 Seller Kit&lt;/a&gt; ($29) is a tested Cloudflare Worker template.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>x402</category>
      <category>webdev</category>
      <category>cors</category>
      <category>api</category>
    </item>
  </channel>
</rss>
