<?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: send21</title>
    <description>The latest articles on DEV Community by send21 (@send21).</description>
    <link>https://dev.to/send21</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%2F4110329%2Fb90443f0-a2d8-4dcd-a56d-38504cb11dcd.png</url>
      <title>DEV Community: send21</title>
      <link>https://dev.to/send21</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/send21"/>
    <language>en</language>
    <item>
      <title>Discuss: should a human sign when your agent hits an x402 paywall?</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Wed, 28 Oct 2026 09:40:07 +0000</pubDate>
      <link>https://dev.to/send21/discuss-should-a-human-sign-when-your-agent-hits-an-x402-paywall-34bl</link>
      <guid>https://dev.to/send21/discuss-should-a-human-sign-when-your-agent-hits-an-x402-paywall-34bl</guid>
      <description>&lt;p&gt;A question for anyone building agents that call paid APIs over x402.&lt;/p&gt;

&lt;p&gt;Your agent sends a request, the server answers 402 Payment Required, and then: the agent has no funded wallet, or the price is above its budget. What should happen?&lt;/p&gt;

&lt;p&gt;One option we're considering for send21 (crypto pay links and API for people and AI agents): turn the 402 payment requirements into a page where a person reviews the amount and signs in their own wallet. The agent retries the request with that signature, and the seller's own facilitator settles it. send21 would stay non-custodial and never broadcast anything.&lt;/p&gt;

&lt;p&gt;To be clear, this isn't built, and send21 doesn't support x402 today. We wrote up how x402, AP2 and send21 differ at &lt;a href="https://send21.io/x402-ap2-agent-payments" rel="noopener noreferrer"&gt;https://send21.io/x402-ap2-agent-payments&lt;/a&gt; if you want the background. It would be the x402 version of our over-budget handoff example: the repo at &lt;a href="https://github.com/send21io/send21-over-budget-handoff" rel="noopener noreferrer"&gt;https://github.com/send21io/send21-over-budget-handoff&lt;/a&gt; does the same thing with a pay link and a signed webhook.&lt;/p&gt;

&lt;p&gt;Would you use this? Concrete answers help most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which chain and token do you pay in?&lt;/li&gt;
&lt;li&gt;Which API are you paying for?&lt;/li&gt;
&lt;li&gt;How often does your agent hit a 402 it shouldn't pay on its own?&lt;/li&gt;
&lt;li&gt;Would signing those approvals feel risky, or just annoying?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"We'd rather just fund a wallet" is a useful answer too.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>web3</category>
      <category>agents</category>
    </item>
    <item>
      <title>Let your agent pay a local helper, while you still sign</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:33:36 +0000</pubDate>
      <link>https://dev.to/send21/let-your-agent-pay-a-local-helper-while-you-still-sign-2ag0</link>
      <guid>https://dev.to/send21/let-your-agent-pay-a-local-helper-while-you-still-sign-2ag0</guid>
      <description>&lt;p&gt;Here's a small, concrete case: your own agent arranges a physical task with someone nearby you already know and trust. A photo check of a delivery at the front door, for example, for 3 EUR. When the helper reports it done, somebody has to pay them.&lt;/p&gt;

&lt;p&gt;You probably don't want the agent to hold funds for that. You also don't want to type an address and an amount by hand every time. This post shows a middle path, in an open-source example: &lt;a href="https://github.com/send21io/send21-examples/tree/main/examples/agent-hires-local-helper" rel="noopener noreferrer"&gt;agent-hires-local-helper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The split is simple: &lt;strong&gt;the agent prepares the payment, you sign it in your own wallet.&lt;/strong&gt; send21 (crypto pay links and API for people and AI agents) sits in between. It creates the payment draft; it never holds keys, never signs and never broadcasts. The agent's key can create and read drafts, but no key can move funds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8wmk3u672xtkv2qp3kv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8wmk3u672xtkv2qp3kv.png" alt="The owner review page: " width="800" height="1155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Task card.&lt;/strong&gt; The task, where, the agreed amount, and the helper's receiving address in groups of four. The helper gave you that address once; it lives in &lt;code&gt;helper.json&lt;/code&gt;. The agent reads it from there and can't change it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task done → two MCP checks and one draft.&lt;/strong&gt; The agent checks the address rules over the send21 MCP server (&lt;code&gt;https://send21.io/mcp&lt;/code&gt;) and creates a payment draft with the REST API, with the same scoped key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner review.&lt;/strong&gt; You see the amount, a QR code, the full address, whether it matches the saved one, and a plain list of what the agent did. One button: &lt;em&gt;Open the payment to sign in your wallet&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid.&lt;/strong&gt; After a signed &lt;code&gt;draft.confirmed&lt;/code&gt; webhook (or the draft status turning &lt;code&gt;Confirmed&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why a draft, not a pay link
&lt;/h2&gt;

