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
- 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.
- Open
/seller. Pick the agent that should receive payments.
- Fill in:
- Label — what the explorer / payer sees.
- Description (optional) — longer context.
- Method —
GET 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.
- 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 URL | Notes |
|---|
solana-devnet | https://devnet-facilitator.leash.market | Leash-operated devnet facilitator. Gas-sponsored; Token-2022 + exact SVM scheme. |
solana-mainnet | https://facilitator.leash.market | Leash-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:
| Hook | When you’d use it |
|---|
webhook_url | Hand the response to a downstream agent without making the buyer poll. |
wrap_receipt | Caller wants the receipt inline (e.g. autonomous agents chaining calls). |
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.