Add an x402 payment gate to an HTTP route and collect on every request that settles.
Add an x402 payment gate to any HTTP route in three steps: point your resource server at the Vellar facilitator, declare the payment requirements, and collect on every request that settles.
By the end of this page you will have a working paid HTTP endpoint that accepts
testnet USDC, catalogs itself in the Bazaar automatically after its first
settled payment, and passes the ownerVerified check so agents have a reason to
trust it.
An existing HTTP server (Express, Hono, Fastify or similar). This page adds a
gate to a route you already have; it does not ask you to restructure anything
A funded testnet account with a trustline to your payment asset. That account
is your payTo
Your payment asset's SEP-41 contract id
Note: If you have none of those yet, the facilitator repo provisions all of
them in one command:
It creates an issuer, a Stellar Asset Contract, a merchant trustlined to it,
and a funded payer, in roughly 40 seconds to 3 minutes, and prints a
paste-ready env block. Pass AGENT_PUBLIC to also provision a Vellar
smart-account wallet for the buyer side. If you would rather use an asset that
already exists, Circle's official testnet USDC issuer is
GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 and its SAC contract
id is CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA.
@x402/core carries the resource server and the HTTP facilitator client,
@x402/stellar the exact scheme for Stellar, and @x402/extensions the
bazaar discovery extension that gets your route listed.
Exactly one thing changes on your server: you point an x402ResourceServer at
the Vellar facilitator. Your routes, your auth, and your business logic stay
where they are.
import { HTTPFacilitatorClient } from "@x402/core/http";import { x402ResourceServer } from "@x402/core/server";import { ExactStellarScheme } from "@x402/stellar/exact/server";import { bazaarResourceServerExtension } from "@x402/extensions/bazaar";const server = new x402ResourceServer( new HTTPFacilitatorClient({ url: "https://vellar-facilitator-testnet-production.up.railway.app", }),) .register("stellar:testnet", new ExactStellarScheme()) .registerExtension(bazaarResourceServerExtension);
The facilitator verifies and settles every payment. Your server never touches
Soroban, never builds or inspects an auth entry, and never handles fees: the
facilitator re-simulates the signed payment to verify it, submits it on-chain,
and sponsors the network fee from
GBUCR6H22CZC5OYHBJIEUS2JFZBOB63AHEGTCV6UEPMD2TMLKG2ZMIW4.
Note: The hosted testnet facilitator is stellar:testnet only.
Amounts are atomic units. SEP-41 assets and Stellar Asset Contracts use 7
decimals, so 1000000 is 0.1 units and 10000000 is 1.0. Treat amounts as
bigints in base units and never as floating point.
The extensions block is what makes the route discoverable. Cataloging happens
on settle, not on verify, so the entry appears after the first real payment, not
when you deploy.
⚠️ Your payTo needs a trustline to the payment asset. Without one, a
payment verifies successfully and then fails at settlement with an on-chain
error that reads exactly like a spend control refusing it, which sends people
debugging policies for an hour. Check the trustline before you debug anything
else. The seller.mjs example checks it at boot for this reason.
The resource URL you advertise is the URL that gets cataloged, and it is the URL
the facilitator re-fetches to decide ownerVerified. A localhost URL can never
pass that check: verification refuses http, loopback, private ranges, and
cloud-metadata addresses before it opens a socket.
ownerVerified: true needs five things to be true about that URL, checked in
order, with any one failure giving unverifiable:
https and publicly resolvable
An unauthenticated GET returns 402 (a 200 or a 401 is unverifiable)
It carries a PAYMENT-REQUIRED header of 64 KiB or less (the verdict comes
entirely from the header, your body is never downloaded)
The challenge's accepts[].payTo includes your address (this is the actual
check)
It answers within 3 seconds with no redirect (a 301 from /quote to
/quote/ reads as unverifiable)
⚠️ Your first settlement against the hosted facilitator writes a public
catalog entry. A localhost URL produces an entry that can never pass
ownership verification, and there is no self-service removal, so every agent
reading the catalog pays the cost of it until the next restart clears the
catalog. For local development, run your own facilitator and set
FACILITATOR_URL. With a localhost URL and the shared facilitator, seller.mjs
refuses to start; ALLOW_UNVERIFIABLE_ON_SHARED=1 bypasses that, and you
should use it only if you understand what it leaves behind.
The response is { "x402Version": ..., "items": [...], "pagination": { "limit": 20, "offset": 0, "total": ... } }. If you would rather not wait for a poll, the
settle response tells you directly. On a successful /settle the facilitator
returns a lowercase extension-responses header, JSON keyed by extension name:
The header is absent on 400s and on 402 challenges, so it is a settle-path
signal only. If cataloged is false, read the reason field:
no_discovery_extension, invalid_payto, ownership_tombstone_mismatch,
unbound_payto, schema_validation_failed, binding_refused,
invalid_tool_name, or cataloging_error. Each one is explained in
Bazaar and discovery.
Note: The hosted catalog is durable — it has CATALOG_DB_URL configured
(libSQL/Turso), so your catalog entry and URL ownership binding survive a
restart. ownerVerified is the exception: it resets on every restart by
design and self-heals after your next settlement, subject to a 15-minute
cooldown.