&lt;p&gt;send21 has two building blocks. A pay link asks to get paid, and send21 says pay links need the owner's own approved receiving addresses, while drafts may pay any approved address. The helper's address isn't yours, so paying it is a draft. That also works on default accounts, where the receiving-address policy is &lt;code&gt;ApprovedOnly&lt;/code&gt;, once you've approved the helper's address.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calls
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;lib/agent.js&lt;/code&gt;, shortened:&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;status&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;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_account_status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// -&amp;gt; receivingAddressPolicy&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;book&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;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list_address_book&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;helper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;helper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&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;approved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;verifiedAt&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;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivingAddressPolicy&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ApprovedOnly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The agent can't approve addresses. It stops, tells the owner, creates nothing.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stopped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The helper's address is not approved in send21 yet...&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;draft&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/drafts&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;receiverAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;helper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// from helper.json, never from a message&lt;/span&gt;
  &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;helper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;// "Solana"&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;helper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;// "Usdt"&lt;/span&gt;
  &lt;span class="na"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MultiOutput&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;singleOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// one QR payment from any wallet&lt;/span&gt;
  &lt;span class="na"&gt;fiatCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiatCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// "EUR"&lt;/span&gt;
  &lt;span class="na"&gt;fiatAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiatAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// 3&lt;/span&gt;
  &lt;span class="na"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: done (task &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`helper-task-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The draft goes through &lt;code&gt;POST /drafts&lt;/code&gt; rather than the MCP &lt;code&gt;create_draft&lt;/code&gt; tool because &lt;code&gt;singleOutput: true&lt;/code&gt; makes it one QR payment any wallet can pay, with the send21 fee billed to the account. &lt;code&gt;create_draft&lt;/code&gt; puts the fee in a separate output, which suits batching wallets.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Idempotency-Key&lt;/code&gt; from the task id is the part we'd copy into any agent. Agents retry. A webhook fires twice, a message gets processed again, someone presses "done" twice. With the same key, send21 returns the original draft instead of creating a second one, and the same key with a different receiver, amount, currency or network is refused. &lt;code&gt;npm run agent&lt;/code&gt; shows it: it asks twice and gets the same draft id back. Drafts have no &lt;code&gt;orderId&lt;/code&gt; or &lt;code&gt;reuseOpenForOrder&lt;/code&gt;; those exist only for pay links, so the task id lives in the key and the memo.&lt;/p&gt;

&lt;p&gt;A fiat price is locked for 15 minutes. If the lock runs out while the draft is still within its life, the agent re-quotes it (&lt;code&gt;POST /drafts/{id}/requote&lt;/code&gt;) instead of creating another one.&lt;/p&gt;

&lt;p&gt;Asset and network are checked before anything is sent: USDT only on Solana or Ethereum, USDC for example on Solana, Polygon or Arbitrum. The example run used USDT on Solana.&lt;/p&gt;

&lt;p&gt;After creating the draft, the example compares the draft's &lt;code&gt;receiverAddress&lt;/code&gt; with the saved one, every character. Only an entry you verified in your send21 address book is shown as &lt;em&gt;pre-approved&lt;/em&gt;; otherwise the UI says &lt;em&gt;Matches the helper address you saved&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The webhook check
&lt;/h2&gt;

&lt;p&gt;The task is marked paid on the server, from a signed webhook, never from the browser. First the signature (&lt;code&gt;lib/webhook.js&lt;/code&gt;):&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;// X-Send21-Signature-V2: t=&amp;lt;unix&amp;gt;,v1=&amp;lt;hex HMAC-SHA256 of "&amp;lt;t&amp;gt;.&amp;lt;delivery id&amp;gt;.&amp;lt;raw body&amp;gt;"&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// older than 5 minutes: refuse&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;deliveryId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&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="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the content (&lt;code&gt;server.js&lt;/code&gt;). A valid signature isn't enough; the payment has to be &lt;em&gt;this task's&lt;/em&gt; payment:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handledDeliveries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;already handled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// retries reuse X-Send21-Delivery&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft.confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;draftId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sentCurrency&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountSats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountBaseUnits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// billed amount in base units&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;orderId&lt;/code&gt; is only set for pay-link payments, so a draft is matched by its &lt;code&gt;draftId&lt;/code&gt;. An unsigned delivery gets a 401. To see the &lt;em&gt;Paid&lt;/em&gt; screen without paying, &lt;code&gt;npm run local-paid&lt;/code&gt; signs a test delivery with a local secret, posts it twice (the second is ignored as already handled), and the UI labels it &lt;strong&gt;Local test webhook&lt;/strong&gt; so it's never mistaken for a real payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it from your own agent
&lt;/h2&gt;

&lt;p&gt;Any MCP client can make the same checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"send21"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://send21.io/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer s21_YOUR_KEY"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a rule like: &lt;em&gt;When a helper task is reported done, create a payment draft to the helper's saved address with the task id in the Idempotency-Key (&lt;code&gt;create_draft&lt;/code&gt; with &lt;code&gt;idempotencyKey&lt;/code&gt;, or &lt;code&gt;POST /drafts&lt;/code&gt;), then give me the draft to sign. Never sign or send payments.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nobody checks the work.&lt;/strong&gt; "Done" means someone pressed &lt;em&gt;Task done&lt;/em&gt;. send21 doesn't hold the money in between and doesn't judge the result. Use this for helpers you know and trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't find helpers.&lt;/strong&gt; You bring the person and their address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Address rules on new accounts.&lt;/strong&gt; New accounts use the receiving-address policy &lt;code&gt;ApprovedOnly&lt;/code&gt;: API keys can only use addresses the owner approved in the send21 app with a passkey or an authenticator code, and the agent stops if the helper's isn't one. Approve the helper's address first. Our test run used an account with inline addresses allowed, so the UI said &lt;em&gt;Matches the helper address you saved&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a demo.&lt;/strong&gt; One task in memory, no database. The webhook needs a public HTTPS endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live networks.&lt;/strong&gt; Account keys use live networks; a draft moves no money until someone signs it. Test with a small amount to your own address and cancel the draft (&lt;code&gt;npm run agent -- --cancel &amp;lt;id&amp;gt;&lt;/code&gt;). The &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;testnet demo&lt;/a&gt; runs in the browser with no account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the address in your wallet.&lt;/strong&gt; The review screen helps, but the wallet's confirm screen is the last word. A phished or forged link is still a risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it
&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/send21io/send21-examples
&lt;span class="nb"&gt;cd &lt;/span&gt;send21-examples/examples/agent-hires-local-helper
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;                 &lt;span class="c"&gt;# no key, no network&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env     &lt;span class="c"&gt;# scoped key from https://send21.io/api-keys&lt;/span&gt;
npm start                &lt;span class="c"&gt;# http://localhost:3022&lt;/span&gt;
npm run agent            &lt;span class="c"&gt;# the agent flow in the terminal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've built something similar, we'd like to hear how you decide when "done" is done. Questions: &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>node</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>Stop copying payee addresses from wallet history: a small pattern for repeat crypto payments</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:18:16 +0000</pubDate>
      <link>https://dev.to/send21/stop-copying-payee-addresses-from-wallet-history-a-small-pattern-for-repeat-crypto-payments-3oid</link>
      <guid>https://dev.to/send21/stop-copying-payee-addresses-from-wallet-history-a-small-pattern-for-repeat-crypto-payments-3oid</guid>
      <description>&lt;p&gt;If you pay the same person in USDC or USDT every month, there is a good chance you do it like this: open the wallet, find last month's transfer, copy the address, paste, send.&lt;/p&gt;

&lt;p&gt;That one step, copying the address from history, is exactly what address poisoning is built for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threat in one paragraph
&lt;/h2&gt;

&lt;p&gt;An attacker watches the chain for your transfers. Right after you pay someone, they send you a tiny or zero-value transfer from an address that starts and ends with the same characters as your real payee. Now your history has two entries that look the same at a glance. Next month you copy the wrong one.&lt;/p&gt;

&lt;p&gt;This is not rare. The USENIX Security '25 paper &lt;a href="https://arxiv.org/abs/2501.16681" rel="noopener noreferrer"&gt;"Blockchain Address Poisoning"&lt;/a&gt; from Carnegie Mellon counted 270 million attack attempts aimed at 17 million victims on Ethereum and BSC alone, between 1 July 2022 and 30 June 2024. 6,633 of those incidents caused at least $83.8M in losses.&lt;/p&gt;

&lt;p&gt;A test transfer doesn't protect you either. On 19 December 2025 (UTC), someone sent 49,999,950 USDT on Ethereum to a lookalike address about 26 minutes after a 50 USDT test transfer to the real one (&lt;a href="https://www.coindesk.com/web3/2025/12/20/crypto-user-loses-usd50-million-in-address-poisoning-scam" rel="noopener noreferrer"&gt;CoinDesk&lt;/a&gt;). The test worked. The address for the big payment came from history.&lt;/p&gt;

&lt;h2&gt;
  
  
  A manual routine anyone can use (no tools needed)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get the address once, from outside the chain.&lt;/strong&gt; An invoice, a contract or a message from the payee. Not from a block explorer and not from your history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save it as a named payee record.&lt;/strong&gt; Your wallet's address book, a password manager entry, a text file you control. Write down the network and the asset next to it (USDT on Solana and USDT on Ethereum are different addresses).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every payment starts from that record.&lt;/strong&gt; Never from "recent", never from the history list, never from a search box that shows past addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare the whole address, in groups of four.&lt;/strong&gt; Lookalikes are generated to match the first and last 4 to 6 characters. Read it group by group against the record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check again on the wallet's confirm screen.&lt;/strong&gt; That is the last place the address can still change, for example through a swapped clipboard or a forged link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the payee says their address changed, confirm through a second channel&lt;/strong&gt; before you update the record.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most of the protection comes from steps 1 to 3: one source of truth, and history is never used as an address book.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same pattern in code
&lt;/h2&gt;

&lt;p&gt;We wrote an open-source example of this routine with send21 (crypto pay links and API for people and AI agents): &lt;a href="https://github.com/send21io/send21-examples/tree/main/examples/repeat-payer-verified-address" rel="noopener noreferrer"&gt;repeat-payer-verified-address&lt;/a&gt;. It is a small Node.js app. It's one way to automate the routine above, not the only one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67koj3mzo4i80tn894ts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67koj3mzo4i80tn894ts.png" alt="The example page: " width="800" height="851"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One stored record.&lt;/strong&gt; The payee lives in &lt;code&gt;payee.json&lt;/code&gt; (label, network, asset, address, monthly amount). The app refuses to start until you replace the placeholder, and the error tells you to copy the address "from the invoice or contract they sent you, never from wallet history". It also refuses an asset on a network where send21 does not offer it (USDT, for example, only runs on Solana and Ethereum).&lt;/p&gt;

&lt;p&gt;If your send21 key can read the address book, the app also checks whether the address is an entry you verified in the send21 app (with a passkey or an authenticator code; no API key can verify one). Only then does the UI say &lt;em&gt;approved payee&lt;/em&gt;. Otherwise it says &lt;em&gt;saved payee&lt;/em&gt; and that the record is your local file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The draft is built from that record, never from a paste:&lt;/strong&gt;&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;// lib/monthly.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`monthly-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;draft&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/drafts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;draftBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Idempotency-Key&lt;/code&gt; is per payee and month, so a double click or a retry returns the same draft instead of a second one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The full address is checked in three places.&lt;/strong&gt; After creating the draft, the app compares the saved address character by character with &lt;code&gt;receiverAddress&lt;/code&gt; in the draft, with the output in the wallet instructions (&lt;code&gt;GET /drafts/{id}/tx-template&lt;/code&gt;) and with the address inside the QR link (Solana Pay or EIP-681 URI):&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;// lib/address.js: exact, case-sensitive, no first-4/last-4 shortcut&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compareAddresses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actual&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;approved&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;??&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...also reports where the first difference is&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test suite includes a lookalike that shares the first and last four characters and fails at position 4. If any check fails, the UI shows &lt;em&gt;Stop. The address does not match&lt;/em&gt; and no wallet button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly without retyping.&lt;/strong&gt; &lt;code&gt;npm run monthly -- --schedule&lt;/code&gt; creates a recurring template (&lt;code&gt;POST /recurring-templates&lt;/code&gt;, &lt;code&gt;interval: "Monthly"&lt;/code&gt;) from the same record, so send21 creates each month's draft from the stored address. The payer still signs every one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price lock.&lt;/strong&gt; A price in EUR (or any of 161 currencies) is locked for 15 minutes. If this month's draft is still valid but the lock ran out, the app re-quotes the same draft (&lt;code&gt;POST /drafts/{id}/requote&lt;/code&gt;) instead of making a twin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your wallet signs.&lt;/strong&gt; The page ends at &lt;em&gt;Waiting for your signature&lt;/em&gt; with a QR code and an &lt;em&gt;Open in your wallet&lt;/em&gt; button. send21 prepares the payment; it never holds keys, never signs and never broadcasts. A signed &lt;code&gt;draft.confirmed&lt;/code&gt; webhook (checked with &lt;code&gt;X-Send21-Signature-V2&lt;/code&gt;) moves it to &lt;em&gt;Paid&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;It removes one risky step: copying an address from history. It does not stop scams in general. A phished or forged pay link, a changed invoice or a tampered payee record is still a risk, and a wrong address saved in step 1 stays wrong every month. So keep step 5: compare the full address on your wallet's confirm screen before you sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&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/send21io/send21-examples
&lt;span class="nb"&gt;cd &lt;/span&gt;send21-examples/examples/repeat-payer-verified-address
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt;     &lt;span class="c"&gt;# tests need no key and no network&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env        &lt;span class="c"&gt;# add a scoped key from https://send21.io/api-keys&lt;/span&gt;
npm start                   &lt;span class="c"&gt;# http://localhost:3021&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Account keys use live networks, and a draft moves no money until someone signs it, so test with a small draft to your own address and cancel it afterwards (&lt;code&gt;npm run monthly -- --cancel &amp;lt;id&amp;gt;&lt;/code&gt;). If you just want to look around, the &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;testnet demo&lt;/a&gt; runs in the browser with no account.&lt;/p&gt;

&lt;p&gt;How do you keep payee addresses today? We're curious what routines people use. Questions about the example: &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>cryptocurrency</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>x402 vs AP2 vs send21: three ways AI agents handle payments</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:24:56 +0000</pubDate>
      <link>https://dev.to/send21/x402-vs-ap2-vs-send21-three-ways-ai-agents-handle-payments-4f5h</link>
      <guid>https://dev.to/send21/x402-vs-ap2-vs-send21-three-ways-ai-agents-handle-payments-4f5h</guid>
      <description>&lt;p&gt;Disclosure: this is written by the team behind send21, one of the three approaches below. We have tried to describe the other two from their own documentation, and we link to it so you can check.&lt;/p&gt;

&lt;p&gt;"How does an AI agent pay?" has at least three different answers right now, and they are not really competitors. They answer different questions: who holds the money, who authorizes the payment, and what kind of thing is being paid for.&lt;/p&gt;

&lt;h2&gt;
  
  
  x402: the agent pays for a request
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402&lt;/a&gt; is an open standard for internet-native payments built on the HTTP status code 402 Payment Required. It was started by Coinbase, and the x402 site now announces an x402 Foundation at the Linux Foundation. The reference code is on &lt;a href="https://github.com/x402-foundation/x402" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow, from the spec's README:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A client (for example an agent) requests a resource.&lt;/li&gt;
&lt;li&gt;The server answers &lt;code&gt;402 Payment Required&lt;/code&gt; with a &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; header listing what it accepts.&lt;/li&gt;
&lt;li&gt;The client picks an option, builds a signed payment payload and repeats the request with a &lt;code&gt;PAYMENT-SIGNATURE&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;The server verifies the payment, locally or through a facilitator's &lt;code&gt;/verify&lt;/code&gt; endpoint, serves the response, and settles the payment on-chain, itself or through the facilitator's &lt;code&gt;/settle&lt;/code&gt; endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the server it can be one piece of middleware per route. x402 describes itself as network, token and currency agnostic: there are SDKs for EVM chains, Solana and several others, stablecoins are the main use, and fiat networks may be added. There are no protocol fees beyond network fees. Its principles say a facilitator or server must never be able to move funds other than as the client intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it fits:&lt;/strong&gt; pay-per-call APIs, paywalled content and data, machine-to-machine payments where a human signing each request would make no sense. The trade-off is that the client needs a key that can sign payments, which in practice means a funded wallet, and spending limits are something the agent's owner sets up around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  AP2: the agent buys with your mandate
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ap2-protocol.org/" rel="noopener noreferrer"&gt;AP2, the Agent Payments Protocol&lt;/a&gt;, is an open protocol published by Google under the Apache 2.0 license (&lt;a href="https://github.com/google-agentic-commerce/AP2" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). It is available as an extension for the Agent2Agent (A2A) protocol and the Universal Commerce Protocol, and the AP2 site says standardization continues in FIDO Alliance working groups.&lt;/p&gt;

&lt;p&gt;AP2 starts from a different problem. Payment systems assume a person clicks "buy". When an agent does it, the merchant and the payment side need to know that the user really authorized this purchase, and who is accountable if it goes wrong.&lt;/p&gt;

&lt;p&gt;Its answer is verifiable digital credentials called mandates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Checkout Mandate&lt;/strong&gt; captures the items and purchase details agreed between the agent and the merchant, and is shared with the merchant.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Payment Mandate&lt;/strong&gt; authorizes a payment against a specific payment instrument, and is shared with the credential provider, the networks and the merchant's payment side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each comes in an open form (the user's constraints, such as a budget, for an agent acting on its own) and a closed form (authorization for one finalized checkout and amount). Chained together they give an audit trail for both human-present and human-not-present purchases.&lt;/p&gt;

&lt;p&gt;The first version supports "pull" payment methods like credit and debit cards. E-wallets, real-time bank transfers and digital currencies are on its roadmap. The AP2 samples already include a human-not-present flow that pays with x402, which shows the two can work together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it fits:&lt;/strong&gt; agents shopping at merchants on a user's behalf, inside existing card networks, where verifiable intent and accountability matter more than speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  send21: the agent prepares, someone else signs
&lt;/h2&gt;

&lt;p&gt;send21 (our project) takes a third route: the agent never holds money and cannot pay. It creates a crypto pay link or payment draft through the &lt;a href="https://send21.io/docs/mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; or the REST API, using a scoped key that cannot move funds. The payment is then signed in a wallet the agent does not control: the payer's own wallet, or the operator's.&lt;/p&gt;

