Quickstart: Seller
Get paid by humans and agents
Every GenesisPay link renders a hosted checkout for browsers and a machine-readable x402 response for agents — same URL, same payment object, settled in USDC. This guide goes from login to your first paid link.
1. Log in and create a key
GenesisPay is a hosted app: your account lives at the origin you are reading these docs on. Log in there with Google, email, or a wallet — no crypto setup required. Pick “Accept payments”in onboarding: GenesisPay assigns a receiving wallet for your payouts (you can override it in Settings) and walks you through creating a seller API key. You can also issue keys any time on the dashboard's Developers page.
Keys look like pp_sk_... and are shown once — store yours as PEERPAY_SELLER_KEY. You only need a key for programmatic access; links can also be created entirely in the dashboard.
The examples below use two environment variables:
# The origin your GenesisPay account lives on — the same URL
# you're reading these docs on (e.g. your deployed app URL,
# or http://localhost:3000 in local dev).
export PEERPAY_BASE_URL="https://<your-peerpay-host>"
# The pp_sk_... key from the dashboard's Developers page.
export PEERPAY_SELLER_KEY="pp_sk_your_seller_key"PEERPAY_BASE_URL is the origin where your GenesisPay account lives — the same URL you are reading these docs on (for the hosted beta, your deployed app URL; for local dev, http://localhost:3000). There is no separate API host: the app, the dashboard, and the API all share this origin. PEERPAY_SELLER_KEY is the pp_sk_... key you just created on the Developers page.
2. Create a payment link
Create a link in the dashboard under Links → New, or programmatically:
curl -X POST "$PEERPAY_BASE_URL/api/v1/links" \
-H "Authorization: Bearer pp_sk_your_seller_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Market report",
"description": "One CSV export of the latest report",
"amount": "5.00",
"asset": "USDC",
"linkType": "reusable",
"metadata": { "orderId": "A-1042" },
"clientReferenceId": "order_1042",
"returnUrl": "https://shop.example/thanks",
"cancelUrl": "https://shop.example/cart"
}'{
"link": {
"publicId": "abc123",
"payUrl": "https://your-peerpay-host/pay/abc123",
"title": "Market report",
"amount": "5.00",
"amountUsdc": "5.00",
"amountUsdcMinor": "5000000",
"asset": "USDC",
"destinationWallet": "0x...",
"chainId": 84532,
"linkType": "reusable",
"status": "active",
"metadata": { "orderId": "A-1042" },
"clientReferenceId": "order_1042",
"returnUrl": "https://shop.example/thanks",
"cancelUrl": "https://shop.example/cart"
}
}destinationWallet is optional — it defaults to your receiving wallet. linkType is single (one payment, then marked paid) or reusable (unlimited payments — ideal for pay-per-call APIs). amountis a decimal string in the link's asset — dollars for USDC, euros for EURC — and every response also carries it as integer minor units (6 decimals) in amountUsdcMinor. amountUsdc is the deprecated older name for amount: still accepted and still returned, but it named a currency the value did not always have.
metadata (a flat map of up to 20 string key/value pairs) and clientReferenceId are yours to use for correlation: GenesisPay stores them untouched and echoes them back when you read the link and on every webhook for it — so you can match an incoming payment to your own order without a lookup table. Exact limits are in the API reference.
returnUrl and cancelUrl bring the payer back to your site. After a confirmed payment the checkout shows a Return to your-shop button pointing at returnUrl with ?peerpay_link_id=<publicId>&peerpay_status=paid appended (your own query parameters are preserved); the unpaid checkout offers cancelUrl as a quiet way out. Both must be https URLs — http is accepted only for localhost and 127.0.0.1. There is no timed auto-redirect: the payer decides when to leave the confirmed on-chain view.
Never fulfil on peerpay_status=paid
Those two query parameters are not signed and prove nothing. Anyone who opens a checkout can note its publicId, abandon the payment, and call https://shop.example/thanks?peerpay_link_id=…&peerpay_status=paid by hand. A landing page that ships goods on that signal ships them for free.
They exist so your page can say “thanks” instead of “pay now” — a UI hint, nothing more. Gate every fulfilment on a verified payment.confirmed webhook or on checkout.retrieve(publicId).paid — both authenticated with your seller key and backed by the on-chain confirmation.
3. Or use the SDK
The legacy PeerPay client wraps the same endpoints for JavaScript and TypeScript — on Node, Edge runtimes, Workers, and Bun.
npm install @genesis-tech/peerpay-sellerimport { PeerPay } from "@genesis-tech/peerpay-seller";
const peerpay = new PeerPay({
apiKey: process.env.PEERPAY_SELLER_KEY!, // pp_sk_...
baseUrl: process.env.PEERPAY_BASE_URL, // your GenesisPay origin
});
const checkout = await peerpay.checkout.create({
title: "Market report",
// Decimal string in `asset` — dollars for USDC, euros for EURC.
// (`amountUsdc` is the deprecated pre-0.6.0 name for this field.)
amount: "5.00",
asset: "USDC",
linkType: "reusable",
// Your identifiers, echoed by retrieve() and on every webhook:
clientReferenceId: order.id,
metadata: { orderId: order.id, buyerId: user.id },
returnUrl: "https://shop.example/thanks",
cancelUrl: "https://shop.example/cart",
});
checkout.publicId; // "abc123"
checkout.payUrl; // send the payer herebaseUrl is your GenesisPay origin — the same PEERPAY_BASE_URL as above. Omit it only when you are on the hosted facilitator that matches your key mode (pp_sk_test_ / pp_sk_live_). The receiving wallet and network are resolved from the key, so no wallet address appears in your code.
checkout.retrieve(publicId)reads a link's current state. paid is the value to poll on: true from the first on-chain confirmation, for reusable links too.
import { PeerPayNotFoundError } from "@genesis-tech/peerpay-seller";
try {
const session = await peerpay.checkout.retrieve(checkout.publicId);
session.paid; // true once >= 1 payment is confirmed on-chain
session.confirmedPaymentCount; // 0, or how often a reusable link was paid
session.clientReferenceId; // "order_1042" — exactly what you sent
session.metadata; // { orderId: "A-1042" } | null
if (session.paid) {
await fulfil(session.clientReferenceId);
}
} catch (error) {
if (error instanceof PeerPayNotFoundError) {
// Unknown publicId, or one belonging to another account — not a config bug.
}
throw error;
}Polling is the fallback — a webhooktells you the same thing without the loop. Either way, that is the check that decides fulfilment; the payer's return URL is not.
4. Get paid — by anyone
Share payUrl. Humans open it as a normal checkout page and pay with their wallet. Agents request the exact same URL over HTTP and get an x402 payment requirement instead:
# The same URL is an x402 endpoint for agents:
curl -i "$PEERPAY_BASE_URL/pay/abc123" -H "Accept: application/json"
# -> 402 Payment Required + PAYMENT-REQUIRED header (x402 V2)You do nothing extra for the agent side — GenesisPay negotiates content per client and verifies every USDC transfer on-chain before a payment counts. Track payments per link in the dashboard, or register a webhook to be notified when money arrives.
Optional: gate your own API
If you would rather charge for an endpoint you already run, wrap the handler with @genesis-tech/peerpay-seller. The gate emits the 402 Payment Required response, validates incoming PAYMENT-SIGNATUREheaders, and delegates on-chain settlement to GenesisPay's facilitator.
import { createPaymentGate, peerPaySettlement } from "@genesis-tech/peerpay-seller";
const gate = createPaymentGate({
amountUsdc: "0.10",
payTo: "0xYourReceivingWallet",
description: "Premium data lookup",
network: "base-sepolia",
});
// Works with Next.js route handlers, Hono, and Bun.serve.
export const GET = gate.wrap(
async () => Response.json({ data: "paid content" }),
{
verifySettlement: peerPaySettlement({
facilitatorBaseUrl: process.env.PEERPAY_BASE_URL!,
apiKey: process.env.PEERPAY_SELLER_KEY!, // pp_sk_...
}),
},
);peerPaySettlement POSTs the signed payment to /api/v1/facilitator/settle with your seller key. GenesisPay broadcasts the EIP-3009 authorization on-chain and verifies the USDC transfer before your handler runs. Once your endpoint is live, consider listing it in discovery so agents can find it.
Pricing models
GenesisPay charges per call: every link and every gated endpoint has one fixed amount that is collected on each paid request. That covers pay-per-call APIs, one-off purchases, and metered access where each request is its own charge.
Subscriptions and metered pay-per-use are covered by payment mandates: the customer signs one gasless spending approval, and you charge per use or per period without a signature per charge — funds still move directly from the payer's wallet to yours. Per-call x402 stays the native fit for agents; mandates add the recurring and metered models on top.