From 0d236726f96c9accc058a412a6527fda59e7fd6a Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Tue, 28 Apr 2026 18:21:35 +0100 Subject: [PATCH 1/8] docs(moonpay-commerce): merge x402 paylink flow into commerce skill Add MoonPay Commerce x402 paylink payment guidance to the moonpay-commerce skill so a single skill covers both Shopify storefront checkout and direct paylink payments via api.hel.io. - Existing Shopify storefront content (mp commerce store/product/cart/checkout) preserved verbatim under the original "# Shop with crypto" heading. - New "## MoonPay Commerce" section covers the x402 paylink flow: api endpoints, payerAddress requirement, 402 response decoding, supported chains, error table, settlement, amount conversion, worked example, custom client notes, security, and troubleshooting. - Frontmatter description and tags widened so the skill is discovered for either Shopify shopping or paylink payment requests. - Solana-only note scoped to the storefront flow; multi-chain (Base, Ethereum, Polygon, Arbitrum, BSC, Solana) applies only to the x402 flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/moonpay-commerce/SKILL.md | 311 ++++++++++++++++++++++++++++++- 1 file changed, 308 insertions(+), 3 deletions(-) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index 0612eee..8e513d0 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -1,7 +1,7 @@ --- name: moonpay-commerce -description: Browse Shopify stores, search products, manage a cart, and checkout with crypto via Solana Pay. No login required. -tags: [commerce, shopping] +description: Browse Solana Pay-enabled Shopify stores, manage a cart, and checkout with crypto — or pay any MoonPay Commerce (Helio) paylink directly via the x402 protocol with USDC across Base, Ethereum, Polygon, Arbitrum, BSC, and Solana. Use when the user supplies a paylink URL (https://api.hel.io/v1/x402/...) or asks to buy/tip/subscribe via MoonPay Commerce. +tags: [commerce, shopping, payments, x402, agent-payments] --- # Shop with crypto @@ -91,10 +91,315 @@ mp commerce checkout \ - Use `product search` to find variant IDs — each variant has a Shopify GID - The `--country` flag takes full country names (e.g. "United States", "Netherlands") - Checkout takes 30-90 seconds — the API automates the Shopify checkout flow -- Currently supports Solana chain only for payment +- The Shopify storefront checkout currently supports Solana only. The MoonPay Commerce x402 paylink flow below supports Base, Ethereum, Polygon, Arbitrum, BSC, and Solana. + +## MoonPay Commerce + +Pay for products on [MoonPay Commerce](https://www.moonpay.com/en-gb/newsroom/moonpay-commerce) using the [x402](https://www.x402.org/) protocol. The agent settles the payment on-chain automatically when a user asks to buy, tip, or subscribe via a MoonPay Commerce paylink. + +### When to use + +- The user supplies a MoonPay Commerce paylink URL (`https://api.hel.io/v1/x402/checkout/` or a merchant product URL resolving to one) +- The user asks to buy, tip, pay, or subscribe to a product powered by MoonPay Commerce +- An agent is operating autonomously and encounters an `https://api.hel.io` x402 endpoint + +### Protocol overview + +x402 is a machine-native payment protocol built on HTTP 402. The flow is: + +``` +1. Agent sends POST to the resource URL (with ?payerAddress=) +2. Server responds HTTP 402 + PAYMENT-REQUIRED header (network, amount, payTo) +3. Agent signs the payment authorization and retries with PAYMENT-SIGNATURE header +4. Server verifies on-chain, settles, returns HTTP 200 + PAYMENT-RESPONSE header +``` + +Both `mp x402 request` (MoonPay CLI) and any x402-compatible client library handle steps 2–4 automatically. + +### API endpoints + +**Base URL:** `https://api.hel.io` + +| Method | Path | Description | +|---|---|---| +| `POST` | `/v1/x402/checkout/{paylinkId}` | One-time checkout payment | +| `POST` | `/v1/x402/deposit/{depositId}` | Recurring / balance-based deposit payment | +| `GET` | `/v1/paylink/{id}/public` | Fetch public product metadata (requires `Origin` header) | +| `GET` | `/v1/health` | Health check — returns `{"status":"ok"}` | + +### Prerequisites + +- **MoonPay CLI (`mp`)** installed and authenticated (`mp wallet list` should show your wallet) +- Payer wallet funded with USDC on the target chain +- A MoonPay Commerce paylink URL from the merchant (e.g. from https://app.hel.io) + +Check your balance before paying: + +```bash +mp token balance list --wallet --chain +``` + +Supported chains: `base`, `ethereum`, `polygon`, `arbitrum`, `bsc`, `solana`. + +### Product discovery + +MoonPay Commerce has no bulk catalog API — merchants supply paylink URLs directly. Before paying, fetch the paylink metadata to display product details and confirm the amount: + +```bash +curl -s "https://api.hel.io/v1/paylink//public" \ + -H "Origin: https://app.hel.io" +``` + +Returns: product name, description, price (in minimal USDC units), and supported blockchains. Use this to present a confirmation summary to the user before executing any payment. + +If the user only has a merchant product URL (not a raw paylink URL), resolve it to a paylink ID first — the paylink ID is typically the last path segment of the checkout URL. + +### Critical: `payerAddress` requirement + +**MoonPay Commerce requires a `?payerAddress=` query parameter on every request.** + +The server pre-allocates a per-payer deposit wallet on the initial 402 request, so it must know the payer's address before it can respond. Standard x402 clients do not send this automatically — you must append it manually. + +Always append `?payerAddress=` to the URL: + +```bash +# Get the EVM address for your wallet first +mp wallet list + +# Then include it in the URL +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/checkout/?payerAddress=" \ + --wallet \ + --chain +``` + +Without this, the server returns HTTP 400 with message `"x-payer-address header or ?payerAddress= query param required"`. + +### Payment flow + +#### Checkout (one-time payments) + +```bash +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/checkout/?payerAddress=" \ + --wallet \ + --chain +``` + +**Fixed-price paylinks** — the merchant has set a price; no `?amount=` needed. + +**Dynamic-price paylinks** — append `?amount=&payerAddress=
`. Default to **$3 (`amount=3000000`)** for first-time or illustrative runs unless the user specifies otherwise. + +#### Deposit (recurring / balance-based payments) + +```bash +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/deposit/?payerAddress=" \ + --wallet \ + --chain +``` + +#### Before running any `mp x402 request` + +Restate the **amount, chain, and recipient** to the user and wait for explicit confirmation. This spends real funds. + +> _"About to send **$10.00 USDC on Base** to `` (paylink ``). Proceed?"_ + +#### After a successful payment + +Summarize: settlement tx hash, amount sent, chain, recipient. + +> _"Paid $10.00 USDC on Base (settle tx `0x…`). Wallet balance: $X → $X-10."_ + +### 402 response structure + +When no valid `PAYMENT-SIGNATURE` header is present, the server returns HTTP 402. The `PAYMENT-REQUIRED` header contains Base64-encoded JSON: + +```json +{ + "x402Version": 1, + "resource": { + "url": "/v1/x402/checkout/", + "description": "", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "", + "asset": "", + "amount": "", + "payTo": "", + "maxTimeoutSeconds": 60, + "extra": { + "name": "", + "version": "" + } + } + ] +} +``` + +Key points: +- **`amount` is the total cost** — fees and gas are already included. No additional calculation needed. +- **`payTo` is a per-payer deposit wallet**, not the merchant's address directly. Funds sweep to the merchant asynchronously after settlement. +- Each entry in `accepts[]` represents one supported chain. The `mp` CLI selects based on `--chain`. + +### Supported chains + +| Chain | CAIP-2 | Notes | +|---|---|---| +| Base | `eip155:8453` | Lowest gas; recommended default | +| Ethereum | `eip155:1` | Higher gas; use when required | +| Polygon | `eip155:137` | Low gas | +| Arbitrum | `eip155:42161` | Low gas | +| BSC | `eip155:56` | Low gas | +| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Fastest settlement (~5–10s) | + +When the 402 response includes multiple chains, prefer lower-gas chains (Base, Polygon, Arbitrum) unless the user's wallet is funded on a specific chain. Solana is fastest if the user has SOL-based USDC. + +If a chain's gas cost spikes beyond its threshold, the server excludes it from `accepts[]`. If **all** chains are excluded, the server returns HTTP 400 instead of 402 — retry later. + +### Error handling + +| HTTP | Error / Condition | Agent Action | +|---|---|---| +| **402** | No payment signature | Decode `PAYMENT-REQUIRED`, sign, retry with `PAYMENT-SIGNATURE` | +| **400** | `PAYLINK_INACTIVE` | Inform user — paylink is disabled; do not retry | +| **400** | `PAYLINK_DELETED` | Inform user — paylink is deleted; do not retry | +| **400** | `X402_UNSUPPORTED_FEATURES` | Paylink requires customer detail fields; x402 not supported for this paylink | +| **400** | `FEE_MARGIN_INSUFFICIENT` | Gas cost exceeds fee margin on all chains; retry later when gas settles | +| **400** | `FEE_RATE_EXCEEDS_PRICE` | Fee rate exceeds payment price; inform user | +| **400** | Empty `accepts[]` | All chains circuit-broken; retry later | +| **400** | Missing `payerAddress` | Append `?payerAddress=` to the request URL | +| **400** | Invalid payer address | Validate format: base58 for Solana, `0x…` hex for EVM | +| **400** | Invalid `amount` param | Must be a positive integer string (minimal units) | +| **403** | `PAYLINK_SANCTIONED` | Abort — access restricted; do not retry | +| **403** | Payer mismatch | Signed payer address doesn't match provisioned identity; verify wallet | +| **404** | Paylink not found | Verify the paylink ID or URL is correct | +| **409** | Settlement in progress | A payment for this tx is already being settled; do not re-submit | +| **429** | Rate limited | Back off — 10 requests per 60 seconds per IP | + +### Settlement behavior + +Settlement is asynchronous. The HTTP 200 is returned as soon as the payment signature is verified. The on-chain sweep from the deposit wallet to the merchant happens in the background (typically 30–120s). + +The `PAYMENT-RESPONSE` header in the 200 response contains the settlement transaction hash — this is the proof of payment to show the user. + +### Amount conversion + +USDC has 6 decimals: `minimalUnits = USD × 1_000_000`. + +| USD | Minimal units | +|---|---| +| $1.00 | `1000000` | +| $3.00 | `3000000` | +| $5.00 | `5000000` | +| $10.00 | `10000000` | +| $100.00 | `100000000` | + +Default example amount for first-time or demo runs: **$3** (`amount=3000000`). + +### Example: paying a paylink + +User says: _"Pay for the DEX Screener Token Boost at `https://api.hel.io/v1/x402/checkout/abc123` — use $10."_ + +**Step 1 — Discover product details:** + +```bash +curl -s "https://api.hel.io/v1/paylink/abc123/public" \ + -H "Origin: https://app.hel.io" +``` + +**Step 2 — Get wallet address:** + +```bash +mp wallet list +``` + +**Step 3 — Confirm with user:** + +> _"About to send **$10.00 USDC on Base** for DEX Screener Token Boost (paylink `abc123`), signed from wallet ``. Proceed?"_ + +**Step 4 — Execute payment:** + +```bash +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/checkout/abc123?amount=10000000&payerAddress=0xYOUR_ADDRESS" \ + --wallet \ + --chain base +``` + +**Step 5 — Summarize:** + +> _"Paid $10.00 USDC on Base (settle tx `0x…`). Wallet balance: $X → $X-10."_ + +### Pre-payment checklist + +Before executing any payment, verify: + +- [ ] Wallet has sufficient USDC balance on the target chain (`mp token balance list`) +- [ ] The paylink ID is valid (confirmed via `GET /v1/paylink/{id}/public`) +- [ ] `?payerAddress=` is included in the URL +- [ ] Amount is correct — in minimal units (6 decimals), matches the product price +- [ ] User has explicitly confirmed: product name, amount, chain, and wallet + +### Custom client integration + +Any x402-compatible HTTP client works with MoonPay Commerce endpoints. The client must: + +1. Send an initial `POST` to the checkout/deposit endpoint with `?payerAddress=
` +2. Parse the `PAYMENT-REQUIRED` response header (Base64-encoded JSON) to extract: network, amount, payTo address, asset +3. Sign and submit the on-chain payment transaction (EIP-3009 `transferWithAuthorization` for EVM; SPL token transfer for Solana) +4. Retry the same `POST` with the signed proof in the `PAYMENT-SIGNATURE` header +5. Read `PAYMENT-RESPONSE` header from the 200 response for the settlement tx hash + +``` +POST https://api.hel.io/v1/x402/checkout/{paylinkId}?payerAddress={address} +→ 402 PAYMENT-REQUIRED header: { accepts: [{ network, amount, payTo, asset }] } + +[sign on-chain transfer to payTo for the given amount] + +POST https://api.hel.io/v1/x402/checkout/{paylinkId}?payerAddress={address} + PAYMENT-SIGNATURE: +→ 200 PAYMENT-RESPONSE header: { txHash, payer, network } +``` + +### Security considerations + +- **Never auto-execute payments** — always confirm amount, chain, and recipient with the user before spending. +- **Verify the paylink** — fetch `GET /v1/paylink/{id}/public` before paying to confirm the product and merchant are legitimate. +- **Check wallet balance first** — insufficient balance causes a failed transaction; check with `mp token balance list` before initiating. +- **Use HTTPS only** — all API requests must use `https://api.hel.io`. The `mp` CLI enforces this; custom clients should too. +- **Protect wallet credentials** — never log or expose private keys or signing material. + +### Troubleshooting + +#### "x-payer-address header or ?payerAddress= query param required" (400) +The `payerAddress` query parameter is missing. Append `?payerAddress=` to the URL — this is required by MoonPay Commerce (non-standard x402 behavior). + +#### "Payment verification failed" / on-chain revert +The payer wallet has no USDC on the network the server allocated the deposit wallet for. Check your balance: `mp token balance list --wallet --chain `. The `accepts[].network` in the 402 response tells you which chain to fund. + +#### "Paylink not found" (404) +Verify the paylink ID is correct. + +#### CORS / `Origin` header required +Some endpoints require an `Origin` header. If using `curl` directly (not `mp`), add `-H 'Origin: https://app.hel.io'`. + +#### All chains 400 instead of 402 +Gas spiked across all supported chains. The circuit breaker excluded them all from `accepts[]`. Wait for gas to settle and retry — typically resolves within minutes. + +#### 409 Conflict — settlement in progress +A previous submission for this transaction is still being processed. Do not re-submit. Wait 30–60s for it to resolve; the payment was likely already accepted. ## Related skills - **moonpay-auth** — Set up a local wallet for signing - **moonpay-check-wallet** — Check USDC balance before checkout - **moonpay-swap-tokens** — Swap tokens to get USDC for payment +- **moonpay-x402** — General x402 paid endpoints (non-Commerce) From 0dbff119eef6fdbd67c036638651bb21036ae08a Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Tue, 5 May 2026 16:36:26 +0200 Subject: [PATCH 2/8] fix: remove incorrect amt conversion section --- skills/moonpay-commerce/SKILL.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index 8e513d0..d5c69cc 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -289,20 +289,6 @@ Settlement is asynchronous. The HTTP 200 is returned as soon as the payment sign The `PAYMENT-RESPONSE` header in the 200 response contains the settlement transaction hash — this is the proof of payment to show the user. -### Amount conversion - -USDC has 6 decimals: `minimalUnits = USD × 1_000_000`. - -| USD | Minimal units | -|---|---| -| $1.00 | `1000000` | -| $3.00 | `3000000` | -| $5.00 | `5000000` | -| $10.00 | `10000000` | -| $100.00 | `100000000` | - -Default example amount for first-time or demo runs: **$3** (`amount=3000000`). - ### Example: paying a paylink User says: _"Pay for the DEX Screener Token Boost at `https://api.hel.io/v1/x402/checkout/abc123` — use $10."_ From 76b611bad5b885162da5457c3b8bac185874c62a Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Thu, 7 May 2026 11:15:31 +0100 Subject: [PATCH 3/8] docs(moonpay-commerce): add x402 charge-resume endpoint and status verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the new POST /v1/x402/checkout/charge/{chargeToken} endpoint (heliofi/backend#4161) that lets an agent settle the in-flight Charge created by Shopify when the buyer selects Solana Pay at checkout — instead of creating a parallel x402 Charge. - Add the charge endpoint to the API table. - Add a "Charge resume (Shopify-source paylinks)" subsection under Payment flow with the Solana-only constraint and the no-amount rule (price comes from Charge.usdcAmount). - Add a "Verifying payment status" subsection: agents must poll GET /v1/charge/{chargeToken} after the 200 to confirm the PaylinkTx and read the on-chain txSignature, which is not returned in the 200 body for the charge-resume flow. - Extend the error table with charge-token-specific cases: 404 NOT_FOUND, 400 NON_SHOPIFY, 409 ALREADY_SETTLED, 410 EXPIRED. - Update the pre-payment checklist and custom-client integration steps to include the charge status poll. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/moonpay-commerce/SKILL.md | 46 ++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index d5c69cc..0827f05 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -124,7 +124,9 @@ Both `mp x402 request` (MoonPay CLI) and any x402-compatible client library hand |---|---|---| | `POST` | `/v1/x402/checkout/{paylinkId}` | One-time checkout payment | | `POST` | `/v1/x402/deposit/{depositId}` | Recurring / balance-based deposit payment | +| `POST` | `/v1/x402/checkout/charge/{chargeToken}` | Settle an in-flight Shopify-source Charge (created when the buyer selected Solana Pay at the Shopify checkout) | | `GET` | `/v1/paylink/{id}/public` | Fetch public product metadata (requires `Origin` header) | +| `GET` | `/v1/charge/{chargeToken}` | Fetch Charge state — use after settlement to confirm payment status | | `GET` | `/v1/health` | Health check — returns `{"status":"ok"}` | ### Prerequisites @@ -202,6 +204,22 @@ mp x402 request \ --chain ``` +#### Charge resume (Shopify-source paylinks) + +When a Shopify buyer selects Solana Pay at checkout, Shopify POSTs `/payment` to the MoonPay Commerce backend, which creates an **in-flight Charge** with a `chargeToken` (UUID) and a price derived from the cart. An agent settles that existing Charge — instead of creating a parallel one — by hitting: + +```bash +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/checkout/charge/?payerAddress=" \ + --wallet \ + --chain solana +``` + +- The amount is **derived from the Charge** (`Charge.usdcAmount`) — do **not** append `?amount=` for charge-resume requests. +- Currently **Solana-only**; EVM (Base / Polygon / Arbitrum / BSC) is on the roadmap. +- On HTTP 200, settlement is fire-and-forget: the server creates the `PaylinkTx`, links it to the Charge, and triggers Shopify's `paymentSessionResolveMutation` to flip the order to paid in the merchant admin. The `txSignature` is **not** returned in the 200 body — confirm it via `GET /v1/charge/{chargeToken}` (see "Verifying payment status" below). + #### Before running any `mp x402 request` Restate the **amount, chain, and recipient** to the user and wait for explicit confirmation. This spends real funds. @@ -280,7 +298,11 @@ If a chain's gas cost spikes beyond its threshold, the server excludes it from ` | **403** | `PAYLINK_SANCTIONED` | Abort — access restricted; do not retry | | **403** | Payer mismatch | Signed payer address doesn't match provisioned identity; verify wallet | | **404** | Paylink not found | Verify the paylink ID or URL is correct | +| **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — re-trigger the Shopify checkout to mint a new one | +| **400** | `CHARGE_NON_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source; use `/v1/x402/checkout/{paylinkId}` instead | | **409** | Settlement in progress | A payment for this tx is already being settled; do not re-submit | +| **409** | `CHARGE_ALREADY_SETTLED` (charge-resume) | The Charge is already paid — confirm via `GET /v1/charge/{chargeToken}` and surface the existing tx | +| **410** | `CHARGE_EXPIRED` (charge-resume) | The Charge expired before settlement — re-trigger the Shopify checkout | | **429** | Rate limited | Back off — 10 requests per 60 seconds per IP | ### Settlement behavior @@ -289,6 +311,24 @@ Settlement is asynchronous. The HTTP 200 is returned as soon as the payment sign The `PAYMENT-RESPONSE` header in the 200 response contains the settlement transaction hash — this is the proof of payment to show the user. +For the **charge-resume** flow (`POST /v1/x402/checkout/charge/{chargeToken}`), settle is fire-and-forget and the 200 body does not carry the on-chain signature. Always confirm payment status by polling `GET /v1/charge/{chargeToken}` after the 200 (see below). + +### Verifying payment status + +At the end of every x402 payment, confirm the Charge has reached a terminal paid state by calling: + +```bash +curl -s "https://api.hel.io/v1/charge/" +``` + +Use this to: + +- Confirm the `PaylinkTx` was created and linked to the Charge. +- Read the on-chain `txSignature` for the charge-resume flow (where it isn't returned in the 200 body). +- Detect settlement failures the 200 response can't surface (e.g. background sweep reverted, Shopify resolve failed). + +Recommended polling: poll once immediately after the 200, then every 5–10s for up to ~120s. Stop polling as soon as the Charge shows a paid/settled state with a transaction signature attached. Surface the settlement tx hash to the user only after this confirmation — do **not** rely on the HTTP 200 alone for the charge-resume flow. + ### Example: paying a paylink User says: _"Pay for the DEX Screener Token Boost at `https://api.hel.io/v1/x402/checkout/abc123` — use $10."_ @@ -333,16 +373,18 @@ Before executing any payment, verify: - [ ] `?payerAddress=` is included in the URL - [ ] Amount is correct — in minimal units (6 decimals), matches the product price - [ ] User has explicitly confirmed: product name, amount, chain, and wallet +- [ ] After settlement, payment status is confirmed via `GET /v1/charge/{chargeToken}` (mandatory for charge-resume; recommended for all flows) ### Custom client integration Any x402-compatible HTTP client works with MoonPay Commerce endpoints. The client must: -1. Send an initial `POST` to the checkout/deposit endpoint with `?payerAddress=
` +1. Send an initial `POST` to the checkout/deposit/charge endpoint with `?payerAddress=
` 2. Parse the `PAYMENT-REQUIRED` response header (Base64-encoded JSON) to extract: network, amount, payTo address, asset 3. Sign and submit the on-chain payment transaction (EIP-3009 `transferWithAuthorization` for EVM; SPL token transfer for Solana) 4. Retry the same `POST` with the signed proof in the `PAYMENT-SIGNATURE` header -5. Read `PAYMENT-RESPONSE` header from the 200 response for the settlement tx hash +5. Read `PAYMENT-RESPONSE` header from the 200 response for the settlement tx hash (paylink/deposit flows only) +6. Poll `GET /v1/charge/{chargeToken}` to confirm the Charge reached a paid/settled state and to retrieve the on-chain `txSignature` (mandatory for the charge-resume flow; recommended for all flows) ``` POST https://api.hel.io/v1/x402/checkout/{paylinkId}?payerAddress={address} From 31e1c3f189c88cefe56bb9927487d3106386f69b Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Thu, 7 May 2026 12:07:27 +0100 Subject: [PATCH 4/8] docs(moonpay-commerce): route bare UUIDs to the charge-resume endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "When to use" bullet and an identifier routing cheat-sheet so the agent recognizes that a bare UUID (e.g. 6d0a1c57-3544-42e2-aa44-b654077c7529) supplied alongside the word "charge" is a chargeToken — and should be sent to POST /v1/x402/checkout/charge/{chargeToken}, not to /checkout/{paylinkId}, /deposit/{depositId}, or /v1/paylink/{id}/public. The cheat-sheet covers the three ID shapes (paylink slug, depositId, UUID chargeToken) and the fallback rule: if the charge endpoint returns CHARGE_NOT_FOUND (404), only then try the other endpoints. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/moonpay-commerce/SKILL.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index 0827f05..49bca47 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -102,6 +102,17 @@ Pay for products on [MoonPay Commerce](https://www.moonpay.com/en-gb/newsroom/mo - The user supplies a MoonPay Commerce paylink URL (`https://api.hel.io/v1/x402/checkout/` or a merchant product URL resolving to one) - The user asks to buy, tip, pay, or subscribe to a product powered by MoonPay Commerce - An agent is operating autonomously and encounters an `https://api.hel.io` x402 endpoint +- The user asks to pay for a **charge** and supplies a UUID like `6d0a1c57-3544-42e2-aa44-b654077c7529` — that's a `chargeToken`, not a paylink ID. Route it to the charge-resume endpoint (`POST /v1/x402/checkout/charge/{chargeToken}`); do **not** try it against `/checkout/{paylinkId}`, `/deposit/{depositId}`, or `/v1/paylink/{id}/public` — those will return 404. + +### Identifier routing cheat-sheet + +| User says | ID looks like | Endpoint | +|---|---|---| +| "pay this paylink", supplies a paylink URL or short ID | short slug or trailing path segment of `https://app.hel.io/pay/` | `POST /v1/x402/checkout/{paylinkId}` | +| "top up / deposit", supplies a deposit ID | depositId from the merchant | `POST /v1/x402/deposit/{depositId}` | +| "pay this charge", or supplies a UUID v4 (e.g. `6d0a1c57-3544-42e2-aa44-b654077c7529`) | `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` (UUID) | `POST /v1/x402/checkout/charge/{chargeToken}` | + +When the supplied ID is a UUID and you can't tell from context, default to the charge-resume endpoint. A 404 from the charge endpoint with `CHARGE_NOT_FOUND` is the signal that it really is a different ID type — only then fall back to checkout/deposit/paylink. ### Protocol overview From 0d3862495712602406d00b750b6766fc7813c6dd Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Thu, 7 May 2026 18:28:42 +0100 Subject: [PATCH 5/8] docs(moonpay-commerce): correct charge routing and add lookup-first flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The charge-resume endpoint POST /v1/x402/checkout/charge/{chargeToken} is Shopify-source-only — the X402ChargeCheckoutGuard rejects non-Shopify charges with 400 CHARGE_NOT_SHOPIFY (helio-api X402ChargeCheckoutGuard.ts L645-L657). Earlier guidance told the agent to "default to the charge-resume endpoint for any UUID", which fails for the common case of a paylink-backed Charge URL like https://moonpay.hel.io/charge/. - Replace "default to charge-resume" guidance with a lookup-first flow: GET /v1/charge/{chargeToken}, inspect for shopifyPaymentDetails, then route to /checkout/charge/{token} (Shopify) or /checkout/{paylinkId} (non-Shopify, with paylink ID read from the Charge response). - Add a "Charge -> paylink fallback (non-Shopify charges)" subsection. - Fix the error code: CHARGE_NON_SHOPIFY -> CHARGE_NOT_SHOPIFY (matches X402ChargeCheckoutGuard.ts L652). - Widen the frontmatter description so the skill triggers on charge URLs (https://moonpay.hel.io/charge/, https://app.hel.io/charge/) and on bare UUIDs paired with the word "charge", so it loads before the agent calls moonpay/pay tools. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/moonpay-commerce/SKILL.md | 47 +++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index 49bca47..1654691 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -1,6 +1,6 @@ --- name: moonpay-commerce -description: Browse Solana Pay-enabled Shopify stores, manage a cart, and checkout with crypto — or pay any MoonPay Commerce (Helio) paylink directly via the x402 protocol with USDC across Base, Ethereum, Polygon, Arbitrum, BSC, and Solana. Use when the user supplies a paylink URL (https://api.hel.io/v1/x402/...) or asks to buy/tip/subscribe via MoonPay Commerce. +description: Browse Solana Pay-enabled Shopify stores, manage a cart, and checkout with crypto — or pay any MoonPay Commerce (Helio) paylink, deposit, or charge directly via the x402 protocol with USDC across Base, Ethereum, Polygon, Arbitrum, BSC, and Solana. Use when the user supplies any hel.io URL (paylink at https://api.hel.io/v1/x402/..., charge at https://moonpay.hel.io/charge/, https://app.hel.io/charge/, or https://api.hel.io/v1/charge/), supplies a bare UUID with the word "charge", or asks to buy/tip/subscribe/pay-charge via MoonPay Commerce. **Load this skill before calling moonpay or pay tools whenever the user is paying anything Helio-related — the routing rules below decide which endpoint to hit.** tags: [commerce, shopping, payments, x402, agent-payments] --- @@ -102,17 +102,28 @@ Pay for products on [MoonPay Commerce](https://www.moonpay.com/en-gb/newsroom/mo - The user supplies a MoonPay Commerce paylink URL (`https://api.hel.io/v1/x402/checkout/` or a merchant product URL resolving to one) - The user asks to buy, tip, pay, or subscribe to a product powered by MoonPay Commerce - An agent is operating autonomously and encounters an `https://api.hel.io` x402 endpoint -- The user asks to pay for a **charge** and supplies a UUID like `6d0a1c57-3544-42e2-aa44-b654077c7529` — that's a `chargeToken`, not a paylink ID. Route it to the charge-resume endpoint (`POST /v1/x402/checkout/charge/{chargeToken}`); do **not** try it against `/checkout/{paylinkId}`, `/deposit/{depositId}`, or `/v1/paylink/{id}/public` — those will return 404. +- The user asks to pay for a **charge** — typically a URL like `https://moonpay.hel.io/charge/`, `https://app.hel.io/charge/`, or just a bare UUID such as `6d0a1c57-3544-42e2-aa44-b654077c7529`. A UUID by itself is a `chargeToken`, **not** a `paylinkId` and **not** a `depositId`. ### Identifier routing cheat-sheet -| User says | ID looks like | Endpoint | +A bare UUID is **never** the right input for `/checkout/{paylinkId}`, `/deposit/{depositId}`, or `/v1/paylink/{id}/public` — all of those expect short merchant-issued IDs and will 404 on a UUID. + +| What the user supplies | First step | Settle endpoint | |---|---|---| -| "pay this paylink", supplies a paylink URL or short ID | short slug or trailing path segment of `https://app.hel.io/pay/` | `POST /v1/x402/checkout/{paylinkId}` | -| "top up / deposit", supplies a deposit ID | depositId from the merchant | `POST /v1/x402/deposit/{depositId}` | -| "pay this charge", or supplies a UUID v4 (e.g. `6d0a1c57-3544-42e2-aa44-b654077c7529`) | `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` (UUID) | `POST /v1/x402/checkout/charge/{chargeToken}` | +| Paylink URL or short slug (e.g. `app.hel.io/pay/`) | optional `GET /v1/paylink/{paylinkId}/public` | `POST /v1/x402/checkout/{paylinkId}` | +| Deposit ID from a merchant | — | `POST /v1/x402/deposit/{depositId}` | +| **Bare UUID** or a `…/charge/` URL | **`GET /v1/charge/{chargeToken}` first** — read the Charge to decide where to settle | see "Routing a charge" below | + +#### Routing a charge + +Charges come in **two flavors** and the right settle endpoint depends on which: + +1. `GET https://api.hel.io/v1/charge/` to fetch the Charge. +2. Inspect the response: + - **Shopify-source charge** — the Charge has `shopifyPaymentDetails` populated (set by `ShopifyPaymentChargeService` when Shopify POSTed `/payment` after the buyer chose Solana Pay). Settle via `POST /v1/x402/checkout/charge/`. Solana-only. + - **Non-Shopify (paylink-backed) charge** — no `shopifyPaymentDetails`; the Charge is linked to a regular paylink. Read the paylink ID from the Charge (`paylink._id` / `paylink.id`) and settle via `POST /v1/x402/checkout/` like a normal paylink payment. Multi-chain. -When the supplied ID is a UUID and you can't tell from context, default to the charge-resume endpoint. A 404 from the charge endpoint with `CHARGE_NOT_FOUND` is the signal that it really is a different ID type — only then fall back to checkout/deposit/paylink. +> **Do not blindly POST a UUID to `/v1/x402/checkout/charge/`.** If the Charge isn't Shopify-source the server returns 400 `CHARGE_NOT_SHOPIFY` with the message *"This endpoint only resumes Shopify-source charges. Use /x402/checkout/:paylinkId for generic agentic checkout."* Look up the Charge first; it's a single GET with no payment side-effect. ### Protocol overview @@ -215,7 +226,9 @@ mp x402 request \ --chain ``` -#### Charge resume (Shopify-source paylinks) +#### Charge resume — Shopify-source charges only + +Use this **only** after `GET /v1/charge/` confirms the Charge has `shopifyPaymentDetails`. For non-Shopify charges, route to `POST /v1/x402/checkout/{paylinkId}` instead (see "Routing a charge" above). When a Shopify buyer selects Solana Pay at checkout, Shopify POSTs `/payment` to the MoonPay Commerce backend, which creates an **in-flight Charge** with a `chargeToken` (UUID) and a price derived from the cart. An agent settles that existing Charge — instead of creating a parallel one — by hitting: @@ -231,6 +244,20 @@ mp x402 request \ - Currently **Solana-only**; EVM (Base / Polygon / Arbitrum / BSC) is on the roadmap. - On HTTP 200, settlement is fire-and-forget: the server creates the `PaylinkTx`, links it to the Charge, and triggers Shopify's `paymentSessionResolveMutation` to flip the order to paid in the merchant admin. The `txSignature` is **not** returned in the 200 body — confirm it via `GET /v1/charge/{chargeToken}` (see "Verifying payment status" below). +#### Charge → paylink fallback (non-Shopify charges) + +If `GET /v1/charge/` returns a Charge **without** `shopifyPaymentDetails`, read the paylink ID from the response (typically `paylink._id` on the populated Charge / `paylink.id` on the enriched response) and settle as a normal paylink payment: + +```bash +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/checkout/?payerAddress=" \ + --wallet \ + --chain +``` + +For dynamic-price paylinks the Charge already carries the price (`Charge.usdcAmount`); pass it through as `?amount=` so the user pays the same amount the original Charge requested. + #### Before running any `mp x402 request` Restate the **amount, chain, and recipient** to the user and wait for explicit confirmation. This spends real funds. @@ -309,8 +336,8 @@ If a chain's gas cost spikes beyond its threshold, the server excludes it from ` | **403** | `PAYLINK_SANCTIONED` | Abort — access restricted; do not retry | | **403** | Payer mismatch | Signed payer address doesn't match provisioned identity; verify wallet | | **404** | Paylink not found | Verify the paylink ID or URL is correct | -| **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — re-trigger the Shopify checkout to mint a new one | -| **400** | `CHARGE_NON_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source; use `/v1/x402/checkout/{paylinkId}` instead | +| **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — confirm via `GET /v1/charge/{chargeToken}`; if that 404s too, re-trigger the upstream checkout | +| **400** | `CHARGE_NOT_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source. **Don't retry the charge-resume endpoint** — fetch the Charge via `GET /v1/charge/{chargeToken}`, read `paylink._id`, and settle via `POST /v1/x402/checkout/{paylinkId}` instead | | **409** | Settlement in progress | A payment for this tx is already being settled; do not re-submit | | **409** | `CHARGE_ALREADY_SETTLED` (charge-resume) | The Charge is already paid — confirm via `GET /v1/charge/{chargeToken}` and surface the existing tx | | **410** | `CHARGE_EXPIRED` (charge-resume) | The Charge expired before settlement — re-trigger the Shopify checkout | From 58fa3d9d83b2a1dab2fa9b746340a1e1c2cef854 Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Fri, 8 May 2026 16:19:39 +0100 Subject: [PATCH 6/8] docs(moonpay-commerce): teach lookup-first charge routing with source/windowClosed/orphan-tx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous wording leaked Shopify-source framing into the charge section, so the agent pre-announced "Shopify-source charge-resume → Solana only" before the GET /v1/charge/ finished. The actual test charge was source: api with windowClosed: true (CoinMarketCap token boost), which the charge-resume endpoint cannot settle — and the agent had to backtrack mid-flow. Restructure the "Routing a charge" subsection so the agent must: - Run GET /v1/charge/ before announcing any routing decision. - Inspect three fields explicitly: shopifyPaymentDetails (the only field the charge-resume endpoint keys off), source (ChargeSource: api/paylink/ x402), and windowClosed. - Pick from a triadic decision table: Shopify-source open / non-Shopify open / windowClosed (any source). Document the orphan-PaylinkTx hazard: paying the underlying paylink does NOT settle the original chargeToken — paylinkTxs[] on the original Charge stays empty, and the issuing system (CMC, billing, Shopify, etc.) keeps seeing the token as unpaid. Surface this to the user before payment, and re-state it during the post-payment GET /v1/charge status check. Add a worked example using the actual CMC boost shape (source: api, windowClosed: true, paylink.id present). Other fixes: - Field name: paylink._id -> paylink.id (matches the response shape). - CHARGE_NOT_SHOPIFY error row now points at paylink.id (not paylink._id) and instructs surfacing the orphan warning. - Verifying-payment-status section now flags the empty-paylinkTxs case for paylink-fallback payments. - Charge-resume heading now lists the full precondition: shopifyPaymentDetails populated AND windowClosed:false AND paylinkTxs empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/moonpay-commerce/SKILL.md | 77 +++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index 1654691..e6c4ab7 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -112,18 +112,38 @@ A bare UUID is **never** the right input for `/checkout/{paylinkId}`, `/deposit/ |---|---|---| | Paylink URL or short slug (e.g. `app.hel.io/pay/`) | optional `GET /v1/paylink/{paylinkId}/public` | `POST /v1/x402/checkout/{paylinkId}` | | Deposit ID from a merchant | — | `POST /v1/x402/deposit/{depositId}` | -| **Bare UUID** or a `…/charge/` URL | **`GET /v1/charge/{chargeToken}` first** — read the Charge to decide where to settle | see "Routing a charge" below | +| **Bare UUID** or a `…/charge/` URL | **`GET /v1/charge/{chargeToken}` first** — read `shopifyPaymentDetails`, `source`, `windowClosed`, `paylink.id`, `paylinkTxs` | see "Routing a charge" below — **do not pre-announce "Shopify-source" or any routing choice before this GET completes** | #### Routing a charge -Charges come in **two flavors** and the right settle endpoint depends on which: +> **Do not announce a routing decision before you've fetched the Charge.** "This is a Shopify-source Charge → charge-resume" is the most common premature framing — and it's wrong for ~every non-Shopify charge (CoinMarketCap boost, generic API-issued charges, `paylink`-source charges, etc.). Route only after the GET, and narrate to the user in terms of what the metadata actually says. -1. `GET https://api.hel.io/v1/charge/` to fetch the Charge. -2. Inspect the response: - - **Shopify-source charge** — the Charge has `shopifyPaymentDetails` populated (set by `ShopifyPaymentChargeService` when Shopify POSTed `/payment` after the buyer chose Solana Pay). Settle via `POST /v1/x402/checkout/charge/`. Solana-only. - - **Non-Shopify (paylink-backed) charge** — no `shopifyPaymentDetails`; the Charge is linked to a regular paylink. Read the paylink ID from the Charge (`paylink._id` / `paylink.id`) and settle via `POST /v1/x402/checkout/` like a normal paylink payment. Multi-chain. +**Step 1 — fetch the Charge** (no payment side-effect, just a read): -> **Do not blindly POST a UUID to `/v1/x402/checkout/charge/`.** If the Charge isn't Shopify-source the server returns 400 `CHARGE_NOT_SHOPIFY` with the message *"This endpoint only resumes Shopify-source charges. Use /x402/checkout/:paylinkId for generic agentic checkout."* Look up the Charge first; it's a single GET with no payment side-effect. +```bash +curl -s "https://api.hel.io/v1/charge/" +``` + +**Step 2 — inspect three fields** to decide the route: + +| Field | Possible values | What it tells you | +|---|---|---| +| `shopifyPaymentDetails` | object \| absent | The **only** field the charge-resume endpoint actually keys off. If absent, charge-resume returns 400 `CHARGE_NOT_SHOPIFY`. | +| `source` | `"api"` / `"paylink"` / `"x402"` (`ChargeSource` enum) | Where the Charge originated. `api` is typical for backend-issued charges (CoinMarketCap boost, billing systems, Solana Pay on Shopify). Narrate this back to the user. | +| `windowClosed` | `true` / `false` | Whether the original deposit window has expired. `true` means the issuing system has stopped waiting for this token to settle — see the orphan warning below. | + +**Step 3 — pick the endpoint:** + +| Charge looks like | Settle via | +|---|---| +| `shopifyPaymentDetails` populated, `windowClosed: false` | `POST /v1/x402/checkout/charge/` (Solana-only) | +| `shopifyPaymentDetails` absent, `windowClosed: false`, has `paylink.id` | `POST /v1/x402/checkout/` — settles the underlying paylink. **The PaylinkTx is not linked back to this Charge token** (see warning). | +| `windowClosed: true` (any source) | `POST /v1/x402/checkout/` — but **the original Charge token will never be marked settled** (see warning). Confirm with the user before proceeding, or recommend the merchant re-issues a Charge with a fresh window. | +| `paylinkTxs` non-empty | Already settled — `GET /v1/charge/` returns the linked transaction; do not pay again. | + +> **Orphan-PaylinkTx warning.** Paying the underlying paylink (any time you're not on the charge-resume endpoint) creates a fresh on-chain payment for the same product/amount, but the new `PaylinkTx` is **not** linked back to the original Charge token. If the issuing system (CMC, a billing flow, etc.) is waiting on **that specific token** to flip to paid, it will continue to see it as unpaid. Surface this to the user before spending. Use `GET /v1/charge/` after payment to confirm — `paylinkTxs` on the original Charge will still be empty. + +> **`paylink.id` is the field name** in the `GET /v1/charge/` response (e.g. `"paylink":{"id":"69fc6d95...","template":"PAYLINK_V2",...}`), not `paylink._id`. ### Protocol overview @@ -226,9 +246,9 @@ mp x402 request \ --chain ``` -#### Charge resume — Shopify-source charges only +#### Charge resume — only after the GET confirms `shopifyPaymentDetails` -Use this **only** after `GET /v1/charge/` confirms the Charge has `shopifyPaymentDetails`. For non-Shopify charges, route to `POST /v1/x402/checkout/{paylinkId}` instead (see "Routing a charge" above). +Use this **only** after `GET /v1/charge/` returns a Charge with `shopifyPaymentDetails` populated **and** `windowClosed: false` **and** `paylinkTxs` empty. For every other case (no `shopifyPaymentDetails`, closed window, already settled), route to `POST /v1/x402/checkout/{paylinkId}` instead (see "Routing a charge" above) — and surface the orphan-PaylinkTx warning. When a Shopify buyer selects Solana Pay at checkout, Shopify POSTs `/payment` to the MoonPay Commerce backend, which creates an **in-flight Charge** with a `chargeToken` (UUID) and a price derived from the cart. An agent settles that existing Charge — instead of creating a parallel one — by hitting: @@ -244,19 +264,44 @@ mp x402 request \ - Currently **Solana-only**; EVM (Base / Polygon / Arbitrum / BSC) is on the roadmap. - On HTTP 200, settlement is fire-and-forget: the server creates the `PaylinkTx`, links it to the Charge, and triggers Shopify's `paymentSessionResolveMutation` to flip the order to paid in the merchant admin. The `txSignature` is **not** returned in the 200 body — confirm it via `GET /v1/charge/{chargeToken}` (see "Verifying payment status" below). -#### Charge → paylink fallback (non-Shopify charges) +#### Charge → paylink fallback (non-Shopify charges, including `windowClosed`) -If `GET /v1/charge/` returns a Charge **without** `shopifyPaymentDetails`, read the paylink ID from the response (typically `paylink._id` on the populated Charge / `paylink.id` on the enriched response) and settle as a normal paylink payment: +If `GET /v1/charge/` returns a Charge **without** `shopifyPaymentDetails` (typical when `source` is `api`, `paylink`, or `x402`), or with `windowClosed: true`, read the paylink ID from `paylink.id` in the response and settle as a normal paylink payment: ```bash mp x402 request \ --method POST \ - --url "https://api.hel.io/v1/x402/checkout/?payerAddress=" \ + --url "https://api.hel.io/v1/x402/checkout/?amount=&payerAddress=" \ --wallet \ --chain ``` -For dynamic-price paylinks the Charge already carries the price (`Charge.usdcAmount`); pass it through as `?amount=` so the user pays the same amount the original Charge requested. +- For dynamic-price paylinks, pass `Charge.usdcAmount` through as `?amount=` so the user pays the same amount the original Charge requested. +- **Always remind the user** that this creates a fresh PaylinkTx and does **not** retire the original `chargeToken`. If a third-party system (CMC, billing, Shopify, etc.) was watching that specific token, it will still see it as unpaid. Recommend the merchant re-issue a Charge with an open window if downstream reconciliation matters. + +**Worked example — API-source Charge with closed window** (e.g. CoinMarketCap token boost): + +```text +GET /v1/charge/6d0a1c57-... → + source: "api" + windowClosed: true + shopifyPaymentDetails: + paylink: { id: "69fc6d95...", template: "PAYLINK_V2", ... } + usdcAmount: "3100000" + paylinkTxs: [] +``` + +Routing decision: not Shopify-source (no `shopifyPaymentDetails`) AND window is closed → **paylink endpoint with orphan warning**: + +```bash +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/checkout/69fc6d95...?amount=3100000&payerAddress=" \ + --wallet moonfi-xzhang \ + --chain solana +``` + +After settlement, `GET /v1/charge/6d0a1c57-...` will still show `paylinkTxs: []` — the original Charge token remains orphaned. The freshly-created PaylinkTx lives on the paylink, not on this Charge. #### Before running any `mp x402 request` @@ -337,7 +382,7 @@ If a chain's gas cost spikes beyond its threshold, the server excludes it from ` | **403** | Payer mismatch | Signed payer address doesn't match provisioned identity; verify wallet | | **404** | Paylink not found | Verify the paylink ID or URL is correct | | **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — confirm via `GET /v1/charge/{chargeToken}`; if that 404s too, re-trigger the upstream checkout | -| **400** | `CHARGE_NOT_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source. **Don't retry the charge-resume endpoint** — fetch the Charge via `GET /v1/charge/{chargeToken}`, read `paylink._id`, and settle via `POST /v1/x402/checkout/{paylinkId}` instead | +| **400** | `CHARGE_NOT_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source (likely `source: 'api'` or `'paylink'`). **Don't retry the charge-resume endpoint** — fetch the Charge via `GET /v1/charge/{chargeToken}`, read `paylink.id`, and settle via `POST /v1/x402/checkout/{paylinkId}` instead. Surface the orphan-PaylinkTx warning before paying. | | **409** | Settlement in progress | A payment for this tx is already being settled; do not re-submit | | **409** | `CHARGE_ALREADY_SETTLED` (charge-resume) | The Charge is already paid — confirm via `GET /v1/charge/{chargeToken}` and surface the existing tx | | **410** | `CHARGE_EXPIRED` (charge-resume) | The Charge expired before settlement — re-trigger the Shopify checkout | @@ -361,12 +406,14 @@ curl -s "https://api.hel.io/v1/charge/" Use this to: -- Confirm the `PaylinkTx` was created and linked to the Charge. +- Confirm the `PaylinkTx` was created and linked to the Charge — check `paylinkTxs[]` (non-empty) on the Charge. - Read the on-chain `txSignature` for the charge-resume flow (where it isn't returned in the 200 body). - Detect settlement failures the 200 response can't surface (e.g. background sweep reverted, Shopify resolve failed). Recommended polling: poll once immediately after the 200, then every 5–10s for up to ~120s. Stop polling as soon as the Charge shows a paid/settled state with a transaction signature attached. Surface the settlement tx hash to the user only after this confirmation — do **not** rely on the HTTP 200 alone for the charge-resume flow. +> **If you fell back to the paylink endpoint** (because the Charge wasn't Shopify-source, or the window was closed), `paylinkTxs[]` on this specific Charge will stay **empty** even after a successful on-chain payment — the new `PaylinkTx` is attached to the paylink, not back to the original Charge token. State this plainly to the user: the on-chain transfer landed, but the original Charge token is orphaned and the issuing system will continue to see it as unpaid until they re-issue or manually reconcile. + ### Example: paying a paylink User says: _"Pay for the DEX Screener Token Boost at `https://api.hel.io/v1/x402/checkout/abc123` — use $10."_ From e2069fe32643a016e73d92db7e11e996b0202dc2 Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Tue, 12 May 2026 16:03:18 +0100 Subject: [PATCH 7/8] docs(moonpay-commerce): forbid silent paylink fallback when charge-resume fails A charge-resume failure (CHARGE_NOT_SHOPIFY, CHARGE_EXPIRED, CHARGE_NOT_FOUND, CHARGE_ALREADY_SETTLED, 5xx, on-chain revert) is NOT a signal to retry the same intent against POST /v1/x402/checkout/{paylink.id}. Settling the paylink mints a fresh Charge that has nothing to do with the original chargeToken: - The original Charge stays unpaid -- the issuing system (CMC, billing, Shopify) keeps seeing it as unsettled. - The on-chain transfer becomes an orphan PaylinkTx -- not linked back to the original Charge. - The user spent real funds for no reconciliation benefit. Add five "no auto-fallback" callouts to make this unambiguous: - "Routing a charge" -- general orphan + no-fallback callout next to the existing orphan-PaylinkTx warning. - "Charge resume" subsection -- on-failure bullet right next to the request example. - Error-table rows: CHARGE_NOT_FOUND, CHARGE_NOT_SHOPIFY, CHARGE_EXPIRED, CHARGE_ALREADY_SETTLED -- each row now states the no-silent-fallback rule and what to do instead (re-issue Charge with fresh window, get explicit user consent, etc.). Co-Authored-By: Claude Opus 4.7 (1M context) --- skills/moonpay-commerce/SKILL.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index e6c4ab7..1c6f12a 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -143,6 +143,8 @@ curl -s "https://api.hel.io/v1/charge/" > **Orphan-PaylinkTx warning.** Paying the underlying paylink (any time you're not on the charge-resume endpoint) creates a fresh on-chain payment for the same product/amount, but the new `PaylinkTx` is **not** linked back to the original Charge token. If the issuing system (CMC, a billing flow, etc.) is waiting on **that specific token** to flip to paid, it will continue to see it as unpaid. Surface this to the user before spending. Use `GET /v1/charge/` after payment to confirm — `paylinkTxs` on the original Charge will still be empty. +> **No auto-fallback on charge-resume failure.** If `POST /v1/x402/checkout/charge/` fails — for **any** reason (`CHARGE_NOT_SHOPIFY`, `CHARGE_EXPIRED`, `CHARGE_ALREADY_SETTLED`, 5xx, on-chain revert, etc.) — **do not silently retry against `/v1/x402/checkout/`.** Settling the paylink mints a *fresh* Charge that has nothing to do with the original `chargeToken`: the original stays unpaid (issuing system still sees it unsettled), and you've also spent the user's funds on an orphan transaction. Stop, report the failure to the user along with the orphan-PaylinkTx implication, and only proceed with the paylink endpoint if the user **explicitly** accepts that trade-off. + > **`paylink.id` is the field name** in the `GET /v1/charge/` response (e.g. `"paylink":{"id":"69fc6d95...","template":"PAYLINK_V2",...}`), not `paylink._id`. ### Protocol overview @@ -263,6 +265,7 @@ mp x402 request \ - The amount is **derived from the Charge** (`Charge.usdcAmount`) — do **not** append `?amount=` for charge-resume requests. - Currently **Solana-only**; EVM (Base / Polygon / Arbitrum / BSC) is on the roadmap. - On HTTP 200, settlement is fire-and-forget: the server creates the `PaylinkTx`, links it to the Charge, and triggers Shopify's `paymentSessionResolveMutation` to flip the order to paid in the merchant admin. The `txSignature` is **not** returned in the 200 body — confirm it via `GET /v1/charge/{chargeToken}` (see "Verifying payment status" below). +- **On failure, stop — do not auto-fall-back.** Any non-200 response from the charge-resume endpoint means the original Charge has not been settled. Silently retrying the same intent against `/v1/x402/checkout/` does **not** rescue the original Charge — it mints a separate, orphan transaction while the original `chargeToken` stays unpaid. Report the error, surface the orphan-PaylinkTx implication, and only proceed with the paylink endpoint if the user explicitly accepts that the original Charge will remain unpaid (and the issuing system will need to reconcile manually or re-issue). #### Charge → paylink fallback (non-Shopify charges, including `windowClosed`) @@ -381,11 +384,11 @@ If a chain's gas cost spikes beyond its threshold, the server excludes it from ` | **403** | `PAYLINK_SANCTIONED` | Abort — access restricted; do not retry | | **403** | Payer mismatch | Signed payer address doesn't match provisioned identity; verify wallet | | **404** | Paylink not found | Verify the paylink ID or URL is correct | -| **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — confirm via `GET /v1/charge/{chargeToken}`; if that 404s too, re-trigger the upstream checkout | -| **400** | `CHARGE_NOT_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source (likely `source: 'api'` or `'paylink'`). **Don't retry the charge-resume endpoint** — fetch the Charge via `GET /v1/charge/{chargeToken}`, read `paylink.id`, and settle via `POST /v1/x402/checkout/{paylinkId}` instead. Surface the orphan-PaylinkTx warning before paying. | +| **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — confirm via `GET /v1/charge/{chargeToken}`; if that 404s too, re-trigger the upstream checkout. **Do not auto-fall-back to the paylink endpoint** — there's no paylink to derive without a Charge. | +| **400** | `CHARGE_NOT_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source (likely `source: 'api'` or `'paylink'`). **Do not silently retry against the paylink endpoint** — settling the paylink mints a fresh Charge that doesn't satisfy the original `chargeToken`. Report the failure, surface the orphan-PaylinkTx warning, and only proceed with `POST /v1/x402/checkout/{paylink.id}` after the user explicitly accepts that the original Charge will remain unpaid. | | **409** | Settlement in progress | A payment for this tx is already being settled; do not re-submit | -| **409** | `CHARGE_ALREADY_SETTLED` (charge-resume) | The Charge is already paid — confirm via `GET /v1/charge/{chargeToken}` and surface the existing tx | -| **410** | `CHARGE_EXPIRED` (charge-resume) | The Charge expired before settlement — re-trigger the Shopify checkout | +| **409** | `CHARGE_ALREADY_SETTLED` (charge-resume) | The Charge is already paid — confirm via `GET /v1/charge/{chargeToken}` and surface the existing tx. **Do not** pay the paylink "again to be safe" — that would double-charge. | +| **410** | `CHARGE_EXPIRED` (charge-resume) | The Charge expired before settlement. **Do not auto-fall-back to the paylink endpoint** — that mints a fresh Charge, leaves the original unpaid, and orphans your transaction. Ask the merchant to re-issue a Charge with a fresh window, or get explicit user consent for the orphan trade-off. | | **429** | Rate limited | Back off — 10 requests per 60 seconds per IP | ### Settlement behavior From 97282643958a88d7d981ade8c3422ef6a3136dac Mon Sep 17 00:00:00 2001 From: Hyde Zhang Date: Tue, 12 May 2026 17:05:40 +0100 Subject: [PATCH 8/8] docs(moonpay-commerce): route any Charge via charge-resume after HCORE-279 The backend (PR moonpay/backend#4253, commit dbfd30880) dropped the Shopify-source restriction from POST /v1/x402/checkout/charge/:token: the endpoint now settles any in-flight Charge regardless of source and links the PaylinkTx back to the original Charge token. The skill still gated this endpoint on shopifyPaymentDetails and pushed agents into a paylink-direct fallback for any non-Shopify Charge. A production CMC-boost test (source: api) followed that fallback verbatim: the on-chain transfer landed on the underlying paylink, the original Charge token stayed orphaned (paylinkTx: null), and the issuing system had no signal that the boost was paid. Changes: - Routing-a-charge decision tree: charge-resume is the default for any non-expired, non-settled Charge regardless of source - Expired Charges: stop and ask the merchant to re-issue, do not auto-route to paylink-direct - Charge-resume section: drop Shopify-only framing - Paylink-direct demoted to an explicit-consent escape hatch - Worked example: replaced with a charge-resume success walkthrough on a source:api / windowClosed:false Charge - Removed dead CHARGE_NOT_SHOPIFY error row - Verifying-payment-status: clarified paylinkTxs non-empty is the canonical paid signal for issuing systems to poll --- skills/moonpay-commerce/SKILL.md | 99 +++++++++++++++----------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/skills/moonpay-commerce/SKILL.md b/skills/moonpay-commerce/SKILL.md index 1c6f12a..244d3b7 100644 --- a/skills/moonpay-commerce/SKILL.md +++ b/skills/moonpay-commerce/SKILL.md @@ -112,11 +112,11 @@ A bare UUID is **never** the right input for `/checkout/{paylinkId}`, `/deposit/ |---|---|---| | Paylink URL or short slug (e.g. `app.hel.io/pay/`) | optional `GET /v1/paylink/{paylinkId}/public` | `POST /v1/x402/checkout/{paylinkId}` | | Deposit ID from a merchant | — | `POST /v1/x402/deposit/{depositId}` | -| **Bare UUID** or a `…/charge/` URL | **`GET /v1/charge/{chargeToken}` first** — read `shopifyPaymentDetails`, `source`, `windowClosed`, `paylink.id`, `paylinkTxs` | see "Routing a charge" below — **do not pre-announce "Shopify-source" or any routing choice before this GET completes** | +| **Bare UUID** or a `…/charge/` URL | **`GET /v1/charge/{chargeToken}` first** — read `paylinkTxs`, `windowClosed`, `paylink.id` | see "Routing a charge" below — charge-resume is the default for any non-expired, non-settled Charge **regardless of `source`** | #### Routing a charge -> **Do not announce a routing decision before you've fetched the Charge.** "This is a Shopify-source Charge → charge-resume" is the most common premature framing — and it's wrong for ~every non-Shopify charge (CoinMarketCap boost, generic API-issued charges, `paylink`-source charges, etc.). Route only after the GET, and narrate to the user in terms of what the metadata actually says. +> **The intent is always: settle the original Charge token so `GET /v1/charge/` flips to paid.** `POST /v1/x402/checkout/charge/` is the only path that does this — it creates the `PaylinkTx` *and* links it back to the Charge (`paylinkTxs` becomes non-empty, `paylinkTx` becomes non-null). It works for every `source` — `shopify`, `api` (e.g. CoinMarketCap boost), `paylink`, `x402`. There is no longer a "Shopify-only" restriction on this endpoint. **Step 1 — fetch the Charge** (no payment side-effect, just a read): @@ -124,28 +124,15 @@ A bare UUID is **never** the right input for `/checkout/{paylinkId}`, `/deposit/ curl -s "https://api.hel.io/v1/charge/" ``` -**Step 2 — inspect three fields** to decide the route: +**Step 2 — decision rules, in order:** -| Field | Possible values | What it tells you | -|---|---|---| -| `shopifyPaymentDetails` | object \| absent | The **only** field the charge-resume endpoint actually keys off. If absent, charge-resume returns 400 `CHARGE_NOT_SHOPIFY`. | -| `source` | `"api"` / `"paylink"` / `"x402"` (`ChargeSource` enum) | Where the Charge originated. `api` is typical for backend-issued charges (CoinMarketCap boost, billing systems, Solana Pay on Shopify). Narrate this back to the user. | -| `windowClosed` | `true` / `false` | Whether the original deposit window has expired. `true` means the issuing system has stopped waiting for this token to settle — see the orphan warning below. | - -**Step 3 — pick the endpoint:** - -| Charge looks like | Settle via | -|---|---| -| `shopifyPaymentDetails` populated, `windowClosed: false` | `POST /v1/x402/checkout/charge/` (Solana-only) | -| `shopifyPaymentDetails` absent, `windowClosed: false`, has `paylink.id` | `POST /v1/x402/checkout/` — settles the underlying paylink. **The PaylinkTx is not linked back to this Charge token** (see warning). | -| `windowClosed: true` (any source) | `POST /v1/x402/checkout/` — but **the original Charge token will never be marked settled** (see warning). Confirm with the user before proceeding, or recommend the merchant re-issues a Charge with a fresh window. | -| `paylinkTxs` non-empty | Already settled — `GET /v1/charge/` returns the linked transaction; do not pay again. | +1. **`paylinkTxs` non-empty** → Charge is already settled. Do not pay again. Surface the linked transaction from the GET response. +2. **`windowClosed: true`** → Charge expired. **Stop.** Tell the user the original window has closed and ask the merchant to re-issue a Charge with a fresh window. Do **not** auto-route to the paylink endpoint — that orphans the payment (see "Paylink-direct as last resort"). Only proceed with paylink-direct if the user has explicitly consented to an orphan payment after being told the merchant cannot re-issue. +3. **Otherwise** → `POST /v1/x402/checkout/charge/` (currently Solana-only; EVM on the roadmap). Settles the Charge in place — works for any `source`. -> **Orphan-PaylinkTx warning.** Paying the underlying paylink (any time you're not on the charge-resume endpoint) creates a fresh on-chain payment for the same product/amount, but the new `PaylinkTx` is **not** linked back to the original Charge token. If the issuing system (CMC, a billing flow, etc.) is waiting on **that specific token** to flip to paid, it will continue to see it as unpaid. Surface this to the user before spending. Use `GET /v1/charge/` after payment to confirm — `paylinkTxs` on the original Charge will still be empty. +> **No auto-fallback on charge-resume failure.** If `POST /v1/x402/checkout/charge/` fails — for **any** reason (`CHARGE_EXPIRED`, `CHARGE_ALREADY_SETTLED`, `CHARGE_NOT_FOUND`, 5xx, on-chain revert, etc.) — **do not silently retry against `/v1/x402/checkout/`.** Settling the paylink mints a *fresh* on-chain payment that is not linked to the original `chargeToken`: the original stays unpaid (issuing system still sees it unsettled), and you've spent the user's funds on an orphan transaction. Stop, report the failure to the user, and only consider "Paylink-direct as last resort" with explicit user consent. -> **No auto-fallback on charge-resume failure.** If `POST /v1/x402/checkout/charge/` fails — for **any** reason (`CHARGE_NOT_SHOPIFY`, `CHARGE_EXPIRED`, `CHARGE_ALREADY_SETTLED`, 5xx, on-chain revert, etc.) — **do not silently retry against `/v1/x402/checkout/`.** Settling the paylink mints a *fresh* Charge that has nothing to do with the original `chargeToken`: the original stays unpaid (issuing system still sees it unsettled), and you've also spent the user's funds on an orphan transaction. Stop, report the failure to the user along with the orphan-PaylinkTx implication, and only proceed with the paylink endpoint if the user **explicitly** accepts that trade-off. - -> **`paylink.id` is the field name** in the `GET /v1/charge/` response (e.g. `"paylink":{"id":"69fc6d95...","template":"PAYLINK_V2",...}`), not `paylink._id`. +> **`paylink.id` is the field name** in the `GET /v1/charge/` response (e.g. `"paylink":{"id":"69fc6d95...","template":"PAYLINK_V2",...}`), not `paylink._id`. You only need it for the explicit orphan escape hatch below. ### Protocol overview @@ -248,11 +235,9 @@ mp x402 request \ --chain ``` -#### Charge resume — only after the GET confirms `shopifyPaymentDetails` - -Use this **only** after `GET /v1/charge/` returns a Charge with `shopifyPaymentDetails` populated **and** `windowClosed: false` **and** `paylinkTxs` empty. For every other case (no `shopifyPaymentDetails`, closed window, already settled), route to `POST /v1/x402/checkout/{paylinkId}` instead (see "Routing a charge" above) — and surface the orphan-PaylinkTx warning. +#### Charge resume — the default path for any Charge -When a Shopify buyer selects Solana Pay at checkout, Shopify POSTs `/payment` to the MoonPay Commerce backend, which creates an **in-flight Charge** with a `chargeToken` (UUID) and a price derived from the cart. An agent settles that existing Charge — instead of creating a parallel one — by hitting: +Use this whenever the Charge is not expired (`windowClosed: false`) and not already settled (`paylinkTxs` empty), **regardless of `source`**. It works for Shopify Solana Pay, API-issued charges (CoinMarketCap token boost, billing systems, x402-issued charges), and `paylink`-source charges alike. The endpoint creates the `PaylinkTx` and links it back to the Charge so `GET /v1/charge/` flips to a paid state. ```bash mp x402 request \ @@ -264,47 +249,56 @@ mp x402 request \ - The amount is **derived from the Charge** (`Charge.usdcAmount`) — do **not** append `?amount=` for charge-resume requests. - Currently **Solana-only**; EVM (Base / Polygon / Arbitrum / BSC) is on the roadmap. -- On HTTP 200, settlement is fire-and-forget: the server creates the `PaylinkTx`, links it to the Charge, and triggers Shopify's `paymentSessionResolveMutation` to flip the order to paid in the merchant admin. The `txSignature` is **not** returned in the 200 body — confirm it via `GET /v1/charge/{chargeToken}` (see "Verifying payment status" below). -- **On failure, stop — do not auto-fall-back.** Any non-200 response from the charge-resume endpoint means the original Charge has not been settled. Silently retrying the same intent against `/v1/x402/checkout/` does **not** rescue the original Charge — it mints a separate, orphan transaction while the original `chargeToken` stays unpaid. Report the error, surface the orphan-PaylinkTx implication, and only proceed with the paylink endpoint if the user explicitly accepts that the original Charge will remain unpaid (and the issuing system will need to reconcile manually or re-issue). - -#### Charge → paylink fallback (non-Shopify charges, including `windowClosed`) - -If `GET /v1/charge/` returns a Charge **without** `shopifyPaymentDetails` (typical when `source` is `api`, `paylink`, or `x402`), or with `windowClosed: true`, read the paylink ID from `paylink.id` in the response and settle as a normal paylink payment: - -```bash -mp x402 request \ - --method POST \ - --url "https://api.hel.io/v1/x402/checkout/?amount=&payerAddress=" \ - --wallet \ - --chain -``` - -- For dynamic-price paylinks, pass `Charge.usdcAmount` through as `?amount=` so the user pays the same amount the original Charge requested. -- **Always remind the user** that this creates a fresh PaylinkTx and does **not** retire the original `chargeToken`. If a third-party system (CMC, billing, Shopify, etc.) was watching that specific token, it will still see it as unpaid. Recommend the merchant re-issue a Charge with an open window if downstream reconciliation matters. +- On HTTP 200, settlement is fire-and-forget: the server creates the `PaylinkTx` and links it to the Charge. For Shopify-source charges it also triggers Shopify's `paymentSessionResolveMutation` to flip the order to paid in the merchant admin. The `txSignature` is **not** returned in the 200 body — confirm it via `GET /v1/charge/{chargeToken}` (see "Verifying payment status" below). +- **On failure, stop — do not auto-fall-back.** Any non-200 response (`CHARGE_EXPIRED`, `CHARGE_ALREADY_SETTLED`, `CHARGE_NOT_FOUND`, 5xx, on-chain revert) means the original Charge has not been settled. Silently retrying against `/v1/x402/checkout/` does **not** rescue the original Charge — it mints a separate, orphan transaction while the original `chargeToken` stays unpaid. Report the error and only consider "Paylink-direct as last resort" below with explicit user consent. -**Worked example — API-source Charge with closed window** (e.g. CoinMarketCap token boost): +**Worked example — API-source Charge with an open window** (e.g. CoinMarketCap token boost): ```text -GET /v1/charge/6d0a1c57-... → +GET /v1/charge/b2b41023-... → source: "api" - windowClosed: true + windowClosed: false shopifyPaymentDetails: paylink: { id: "69fc6d95...", template: "PAYLINK_V2", ... } usdcAmount: "3100000" paylinkTxs: [] ``` -Routing decision: not Shopify-source (no `shopifyPaymentDetails`) AND window is closed → **paylink endpoint with orphan warning**: +Routing decision: not expired, not settled → **charge-resume**. Source does not matter. ```bash mp x402 request \ --method POST \ - --url "https://api.hel.io/v1/x402/checkout/69fc6d95...?amount=3100000&payerAddress=" \ + --url "https://api.hel.io/v1/x402/checkout/charge/b2b41023-...?payerAddress=" \ --wallet moonfi-xzhang \ --chain solana ``` -After settlement, `GET /v1/charge/6d0a1c57-...` will still show `paylinkTxs: []` — the original Charge token remains orphaned. The freshly-created PaylinkTx lives on the paylink, not on this Charge. +After the 200, poll the Charge: + +```bash +curl -s "https://api.hel.io/v1/charge/b2b41023-..." | jq '{paylinkTx, paylinkTxs}' +``` + +Expected result within ~10s — `paylinkTxs` populated and `paylinkTx` (singular) non-null. That is the canonical "paid" signal that the issuing system (CMC, billing, etc.) polls for. + +#### Paylink-direct as last resort (orphan payment) + +> **Do not enter this path automatically.** It exists only as an explicit escape hatch when (a) charge-resume failed with an irrecoverable error (typically `CHARGE_EXPIRED`), (b) the merchant has confirmed they cannot re-issue the Charge, and (c) the user has been told the consequences and explicitly consented to an orphan payment. + +If those three conditions are met, read the paylink ID from `paylink.id` in the GET response and pay the underlying paylink: + +```bash +mp x402 request \ + --method POST \ + --url "https://api.hel.io/v1/x402/checkout/?amount=&payerAddress=" \ + --wallet \ + --chain +``` + +- For dynamic-price paylinks, pass `Charge.usdcAmount` through as `?amount=` so the user pays the same amount the original Charge requested. +- **Orphan-PaylinkTx consequence.** This creates a fresh on-chain payment for the same product/amount, but the new `PaylinkTx` is **not** linked back to the original Charge token. `GET /v1/charge/` will continue to show `paylinkTxs: []` and `paylinkTx: null` even after a successful on-chain transfer. Any issuing system watching that specific token (CMC, billing, Shopify) will continue to see the Charge as unpaid — the merchant must reconcile manually. +- Always state the orphan consequence to the user **before** spending and require explicit confirmation. Default response when in doubt is "stop and ask the merchant to re-issue", not "pay the paylink anyway". #### Before running any `mp x402 request` @@ -384,11 +378,10 @@ If a chain's gas cost spikes beyond its threshold, the server excludes it from ` | **403** | `PAYLINK_SANCTIONED` | Abort — access restricted; do not retry | | **403** | Payer mismatch | Signed payer address doesn't match provisioned identity; verify wallet | | **404** | Paylink not found | Verify the paylink ID or URL is correct | -| **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — confirm via `GET /v1/charge/{chargeToken}`; if that 404s too, re-trigger the upstream checkout. **Do not auto-fall-back to the paylink endpoint** — there's no paylink to derive without a Charge. | -| **400** | `CHARGE_NOT_SHOPIFY` (charge-resume) | The Charge isn't Shopify-source (likely `source: 'api'` or `'paylink'`). **Do not silently retry against the paylink endpoint** — settling the paylink mints a fresh Charge that doesn't satisfy the original `chargeToken`. Report the failure, surface the orphan-PaylinkTx warning, and only proceed with `POST /v1/x402/checkout/{paylink.id}` after the user explicitly accepts that the original Charge will remain unpaid. | +| **404** | `CHARGE_NOT_FOUND` (charge-resume) | The `chargeToken` does not match an in-flight Charge — confirm via `GET /v1/charge/{chargeToken}`; if that 404s too, re-trigger the upstream checkout. **Stop. Do not auto-fall-back to the paylink endpoint.** | | **409** | Settlement in progress | A payment for this tx is already being settled; do not re-submit | | **409** | `CHARGE_ALREADY_SETTLED` (charge-resume) | The Charge is already paid — confirm via `GET /v1/charge/{chargeToken}` and surface the existing tx. **Do not** pay the paylink "again to be safe" — that would double-charge. | -| **410** | `CHARGE_EXPIRED` (charge-resume) | The Charge expired before settlement. **Do not auto-fall-back to the paylink endpoint** — that mints a fresh Charge, leaves the original unpaid, and orphans your transaction. Ask the merchant to re-issue a Charge with a fresh window, or get explicit user consent for the orphan trade-off. | +| **410** | `CHARGE_EXPIRED` (charge-resume) | The Charge expired before settlement. **Stop. Ask the merchant to re-issue a Charge with a fresh window.** Do NOT auto-fall-back to the paylink endpoint — that orphans the payment. The user may explicitly invoke "Paylink-direct as last resort" only if the merchant cannot re-issue and they consent to an orphan transaction. | | **429** | Rate limited | Back off — 10 requests per 60 seconds per IP | ### Settlement behavior @@ -413,9 +406,11 @@ Use this to: - Read the on-chain `txSignature` for the charge-resume flow (where it isn't returned in the 200 body). - Detect settlement failures the 200 response can't surface (e.g. background sweep reverted, Shopify resolve failed). +The canonical success signal for any charge-resume flow is `paylinkTxs` non-empty (equivalently, `paylinkTx` non-null) on `GET /v1/charge/`. This is the field issuing systems (CMC, billing, Shopify) actually poll. Reporting "paid" before that flips is premature. + Recommended polling: poll once immediately after the 200, then every 5–10s for up to ~120s. Stop polling as soon as the Charge shows a paid/settled state with a transaction signature attached. Surface the settlement tx hash to the user only after this confirmation — do **not** rely on the HTTP 200 alone for the charge-resume flow. -> **If you fell back to the paylink endpoint** (because the Charge wasn't Shopify-source, or the window was closed), `paylinkTxs[]` on this specific Charge will stay **empty** even after a successful on-chain payment — the new `PaylinkTx` is attached to the paylink, not back to the original Charge token. State this plainly to the user: the on-chain transfer landed, but the original Charge token is orphaned and the issuing system will continue to see it as unpaid until they re-issue or manually reconcile. +> If you took the "Paylink-direct as last resort" path, `paylinkTxs[]` on this specific Charge stays **empty** even after a successful on-chain payment — see that section for the orphan consequence to surface to the user. ### Example: paying a paylink