<?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>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, 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>langgraph</category>
      <category>payments</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>bitcoin</category>
    </item>
  </channel>
</rss>
