@leash/buyer-kit) hit it, get a 402 with
PAYMENT-REQUIRED, sign an SPL TransferChecked, replay the request,
and your configured response body comes back. Funds settle directly
into the owner agent’s Asset Signer PDA (the same treasury
balance you see on GET /v1/agents/{mint}/treasury/balances).
You don’t need to host anything. You don’t need to run a server. The
API is the seller for you.
When to use payment links vs the SDK
| You want… | Use this |
|---|---|
| Charge per call on a JSON endpoint without writing code | Payment links (POST /v1/payment-links) |
| Charge per call on a route in your existing Hono app | @leash/seller-kit |
| Charge per call from Python / Go / Rust on your own host | Edge mode (see Monetise an API) |
| Build a one-off “Pay $X to download Y” share link | Payment links (sharable URL out of the box) |
createSeller from @leash/seller-kit). The difference is purely
where the route lives — your process or ours.
Lifecycle
Every paid request also writes anearn ReceiptV1 for the owner
agent and emits receipt.published so the explorer’s per-agent
timeline lights up automatically. The chain indexer picks the
pay_to PDA up via ensureWatched the first time the link is
created, so balance changes show up too.
Create a link
| Field | Required | Notes |
|---|---|---|
label | Yes | Short human name. Shows up in the explorer + dashboards. |
owner_agent | Yes | Mint of the agent that receives funds. Must be on the caller-scoped network. |
method | Yes | GET / POST / PUT / PATCH / DELETE. Picks how buyers call the link. |
price | Yes | Display string. Same parser as @leash/seller-kit: "$0.001", "0.5 USDT". |
currency | No | Default USDC. The primary stable used for the atomic amount. |
accepts_currencies | No | Up to 3 extra stables advertised in accepts[] (cross-stable settlement). |
response.status | No | Default 200. Status code returned on a settled call. |
response.mimeType | No | Default application/json. |
response.body | Yes | Object or string. Returned verbatim on settlement. |
webhook_url | No | Optional per-link webhook. Fires on payment_link.settled. |
wrap_receipt | No | When true, the response body wraps your template alongside the receipt. |
metadata | No | Free-form JSON returned on every read. |
id | No | Pick your own slug (URL-safe). When omitted, the API mints a ULID. |
parsePrice semantics as the SDK — see
Seller utilities → parse-price for
the wire-level breakdown of every accepted form.
Read a link
POST, with live counters
(call_count, settled_count, last_tx_sig, last_settled_at).
GET /v1/payment-links (no id) returns a paginated list of links
owned by the caller, newest first. Filter by ?owner_agent= and
control pagination with ?cursor=&limit=.
Update a link
410 Gone from the public paywall but stay
queryable via GET /v1/payment-links/{id} so you can re-enable them
or read past counters. Disabling emits a payment_link.updated event.
Delete a link
204 No Content. Emits a payment_link.deleted event. The
slug becomes available for reuse on the same network.
Preview a draft (no persist)
accepts[], the resolved pay_to, and the facilitator
the link would use, without writing anything to the database. UIs
use this to render a live “what buyers will see” preview while the
user is editing the form.
The public paywall: GET|POST /x/{id}
The endpoint that serves the link is anonymous on purpose — random
buyers shouldn’t need a Leash API key to pay you. It accepts the same
HTTP method you set on the link.
PAYMENT-REQUIRED (base64 → JSON) to see the offer. The
exact same shape is on accepts[] from the GET /v1/payment-links/{id}
response — UIs typically pre-render it from the API call, not from
probing the paywall.
To pay, the buyer:
- Builds an SPL
TransferCheckedfrom their treasury ATA → the link’spay_to. - Signs it.
- Replays the request with the signed transaction in the
X-PAYMENTheader.
response.body plus a PAYMENT-RESPONSE header
containing the tx signature.
wrong_network
hint pointing at the correct cluster. Wrong method? 405. Disabled?
410. Missing slug? 404.
The ?network= query string lets buyers force the network when
sharing devnet links. By default, the public paywall resolves to
solana-mainnet so production share URLs work without a query
string.
What lands in the explorer
Every paywall hit emits one or more events:| Event | When it fires |
|---|---|
payment_link.created | POST /v1/payment-links succeeds. |
payment_link.updated | PATCH /v1/payment-links/{id} succeeds (incl. disabled toggles). |
payment_link.deleted | DELETE /v1/payment-links/{id} succeeds. |
payment_link.served | A buyer hit the public paywall and the seller-kit returned a response. |
payment_link.settled | The buyer paid and the SPL transfer landed on chain. Carries tx_sig. |
receipt.published | The owner-agent earn receipt ingested for that settlement. |
GET /v1/events?kind=payment_link.*
and rendered on the explorer’s payment-link page at
https://explorer.leash.market/payment-links/{id} (when enabled).
Webhooks
If you want push notifications, subscribe to any of the events above viaPOST /v1/webhooks. The payment_link.settled
event in particular carries the tx_sig, the agent that received
the funds, and the payment_link_id so your billing system can
reconcile cleanly.
See also
@leash/seller-kit— the same middleware, hosted in your own process.- Seller utilities —
parse-price,facilitator,networks,pay-to. Useful for validating drafts before you POST them. - Buyer endpoints — the symmetric surface for the
buyer side:
quote,payment/prepare,payment/execute. - Monetise an API — when to use payment links vs the SDK vs an edge proxy.

