Docs / Guides
Getting started for sellers
Four steps turn an x402 endpoint into a bonded one: a dedicated signing key, terms you can keep, a registered and funded service, and the lopw extension on your resource server.
1. Install
npm install @lopw/seller-middleware @lopw/protocol @x402/express @x402/core @x402/stellar # LOPW packages: access for approved integrators2. Generate a signing key
Each service signs with an Ed25519 key that is separate from the seller’s account. The server holds its 32-byte seed; it can sign offers and receipts but cannot move stake. Keep the seed as a server secret and never reuse the test-vector seed.
import { randomBytes } from "node:crypto";
import { signingKeyFromSeed, publicKeyHex } from "@lopw/protocol";
const signingSeed = randomBytes(32).toString("hex"); // store as a secret (SIGNING_SEED)
const signingKey = publicKeyHex(signingKeyFromSeed(signingSeed)); // 64 hex chars, registered on-chain3. Choose your terms
Terms are the predicates you promise, their limits, and a claim window. Promise only what you can keep: see Choose predicates and limits. This example is the warranty of the reference FX quote service.
import type { Terms } from "@lopw/protocol";
const terms: Terms = {
predicates: [1, 2, 3, 4, 6], // equivocation, terms mismatch, stale data, bad status, oracle deviation
claim_window: 3600n, // seconds; at most 86400, at most 3600 when P6 is listed
max_data_age: 600n, // P3: data no older than 10 minutes when served
allowed_status: [200], // P4: only 200 is chargeable
max_response_bytes: 4096, // P5 limit (P5 not listed here, so not enforced)
oracle: { oracle_contract: network.oracle, oracle_asset: { Other: "EUR" }, max_deviation_bps: 50, max_oracle_age: 600n },
};4. Register and stake
Registration needs the seller’s account key (it signs the transaction and pays the registration fee, if one is set) and returns the 32-byte service_id. The service starts Pending; the first deposit that reaches min_stake makes it Active.
import { Keypair } from "@stellar/stellar-sdk";
import { StellarBinding, getNetwork } from "@lopw/protocol";
const network = getNetwork(); // "testnet" unless LOPW_NETWORK says otherwise
const binding = new StellarBinding(network);
const seller = Keypair.fromSecret(process.env.SELLER_SECRET);
const { serviceId } = await binding.registerService(seller, signingKey, terms, 100n * 10_000_000n); // min_stake: 100 USDC
await binding.deposit(seller, serviceId, 150n * 10_000_000n); // stake 150 USDC -> Activeregister_service(seller, signing_key, terms, min_stake) -> service_id // seller signs; min_stake >= 100 USDC
deposit(service_id, amount) // reaching min_stake: Pending -> ActiveErrors you can meet here: StakeBelowFloor (minimum under 100 USDC), InvalidTerms (see the terms rules), NotAllowlisted or Paused during a guarded launch, StakeCapReached when a launch limit is hit.
5. Add the middleware
The warranty is an x402 v2 extension. Register it on your resource server, run lopwRequestContext() before paymentMiddleware, and enable it per route with extensions: { lopw: {} }.
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { ExactStellarScheme } from "@x402/stellar/exact/server";
import { StellarBinding, getNetwork } from "@lopw/protocol";
import { createLopwExtension, lopwRequestContext, DATA_AS_OF_HEADER } from "@lopw/seller-middleware";
const network = getNetwork();
const lopw = createLopwExtension({
binding: new StellarBinding(network),
serviceId: process.env.SERVICE_ID,
signingSeed: process.env.SIGNING_SEED,
maxClaim: 5n * 10_000_000n, // most one receipt can pay out: 5 USDC
});
const server = new x402ResourceServer(new HTTPFacilitatorClient({ url: process.env.X402_FACILITATOR }))
.register(network.caip2, new ExactStellarScheme())
.registerExtension(lopw);
app.use(lopwRequestContext());
app.use(paymentMiddleware({
"GET /v1/quote": { accepts: [{ scheme: "exact", price: "$0.01", network: network.caip2, payTo, maxTimeoutSeconds: 60 }], extensions: { lopw: {} } },
}, server));
app.get("/v1/quote", (req, res) => { res.set(DATA_AS_OF_HEADER, String(asOf)); res.json(quote); });LopwSellerConfig field | Meaning |
|---|---|
binding | The chain binding that reads your service’s state and terms |
serviceId | Your 32-byte service ID as hex |
signingSeed | Hex Ed25519 seed of the service signing key (not the seller’s account key) |
maxClaim | Most a single receipt can pay out, in the asset’s smallest unit; goes into every offer |
offerValiditySeconds | Seconds an offer stays payable. Default 120 |
stateCacheSeconds | Seconds to cache on-chain service state. Default 15 |
faults | Test hook for fault injection in reference services. Never set in production |
From now on every 402 carries a signed offer under extensions.lopw, and every paid response carries a signed receipt in PAYMENT-RESPONSE under extensions.lopw. The middleware only issues offers while the service is Active; otherwise routes serve plain x402 terms until you deposit again. lopw.refresh() re-reads the state on demand.
6. Report freshness and oracle evidence
| Response header | Constant | When | Value |
|---|---|---|---|
LOPW-Data-As-Of | DATA_AS_OF_HEADER | Any route that promises P3 | Unix seconds the data was true as of. Omitted or 0 means not applicable; P3 is then never proven, but the buyer SDK flags a P3 service reporting 0 as untrusted |
LOPW-Oracle-Evidence | ORACLE_EVIDENCE_HEADER | Any route that promises P6 | JSON {"oracle_timestamp","oracle_price","reported_value"} as decimal strings; the middleware fills in the oracle contract and asset from the terms |
const { price, timestamp } = await binding.lastPrice(terms.oracle.oracle_contract, terms.oracle.oracle_asset); // SEP-40 lastprice()
res.set(ORACLE_EVIDENCE_HEADER, JSON.stringify({ oracle_timestamp: timestamp.toString(), oracle_price: price.toString(), reported_value: served.toString() }));Read the oracle’s last completed observation before quoting and cite the timestamp the oracle returned; the validator will fetch that same observation on-chain. See P6 for the exact test.
Why it pays to bond
Agents with a policy read your stake and terms before they pay. A bonded service is one they can trust without a human in the loop; the stake is the signal.
Next: the checklist version, rotate a key, withdraw stake safely.