The Leash playground ships a Payment-Link Builder — the closest thing to “Stripe Payment Links” for x402 on Solana. You don’t need to host a Hono server, manage facilitator wiring, or write any code; the runner stores your EndpointV1 descriptors and the Next.js app exposes them at https://<host>/x/<id> as fully-functional x402 paywalls served by @leash/seller-kit.

Walkthrough

  1. Mint an agent if you don’t already have one — /agents/new in the playground. The agent’s Asset Signer PDA becomes the on-chain payTo for every payment link you anchor under it.
  2. Open /seller. Pick the agent that should receive payments.
  3. Fill in:
    • Label — what the explorer / payer sees.
    • Description (optional) — longer context.
    • MethodGET or POST.
    • Settlement currency — primary stable the price is quoted in (USDC, USDT, or USDG).
    • Also accept (optional) — extra stables buyers may pay in; each becomes another row in x402 accepts[] at the same atomic amount.
    • Price — dollar-style ($0.001), suffixed (0.01 USDC), or bare decimal (uses settlement currency).
    • Custom slug (optional) — the bit after /x/.
    • Response body — JSON returned verbatim after settlement.
    • Post-payment hooks (all optional, see below):
      • webhook_url — fire-and-forget POST of { payment, response }.
      • wrap_receipt — embed the receipt envelope in the JSON body.
  4. Click Create payment link. The runner stores the descriptor and the live URL appears in the right column. Copy it, share it, embed it.

Facilitator

When someone pays a link, an x402 facilitator verifies the buyer’s signed SPL transfer and broadcasts it as the Solana fee payer (so the buyer only spends stables, not SOL for fees). The facilitator URL is stamped on every ReceiptV1.facilitator and appears in payment-link discovery JSON (the docs field points here).

Built-in defaults by network

@leash/facilitator is now the default for both networks:
Network (solana-*)Default URLNotes
solana-devnethttps://devnet-facilitator.leash.marketLeash-operated devnet facilitator. Gas-sponsored; Token-2022 + exact SVM scheme.
solana-mainnethttps://facilitator.leash.marketLeash-operated mainnet facilitator. Gas-sponsored; same wire contract.

Other facilitators (fully supported)

You are not locked in to the Leash facilitators. Any HTTPS base URL that implements x402’s GET /supported, POST /verify, POST /settle and advertises the exact SVM scheme for your cluster works as a drop-in replacement. Override via:
  • facilitator: 'https://…' on createBuyer / createSeller, or
  • LEASH_FACILITATOR_URL / NEXT_PUBLIC_LEASH_FACILITATOR_URL / LEASH_API_FACILITATOR_URL (depending on surface).
Third-party examples: devnet-facilitator.leash.market (devnet), facilitator.leash.market (mainnet), community deployments, or your own self-hosted instance. Resolution order: defaultFacilitatorFor.

Playground (apps/web)

  • Browser (/buyer) — set NEXT_PUBLIC_LEASH_FACILITATOR_URL (inlined at build time).
  • Server routes (/x/<id>, /api/seller/echo) — also read LEASH_FACILITATOR_URL.
Both are optional; if unset, the app uses the same defaults as the table above. Point both at http://localhost:8787 when running @leash/facilitator-app locally.

Hosted API (api.leash.market)

Payment links served at https://api.leash.market/x/{id} use whatever the API operator configured as LEASH_API_FACILITATOR_URL on the server (falls back to the public devnet default if unset). That is independent of the playground env vars.

Self-hosting or using a third-party facilitator (optional)

The Leash defaults work for most users. If you need a custom settlement path — self-hosted @leash/facilitator, a community host, or a third-party provider like devnet-facilitator.leash.market / facilitator.leash.market — pass a facilitator: override or set the env var. The wire contract is the same across all of them. See Run a Leash facilitator.

Post-payment hooks

Once a buyer pays, the seller has two orthogonal ways to react:
HookWhen you’d use it
webhook_urlHand the response to a downstream agent without making the buyer poll.
wrap_receiptCaller wants the receipt inline (e.g. autonomous agents chaining calls).

X-Leash-* response headers (always set)

Every successful settlement returns these headers — useful even if you opt out of all the hooks above:
X-Leash-Tx-Sig:           <solana tx signature>
X-Leash-Receipt-Hash:     <canonical receipt hash>
X-Leash-Agent:            <owner_agent core mint>
X-Leash-Tx-Explorer:      https://solscan.io/tx/<sig>?cluster=devnet
X-Leash-Agent-Explorer:   https://yourapp/agents/<mint>
access-control-expose-headers is set so browser clients can read them.

webhook_url — fire-and-forget agent-to-agent