&lt;p&gt;The agent can track the payment and react to an HMAC-signed webhook when it confirms. send21 retries failed deliveries, so the same delivery can arrive more than once, and the receiver should key on the delivery id. On new accounts, API keys can only use receiving addresses the owner approved in the app with a passkey or an authenticator code. An agent can propose an address; it cannot approve one.&lt;/p&gt;

&lt;p&gt;Payers can use BTC (on-chain or Lightning), USDC, USDT (on Solana and Ethereum), EURC, SOL or ETH. send21 never holds keys or funds, and it does not implement x402 or AP2 today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it fits:&lt;/strong&gt; agents that bill people (invoices, checkouts, pay links), agents that hit their budget and hand a purchase to a human with a link to pay, and teams that do not want an agent holding a funded wallet. It is a poor fit for paying per API call: nobody wants to sign a transaction for every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;x402&lt;/th&gt;
&lt;th&gt;AP2&lt;/th&gt;
&lt;th&gt;send21&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Question it answers&lt;/td&gt;
&lt;td&gt;How does a client pay for this HTTP request?&lt;/td&gt;
&lt;td&gt;How does a merchant know the user authorized this purchase?&lt;/td&gt;
&lt;td&gt;How does an agent get a person paid, or ask a person to pay, without holding money?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who pays&lt;/td&gt;
&lt;td&gt;The client, from its own wallet&lt;/td&gt;
&lt;td&gt;The user, through their payment credentials and mandate&lt;/td&gt;
&lt;td&gt;The payer or operator, signing in their own wallet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the agent holds&lt;/td&gt;
&lt;td&gt;A key that can sign payments&lt;/td&gt;
&lt;td&gt;Mandates, not the money&lt;/td&gt;
&lt;td&gt;A scoped API key that cannot move funds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Money&lt;/td&gt;
&lt;td&gt;Stablecoins first, many chains&lt;/td&gt;
&lt;td&gt;Cards first&lt;/td&gt;
&lt;td&gt;BTC, Lightning, USDC, USDT, EURC, SOL, ETH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human approval&lt;/td&gt;
&lt;td&gt;Optional, set up by the owner&lt;/td&gt;
&lt;td&gt;Through the signed mandate&lt;/td&gt;
&lt;td&gt;A person or the operator's wallet signs each payment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;Start from the question you actually have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is your agent the customer of an API or a data source?&lt;/strong&gt; x402 is built for that. Put a budget around the wallet it uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is your agent shopping at merchants for a user, with cards?&lt;/strong&gt; AP2 is designed for that, with mandates that make the user's intent checkable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is your agent the one asking for money, or does it need a person to decide?&lt;/strong&gt; That is where send21 fits: the agent prepares the payment, and the money moves only when someone signs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many agents will end up using more than one. A research agent might pay for data with x402, book a flight under an AP2 mandate, and send its customer an invoice through send21.&lt;/p&gt;

&lt;p&gt;The protocols are moving quickly, so check the primary sources before you build: &lt;a href="https://www.x402.org/" rel="noopener noreferrer"&gt;x402.org&lt;/a&gt;, &lt;a href="https://ap2-protocol.org/" rel="noopener noreferrer"&gt;ap2-protocol.org&lt;/a&gt;, and for send21 the &lt;a href="https://send21.io/docs" rel="noopener noreferrer"&gt;docs&lt;/a&gt; and the &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;testnet demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our own summary page with the same comparison: &lt;a href="https://send21.io/x402-ap2-agent-payments" rel="noopener noreferrer"&gt;x402, AP2 and send21: AI agent payments explained&lt;/a&gt;. Corrections are welcome at &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>payments</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to send a crypto invoice and get paid straight to your wallet</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:24:07 +0000</pubDate>
      <link>https://dev.to/send21/how-to-send-a-crypto-invoice-and-get-paid-straight-to-your-wallet-2h6j</link>
      <guid>https://dev.to/send21/how-to-send-a-crypto-invoice-and-get-paid-straight-to-your-wallet-2h6j</guid>
      <description>&lt;p&gt;Disclosure: this is written by the team behind send21.&lt;/p&gt;

&lt;p&gt;Most crypto invoices are a PDF with a wallet address pasted at the bottom. Then the questions start. Which rate turns your EUR 1,200 into USDC? Which network should the client use? Did the money arrive? And what if an exchange withdrawal lands a few euros short?&lt;/p&gt;

