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 gp_sk_... and are shown once — store yours as GENESISPAY_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:

Environment
# 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 GENESISPAY_BASE_URL="https://<your-genesispay-host>"
# The gp_sk_... key from the dashboard's Developers page.
export GENESISPAY_SELLER_KEY="gp_sk_your_seller_key"

GENESISPAY_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. GENESISPAY_SELLER_KEY is the gp_sk_... key you just created on the Developers page.

3. Or use the SDK

The legacy GenesisPay client wraps the same endpoints for JavaScript and TypeScript — on Node, Edge runtimes, Workers, and Bun.

Terminal
npm install @genesis-tech/genesispay-seller
create-checkout.ts
import { GenesisPay } from "@genesis-tech/genesispay-seller";

const genesispay = new GenesisPay({
  apiKey: process.env.GENESISPAY_SELLER_KEY!, // gp_sk_...
  baseUrl: process.env.GENESISPAY_BASE_URL,   // your GenesisPay origin
});

const checkout = await genesispay.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 here

baseUrl is your GenesisPay origin — the same GENESISPAY_BASE_URL as above. Omit it only when you are on the hosted facilitator that matches your key mode (gp_sk_test_ / gp_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.

poll-checkout.ts
import { GenesisPayNotFoundError } from "@genesis-tech/genesispay-seller";

try {
  const session = await genesispay.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 GenesisPayNotFoundError) {
    // Unknown publicId, or one belonging to another account — not a config bug.
  }
  throw error;
}

Polling is the fallback — a webhook tells you the same thing without the loop. Either way, that is the check that decides fulfilment; the payer's return URL is not.

When the payer returns, parseCheckoutReturnHint reads the genesispay_link_id / genesispay_status parameters off the URL — then you confirm with an authenticated retrieve before acting:

return-route.ts
import { parseCheckoutReturnHint } from "@genesis-tech/genesispay-seller";

export async function GET(request: Request) {
  const hint = parseCheckoutReturnHint(new URL(request.url));
  if (!hint) return Response.json({ ok: true }); // no return params — nothing to do

  // Verify with your seller key, never trust the URL:
  const session = await genesispay.checkout.retrieve(hint.linkId);

  // Only single-use links are fulfilled here. A reusable link's "paid" means
  // "ever paid"; fulfil those on a payment.confirmed webhook keyed by attempt.id.
  if (session.linkType !== "single") return Response.json({ ok: true });

  // Single-use link: fulfil once, keyed by the link itself.
  if (session.paid) await fulfilOnce(hint.linkId);

  return Response.json({ ok: true });
}

The sample fulfils single-use links only: it keys on hint.linkId and refuses any other linkType. A reusable link's paid means “ever paid” and the return URL cannot say which payment triggered it — fulfil those on a verified payment.confirmed webhook keyed by attempt.id instead.

Never fulfil on genesispay_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?genesispay_link_id=…&genesispay_status=paid by hand — a landing page that ships goods on that signal ships them for free.

parseCheckoutReturnHint performs no verification; it only tells you which link to look up. 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.

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:

Terminal
# The same URL is an x402 endpoint for agents:
curl -i "$GENESISPAY_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, create a product with delivery: { type: "gate" } and protect the matching route with @genesis-tech/genesispay-seller. The canonical product link supplies the price, USDC asset, Base network and receiving wallet; the browser never chooses money values.

route.ts
import { GenesisPay } from "@genesis-tech/genesispay-seller";

const genesispay = new GenesisPay({
  apiKey: process.env.GENESISPAY_SELLER_KEY!,
  expectedPayTo: process.env.GENESISPAY_PAY_TO!,
});

// Create this once in provisioning, then store the returned product ID.
const forecastGate = genesispay.products.gate(
  process.env.GENESISPAY_FORECAST_PRODUCT_ID!,
);

export async function POST(request: Request) {
  const body = await request.clone().text();
  validateForecastRequest(body); // reject invalid input before charging

  return forecastGate.protect(request, async (_request, purchase) => {
    // Payment confirmation is exactly-once; handler delivery is at-least-once.
    // Persist/reuse results by purchase.payment.attemptId.
    const forecast = await getOrCreateForecast({
      paymentAttemptId: purchase.payment.attemptId,
      body,
    });
    return Response.json(forecast);
  });
}

The first valid request receives 402 Payment Required. GenesisPay binds its pending attempt to the method, registered URL and a raw-body fingerprint, settles the EIP-3009 USDC authorization, and then runs your handler. Save effects by the payment attempt ID because a confirmed retry can invoke the handler again. 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.