Set webhook_url: "https://your-agent.com/leash-callback" and after each settlement the runner will POST a versioned WebhookPayload:
{
  "v": "0.1",
  "kind": "leash.payment.settled",
  "ts": "2026-04-22T18:30:00.000Z",
  "payment": {
    "tx_sig": "5J7…",
    "receipt_hash": "leash:receipt:v1:…",
    "agent": "33Qv…",
    "network": "solana-devnet",
    "amount": { "amount": "1000", "currency": "USDC" },
    "facilitator": "https://devnet-facilitator.leash.market",
    "explorer": {
      "tx": "https://solscan.io/tx/5J7…?cluster=devnet",
      "agent": "https://yourapp/agents/33Qv…",
    },
  },
  "response": "<your endpoint response body>",
}
It’s fire-and-forget — webhook outages don’t surface as the buyer’s HTTP error. The receipt feed under /a/<agent>/receipts is the canonical source of truth. Downstream agents that receive these payloads should validate them with parseWebhookPayload from @leash/core rather than parsing the JSON by hand:
import { parseWebhookPayload } from '@leash/core';

export async function POST(req: Request) {
  const body = await req.json();
  const event = parseWebhookPayload(body); // throws on shape mismatch
  // event.payment.tx_sig, event.payment.amount.currency, event.response …
}
Per-call override: buyers can add an x-leash-callback: <url> request header (LEASH_CALLBACK_HEADER from @leash/core) to fire one of their own webhooks. This is on top of the seller’s webhook_url (both will be called) — so the seller and buyer can each pipe the response into different downstream agents.

wrap_receipt — embed the receipt in the body

Set wrap_receipt: true and JSON responses become:
{
  "data": <your original response body>,
  "_leash": {
    "tx_sig": "5J7…",
    "receipt_hash": "leash:receipt:v1:…",
    "agent": "33Qv…",
    "explorer": {
      "tx": "https://solscan.io/tx/5J7…?cluster=devnet",
      "agent": "https://yourapp/agents/33Qv…"
    }
  }
}
Use this when callers (especially autonomous agents) shouldn’t have to read response headers separately. Ignored when the response isn’t JSON. The shareable URL is a real x402 paywall:
# Probe (no payment) → 402 + PAYMENT-REQUIRED
curl -i -X POST https://yourapp/x/<id> -d '{"hello":"leash"}'

# Pay → 200 + your response template + a real Solana tx_sig
# (use @leash/buyer-kit or the autonomous-agent cockpit)

# Discovery (browser-friendly) — open the link directly in a browser to get
# a JSON descriptor of the offer. Works for any endpoint method without
# triggering a payment, useful for sharing a link in chat or docs.
curl https://yourapp/x/<id>
# {
#   "ok": true,
#   "kind": "leash.payment-link",
#   "endpoint": {
#     "id":"…",
#     "method":"POST",
#     "price":"$0.001",
#     "currency":"USDC",
#     "accepts_currencies":["USDG"],
#     "payTo":"…",
#     …
#   },
#   "facilitator": "https://devnet-facilitator.leash.market"
# }
The discovery view only fires for GET probes that don’t carry an X-PAYMENT header on a method-mismatched route. A buyer that sends the configured method with payment headers always hits the real x402 middleware and pays as expected.

What the runner stores

{
  "v": "0.1",
  "id": "hello-world",
  "label": "Hello world echo",
  "owner_agent": "33QvAYjEiK8UMrmpy3LW6W8v2wpPMahnw7Jvr7JpeQrR",
  "method": "POST",
  "price": "$0.001",
  "currency": "USDC",
  "accepts_currencies": ["USDG"],
  "network": "solana-devnet",
  "response": {
    "status": 200,
    "mimeType": "application/json",
    "body": { "ok": true, "hello": "leash" },
  },
  "webhook_url": "https://your-agent.com/cb", // optional
  "wrap_receipt": true, // optional
  "created_at": "2026-04-21T11:06:20.906Z",
  "updated_at": "2026-04-21T11:06:20.906Z",
}
Schema: EndpointV1. The runner persists this to ./.leash/endpoints.jsonl (override with LEASH_RUNNER_DATA) so the links survive restarts.

Programmatic CRUD

# List
curl http://localhost:8787/endpoints
curl 'http://localhost:8787/endpoints?owner_agent=<mint>'

# Create
curl -X POST http://localhost:8787/endpoints \
  -H 'content-type: application/json' \
  -d '{"label":"Echo","owner_agent":"<mint>","method":"POST","price":"$0.001",
       "currency":"USDC","accepts_currencies":["USDG"],
       "response":{"status":200,"body":{"ok":true}}}'

# Delete
curl -X DELETE http://localhost:8787/endpoints/hello-world
The Next.js proxy at /api/endpoints forwards these same routes for browser callers (CORS-safe).

Why this is “real” x402

/x/[id] mounts @leash/seller-kit’s createSeller with the EndpointV1 you registered. The middleware:
  • Returns 402 PAYMENT REQUIRED plus a base64 PAYMENT-REQUIRED header on every unpaid request.
  • Resolves the seller payTo to your agent’s Asset Signer PDA — no extra key, no wallet to fund.
  • Settles via devnet-facilitator.leash.market by default (Leash-operated, gas-sponsored). Override with LEASH_FACILITATOR_URL or facilitator: option.
  • Emits an earn ReceiptV1 to the runner under /a/<owner_agent>/receipts — so the agent’s profile feed updates in real time.
Anyone with a @leash/buyer-kit client (or any @x402/fetch consumer) can pay it.