LOPWDocumentation

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

Terminal
npm install @lopw/seller-middleware @lopw/protocol @x402/express @x402/core @x402/stellar   # LOPW packages: access for approved integrators

2. 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.

TypeScript
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-chain

3. 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.

TypeScript
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.

TypeScript
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 -> Active

Errors 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: {} }.

TypeScript
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 fieldMeaning
bindingThe chain binding that reads your service’s state and terms
serviceIdYour 32-byte service ID as hex
signingSeedHex Ed25519 seed of the service signing key (not the seller’s account key)
maxClaimMost a single receipt can pay out, in the asset’s smallest unit; goes into every offer
offerValiditySecondsSeconds an offer stays payable. Default 120
stateCacheSecondsSeconds to cache on-chain service state. Default 15
faultsTest 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 headerConstantWhenValue
LOPW-Data-As-OfDATA_AS_OF_HEADERAny route that promises P3Unix 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-EvidenceORACLE_EVIDENCE_HEADERAny route that promises P6JSON {"oracle_timestamp","oracle_price","reported_value"} as decimal strings; the middleware fills in the oracle contract and asset from the terms
TypeScript
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.