&lt;p&gt;This guide walks through sending a crypto invoice as a pay link instead, with send21, so the client pays from their own wallet straight into yours. send21 is non-custodial software: it prepares the payment instructions and watches the chain, but it never holds keys or funds. The short version is on the &lt;a href="https://send21.io/crypto-invoice-generator" rel="noopener noreferrer"&gt;crypto invoice generator page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A wallet you control&lt;/strong&gt; for each network you want to accept. For example a Solana wallet for USDC or EURC on Solana, an Ethereum wallet for Ethereum or Base, a Bitcoin wallet for on-chain BTC, or a Lightning wallet with a Lightning address (it looks like &lt;a href="mailto:name@wallet.com"&gt;name@wallet.com&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A send21 account.&lt;/strong&gt; Sign-up is free with an email address. There is no subscription and no KYC step, because send21 never holds your money. The rules that apply to your own business still apply to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Price the invoice in your own currency
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Get paid&lt;/strong&gt; in the app and enter the amount in the currency your books are kept in: EUR, USD, SEK or any of 161 fiat currencies. You can also ask for an exact crypto amount instead.&lt;/p&gt;

&lt;p&gt;Add a message the client will see, such as "Invoice 2026-041, logo design", and your invoice number as the order id. The order id comes back with every notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Choose what you accept
&lt;/h2&gt;

&lt;p&gt;Pick the currencies and networks you are happy to receive, each with your own receiving address:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Currency&lt;/th&gt;
&lt;th&gt;Networks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BTC&lt;/td&gt;
&lt;td&gt;Bitcoin on-chain, or Lightning to your Lightning address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDC&lt;/td&gt;
&lt;td&gt;Solana, Ethereum, Base, Arbitrum, Polygon (native USDC only on Arbitrum and Polygon)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDT&lt;/td&gt;
&lt;td&gt;Solana and Ethereum only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EURC&lt;/td&gt;
&lt;td&gt;Solana, Ethereum, Base&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOL&lt;/td&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETH&lt;/td&gt;
&lt;td&gt;Ethereum, Base, Arbitrum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Offer only what you want to hold. If your clients are in the EU, USDC and EURC are usually the easiest choice, since EU exchanges delisted USDT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Send the link
&lt;/h2&gt;

&lt;p&gt;Send the pay link by email or chat, or show its QR code. When the client opens it, they see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your amount in your currency, your message, and the currencies you accept;&lt;/li&gt;
&lt;li&gt;after picking one, a single QR code with the exact crypto amount, which network to use, and how long the rate is locked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rate locks for 15 minutes when the client picks a currency, and the rate, its source and the time are recorded. So a price in EUR stays a price in EUR, and you can see later exactly how it was converted.&lt;/p&gt;

&lt;p&gt;The client pays from any wallet or exchange that can send to an address. They sign in their own wallet, and the money goes from their wallet to yours. Nothing passes through send21.&lt;/p&gt;

&lt;p&gt;Ask the client to compare the address in their wallet with the one on the pay page. send21 highlights its start and end, the parts a look-alike address from clipboard malware would have to copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Get told when it is paid
&lt;/h2&gt;

&lt;p&gt;send21 watches the chain and tells you by email, in the app and, if you set one up, by a signed webhook to your own system. You hear when the payment is seen and again when it is confirmed. Before marking a payment confirmed, send21 looks the transaction up once more at a second source.&lt;/p&gt;

&lt;p&gt;Each pay link is for one payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Handle a payment that arrives a little short
&lt;/h2&gt;

&lt;p&gt;Exchanges often deduct their withdrawal fee from the amount, so EUR 1,200 arrives as EUR 1,198.50.&lt;/p&gt;

&lt;p&gt;When a transfer within 10% of the billed amount reaches your address, send21 flags it instead of ignoring it. You get an alert, and you decide: accept it as paid, or decline it with a reason. The client sees a notice not to pay again. If you have not decided 2 hours before the payment expires, you get a reminder. You can also choose to accept small short payments automatically (up to 10%, and you absorb the difference). Overpayments always wait for you.&lt;/p&gt;

&lt;p&gt;Two honest limits: a transfer more than 10% off is not flagged, and the money is already in your wallet either way, so any refund is a new payment you make yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Keep the records
&lt;/h2&gt;

&lt;p&gt;Each paid invoice keeps the amount, the rate with its source and time, the transaction and your order id, and you can download a PDF of the payment. send21 does not convert crypto to fiat or pay out to a bank, so taxes and bookkeeping in your country stay with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Creating and sending invoices is free. When one is paid, send21 bills a fee to your account: 0.49% on the smallest payments, falling by tier to 0.01% above $100,000, never more than $100 per payment. The fee is never taken out of the payment, so you receive the full amount. Fees add up on your account, and you settle them in one payment at $10 or after 30 days. The full schedule is on the &lt;a href="https://send21.io/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things to know first
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crypto payments cannot be reversed.&lt;/strong&gt; A refund is a new payment from your wallet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightning confirms automatically only when your wallet provider supports it&lt;/strong&gt; (LUD-21). Otherwise the payment is confirmed with the proof of payment that the paying wallet shows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ETH seen only as a balance increase&lt;/strong&gt; (where no block explorer is available) waits for you to confirm it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it without real money
&lt;/h2&gt;

&lt;p&gt;To see the flow from both sides first, the &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;testnet demo&lt;/a&gt; runs a real payment with test coins, with no account and no email.&lt;/p&gt;

&lt;p&gt;From your own software, the same pay link is one call to &lt;code&gt;POST /api/v1/payment-requests&lt;/code&gt; (see the &lt;a href="https://send21.io/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Start here: &lt;a href="https://send21.io/crypto-invoice-generator" rel="noopener noreferrer"&gt;send21 crypto invoice generator&lt;/a&gt;. Questions or corrections are welcome at &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>freelance</category>
      <category>payments</category>
      <category>bitcoin</category>
    </item>
    <item>
      <title>Let an AI agent create crypto pay links over MCP (without handing it your keys)</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:42:08 +0000</pubDate>
      <link>https://dev.to/send21/let-an-ai-agent-create-crypto-pay-links-over-mcp-without-handing-it-your-keys-16hf</link>
      <guid>https://dev.to/send21/let-an-ai-agent-create-crypto-pay-links-over-mcp-without-handing-it-your-keys-16hf</guid>
      <description>&lt;p&gt;Agents are good at preparing payments and bad places to keep money. This tutorial shows a split that works: your AI agent creates a crypto pay link over the Model Context Protocol (MCP), the payer pays from their own wallet straight to your address, and your server learns about it through a signed webhook.&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://send21.io/" rel="noopener noreferrer"&gt;send21&lt;/a&gt;: &lt;em&gt;Crypto pay links and API for people and AI agents.&lt;/em&gt; send21 is non-custodial payment software. It prepares payment instructions and watches the chain. It never holds private keys or funds, never signs and never broadcasts. No API key can move funds.&lt;/p&gt;

&lt;p&gt;What you will build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect an MCP client to the send21 MCP server.&lt;/li&gt;
&lt;li&gt;Ask the agent what is still missing (&lt;code&gt;get_account_status&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Approve a receiving address yourself, in the send21 app.&lt;/li&gt;
&lt;li&gt;Let the agent create a pay link (&lt;code&gt;create_payment_request&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Let the agent check it (&lt;code&gt;get_payment_request&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Optional: verify webhooks on your own server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full docs: &lt;a href="https://send21.io/docs/mcp" rel="noopener noreferrer"&gt;send21.io/docs/mcp&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Connect your MCP client
&lt;/h2&gt;

&lt;p&gt;The server lives at &lt;code&gt;https://send21.io/mcp&lt;/code&gt; (Streamable HTTP, stateless). There are two ways in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: sign in, no key to copy.&lt;/strong&gt; Clients that support MCP sign-in (OAuth) can add &lt;code&gt;https://send21.io/mcp/oauth&lt;/code&gt; as a remote server with no headers. The client opens send21 in your browser, you sign in, see what the app asks for, and approve. In Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http send21 https://send21.io/mcp/oauth
&lt;span class="c"&gt;# then run /mcp in Claude Code and choose send21 to sign in&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A signed-in connection only ever uses receiving addresses you approved, whatever your account setting. It shows up under API keys as a connected app, and Disconnect ends it at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B: a scoped API key.&lt;/strong&gt; Sign in, open &lt;a href="https://send21.io/api-keys" rel="noopener noreferrer"&gt;API keys&lt;/a&gt; and create a key with only the scopes the job needs. For this tutorial that is &lt;code&gt;drafts:read&lt;/code&gt; and &lt;code&gt;drafts:write&lt;/code&gt;, plus &lt;code&gt;addressbook:write&lt;/code&gt; if the agent should propose addresses and &lt;code&gt;webhooks:manage&lt;/code&gt; for step 6. Keys look like &lt;code&gt;s21_...&lt;/code&gt; and are shown once.&lt;/p&gt;

&lt;p&gt;Cursor (&lt;code&gt;.cursor/mcp.json&lt;/code&gt;) and other clients that use &lt;code&gt;mcpServers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"send21"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://send21.io/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer s21_YOUR_KEY"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VS Code (&lt;code&gt;.vscode/mcp.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"send21"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://send21.io/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer s21_YOUR_KEY"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code with a key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http send21 https://send21.io/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer s21_YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listing the tools and reading the fee schedule work without a key. Every other tool needs one (or a sign-in).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ask the agent what is missing
&lt;/h2&gt;

&lt;p&gt;Start every session with &lt;code&gt;get_account_status&lt;/code&gt;. It reports whether your email is confirmed, your receiving-address policy, how many addresses are approved or waiting, unsettled fees, the key's scopes, and the next steps.&lt;/p&gt;

&lt;p&gt;Here is a real response from our test account (email redacted):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;redacted&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emailVerified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receivingAddressPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InlineAllowed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approvedReceivingAddresses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approvedPayingAddresses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proposedAddresses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ownerCanApprove"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyScopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"drafts:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drafts:write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webhooks:manage"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outstandingFeesUsd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"feesBlockCreation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nextSteps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Ready: create pay links."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our test account is an older one with the &lt;code&gt;InlineAllowed&lt;/code&gt; policy. New accounts start with &lt;code&gt;ApprovedOnly&lt;/code&gt;, and older accounts can turn it on in Settings. On &lt;code&gt;ApprovedOnly&lt;/code&gt;, &lt;code&gt;get_account_status&lt;/code&gt; is how the agent learns what is still missing before it can create a pay link. We recommend &lt;code&gt;ApprovedOnly&lt;/code&gt; for any account an agent touches.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. You approve the receiving address, not the agent
&lt;/h2&gt;

&lt;p&gt;This is the step that keeps a prompt-injected agent or a leaked key from sending your customers' money somewhere else.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;ApprovedOnly&lt;/code&gt;, an API key can only use addresses the owner approved. The agent can &lt;strong&gt;propose&lt;/strong&gt; one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"propose_receiving_address"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My Phantom wallet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourSolanaAddress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solana"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The address is saved unapproved, and you get an email. That email has no approve link. You sign in to send21, compare the address character by character, and approve it with a passkey or an authenticator code. An emailed code is never enough, because an agent that reads your inbox could use it. No API key and no scope can approve an address.&lt;/p&gt;

&lt;p&gt;If the agent names any other address in a pay link, the call fails with &lt;code&gt;422 address_not_approved&lt;/code&gt; and nothing is created. A good agent then calls &lt;code&gt;get_account_status&lt;/code&gt;, tells you, and waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Create a pay link
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;create_payment_request&lt;/code&gt; makes a pay link priced in fiat (any of 161 currencies). You list the currencies you accept, each with your own receiving address. The payer opens the link, picks one, the rate locks for 15 minutes, and they send a single transfer from their own wallet straight to your address.&lt;/p&gt;

&lt;p&gt;The call we ran against the live server (addresses redacted):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create_payment_request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fiatCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fiatAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TPL-MCP-DEVTO-20261022-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MCP tutorial test run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expiryHours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"idempotencyKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tpl-mcp-devto-20261022-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reuseOpenForOrder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usdc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-solana-address&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usdt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-solana-address&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two arguments are worth having from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;idempotencyKey&lt;/code&gt;: a retry with the same key and the same parameters returns the original pay link. The same key with another amount, currency or address is refused (422), and nothing is created.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;orderId&lt;/code&gt; with &lt;code&gt;reuseOpenForOrder: true&lt;/code&gt;: if the agent retries with a &lt;em&gt;new&lt;/em&gt; key, it still gets the open link for that order instead of a second one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other useful options: &lt;code&gt;declineNote&lt;/code&gt; (what happens if the payer does not pay, shown on the pay page), &lt;code&gt;shortfallAbsorbUpToPercent&lt;/code&gt; (accept small short payments automatically, 0 to 10; overpayments always wait for a person), and &lt;code&gt;expiryHours&lt;/code&gt; (default 24).&lt;/p&gt;

&lt;p&gt;Want a different asset mix? USDT works on Solana and Ethereum. On Base you can take USDC, EURC or ETH. Bitcoin works on-chain or over Lightning (&lt;code&gt;"method": "Lightning"&lt;/code&gt; with your Lightning address).&lt;/p&gt;

&lt;p&gt;The real response, with ids, token and address redacted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;uuid&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shareToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;token&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/pay/&amp;lt;token&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TPL-MCP-DEVTO-20261022-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fiatCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fiatAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MCP tutorial test run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-22T19:38:58.466Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-22T18:38:58.466Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usdc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;solana-address&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MultiOutput"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usdt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;solana-address&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MultiOutput"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"activeDraftId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"draftStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amountMismatch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"declineNote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shortfallPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changeSignals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changeNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"optionChangeSignals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usdc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MultiOutput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Usdt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MultiOutput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The link to share is &lt;code&gt;https://send21.io&lt;/code&gt; plus &lt;code&gt;payPath&lt;/code&gt;. The payer scans one standard QR code (Solana Pay here) with any wallet. Hand the link to a human. The agent does not pay it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;changeSignals&lt;/code&gt; is empty here. It fills with &lt;code&gt;new_address&lt;/code&gt;, &lt;code&gt;new_asset&lt;/code&gt; or &lt;code&gt;unusual_amount&lt;/code&gt; when something differs from your paid payments, and the pay page shows the payer the same lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Check the status
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;get_payment_request&lt;/code&gt; takes the &lt;code&gt;paymentRequestId&lt;/code&gt; and returns the same shape. Right after creation ours was identical to the response above: &lt;code&gt;status: "Open"&lt;/code&gt;, &lt;code&gt;draftStatus: null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How to read it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pay link state&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;draftStatus: null&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Nobody picked a currency yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;draftStatus: AwaitingPayment&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The payer picked a currency, the price is locked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;draftStatus: Seen&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Payment seen, not confirmed yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Completed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Expired&lt;/code&gt; / &lt;code&gt;Cancelled&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Unpaid / cancelled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A transfer within 10% of the billed amount never confirms on its own. It waits for you to accept or decline it. That decision is yours, not the agent's.&lt;/p&gt;

&lt;p&gt;Polling is fine for a chat session. For anything that ships goods, use webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Optional: verify webhooks on your server
&lt;/h2&gt;

&lt;p&gt;Register an endpoint with &lt;code&gt;POST /api/v1/webhooks&lt;/code&gt; (scope &lt;code&gt;webhooks:manage&lt;/code&gt;). The signing secret is shown once. Each delivery carries &lt;code&gt;X-Send21-Delivery&lt;/code&gt; (retries reuse it) and &lt;code&gt;X-Send21-Signature-V2: t=&amp;lt;unix&amp;gt;,v1=&amp;lt;hex&amp;gt;&lt;/code&gt;, an HMAC-SHA256 of &lt;code&gt;&amp;lt;t&amp;gt;.&amp;lt;delivery id&amp;gt;.&amp;lt;raw body&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your receiver has three jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the V2 signature over the raw bytes, in constant time.&lt;/li&gt;
&lt;li&gt;Reject deliveries older than 5 minutes.&lt;/li&gt;
&lt;li&gt;Dedupe by delivery id. send21 retries failed deliveries (up to 8 attempts), so the same delivery can arrive more than once. Use its delivery id to ignore repeats.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal Node receiver, no dependencies. The signature lines come from the send21 docs. The HTTP wrapper and the in-memory dedupe are ours, and we tested them locally with self-signed sample deliveries, not live send21 traffic:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&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="s1"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&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;SEND21_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// shown once when you register the endpoint&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// demo only: use a database table with a unique key in production&lt;/span&gt;

&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&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;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;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="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&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;url&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/send21&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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;chunks&lt;/span&gt; &lt;span class="o"&gt;=&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="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&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="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&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="o"&gt;=&amp;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;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// raw bytes, before any JSON parsing&lt;/span&gt;

    &lt;span class="c1"&gt;// X-Send21-Signature-V2: t=&amp;lt;unix seconds&amp;gt;,v1=&amp;lt;hex HMAC-SHA256 of "&amp;lt;t&amp;gt;.&amp;lt;delivery id&amp;gt;.&amp;lt;raw body&amp;gt;"&amp;gt;&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="nc"&gt;String&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-send21-signature-v2&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="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;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;^|,&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;t=&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&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="mi"&gt;1&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;v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;^|,&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;v1=&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{64})&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&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="mi"&gt;1&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;deliveryId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-send21-delivery&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="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;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;300&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;deliveryId&lt;/span&gt; &lt;span class="o"&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="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;v1&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Retries reuse X-Send21-Delivery, so ignore ids you've already handled.&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;handled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deliveryId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;handled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deliveryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&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="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&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;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft.confirmed&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;// Check orderId, amount and asset against your own order before you ship anything.&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sentAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sentCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any 2xx counts as delivered. Endpoints must be public HTTPS. Reconcile on &lt;code&gt;orderId&lt;/code&gt; and the exact amount, not on arrival time, and never mark an order paid from the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules worth pasting into your agent
&lt;/h2&gt;

&lt;p&gt;The send21 docs ship a rules block for &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt; or Cursor rules. The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create pay links with &lt;code&gt;create_payment_request&lt;/code&gt;, with &lt;code&gt;orderId&lt;/code&gt; and &lt;code&gt;reuseOpenForOrder: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Only use approved addresses. On &lt;code&gt;422 address_not_approved&lt;/code&gt;, propose the address and tell the owner.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;get_account_status&lt;/code&gt; when something is blocked.&lt;/li&gt;
&lt;li&gt;Amount mismatches are the owner's to decide.&lt;/li&gt;
&lt;li&gt;Never hold keys, sign or send payments. The payer signs in their own wallet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Runnable code: &lt;a href="https://github.com/send21io/send21-examples" rel="noopener noreferrer"&gt;send21-examples&lt;/a&gt; has an MCP pay-link example, webhook receivers, and LangChain, Vercel AI SDK and Shopify samples.&lt;/li&gt;
&lt;li&gt;No account yet? The &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;testnet demo&lt;/a&gt; runs the whole flow on test networks, with no sign-up.&lt;/li&gt;
&lt;li&gt;MCP reference (tools, resources, prompts): &lt;a href="https://send21.io/docs/mcp" rel="noopener noreferrer"&gt;send21.io/docs/mcp&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions: &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by send21.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>crypto</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Address poisoning: the copy-paste habit that cost one trader 49,999,950 USDT</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:11:06 +0000</pubDate>
      <link>https://dev.to/send21/address-poisoning-the-copy-paste-habit-that-cost-one-trader-49999950-usdt-30nm</link>
      <guid>https://dev.to/send21/address-poisoning-the-copy-paste-habit-that-cost-one-trader-49999950-usdt-30nm</guid>
      <description>&lt;p&gt;&lt;em&gt;By send21 (&lt;a href="https://send21.io" rel="noopener noreferrer"&gt;send21.io&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Many crypto theft stories involve a hacked exchange or a leaked seed phrase. Address poisoning needs neither. No key is stolen and no code is exploited. The victim signs a perfectly valid transaction from their own wallet, to an address they chose themselves. It just happens to be the wrong address.&lt;/p&gt;

&lt;p&gt;This post explains how the attack works, what the research says about its scale, and the habits that make it much harder to fall for.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the attack works
&lt;/h2&gt;

&lt;p&gt;On chains like Ethereum and BNB Smart Chain, an address is a 40-character hexadecimal string. Nobody memorizes those. So, as a peer-reviewed USENIX Security 2025 paper titled "Blockchain Address Poisoning" puts it, users often select addresses from their recent transaction history. That habit is the whole attack surface.&lt;/p&gt;

&lt;p&gt;The attack runs in three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Watch.&lt;/strong&gt; The attacker monitors the chain for transfers. When you send tokens to an address, they note who you paid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate a lookalike.&lt;/strong&gt; They brute-force a new address whose first and last characters match the one you paid. Wallets often shorten addresses to something like &lt;code&gt;0xAbC12…9f3E&lt;/code&gt;, so a match on the visible ends is enough to look right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poison your history.&lt;/strong&gt; They make that lookalike address show up in your transaction list, ideally near the top.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then they wait. The next time you go to pay the same counterparty and grab the address from your history, there's a chance you grab theirs.&lt;/p&gt;

&lt;p&gt;The paper describes three ways attackers plant the lookalike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiny transfers:&lt;/strong&gt; a small amount of the same token, sent from the lookalike address to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-value transfers:&lt;/strong&gt; a transfer of zero tokens that appears to go &lt;em&gt;from you&lt;/em&gt; to the lookalike address. Popular token implementations let anyone trigger a zero-value &lt;code&gt;transferFrom&lt;/code&gt; with an arbitrary sender, so your history can show an outgoing "payment" you never made. The researchers call these potentially even more dangerous than tiny transfers, because it looks as if you've paid that address before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counterfeit token transfers:&lt;/strong&gt; a fake token with a name close to a real one, "sent" from you to the lookalike.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because blockchain transactions are irreversible, a single mistake is usually final.&lt;/p&gt;

&lt;h2&gt;
  
  
  How big is it?
&lt;/h2&gt;

&lt;p&gt;The USENIX paper measured address poisoning on Ethereum and BNB Smart Chain between 1 July 2022 and 30 June 2024. On those two chains alone, the researchers found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;over &lt;strong&gt;270 million&lt;/strong&gt; attack attempts,&lt;/li&gt;
&lt;li&gt;targeting over &lt;strong&gt;17 million&lt;/strong&gt; victims,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6,633&lt;/strong&gt; successful incidents, with at least &lt;strong&gt;$83.8 million&lt;/strong&gt; lost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The authors call their loss figure a conservative lower bound: they only tracked a handful of major stablecoins. They also note the attack can affect any blockchain whose addresses are hard for people to compare at a glance, not only hexadecimal ones.&lt;/p&gt;

&lt;p&gt;It hasn't slowed down. Blockaid reported in March 2026 that it had flagged over 65.4 million address poisoning transactions on-chain since January 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  The December 2025 case: the test transfer didn't help
&lt;/h2&gt;

&lt;p&gt;On 20 December 2025, a trader lost 49,999,950 USDT, nearly $50 million, to address poisoning. According to The Block, the trader had withdrawn the funds from Binance and was moving them to their own wallet. They did what security guides tell you to do: they sent a small test transaction first, 50 USDT, to the right address.&lt;/p&gt;

&lt;p&gt;That test is what the attacker was waiting for. An automated script generated a lookalike address that matched the intended one in its first five and last four characters. The differences were only in the middle, which many wallet interfaces hide behind an ellipsis. The attacker then sent small transactions from the lookalike to the victim's wallet. When the victim copied an address from their history for the full transfer, they likely picked the attacker's.&lt;/p&gt;

&lt;p&gt;Etherscan data cited by The Block puts the test transaction at 3:06 UTC and the big transfer about 26 minutes later, at 3:32 UTC. Within 30 minutes the attacker had swapped the USDT to DAI (which can't be frozen the way USDT can), then to ETH, and moved around 16,680 ETH into Tornado Cash.&lt;/p&gt;

&lt;p&gt;Blockaid describes this as test-transaction interception: bots watch for small "test" transfers, plant a lookalike right after, and wait for the big one. In Blockaid's words, the technique "specifically targets security-conscious users."&lt;/p&gt;

&lt;p&gt;The lesson isn't that test transfers are useless. A test checks that the address you typed works. It does nothing to check that the address you use &lt;em&gt;next&lt;/em&gt; is the same one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habits that actually help
&lt;/h2&gt;

&lt;p&gt;None of these are new, but the December case shows why they matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Never copy a payee address from your transaction history.&lt;/strong&gt; This is the attack vector. Get the address from a source you trust: your saved contacts, the recipient's own page or invoice, or direct confirmation from them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use an address book or saved contacts.&lt;/strong&gt; Save and label addresses you pay often, once, after checking them properly. Then always pay from the saved entry. The USENIX researchers list building an allow-list of trusted addresses as a user-level mitigation, and Blockaid recommends the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Verify the full address, not just the ends.&lt;/strong&gt; Attackers generate addresses that match exactly the characters people check. Compare the middle too, character by character, ideally on your hardware wallet's screen before you sign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Ignore zero-value and dust transfers.&lt;/strong&gt; An unexpected tiny transfer from an address that looks like one of your contacts is a warning sign, not a payment. A zero-value "transfer" from you that you don't remember making is almost certainly poisoning. Don't interact with it, and don't use it as a source of addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Treat a test transfer as one step, not as proof.&lt;/strong&gt; After a test, confirm with the recipient through a separate channel that it arrived. Then send the full amount to the same verified address, from your address book, not from history. Blockaid notes that a test alone is not sufficient without independent verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where pay links fit in
&lt;/h2&gt;

&lt;p&gt;We build send21: crypto pay links and an API for people and AI agents. It's relevant here because of where the address comes from, not because it makes the attack go away.&lt;/p&gt;

&lt;p&gt;Address poisoning works when the payer copies the payee's address from their own transaction history. With a pay link, the payer doesn't do that. The receiver sends a link, and the payer gets the address and amount from it as a standard QR code or payment URI (BIP-21, Solana Pay, EIP-681 or a Lightning invoice). They then sign and send from their own wallet, straight to the receiver's own address. send21 is non-custodial: it never holds private keys or funds, and never signs or broadcasts a transaction.&lt;/p&gt;

&lt;p&gt;On the receiving side, the owner decides which addresses get used. An AI agent working through the send21 API or MCP server can only &lt;em&gt;propose&lt;/em&gt; a receiving address. The owner signs in to the app, compares it character by character, and approves it with a passkey or an authenticator code. On new accounts, an API key that names any other address gets &lt;code&gt;422 address_not_approved&lt;/code&gt; and nothing is created.&lt;/p&gt;

&lt;p&gt;The limits matter just as much:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;fake link&lt;/strong&gt; sent by someone pretending to be the receiver still points to the wrong address. Only open links that came from someone you know, through a channel you trust.&lt;/li&gt;
&lt;li&gt;If the &lt;strong&gt;receiver's account is compromised&lt;/strong&gt;, the attacker could get an address of their own approved.&lt;/li&gt;
&lt;li&gt;Your wallet history can still be poisoned. A pay link only changes where the address comes from for that payment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the habit from the top of this post still applies: before you sign, check the full address in your wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Address poisoning doesn't break cryptography. It works on a habit: copying addresses from history and checking only the first and last few characters. Break that habit. Pay from saved contacts or from the recipient's own link, read the whole address, ignore dust and zero-value transfers, and don't let a successful test make you relax for the next transfer.&lt;/p&gt;

&lt;p&gt;Questions or corrections: &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>crypto</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>We built a white-label P2P money app on send21: here's how to rebrand it with one file</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Fri, 16 Oct 2026 09:31:54 +0000</pubDate>
      <link>https://dev.to/send21/we-built-a-white-label-p2p-money-app-on-send21-heres-how-to-rebrand-it-with-one-file-25f4</link>
      <guid>https://dev.to/send21/we-built-a-white-label-p2p-money-app-on-send21-heres-how-to-rebrand-it-with-one-file-25f4</guid>
      <description>&lt;p&gt;Disclosure: this is written by the team behind send21 (&lt;a href="https://send21.io" rel="noopener noreferrer"&gt;send21.io&lt;/a&gt;), and dewby is built on send21.&lt;/p&gt;

&lt;p&gt;dewby is a small P2P money app for paying friends back. It's a testnet demo: test money on test networks, nothing in it has value. It's a white-label version of our open-source example app, send21 pair: the same payments engine with its own name, logo, colors and UI. And the whole brand lives in one file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://send21io.github.io/send21-examples/dewby/" rel="noopener noreferrer"&gt;https://send21io.github.io/send21-examples/dewby/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/send21io/send21-examples/tree/main/apps/dewby" rel="noopener noreferrer"&gt;https://github.com/send21io/send21-examples/tree/main/apps/dewby&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Friends connect once by scanning a code. Then there are three things to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pay back.&lt;/strong&gt; Ask a friend for 120 in your own currency, or pay them. They get a request with the amount, a note and an &lt;strong&gt;Open wallet&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split.&lt;/strong&gt; Pick two or more friends, enter the total, and everyone gets their share as a separate request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR anyone can pay.&lt;/strong&gt; Show a QR for an amount. Whoever scans it pays from their own wallet, with no app and no account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each payment goes straight to the &lt;strong&gt;receiver's own wallet address&lt;/strong&gt;. The payer signs in their own wallet. send21 never holds keys or funds, never signs and never broadcasts, and neither does dewby.&lt;/p&gt;

&lt;p&gt;You can try it alone: tap &lt;strong&gt;Try it alone with a demo friend&lt;/strong&gt;, send the prefilled request, tap &lt;strong&gt;Let Demo friend pay&lt;/strong&gt;, and watch it go from waiting to paid. On desktop, the app sits in a phone frame next to a QR code you can scan to open it on your phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Every request is a send21 &lt;strong&gt;draft&lt;/strong&gt;: payment instructions for one amount, asset and network, addressed to the receiver. Four details carry most of the safety:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The receiver address is fixed on the server.&lt;/strong&gt; The client sends who to ask and how much. The server creates the draft with the receiver's saved address, and fails if the draft comes back with a different one. A tampered client can't change where the money goes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;V2 signed webhooks.&lt;/strong&gt; send21 reports status (&lt;code&gt;draft.seen&lt;/code&gt;, &lt;code&gt;draft.confirmed&lt;/code&gt;, …) by webhook. dewby verifies the &lt;code&gt;x-send21-signature-v2&lt;/code&gt; header: an HMAC over the timestamp, the delivery id and the raw body. It turns off the legacy body-only signature and refuses deliveries more than five minutes old, so an old delivery can't be replayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Its own dedupe, on top of send21's.&lt;/strong&gt; dewby stores each delivery id in the same transaction as the status change, so a repeated delivery is a no-op and can't confirm twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A check against the stored request.&lt;/strong&gt; On &lt;code&gt;draft.confirmed&lt;/code&gt;, dewby compares the asset, network and amount with what it stored when it created the request. If anything differs, the request goes to review instead of Paid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app is a vanilla PWA: no framework, no build step, a service worker and Web Push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three modes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mock&lt;/strong&gt;: no key and no network. It runs on a local server, or fully in the browser like the GitHub Pages demo, where links and QR codes carry the data between phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;demo&lt;/strong&gt;: real testnet drafts from send21's public demo API, with status checked by polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;api&lt;/strong&gt;: your own scoped send21 key and webhooks. send21 accounts use live networks, so this mode refuses to start unless you set &lt;code&gt;APP_ALLOW_LIVE=1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rebrand it with one file
&lt;/h2&gt;

&lt;p&gt;Everything brand-specific is in &lt;code&gt;web/brand.js&lt;/code&gt;:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BRAND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dewby&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// UI name and wallet memo prefix&lt;/span&gt;
  &lt;span class="na"&gt;tagline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pay friends back. Fresh.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storagePrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dewby&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// keeps two apps on one origin apart&lt;/span&gt;
  &lt;span class="na"&gt;siteUrl&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://…/dewby/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// share card URLs&lt;/span&gt;
  &lt;span class="na"&gt;poweredBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// a small "powered by send21" line in Settings&lt;/span&gt;
  &lt;span class="na"&gt;light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;forcedDark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;             &lt;span class="c1"&gt;// palette for browsers that force dark mode&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;theme.js&lt;/code&gt; turns the palette into CSS variables before first paint, so the stylesheet never hard-codes a brand color. The server reads the same file, so the memo in the payer's wallet shows your app's name. To rebrand:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Edit &lt;code&gt;web/brand.js&lt;/code&gt; and swap &lt;code&gt;web/icons/mark.svg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm run brand&lt;/code&gt;. It rewrites the manifest, title, theme color and share-card meta, and redraws the icons from your mark.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm test&lt;/code&gt;. It checks every text color for WCAG AA contrast, that the manifest and HTML match the brand file, the share card size, and that no stray send21 branding is left in the UI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson: keep the brand as data, and let tests check what's easy to forget when you rebrand, like contrast, storage keys and leftover strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testnet only.&lt;/strong&gt; The demo uses Solana Devnet and Base Sepolia with test tokens. Nothing has value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A prototype.&lt;/strong&gt; It's an example to read and fork, not a product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One extra tap on phones.&lt;/strong&gt; A notification can open a web page but not a wallet, so the payer taps &lt;strong&gt;Open wallet&lt;/strong&gt; to sign.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try the demo friend, then open &lt;code&gt;brand.js&lt;/code&gt; and change the colors. Questions or ideas: &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>pwa</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Let your agent get paid without holding keys</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Thu, 15 Oct 2026 19:42:47 +0000</pubDate>
      <link>https://dev.to/send21/let-your-agent-get-paid-without-holding-keys-8b2</link>
      <guid>https://dev.to/send21/let-your-agent-get-paid-without-holding-keys-8b2</guid>
      <description>&lt;p&gt;Sooner or later an agent you build has to deal with money. A booking agent hits a deposit. A freelancing assistant needs to send an invoice. A support bot has to collect a fee before it ships a replacement part.&lt;/p&gt;

&lt;p&gt;The quick answer is to hand the agent a wallet. Fund it, put the private key in an environment variable, let the model call a &lt;code&gt;send&lt;/code&gt; tool. That works in a demo. In production it means a single prompt injection, a confused tool call or a leaked log line can empty the wallet, and nothing in the stack can tell a legitimate payment from a manipulated one, because the agent is the one signing.&lt;/p&gt;

&lt;p&gt;There is a simpler split that removes most of that risk: the agent prepares the payment, and a human signs it in their own wallet. This article shows how to do that with send21, a non-custodial service that prepares payment instructions, over its MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split: preparing is not paying
&lt;/h2&gt;

&lt;p&gt;A payment has two halves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preparing&lt;/strong&gt;: who gets paid, how much, in which asset and network, with what reference, and at which exchange rate if the price is in euros or dollars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorising&lt;/strong&gt;: signing a transaction with a private key and broadcasting it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Agents are good at the first half. It is structured data, it benefits from context, and mistakes are cheap if someone checks before signing. The second half is where the risk lives, and it is exactly the part an agent does not need.&lt;/p&gt;

&lt;p&gt;send21 only does the first half. It never holds keys or funds, never signs and never broadcasts. Funds go straight from the payer's wallet to the receiver's wallet. Since there is no signing code on the server, an API key you give an agent cannot move money, however it is misused. The worst an abused key can do is create payment instructions that a human then has to approve in their own wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent can do
&lt;/h2&gt;

&lt;p&gt;The send21 MCP server runs at &lt;code&gt;https://send21.io/mcp&lt;/code&gt; over Streamable HTTP and is listed in the official MCP Registry as &lt;code&gt;io.github.send21io/mcp&lt;/code&gt;. It exposes 11 tools, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;create_payment_request&lt;/code&gt;: create a pay link where you are the receiver. Price it in a fiat currency, list the assets you accept with your own receiving addresses, and get back a link.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_payment_request&lt;/code&gt;: read its status (Open, Completed, Cancelled, Expired).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_draft&lt;/code&gt; and &lt;code&gt;get_transaction_template&lt;/code&gt;: the other direction, where the owner pays someone. The template gives the wallet everything it needs to fund, review and sign.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_rate&lt;/code&gt; and &lt;code&gt;get_fee_schedule&lt;/code&gt;: rates with their source, and the public fee table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Listing the tools and reading the fee schedule work without a key. Everything else needs a scoped API key that the account owner creates. For an agent that only issues pay links, &lt;code&gt;drafts:write&lt;/code&gt; plus &lt;code&gt;drafts:read&lt;/code&gt; is enough.&lt;/p&gt;

&lt;p&gt;Supported today: BTC on-chain and over Lightning (paid to the receiver's own Lightning address), USDC on Solana, Ethereum, Base, Arbitrum and Polygon, USDT on Solana and Ethereum, EURC on Solana, Ethereum and Base, plus native SOL and ETH. Prices can be set in 161 fiat currencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect it in one block
&lt;/h2&gt;

&lt;p&gt;In Cursor, &lt;code&gt;.cursor/mcp.json&lt;/code&gt; from the send21-examples repo looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"send21"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://send21.io/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ${env:SEND21_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Desktop needs a stdio bridge, so the example uses &lt;code&gt;mcp-remote&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"send21"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://send21.io/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="s2"&gt;"--header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authorization:${SEND21_AUTH_HEADER}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"SEND21_AUTH_HEADER"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer s21_your_key_here"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask: "Create a pay link for 40 EUR for invoice 1042, payable in USDC on Solana to my address." The model fills in &lt;code&gt;create_payment_request&lt;/code&gt; and hands you a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it from code
&lt;/h2&gt;

&lt;p&gt;Inside your own agent you can call the same tool with the MCP TypeScript SDK. This is trimmed from &lt;code&gt;examples/mcp-payment-request&lt;/code&gt; in the examples repo:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&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;node:crypto&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;Client&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;@modelcontextprotocol/sdk/client/index.js&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;StreamableHTTPClientTransport&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;@modelcontextprotocol/sdk/client/streamableHttp.js&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&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;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&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://send21.io/mcp&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;requestInit&lt;/span&gt;&lt;span class="p"&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &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;SEND21_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transport&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;result&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create_payment_request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fiatCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fiatAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;options&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="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Usdc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Solana&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;address&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;MY_SOLANA_ADDRESS&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Btc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Mainnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name@wallet.com&lt;/span&gt;&lt;span class="dl"&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;Lightning&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;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inv-1042&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Logo design, round 2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response carries an &lt;code&gt;id&lt;/code&gt;, a &lt;code&gt;payPath&lt;/code&gt; and an expiry. Join &lt;code&gt;payPath&lt;/code&gt; with &lt;code&gt;https://send21.io&lt;/code&gt; and send it to the payer. They open the page, pick a currency (the rate locks for 15 minutes at that point), scan one QR code with any wallet and sign.&lt;/p&gt;

&lt;p&gt;The example also has a dry-run mode that needs no key. It connects, fetches the live tool list and checks your arguments against the published input schema without creating anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dry-run &lt;span class="nt"&gt;--&lt;/span&gt; 25 EUR &lt;span class="nt"&gt;--option&lt;/span&gt; Usdc:Solana:&amp;lt;your-solana-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a cheap way to catch a wrong enum value before an agent does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three details that matter for agents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fix the addresses in code, not in the prompt.&lt;/strong&gt; The receiving address is the one field an attacker would want to change. If your app passes its own addresses into &lt;code&gt;options&lt;/code&gt; and ignores whatever the model suggests, a prompt injection can at worst change the amount or memo on a link that a human still has to pay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries are safe.&lt;/strong&gt; Agents retry. Every create call can carry an idempotency key: the same key with the same parameters returns the original request, and the same key with a different amount, currency or address is refused with a 422 and creates nothing. If your agent might retry with a new key, set &lt;code&gt;reuseOpenForOrder: true&lt;/code&gt; with an &lt;code&gt;orderId&lt;/code&gt;, and you get the existing open link back instead of a second one for the same order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Say what happens if nobody pays.&lt;/strong&gt; When an agent hits a purchase over its budget and hands it to a human, the pay link can carry a &lt;code&gt;declineNote&lt;/code&gt;, shown on the pay page as "If you don't pay: the booking is released at 18:00." The human gets the context the agent had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing when it was paid
&lt;/h2&gt;

&lt;p&gt;You do not need to poll. send21 sends HMAC-signed webhooks with your &lt;code&gt;orderId&lt;/code&gt;, for example &lt;code&gt;draft.confirmed&lt;/code&gt; when the payment has its confirmations. Verify the &lt;code&gt;X-Send21-Signature-V2&lt;/code&gt; header: &lt;code&gt;t=&amp;lt;timestamp&amp;gt;,v1=&amp;lt;hex&amp;gt;&lt;/code&gt;, an HMAC-SHA256 of &lt;code&gt;"&amp;lt;timestamp&amp;gt;.&amp;lt;delivery id&amp;gt;.&amp;lt;raw body&amp;gt;"&lt;/code&gt; keyed with your signing secret. Because the time and the delivery id are signed too, refuse anything older than 5 minutes and a copied delivery cannot be replayed. The webhook receiver in the examples repo does this in constant time:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingSafeEqual&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;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifySignatureV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deliveryId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;deliveryId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;deliveryId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&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="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;??&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// older than 5 minutes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;deliveryId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;rawBody&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&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;received&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original &lt;code&gt;X-Send21-Signature&lt;/code&gt; (HMAC of the raw body only, no timestamp) is still sent so existing integrations keep working; use it only as a fallback when no V2 header is present. The webhook guide at send21.io/docs/webhooks describes both. Deliveries are retried with the same &lt;code&gt;X-Send21-Delivery&lt;/code&gt; id, so skip one you have already handled.&lt;/p&gt;

&lt;p&gt;Real payments are messy, and the events reflect that. A transfer from an exchange often arrives a little short because the exchange took its withdrawal fee. When a transfer within 10% of the billed amount reaches the address, send21 does not mark it paid; it sends &lt;code&gt;draft.amount_mismatch&lt;/code&gt; and the receiver accepts or declines it, with a reason. Your agent can surface that to a human instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a human sees before signing
&lt;/h2&gt;

&lt;p&gt;The person who signs is the safety net, so the pay page tries to make the unusual visible. It shows a short "what changed" line only when something is out of pattern, such as a first payment to an address or an amount about three times the usual. Agents get the same signals through &lt;code&gt;get_payment_request&lt;/code&gt; as &lt;code&gt;changeSignals&lt;/code&gt; and &lt;code&gt;changeNotes&lt;/code&gt;, so an agent can stop and ask before forwarding a link that looks off. Addresses are shown with the characters a look-alike address would have to copy highlighted, so the payer can compare them with their wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;To be precise about the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is not an agent wallet. An agent cannot sign or pay on its own through send21.&lt;/li&gt;
&lt;li&gt;There is no escrow and no "pay now, release later". Money moves once, wallet to wallet.&lt;/li&gt;
&lt;li&gt;There are no payouts to bank accounts and no conversion to fiat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need fully autonomous spending, this is the wrong tool. If you want an agent that does the paperwork while a human keeps the keys, it fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Fees are 0.49% on small payments, falling by size to 0.01%, capped at $100, and the receiver always gets the exact amount. You can try a real payment with test coins at &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;https://send21.io/demo&lt;/a&gt; without an account or email, and the configs and examples above are in &lt;a href="https://github.com/send21io/send21-examples" rel="noopener noreferrer"&gt;https://github.com/send21io/send21-examples&lt;/a&gt;. MCP setup details are at &lt;a href="https://send21.io/docs/mcp" rel="noopener noreferrer"&gt;https://send21.io/docs/mcp&lt;/a&gt;. Questions go to &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>webdev</category>
      <category>crypto</category>
    </item>
    <item>
      <title>What should an AI agent do when it hits its budget?</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Sat, 10 Oct 2026 18:26:28 +0000</pubDate>
      <link>https://dev.to/send21/what-should-an-ai-agent-do-when-it-hits-its-budget-3c1a</link>
      <guid>https://dev.to/send21/what-should-an-ai-agent-do-when-it-hits-its-budget-3c1a</guid>
      <description>&lt;p&gt;Give an agent a budget and sooner or later it reaches the edge of it. A research agent has spent 4.20 of its 5.00 USD cap, and the next step (one more paid API call, a dataset, a GPU minute) costs 1.50. What should happen now?&lt;/p&gt;

&lt;p&gt;This post looks at three options and shows one way to build the third, with a small open-source example you can run offline.&lt;/p&gt;

&lt;p&gt;Disclosure: I'm with send21. The example uses send21 to prepare a payment request, but the pattern works with any approval channel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58vetgi58f5agzb0c5sz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58vetgi58f5agzb0c5sz.gif" alt="Over-budget handoff in mock mode" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Spend caps exist because agents are not good judges of when something is worth paying for. A loop, a bad plan, or a prompt injection can turn "one more call" into a large bill. So we put a hard number in front of the agent.&lt;/p&gt;

&lt;p&gt;The cap is the easy part. The hard part is what the agent does when the cap says no, because that is exactly the moment when the task is often nearly done and the extra spend is often justified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: raise an error
&lt;/h2&gt;

&lt;p&gt;The simplest thing: throw, log, stop.&lt;/p&gt;

&lt;p&gt;It is safe. Nothing gets spent. But the run is lost, the context is gone, and someone has to notice the failure, read the logs, bump a config value and start again. In practice people respond by setting caps high enough that they never trigger, which defeats the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: overspend
&lt;/h2&gt;

&lt;p&gt;Let the agent go over "just this once", maybe with a soft limit and an alert.&lt;/p&gt;

&lt;p&gt;This keeps the task alive, but now the cap is a suggestion. If the agent can decide to exceed it, so can a buggy plan or an injected instruction. Any limit the agent can talk itself past is not a limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: hand off to a human
&lt;/h2&gt;

&lt;p&gt;The agent stops at the cap, explains what it needs and why, and waits. A human decides. If they approve and pay, the agent continues from where it paused, with its state intact.&lt;/p&gt;

&lt;p&gt;This keeps the cap hard and the task alive. It needs three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A guard that says "ok" or "handoff" before each paid step.&lt;/li&gt;
&lt;li&gt;A way to ask a human for exactly the shortfall, with a link they can act on.&lt;/li&gt;
&lt;li&gt;A way to resume the paused run safely when the human has acted.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Building option 3
&lt;/h2&gt;

&lt;p&gt;The example repo is &lt;a href="https://github.com/send21io/send21-over-budget-handoff" rel="noopener noreferrer"&gt;send21-over-budget-handoff&lt;/a&gt;: Python, MIT, a LangGraph adapter, an offline mock mode and 25 tests. It is an example, not an official LangGraph integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The guard
&lt;/h3&gt;

&lt;p&gt;The cap check is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BudgetGuard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
    &lt;span class="n"&gt;spent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handoff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handoff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4.20 + 1.50 &amp;gt; 5.00, so the step does not run.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The ask
&lt;/h3&gt;

&lt;p&gt;On "handoff", the agent calls the send21 MCP tool &lt;code&gt;create_payment_request&lt;/code&gt; with an amount, a payee address, a memo and an &lt;code&gt;orderId&lt;/code&gt;. It gets back an &lt;code&gt;id&lt;/code&gt; and a &lt;code&gt;payPath&lt;/code&gt;, prints the pay link, and pauses with LangGraph's &lt;code&gt;interrupt()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pay_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pay_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="c1"&gt;# Resumed by a verified draft.confirmed. Raise the cap by the confirmed amount.
&lt;/span&gt;    &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fiatAmount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handoff_step&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is what the agent's key can do. It is created with the &lt;code&gt;drafts:write&lt;/code&gt; scope only. send21 prepares payment instructions; the human opens the link and signs in their own wallet, and funds go straight from the payer's wallet to the receiver's wallet. send21 never holds keys or funds, and the agent's key cannot move funds. There is no signing code and no private key in the repo. The only way past the cap is a human signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The resume
&lt;/h3&gt;

&lt;p&gt;When the payment is confirmed, send21 sends an HMAC-signed &lt;code&gt;draft.confirmed&lt;/code&gt; webhook. The receiver checks &lt;code&gt;X-Send21-Signature-V2&lt;/code&gt; in constant time before parsing anything. It is an HMAC-SHA256 of &lt;code&gt;"&amp;lt;timestamp&amp;gt;.&amp;lt;delivery id&amp;gt;.&amp;lt;raw body&amp;gt;"&lt;/code&gt;, so the time and the delivery id are signed too: deliveries older than 5 minutes are refused and a copied delivery cannot be replayed. The legacy &lt;code&gt;X-Send21-Signature&lt;/code&gt; (raw body only, no timestamp) is still sent for compatibility and is used only when no V2 header is present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_signature_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# t=&amp;lt;timestamp&amp;gt;,v1=&amp;lt;hex&amp;gt;
&lt;/span&gt;    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# refuse &amp;gt; 5 minutes old
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delivery_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_delivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Send21-Signature-V2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# V2 first; no fallback when a V2 header is present
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;verify_signature_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Send21-Delivery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;secret&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;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Send21-Signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# legacy fallback
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it stores the delivery id so retries do nothing, matches &lt;code&gt;orderId&lt;/code&gt; to the paused run, and resumes it with &lt;code&gt;Command(resume=...)&lt;/code&gt;. Run status only moves forward, so a late or out-of-order event cannot undo a resume. A &lt;code&gt;draft.amount_mismatch&lt;/code&gt; event marks the run for review instead of resuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without paying anything
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.11 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
python scripts/run_demo.py &lt;span class="nt"&gt;--mock&lt;/span&gt;
pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mock mode uses a fake client and a locally signed fake webhook, so it needs no network and no account. The output is the same every run, and it is what the GIF above shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using another framework
&lt;/h2&gt;

&lt;p&gt;The core in &lt;code&gt;handoff/&lt;/code&gt; has no LangGraph imports. To port it, keep the guard, the client and the webhook receiver, and replace the adapter: pause at the handoff, and call &lt;code&gt;resume(run_id, event_data)&lt;/code&gt; when the webhook arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still open
&lt;/h2&gt;

&lt;p&gt;The README lists what is not yet verified against the live API, for example testnet support for API keys and the exact &lt;code&gt;payPath&lt;/code&gt; format. Live mode keeps checkpoints in memory, so use a persistent checkpointer for anything beyond a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A spend cap is only useful if hitting it is a normal, recoverable event. Erroring throws work away, overspending makes the cap meaningless. Handing off keeps the cap hard and puts the decision with a person, who approves with a signature the agent cannot forge.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/send21io/send21-over-budget-handoff" rel="noopener noreferrer"&gt;https://github.com/send21io/send21-over-budget-handoff&lt;/a&gt;&lt;br&gt;
More examples: &lt;a href="https://github.com/send21io/send21-examples" rel="noopener noreferrer"&gt;https://github.com/send21io/send21-examples&lt;/a&gt;&lt;br&gt;
send21 MCP: &lt;a href="https://send21.io/mcp" rel="noopener noreferrer"&gt;https://send21.io/mcp&lt;/a&gt;&lt;br&gt;
Questions: &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>crypto</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to invoice in EUR or SEK and get paid in EURC to your own wallet</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Tue, 06 Oct 2026 09:36:24 +0000</pubDate>
      <link>https://dev.to/send21/how-to-invoice-in-eur-or-sek-and-get-paid-in-eurc-to-your-own-wallet-59cc</link>
      <guid>https://dev.to/send21/how-to-invoice-in-eur-or-sek-and-get-paid-in-eurc-to-your-own-wallet-59cc</guid>
      <description>&lt;p&gt;Disclosure: this is written by the team behind send21.&lt;/p&gt;

&lt;p&gt;You have a client in another country who is happy to pay in stablecoins. You invoice in EUR or SEK, because that is what your books are kept in. Then the questions start. Which rate turns the invoice into a stablecoin amount? What exactly should the client send? What if the amount that arrives is a little short?&lt;/p&gt;

&lt;p&gt;This guide shows how to handle that with send21, non-custodial software that creates pay links and prepares payment instructions. The payer signs in their own wallet, and the funds go from their wallet straight to yours. send21 never holds keys or funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Card and bank fees on cross-border payments add up. Stablecoins avoid some of that, but bring bookkeeping questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The invoice says 10 000 SEK. The client pays in EURC. What rate was used?&lt;/li&gt;
&lt;li&gt;When was that rate taken, and from which source?&lt;/li&gt;
&lt;li&gt;Which document goes into your books?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A wallet address pasted into an email answers none of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A self-custody wallet that can receive EURC or USDC on Solana, Ethereum or Base.&lt;/li&gt;
&lt;li&gt;A send21 account. You sign in with a passkey, or with email and password plus a second factor.&lt;/li&gt;
&lt;li&gt;A client who can pay in EURC or USDC from a wallet or an exchange.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see the whole flow first, the testnet demo at &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;send21.io/demo&lt;/a&gt; runs a real payment with test coins. No account and no email needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by step: a pay link priced in EUR or SEK
&lt;/h2&gt;

&lt;p&gt;New accounts start on a "Get paid" screen. It helps you create your first pay link. The steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start a new payment request from the "Get paid" screen.&lt;/li&gt;
&lt;li&gt;Set the price in your invoice currency, such as EUR or SEK. Prices work in 161 fiat currencies.&lt;/li&gt;
&lt;li&gt;Choose where you want to be paid: your own wallet, on the network you use for EURC or USDC.&lt;/li&gt;
&lt;li&gt;Create the link and send it to your client together with your invoice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For clients in the EU, EURC is the natural first choice, with USDC as the alternative. EURC and USDC both work on Solana, Ethereum and Base.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your client sees
&lt;/h2&gt;

&lt;p&gt;The client opens the link and picks a currency. They scan one QR code and pay from any wallet. They do not need a special wallet or a send21 account.&lt;/p&gt;

&lt;p&gt;The client signs the payment in their own wallet. send21 does not sign anything and does not broadcast anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rate is locked and recorded
&lt;/h2&gt;

&lt;p&gt;The fiat price is converted at an exchange rate that is locked for 15 minutes. send21 records the rate source and the time.&lt;/p&gt;

&lt;p&gt;send21 also creates a branded PDF with the payment details. Keep it with your invoice. Rules for recording stablecoin payments differ between countries, so check your bookkeeping with your accountant.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an exchange withdrawal arrives a bit short
&lt;/h2&gt;

&lt;p&gt;Exchanges often deduct a withdrawal fee from the amount sent. Your invoice says one amount and slightly less arrives.&lt;/p&gt;

&lt;p&gt;send21 flags this. When a payment within 10% of the billed amount reaches your receiving address after the pay link was created, send21 alerts you by email, in the app and by webhook. You decide what happens next. You can accept it as paid with one click, or ask the client for the difference. The client sees a notice not to pay again, so they do not pay twice.&lt;/p&gt;

&lt;p&gt;This has limits. A payment more than 10% off is not flagged, and neither is one that could belong to several open payments on the same address. Then you check your wallet yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;The fee is 0.49% on payments under $10, falling by tier to 0.01% above $100,000. The minimum is $0.01 and the maximum is $100 per payment. On pay links the fee is billed to your account and settled later in one payment. It is never taken out of the client's payment, so you receive the exact amount. There is no subscription, no setup fee and no exchange-rate spread. The full schedule is at &lt;a href="https://send21.io/pricing" rel="noopener noreferrer"&gt;send21.io/pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For developers
&lt;/h2&gt;

&lt;p&gt;If you build tools for freelancers, the same flow is available in code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP server&lt;/strong&gt; at &lt;code&gt;https://send21.io/mcp&lt;/code&gt; (Streamable HTTP), with tools for drafts, payment requests, rates and fees. An AI agent can create and track a pay link. It cannot pay, because no key send21 issues can move funds. A human signs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST API&lt;/strong&gt; with scoped keys, idempotency keys and batch creation of up to 25 per call. Docs are at &lt;a href="https://send21.io/swagger" rel="noopener noreferrer"&gt;send21.io/swagger&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; signed with HMAC, so your backend can verify that an event came from send21 before marking an invoice as paid.&lt;/li&gt;
&lt;li&gt;A machine-readable overview at &lt;a href="https://send21.io/llms.txt" rel="noopener noreferrer"&gt;send21.io/llms.txt&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public examples repo at &lt;a href="https://github.com/send21io/send21-examples" rel="noopener noreferrer"&gt;github.com/send21io/send21-examples&lt;/a&gt; has a script that creates a pay link through MCP and a webhook receiver that verifies the signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;Know what send21 does not do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No payouts to bank accounts and no conversion to fiat. You receive stablecoins in your own wallet.&lt;/li&gt;
&lt;li&gt;A human always signs. Neither send21 nor an agent using its API can pay on anyone's behalf.&lt;/li&gt;
&lt;li&gt;Payments that are more than 10% off, or that could match several open payments, are not flagged and need a manual check.&lt;/li&gt;
&lt;li&gt;This guide covers EURC and USDC, the stablecoins we suggest for EU clients.&lt;/li&gt;
&lt;li&gt;It is for people who are comfortable holding stablecoins and looking after their own wallet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Run a test payment in the demo at &lt;a href="https://send21.io/demo" rel="noopener noreferrer"&gt;send21.io/demo&lt;/a&gt;. No account needed. Then create a real pay link from the "Get paid" screen for your next invoice.&lt;/p&gt;

&lt;p&gt;Questions, or a workflow we do not cover yet? Write to &lt;a href="mailto:info@send21.io"&gt;info@send21.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>crypto</category>
      <category>payments</category>
      <category>webdev</category>
    </item>
    <item>
      <title>An agent creates a payment draft; a human signs</title>
      <dc:creator>send21</dc:creator>
      <pubDate>Sun, 06 Sep 2026 05:26:25 +0000</pubDate>
      <link>https://dev.to/send21/an-agent-creates-a-payment-draft-a-human-signs-2oe7</link>
      <guid>https://dev.to/send21/an-agent-creates-a-payment-draft-a-human-signs-2oe7</guid>
      <description>&lt;p&gt;If you are building an AI agent that needs to move money, you have a custody problem the moment the agent can sign.&lt;/p&gt;

&lt;p&gt;That is the quiet part of most "agent payments" demos. The agent can create an invoice. The agent can call an API. The agent can even assemble a transaction. Then someone asks the only question that matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do the keys live?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "in the agent," you did not build a payment agent. You built a hot wallet with a chat UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Agents are good at structured work: create a draft, attach a memo, lock a rate, wait for a webhook, book the order. They are terrible at being custodians.&lt;/p&gt;

&lt;p&gt;A model can be jailbroken. A tool loop can be tricked into calling the wrong endpoint. A compromised server can dump process memory. A "scoped assistant" that still holds a seed is not scoped at all — it is a custodian that also writes poetry.&lt;/p&gt;

&lt;p&gt;So the product question is not "can an agent pay?"&lt;/p&gt;

&lt;p&gt;The product question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an agent prepare a payment without ever being able to spend?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That single constraint changes the architecture. It also changes what you are allowed to claim in marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong patterns (keys in the agent)
&lt;/h2&gt;

&lt;p&gt;A few patterns show up again and again.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Seed or private key in the agent runtime
&lt;/h3&gt;

&lt;p&gt;The agent "pays" by signing. It is convenient in a demo. It is catastrophic in production. Anyone who can prompt the model, call its tools, or exfiltrate the process can spend.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hot wallet behind the agent with broad spend rights
&lt;/h3&gt;

&lt;p&gt;Same failure mode with an extra hop. The agent does not hold the key itself; it holds the ability to move funds. That is still custody by another name.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. "The agent pays by itself" as a feature
&lt;/h3&gt;

&lt;p&gt;Auto-debit sounds magical until you need an audit trail, a human review step, or a recovery story after a bad tool call. If the agent can broadcast, the blast radius is the balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Treating "non-custodial" as a promise
&lt;/h3&gt;

&lt;p&gt;Marketing that says "we never steal your funds" while the server can sign is a &lt;strong&gt;promise&lt;/strong&gt;. Structure would mean the server cannot sign — even when compromised, even when an engineer is wrong, even when an agent is clever.&lt;/p&gt;

&lt;p&gt;If your architecture needs the agent to hold keys, sign, or broadcast, you are not solving agent payments. You are moving a wallet into a less trustworthy process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right pattern: instructions only
&lt;/h2&gt;

&lt;p&gt;A cleaner split looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An agent (or a human) creates a &lt;strong&gt;payment draft&lt;/strong&gt; — receiver address, amount (exact or fiat-pegged), network, memo.&lt;/li&gt;
&lt;li&gt;The sender's own wallet funds and signs.&lt;/li&gt;
&lt;li&gt;The sender's own node or wallet broadcasts.&lt;/li&gt;
&lt;li&gt;Your backend watches the chain and fires signed webhooks when the payment is seen and confirmed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In that model, software prepares &lt;strong&gt;instructions&lt;/strong&gt;. Wallets do the paying.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://send21.io" rel="noopener noreferrer"&gt;send21&lt;/a&gt; is built for.&lt;/p&gt;

&lt;p&gt;send21 creates clean, verifiable payment instructions for BTC, USDC, USDT, and EURC. Funds move from the sender's own wallet to the receiver.&lt;/p&gt;

&lt;p&gt;We never touch keys. We never touch coins.&lt;/p&gt;

&lt;p&gt;Non-custodial by structure, not by promise: no hot wallets, no private keys stored, no signing code in the product path. A full server compromise cannot move a single satoshi, because there is nothing to move with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoped API keys that cannot spend
&lt;/h2&gt;

&lt;p&gt;Agents talk to send21 over REST — the same surface a human uses in the UI. Everything a human does in the product, an agent can do with an API key. The difference is what that key is allowed to do.&lt;/p&gt;

&lt;p&gt;Grant only what the agent needs, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;drafts:write&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;drafts:read&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webhooks:manage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No scope can ever move funds.&lt;/strong&gt; Nobody can. That is the point.&lt;/p&gt;

&lt;p&gt;So the agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a draft&lt;/li&gt;
&lt;li&gt;fetch a transaction template&lt;/li&gt;
&lt;li&gt;track status&lt;/li&gt;
&lt;li&gt;receive &lt;code&gt;draft.seen&lt;/code&gt; / &lt;code&gt;draft.confirmed&lt;/code&gt; webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sign&lt;/li&gt;
&lt;li&gt;broadcast&lt;/li&gt;
&lt;li&gt;empty a wallet&lt;/li&gt;
&lt;li&gt;"just send it"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human — or the wallet / HSM that holds the keys — remains the only party that can authorize value transfer.&lt;/p&gt;

&lt;p&gt;A typical flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent → create draft → fetch tx template → fund &amp;amp; sign in own wallet → broadcast → webhook: draft.confirmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Idempotent creation (retry-safe &lt;code&gt;Idempotency-Key&lt;/code&gt;), locked rates for fiat-priced drafts, and HMAC-SHA256 signed webhooks make that loop machine-readable without turning the agent into a custodian.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment drafts vs pay links
&lt;/h2&gt;

&lt;p&gt;Same non-custodial idea. Different wallet shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payment drafts (Bitcoin multi-output / PSBT path)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Need a PSBT-capable wallet such as Bitcoin Core, Sparrow, or Electrum. The sender reviews every output — including any transparent platform fee output — then signs locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkout pay links / QR&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Single-transfer pay links with a standard QR (&lt;code&gt;bitcoin:&lt;/code&gt; / Solana / Ethereum URI). Any wallet that can scan a QR and send an exact amount works. Useful for shops and "any screen" checkouts — phone, tablet, kiosk — without a branded terminal in the middle.&lt;/p&gt;

&lt;p&gt;Either way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;instructions first&lt;/li&gt;
&lt;li&gt;signature in the user's wallet&lt;/li&gt;
&lt;li&gt;confirmation on-chain&lt;/li&gt;
&lt;li&gt;webhook to book the order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents that prepare &lt;strong&gt;drafts&lt;/strong&gt; stay on the draft API with spend-impossible scopes. Shop and stall checkouts use &lt;strong&gt;payment requests / pay links / QR&lt;/strong&gt;. Do not collapse those into "the agent paid."&lt;/p&gt;

&lt;h2&gt;
  
  
  When send21 fits
&lt;/h2&gt;

&lt;p&gt;send21 fits when you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agents that prepare payments without spending rights&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humans in the loop&lt;/strong&gt; for signing and broadcast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fiat-priced drafts or pay links&lt;/strong&gt; with a locked rate, paid in BTC / USDC / USDT / EURC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct wallet-to-wallet settlement&lt;/strong&gt; with no platform custody&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook-first reconciliation&lt;/strong&gt; for shops, agents, and backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is less of a fit when you need a full self-hosted store stack, Lightning as a processor product, fiat payout rails, or an agent that auto-debits without a human signature. Those are different problems — and some of them already have strong tools. Use those tools when they fit. send21 is for the slice where the agent must prepare money movement without ever being able to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;An agent creates a payment draft. A human signs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is how you get automation without handing the keys to the model.&lt;/p&gt;

&lt;p&gt;Direct. Non-custodial. Yours.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://send21.io" rel="noopener noreferrer"&gt;https://send21.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>crypto</category>
      <category>bitcoin</category>
    </item>
  </channel>
</rss>
