All skills

ROZO Checkout

Community builtSKILL.md

Pay for AI services with Stellar USDC — e.g.

Install
npx skills add https://github.com/RozoAI/rozo-checkout-skill
About

>

Links
Tags
rozo-checkout · community · RozoAI
SKILL.md

Pay a Coinbase Payment Link with Stellar, Solana, BNB Chain, Bitcoin and more

What this is

A Coinbase Payment Link only accepts USDC on Base. This skill lets the caller pay one with something else:

You holdChain
USDT or USDCSolana, BNB Chain, Ethereum, Polygon
USDCBase, Stellar
BTCLightning (BOLT11) — any wallet that pays an invoice works, including Cashu/ecash wallets that melt to Lightning

Native SOL, native BNB, native ETH and on-chain BTC are not supported — always say "USDT on Solana", never "SOL".

How it works: the router quotes the Coinbase link, creates a one-time bridge order with a deposit address for the coin you chose, and once your deposit lands, a funder wallet pays the Coinbase invoice. There is no discount — callerPays equals the invoice amount. If a response ever shows a discount, or callerPays differs from the invoice, stop and explain; do not proceed.

Bitrefill invoice

Supported targets are Coinbase payment links and Bitrefill invoices. Stripe checkout is not supported by this skill.

  1. The user (or their agent, via Bitrefill's site or MCP) creates a Bitrefill invoice and chooses USDC on Base as the payment method. Bitrefill shows an invoice id, a Base receiving address (0x…), an exact USDC amount and an expiry.

  2. Run, with the coin the user actually holds:

    npx @rozoai/checkout pay --bitrefill-invoice <id> --to <0x…> --amount <USDC> \
      --expires-at <ISO> --with usdc-stellar      # or --from stellar-usdc, usdt-solana, …
    

    There is no quote step: mpprouter creates a Rozo exactOut order delivering exactly that USDC amount to the Bitrefill address (the skill never writes to payment-api directly). The deposit you send is that amount plus bridge fees.

  3. Rails, all hard aborts: the address must be a 0x EVM address, the amount a positive USDC decimal (≤ 6 dp), --expires-at is required (the invoice expiry, ≤ 30 min out); at least 5 minutes must remain to create an order and 2 minutes to show the deposit or send (the invoice expiry is the only clock used), and neither the Bitrefill address nor the deposit address may be on the compromised-address list. The router's response and the live order must echo the same invoice id, address and amount (BITREFILL_ECHO_MISMATCH otherwise) before any deposit is shown or any send happens. Re-running for the same invoice resumes its order (router DUPLICATE_INVOICE).

  4. Same binding confirmation and --send rules as a Coinbase link. Status: rozo-checkout status <rozoPaymentId> reports settled once the Rozo payout to the Bitrefill address completes (payment_payout_completed, or payment_completed with a payout tx hash); there is no Coinbase stage. Works without a local record (provider read from the order).

Router errors to relay as-is: BITREFILL_DISABLED, INVALID_ADDRESS, INVALID_AMOUNT, AMOUNT_OUT_OF_RANGE, INVOICE_EXPIRING, BLOCKED_ADDRESS, UNSUPPORTED_SOURCE, INVALID_INPUT (400), and BITREFILL_NOT_CONFIGURED / RETRY_LATER (503: nothing was created, retry later).

OpenRouter top-up — when there is no link yet

OpenRouter has no public API for creating crypto credit checkouts (the old POST /api/v1/credits/coinbase returns HTTP 410 Gone). The link can only be created inside the user's own logged-in OpenRouter session. Two ways to get it:

Default — the user creates it (works everywhere)

Give these steps and wait for the URL:

1. Open https://openrouter.ai/settings/credits
2. Enable "Use crypto".
3. Enter the credit amount and click "Purchase".
4. Copy the resulting payments.coinbase.com/payment-sessions/paymentSession_*
   URL and paste it here.

Then continue with the normal flow at Step 1 (quote).

Accelerator — only when browser control already exists

If, and only if, this session already has a working browser-control capability and that browser is already signed in to OpenRouter, you may offer to click through yourself. Never tell the user to install a browser, an extension, or a plugin to unlock this — when the capability is absent, give the default steps immediately and say nothing about automation.

Ask first, in words that separate invoice creation from payment:

I can open your logged-in OpenRouter Credits page and generate the $N invoice there — that creates an unpaid checkout and moves no money. Continue?

On yes: navigate to https://openrouter.ai/settings/credits, enable Use crypto, enter the amount, click Purchase, and capture the resulting paymentSession_* URL. Targeted actions only — no screenshots and no full-page DOM dumps. On a CAPTCHA, a login wall, or an unexpected page layout, stop the automation immediately and fall back to the default steps; do not wait for the user to log in and then resume driving the browser — the accelerator only ever operates on a session that was authenticated before it started.

Verify before offering payment

Whichever path produced the URL, run Step 1 (quote) on it and fail closed unless all of these hold: the merchant is exactly OpenRouter, Inc; the status is payable; the invoice total is consistent with the requested credit amount after OpenRouter's ~5% crypto fee — accept when credit ≤ invoice ≤ credit × 1.10 (e.g. a $5.00 credit yields a $5.25 invoice; $1.00 yields $1.05), and stop and show both numbers when the invoice is below the credit or more than 10% above it; and the checkout is unexpired.

Do not enforce a minimum amount yourself — a $1.00 credit checkout has been created successfully in practice. Pass the user's amount through and surface OpenRouter's own validation error if one appears.

Consent to create the invoice is never consent to pay it. Payment continues at Step 2 of the normal flow with its own confirmation thresholds, unchanged.

Runtime

Run every command from the repository root (the directory containing SKILL.md, scripts/, package.json). If the harness sets ${CLAUDE_PLUGIN_ROOT}, use that.

cd "$SKILL_ROOT"
node scripts/dist/quote.js --url "<coinbase link>"

Each script prints one JSON object on stdout. Exit codes:

CodeMeaning
0success
1refused or failed — read error.code
2usage error
3submitted but not confirmed in the wait window (money may be in flight)

The bundles in scripts/dist/ are self-contained; npm install is only needed to rebuild them (npm run build).

Environment variables

Mode A — the default path — needs no key, no environment variable and no configuration at all. The variables below exist only for Mode B (--send), where this machine signs on the user's behalf. Never ask a user to set one unless they have explicitly asked for Mode B.

The recommended way to have an agent send the deposit is not to configure hot-wallet keys here. In order of preference: first, the default keyless path — the user pays from their own wallet. Second, for Stellar sources, the separate stellar-agent-wallet skill (ClawHub: shawnmuggle/stellar-agentic-wallet) pays the deposit address and memo via its send-raw command, with its own file-based key handling and confirmation prompts — no ROZO_CHECKOUT_* variable is involved. Only for unattended EVM/Solana automation does Mode B apply, and then with a dedicated hot wallet holding a low balance.

Mode B takes its signing key from the first of these that exists: --keyfile <path>; then ~/.config/solana/id.json for Solana or ROZO_CHECKOUT_EVM_KEYSTORE for EVM; then a raw key in the environment. The environment variables below may also be set in a .env — either in the working directory or at ~/.rozo-checkout/.env, whichever is found first, or an explicit --env-file <path>. Only ROZO_CHECKOUT_* keys are read from it, and the real environment wins over the file. Note that the working directory is the skill's own directory whenever you follow the run commands above, so ~/.rozo-checkout/.env is the right place for a user's own settings. A generic ~/.env is deliberately NOT read: it belongs to the user and usually holds unrelated credentials. If theirs lives elsewhere, pass --env-file <path> rather than asking them to move it.

VariableUsed byNotes
ROZO_CHECKOUT_EVM_KEYSTOREsend-evm.jsMode B only. Path to an encrypted V3 JSON keystore. Preferred over a raw key.
ROZO_CHECKOUT_KEYSTORE_PASSPHRASEsend-evm.jsMode B only. Keystore passphrase for unattended runs. On a terminal it is prompted instead.
ROZO_CHECKOUT_EVM_KEYsend-evm.jsMode B only. Raw 32-byte hex private key; the 0x prefix is optional. For unattended automation.
ROZO_CHECKOUT_SOL_KEYsend-sol.jsMode B only. Raw base58 secret key or JSON byte array. For unattended automation; ~/.config/solana/id.json is used first when present.
ROZO_CHECKOUT_RPC_<chainId>send scriptsoptional RPC override, e.g. ROZO_CHECKOUT_RPC_8453
ROZO_CHECKOUT_STATE_DIRalloptional; defaults to $HOME/.rozo-checkout/state

Keys are read from the environment only. Never pass a key as a command-line argument, never print one, never write one into a file, a prompt or a commit message. If the shell does not already export the key, ask the user to export it themselves — do not go hunting through .env files. The send scripts refuse to run if a .env in the working directory is tracked by git.

Confirmation thresholds

Read the USD amount from the invoice (invoice.amount).

AmountConfirmationNarration
≤ $1.00none — proceedsilent, report the result
≤ $10.00none — proceedone narrating line
> $10.00one explicit yes/nofull summary, wait for "yes"

The binding confirmation happens at step 4 (final confirm), never before — the pre-create preview cannot bind because the deposit address, the exact deposit amount and the order expiry do not exist yet.

This is enforced in code, not just documented. create-order.js withholds the full deposit address, memo and BOLT11 until it is re-run with --confirm, and --confirm writes a confirmation record bound to a digest of those exact instructions. The send scripts refuse to run without both --send and a confirmation record whose digest still matches the live deposit data.

The flow

Step 1 — quote (read-only, costs nothing)

node scripts/dist/quote.js --url "https://payments.coinbase.com/payment-links/pl_01..."

Reports merchant, invoice amount, callerPays, the Coinbase expiry and the supported source list. Exit 1 with LINK_NO_LONGER_PAYABLE means the link is used or expired — ask the merchant for a fresh one and stop.

The quoteReceipt in the output lives about 60 seconds. Do not save it; create-order.js takes its own fresh quote.

Step 2 — choose the source and preview

Ask the caller which coin and chain they want to pay with, or use what they already told you. Narrate the plan: invoice amount, merchant, chosen chain/token, callerPays (equal to the invoice — no discount). This is not the binding confirmation.

Step 3 — create the order

node scripts/dist/create-order.js --url "<coinbase link>" --chain 900 --token USDT

Chain ids: 1 Ethereum · 56 BNB Chain · 137 Polygon · 8453 Base · 900 Solana · 1500 Stellar · lightning Bitcoin Lightning.

Creating an order moves no money and costs nothing if left unfunded. In one run it creates the order, verifies it against the quote, fetches the authoritative deposit instructions, runs the reuse guard, validates that the deposit instructions are complete, checks the expiry margins, checks the deposit address against the compromised-address list, and re-checks that the Coinbase link is still payable.

Without --confirm the full deposit address, memo and BOLT11 are withheld (deposit: null, depositWithheld: true). You get everything you need to ask the user — masked address, exact amount, both expiries, the reused flag — and nothing that could be paid by accident.

Any non-zero exit here means do not fund the order. See Troubleshooting.

Step 4 — final confirm (binding)

Present, from the display and expiry blocks of the run above:

About to pay:
  Merchant:   {merchant}
  Invoice:    {invoice.amount} USD   (you pay the full amount, no discount)
  Send:       {display.amount} on {display.chain}
  To:         {display.payToMasked}                <- masked, always
  Order ends: {expiry.effectiveDeadlineIso}  ({expiry.minutesOfSlack} min of slack)
  Reused existing order: {reused}

  The deposit amount can exceed the invoice: it includes the bridge and
  network fees.
  Wrong token, wrong network or wrong amount is usually unrecoverable.
  Send exactly once — a second send to this one-time address is not
  guaranteed to be credited.

Confirm? (yes/no)

Only after an explicit yes (per the threshold table), re-run the exact same command with --confirm:

node scripts/dist/create-order.js --url "<coinbase link>" --chain 900 --token USDT --confirm

That reuses the same order, re-runs every check, releases the full deposit block, and records the confirmation. Use the masked address in prose. The full address, memo and BOLT11 string live only in the machine-readable deposit object — hand that block over verbatim when the user needs to copy it, and never re-type an address by hand.

Then pick a mode.

"Mode A" and "Mode B" are internal vocabulary. Never say them to the user. They are how this document names a branch you take; they carry no meaning for someone who just wants an invoice paid. Say who sends the money and what the user has to do — nothing else about the mechanism.

Do not saySay
"Stellar is Mode A only, no --send""I can't send Stellar for you — here's the address and memo to pay from your wallet" (or: "I'll pay it with your stellar-agent-wallet")
"Mode B needs ROZO_CHECKOUT_EVM_KEY""To have me pay it directly I'd need a hot-wallet key configured; otherwise pay from your own wallet and I'll watch for it"
"CAP_PER_TX: over the Mode B limit""That's over the $1,100 I'll sign for automatically — pay this one from your own wallet, which has no limit"

Whichever branch you take, the user should end up reading the same four things: the amount, the destination, the memo if there is one, and what they personally need to do next.

Mode A (default) — the user pays from their own wallet. No key, no env var, no setup. Give them the deposit block. For Lightning, deposit.lnInvoice is the BOLT11 string to scan or paste, and deposit.amount is in satoshis (deposit.isSats is true) — never call it "X BTC".

Mode B (--send, optional) — this machine pays from a hot wallet. Only when the user has asked for it. Only EVM chains and Solana — there is no --send for Stellar or Lightning; those are Mode A only. Only after the confirmation above. This is the only path that needs a key. On Solana it uses the ~/.config/solana/id.json that solana-keygen already wrote; on EVM an encrypted V3 keystore whose passphrase is prompted (never a flag). Either can be named with --keyfile. A raw key in the environment still works for unattended automation:

# EVM (Ethereum, BNB Chain, Polygon, Base)
ROZO_CHECKOUT_EVM_KEY=... node scripts/dist/send-evm.js --rozo-payment-id <uuid> --send

# Solana
ROZO_CHECKOUT_SOL_KEY=... node scripts/dist/send-sol.js --rozo-payment-id <uuid> --send

# --dry-run shows exactly what would be signed and signs nothing (no --send needed)

--send is mandatory; without it the script exits with SEND_NOT_OPTED_IN and does nothing. Mode B re-runs every check live before signing, re-proves payability as the last step before broadcast, verifies the RPC really is on the intended chain, verifies the token's on-chain decimals, signs before broadcasting so the transaction hash is known in advance, and records the send locally before broadcasting so the same order can never be paid twice. One limit: a single payment may not exceed $1,100. There is no override flag. A larger invoice is paid via Mode A, which needs no key and has no limit.

Step 5 — poll

node scripts/dist/status.js --rozo-payment-id <uuid> --watch --timeout 600

Always pass --rozo-payment-id when you have it. A link-only query cannot reach the authoritative pay-in view (the router does not resolve the id from a link alone), so the money-detected rule cannot be enforced; the script says so via authoritativeView: false and exits non-zero rather than guessing.

States: awaiting_deposit → payin_detected → payin_confirmed → bridging → paying_coinbase → settled. Other terminal or exceptional states: expired_unfunded, underpaid, stuck_after_payment, and unknown (the backend could not be read — not evidence that nothing was paid).

settled is only reported on real settlement evidence. The bridge reaching payment_completed is not that evidence and shows as paying_coinbase.

Step 6 — report

✓ Paid {invoice.amount} USD to {merchant} with {token} on {chain}.
  linkId:        {linkId}
  rozoPaymentId: {rozoPaymentId}
  pay-in tx:     {payin.txHash}

The money-detected rule

Once any pay-in exists (payin.txHash, payin.confirmedAt or a non-zero amountReceived), for the rest of the conversation:

  • never describe the order as a plain failure
  • never advise paying again, topping up, or "trying a different chain"
  • never create a new order for the same Coinbase link
  • preserve linkId, rozoPaymentId and every tx hash in your reply

If the state is stuck_after_payment or underpaid, escalate for manual reconciliation with this wording:

Your payment arrived on chain but the invoice has not been settled. I am not going to retry, because retrying could take a second payment. I have kept every identifier below and this needs a human to reconcile it.

linkId: {linkId} · rozoPaymentId: {rozoPaymentId} · pay-in tx: {payin.txHash} · state: {state}

Then stop and hand off. Do not run any send script again.

Troubleshooting

error.codeWhat happenedWhat to do
SEND_NOT_OPTED_INa send script was run without --sendintentional; add --send only after the user has confirmed
NOT_CONFIRMEDno confirmation record for this orderrun create-order.js ... --confirm after the user says yes
CONFIRMATION_STALEthe live deposit details changed since the confirmationre-confirm with fresh details; never send against the old ones
LINK_NO_LONGER_PAYABLEthe Coinbase link is used, expired, settled, or was consumed by someone else between quote and nowask the merchant for a fresh link; do not fund anything
LINK_PAYABILITY_UNKNOWNthe Coinbase state was incomplete, so payability could not be provedtreat as not payable; retry, and do not fund on a guess
DEPOSIT_INCOMPLETEno positive amount, or a Lightning order with no BOLT11 yetwait and re-run; nothing is payable yet
DEPOSIT_MEMO_REQUIREDa Stellar order arrived without its memodo not send; Stellar routes on a shared hub address plus the memo, so without it the payment is lost
LOCK_TIMEOUTanother rozo-checkout process holds the send lockwait for it; never bypass by clearing the lock mid-flight
TRACKED_DOTENV_UNVERIFIABLEenv files exist but git could not prove they are untrackedfix git, or run from a directory with no env files
TX_REVERTED / TX_FAILEDthe transfer landed and failedno funds moved, but the order stays locked; investigate before retrying
ORDER_ALREADY_FUNDEDan order for this link already shows moneymoney-detected rule — do not pay again, escalate
REUSED_SOURCE_MISMATCHan existing order expects a different chain/token than the caller choselet it expire, or pay the chain the order actually expects after re-confirming with the user
CREATE_DRIFTthe created order disagrees with the quote (merchant, amount or link)do not fund it; let it expire unfunded and report the drift
NO_DISCOUNT_VIOLATIONthe server reported a discount, or callerPays ≠ invoicestop; this flow must charge the full invoice
EXPIRY_MARGIN / EXPIREDnot enough time left to fund, bridge and settlestart over with a fresh link
BOLT11_TOO_SHORTthe Lightning invoice has under 10 minutes leftsee "Lightning invoice too short" below
BLACKLIST_HITthe deposit address or the sender is on the compromised-address listsend nothing; report it to the operator immediately
BLACKLIST_UNAVAILABLEthe vendored list is missing, malformed or its digest does not matchthe skill fails closed by design; do not work around it
ALREADY_SENTa send is already recorded for this orderdo not send again; poll status.js and check the chain
DEPOSIT_CHANGEDthe live deposit details differ from what was confirmedabort; re-run create-order.js and re-confirm
BROADCAST_AMBIGUOUSthe RPC errored but a transaction may be in flightdo not resend; check the sender on an explorer, then poll
RPC_CHAIN_MISMATCHthe RPC is not on the chain the order settles onfix ROZO_CHECKOUT_RPC_<chainId>; never sign against it
DECIMALS_MISMATCHthe token's on-chain decimals disagree with expectationsdo not sign — the amount could be off by orders of magnitude
CAP_PER_TXabove the $1,100 per-payment limit for automated sendingno override exists; have the user pay from their own wallet (Mode A), which has no limit
UNSUPPORTED_SOURCEthat coin/chain pair is not acceptedoffer the table at the top of this file (the server's own list omits Lightning)
NO_KEY_SOURCEno keyfile, no ~/.config/solana/id.json, no key env vartell the user the three options; do not pick one for them. The message lists the .env paths that were searched — if theirs is somewhere else, pass --env-file <path> rather than moving their file
KEYFILE_PERMISSIONSthe key file is readable by other usershave them run chmod 600 <path>
TRACKED_KEYFILEthe key file is tracked by githave them untrack it before signing with it
ENV_FILE_PERMISSIONSthe .env is readable by other usershave them run chmod 600 .env
BAD_ENV_FILEa line in the .env is not KEY=VALUEthe error names the line number only; do not ask them to paste the line
KEYSTORE_BAD_PASSPHRASEwrong keystore passphraselet them retry; never echo or store what they typed
KEYSTORE_PASSPHRASE_REQUIREDa keystore needs a passphrase but there is no terminalset ROZO_CHECKOUT_KEYSTORE_PASSPHRASE for unattended runs
MISSING_KEYthe key env var is not exportedask the user to export it in their own shell
TRACKED_DOTENVa .env in this directory is tracked by gituntrack it before using hot-wallet keys here
EXPIRY_UNPARSABLEa deadline is missing or unreadableabort; never treat an unknown deadline as "probably fine"

Lightning invoice too short

Re-running create-order.js does not mint a fresh BOLT11: while the existing order is unexpired the router reuses it, invoice and all. The options are to wait for the order to expire and then create a new one, or to ask the merchant for a fresh Coinbase link. Do not pay a BOLT11 with under ten minutes left.

The user pasted a commerce.coinbase.com/pay/{uuid} URL

That is a different, legacy protocol. Say so and stop.

The user wants to pay in Base USDC

Not a special case. Base is a supported source like any other — chain 8453, token USDC — so run the normal flow and say nothing about modes, bridges or alternative tools. Same steps, same confirmation, same deposit block, same polling.

Do not send the user off to another skill for this. One consistent path is worth more than a theoretical shortcut, and the shortcut is not obviously cheaper anyway: a same-chain order does not bridge.

Underpaid

Report the shortfall and escalate. Do not send a top-up to the same deposit address — a second payment to a one-time address is not guaranteed to be credited.

Related skills