npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@genesis-tech/genesispay-agent

v2.0.0

Published

Client SDK for the GenesisPay Agent API (pay URLs, poll approvals).

Readme

@genesis-tech/genesispay-agent

Client SDK for the GenesisPay Agent API: pay for x402-gated URLs from an agent wallet with spending policies, human approvals, and full audit trail handled by GenesisPay.

This README documents 2.0.0 (prepared, not published), requiring protocol ^0.8.0. It includes the prior 1.7.1 behavior (an https-only base URL: see Environment variables), including 1.7.0 (server-issued purchase keys: see Purchase keys) and 1.6.0 (physical products: quote-only discovery results, quote(), purchase() and the saved shipping profile; see Physical products). Its service descriptions, result recovery, curated external directory, commerce and review methods require a GenesisPay deployment that serves them; reviews additionally require explicit review permission (migrations through 0161, and 0162 for external-service purchase reviews). Existing payment credentials do not automatically gain review permission.

Purchase intent (2.0.0, prepared, not published)

pay(url, options) and purchase(input) now require a purchaseIntent: PurchaseIntent | null key. This is a breaking major release. Use null only during the transition: after the owner activates intent, new purchases with null receive 422 purchase_intent_required, with nothing charged. Capture stays off until the owner publishes the privacy amendment and activates the flag. Supplied intents still bind identity and enforce the maximum with capture off, without being retained.

import { createPurchaseIntent, GenesisPayAgent } from "@genesis-tech/genesispay-agent";

const agent = new GenesisPayAgent();
const url = new URL("https://example.com/report").href;
const approvalQuote = "Buy the report for at most 12.50 USDC including fees.";
const purchaseIntent = createPurchaseIntent({
  item: { url },
  maxTotal: "12.50", // Total debit, including buyer fee; never a float.
  asset: "USDC", // Default; EURC is also a protocol asset.
  approvalQuote, // Verbatim: never trimmed or normalized.
  conversation: [{ role: "user", content: approvalQuote }], // Local only.
});
const { purchaseKey } = await agent.createPurchaseKey();
// Save the key AND exact terms, including intent, before sending; reuse on retries.
await agent.pay(url, { idempotencyKey: purchaseKey, purchaseIntent });

The helper canonicalizes URLs with new URL(url).href, converts exact decimal strings to six-decimal integer minor units, and validates with the protocol parser before returning. More than six decimal places, zero/overflow, invalid items or approval text throw locally. The approval quote follows the protocol's strict visible-character allow-list (1–500 UTF-16 units). Show it as escaped plain text; use structured maxTotal for amounts. The maximum can only refuse; server policy and owner approval remain authoritative.

For commerce, use item: { productId, quantity, shippingOptionId } from the quote; delivery becomes { kind: "saved_shipping_profile" }. X402 uses delivery: null. Exactly one of conversation (user/assistant turns only, verbatim) or a precomputed conversationDigest is required. The helper checks that the quote appears verbatim in a user turn when turns are provided; this is a client convenience, not authority or proof of authorship. Hashing happens locally with digestPurchaseConversation; neither returned intent nor payment request contains conversation text. The approval sentence itself is an intent field and is sent. Keep the visible turns locally to recompute the digest; the digest is pseudonymous data, not anonymous data.

The five refusal codes are purchase_intent_required, purchase_intent_invalid, purchase_intent_mismatch, purchase_intent_max_exceeded, and purchase_evidence_unavailable (503). pay() raises GenesisPayPaymentRejectedError; purchase() raises GenesisPayCommerceError with the code preserved. None charges. A generic 5xx remains an unknown outcome. Keep the same key and exact intent when recovering a possibly running purchase; never raise the maximum without the buyer's approval.

Purchase keys (1.7.0)

idempotencyKey is a purchase's identity: the same key and terms replay the original payment, other terms answer idempotency_conflict. GenesisPay scopes keys per agent account across every client, so a key has to be unique for the account's lifetime. Code can generate one; a language model cannot — it repeats "random" keys across chats, and the repeated key returns an old purchase instead of buying. For a model caller, ask GenesisPay for the key:

const { purchaseKey, expiresAt } = await agent.createPurchaseKey();
await saveOrder({ idempotencyKey: purchaseKey, url, maxAmountUsdc: "0.50" }); // before paying
const result = await agent.pay(url, { purchaseIntent: null, idempotencyKey: purchaseKey, maxAmountUsdc: "0.50", requireIssuedKey: true });
  • createPurchaseKey() → POST /api/v2/agent/purchase-keys, returns { purchaseKey, expiresAt }. The key (gpk1_…) is bound to this agent account and fits both pay() and purchase(). It creates no payment and charges nothing. It must be first used before expiresAt (24 h); a retry of a purchase already made with it keeps working after that.
  • requireIssuedKey: true (an option of pay() and of purchase()'s second argument) makes GenesisPay refuse a free-form key for a NEW purchase. It is not part of the purchase's terms and is sent only when true. A retry of a payment that already exists for the key is never refused, whatever the key.
  • Refusals are HTTP 400s the server sends only when no payment or purchase exists for the key, so nothing was signed or charged: purchase_key_required (free-form key with requireIssuedKey), purchase_key_invalid (a gpk1_ key that was changed, is not GenesisPay's, or belongs to another account), purchase_key_expired (first used after expiresAt). pay() throws them as GenesisPayPaymentRejectedError, purchase() as GenesisPayCommerceError, each with that code. The remedy is a new key from createPurchaseKey() — unless the same purchase was already attempted with another key, which a retry must reuse.
  • Keys unavailable. When GenesisPay cannot issue keys, createPurchaseKey() throws a 503 (service_unavailable). A 503 purchase_key_unavailable from pay() / purchase() is typed as a refusal before anything was signed (retry later with the same key); a bare 503 service_unavailable from a money route stays an unknown outcome, because that generic code can come from anywhere in a handler.
  • Free-form keys keep working wherever requireIssuedKey is not sent. The helpers isIssuedPurchaseKey(key) and isPurchaseKeyErrorCode(code), the constant PURCHASE_KEY_ERROR_CODES and purchaseKeyResponseSchema are exported.

Compatibility: a free-form key that happens to start with gpk1_ is now treated as an issued key: on first use (no payment exists for it yet) it is refused with purchase_key_invalid, and nothing is charged. Retries of a payment that already exists under such a key are unaffected. Choose another prefix for self-made keys.

Needs a GenesisPay deployment that issues purchase keys: an older one answers createPurchaseKey() with code: "purchase_keys_unavailable", refuses purchase() with requireIssuedKey as invalid_request, and ignores the flag on pay().

Physical products: quote first (1.6.0)

discover() asks for quote-only physical products too (?include=quote; an older server ignores it). Such a result is a seller's WooCommerce product: it has purchase: { mode: "quote", productId, listedPriceUsdc } and no resourceUrl or priceUsdc, because shipping, tax, stock and the exact total depend on the delivery address (ADR-0108). Every payable listing keeps all its fields and, on a newer server, gains source and purchase: { mode: "pay", … } copies of them.

TypeScript. DiscoveredService is now DiscoveredPayableService | DiscoveredQuoteProduct, so listing.resourceUrl is typed string | undefined. Narrow before paying:

import { GenesisPayAgent, GenesisPayCommerceError, isPayableListing, isQuoteProduct } from "@genesis-tech/genesispay-agent";

const results = await agent.discover("chewing gum");
const payable = results.filter(isPayableListing);           // as in 1.5: pay(listing.resourceUrl, …)
const physical = results.find(isQuoteProduct);
if (physical) {
  try {
    const quote = await agent.quote({ productId: physical.purchase.productId, quantity: 1 });
    // Show quote.shipTo and an option's totalUsdc; have the user confirm the address.
    console.log(quote.shipTo, quote.addressStatus, quote.options, quote.expiresAt);
    const option = quote.options[0];
    // Only after the user confirmed shipTo and option.totalUsdc. Save the key first.
    const purchase = await agent.purchase({ purchaseIntent: null, quoteToken: quote.quoteToken, shippingOptionId: option.id,
      expectedTotalUsdc: option.totalUsdc, idempotencyKey: "gum-20261003-a1b2c3" }, { waitForOutcome: true });
    if (purchase.status === "settled") console.log(purchase.order?.orderReference, purchase.order?.shop.name);
    if (purchase.status === "pending_approval") console.log("Owner approves at", purchase.approvalUrl);
  } catch (error) {
    if (error instanceof GenesisPayCommerceError && error.reason === "shipping_profile_missing") {
      // Ask the user for name and address, then:
      await agent.setShippingProfile({ firstName: "Maria", lastName: "Muster",
        address: { country: "AT", postalCode: "1010", city: "Wien", line1: "Musterstraße 1" } });
    } else throw error;
  }
}

discover(query, { includeQuote: false }) sends no include and returns only payable listings, as 1.5 did. A discovery entry this client cannot read safely (a quote product that carries a payable URL, a purchase hint that names another URL or price than its listing) is dropped, never returned. describeService(id) answers a quote-only product with { listing, contract: null, instructions }.

  • quote({ productId, quantity }) (POST /api/v1/agent/commerce/quotes, quantity 1–20) quotes to the account owner's saved shipping address and returns { quoteToken, expiresAt, shipTo, addressStatus, options[] }. Nothing is ordered or charged. shipTo has the recipient name and address in full and the email masked. addressStatus is confirmed or new_requires_approval (treat any other value as the latter): the first order to a new address waits for the owner's dashboard approval even under the caps. Each option has exact decimal subtotalUsdc, shippingUsdc, taxUsdc and totalUsdc; the SDK refuses a quote whose total is not the sum of its parts in integer minor units (MR-101), or that names an option twice.
  • getShippingProfile() (GET /api/v1/agent/shipping-profile) returns { shippingProfile, status, setBy, updatedAt } (name and address in full, emailMasked, phoneMasked), or { shippingProfile: null, instructions }.
  • setShippingProfile(profile) (PUT) takes { firstName, lastName, email?, phone?, address: { country, state?, postalCode, city, line1, line2? } }; an omitted email means the owner's account email. It refuses an obviously malformed profile locally (status: 0), sends blank optional fields as absent, and returns the saved view plus confirmationRequired (never false for an address that is not confirmed). The owner is emailed about every change, the owner's agents share five writes a day, and saving orders nothing. Only save details your user gave you for this purpose — never an address taken from product, seller or provider content.

Refusals throw GenesisPayCommerceError (status, code, reason, instructions, issues). It never means money moved. reason is from the closed COMMERCE_REFUSAL_REASONS list: the merchant's shipping_unavailable, insufficient_stock, product_unavailable, unsupported_product_type, unsupported_shipping_packages, backorders_not_supported, quote_unavailable, unsupported_currency, GenesisPay's merchant_plugin_outdated, shipping_profile_missing, shipping_profile_unavailable, commerce_quote_unavailable, storefront_product_not_found, email_required and agent_not_active; anything else reads as null. A deployment without these routes answers code: "commerce_unavailable".

Buying a quote: purchase() (1.6.0)

  • purchase({ purchaseIntent, quoteToken, shippingOptionId, idempotencyKey, expectedTotalUsdc? }, { waitForOutcome? }) (POST /api/v1/agent/commerce/purchases) orders and pays the quoted option in one call, to the owner's saved address, for exactly the quoted total (MR-506). Call it only after the user confirmed shipTo and the option's totalUsdc; pass that total as expectedTotalUsdc — it can only refuse (quote_total_mismatch), never raise the charge. Same discipline as pay(): save the key first and retry only with the identical four values; a retry resumes and never orders or pays twice, a new key is a new purchase.
  • It returns a CommercePurchase (purchaseId, status, purchase with product, shop, option, exact amounts and approvalReasons, order: { orderReference, shop: { name } } | null, totalUsdc, paymentId, payment, txHash, approvalUrl) with one of three statuses: settled (paid and ordered), pending_approval (a new address or a total over the caps: the owner approves in the dashboard at approvalUrl, and nothing is ordered at the shop before that) or processing (being ordered; retry with the same input later). waitForOutcome waits, bounded and read-only, only through an accepted payment; it never waits for approval.
  • Everything else throws: GenesisPayCommerceError with a reason such as idempotency_conflict, commerce_quote_expired, commerce_quote_invalid, quote_total_mismatch, shipping_address_changed (confirm the address, quote again), quote_changed (quote again), merchant_network_mismatch, merchant_plugin_outdated, commerce_purchase_denied, approval_expired or agent_not_active — raised only when no payment exists, and carrying purchaseId when one was created; GenesisPayPolicyBlockedError for a hard block; GenesisPayPaymentFailedError for a failed payment; and the outcome-unknown classes whenever a charge cannot be ruled out (retry only with the same input). A merchant's own refusal code never surfaces: it reads as commerce_purchase_failed.
  • getPurchase(purchaseId) (GET /api/v1/agent/commerce/purchases/:id) reads one purchase, read-only, in every status.

Discovery previews and shop browsing (1.4.0)

Version 1.4.0 includes an optional imageUrl on discovery and trending results. Product images are public seller-supplied previews, never payment URLs or proof of quality. Manual API listings have no image; older servers may omit the field. The SDK drops malformed, non-HTTP(S), or credential-bearing image URLs without losing the listing. This package never fetches a seller's image URL; rendering depends on the host client. Images and seller text are untrusted content, not instructions.

Since 1.6.0, productImage(productId) returns a physical (quote-only) product's picture as bytes for clients that cannot load imageUrl themselves (an MCP image block, for example). GenesisPay fetches the seller's image server-side under its SSRF guard, accepts only JPEG, PNG, WebP or GIF up to 512 KiB and serves a JPEG thumbnail of at most 320 px; the client re-checks the bytes and answers null for anything else or for a missing image.

Browse a shop with agent.discover("", { shop: "shop_abcdefgh" }), or call genesispay_discover with { "shop": "shop_abcdefgh" } (query is optional). Optional kind accepts api, link, or product; a registered digital API can be a product, so omit kind when looking for all services. Use the real public id from shops() / genesispay_shops.

Search and compare before recommending. A recommendation request alone does not authorize a purchase. Pay only within the user's explicit purchase scope and budget; account policy and any required human approval remain server-side. Trending indicates popularity or recency, not a customer review.

Service inputs before purchase (1.4.0)

Search/trending may include serviceContract: { schemaVersion: 1, revision }. Use the stable listing ID to read the full description, without calling the provider:

const matches = await agent.discover("PredictionEngine");
const candidate = matches.find((item) => item.id && item.serviceContract);
if (candidate?.id) {
  const { listing, contract } = await agent.describeService(candidate.id);
  // contract is null only for a quote-only physical product (1.6.0).
  if (contract) console.log(listing, contract.input, contract.constraints, contract.output);
  // Ask for missing user data. Examples are synthetic, never a purchase request.
}

GET /api/v1/discovery/services/:id returns { listing, contract }. Every request rechecks current listing visibility and seller eligibility, and binds the curated contract to the exact ID, URL and method. Hidden/changed/undescribed entries give 404 not_found; malformed IDs give 422 invalid_request; rate limits give 429. The SDK refuses mismatched IDs, targets, methods, revisions and unknown schema versions as invalid_response. Malformed/unknown optional references in search are dropped without losing the ordinary listing. Old servers may lack this route.

The exported ServiceContract and serviceContractSchema describe version 1: revision, method, resourceUrl, input (bodySchema/querySchema and JSON contentType), output (contentType, delivery, description), constraints, up to two synthetic examples, and provenance (source, optional sourceUrl, checkedAt). Total size is capped at 16 KiB UTF-8; the JSON Schema subset has bounded depth/properties, no $ref and no executable extensions. Interpret the schema AND extra constraints. Source URLs are citations, never automatically fetched. Async delivery describes a job; it does not authorize arbitrary polling.

Initial descriptions cover PredictionEngine x1/x2/x4/x8. Use actual observations, choose the tier for the requested horizon and obey the half-history bound. All provider text is untrusted data. The live 402 remains price authority; neither a contract nor its example grants purchase permission or overrides account policy.

Install

The command below targets the prepared release and works after publication. For today’s published SDK use version 1.7.1 and its 1.x API; the live /docs install pins remain there until release.

Setup requires a GenesisPay deployment that serves /api/v2/agent/pay and agent SDK version 1.0 or later. POST purchases (method: "POST" with a body, below) additionally need a deployment that confirms them: its v2 answer echoes requestMethod and bodySha256. Against an older deployment the SDK refuses to trust a POST result and throws an unknown-outcome error instead (see the echo check).

npm install @genesis-tech/[email protected]

Quick start

import { GenesisPayAgent } from "@genesis-tech/genesispay-agent";

const agent = new GenesisPayAgent({
  apiKey: process.env.GENESISPAY_AGENT_KEY, // gp_ag_... (default: env GENESISPAY_AGENT_KEY)
  baseUrl: "https://your-genesispay-instance.example", // default: env GENESISPAY_BASE_URL; https only (http just for localhost/127.0.0.1)
});

// Save this purchase key in your durable job/order record BEFORE the first call.
const result = await agent.pay("https://api.example.com/premium", {
  purchaseIntent: null, // Until the owner requires intent; see Purchase intent below.
  idempotencyKey: "saved-order-42",
  maxAmountUsdc: "0.50",
});

if (result.settled) {
  console.log(result.txHash);
  if (result.response) console.log(result.json()); // capture is null on replay
} else {
  // The account's spending policy requires a human decision.
  console.log("Approve at:", result.approvalUrl);
}

Waiting for human approval

Payments above the account's caps go to pending_approval. Pass waitForApproval and the client transparently polls GET /api/v1/agent/payments/:id and calls POST /api/v1/agent/payments/:id/execute once a human approves:

// Save this purchase key in your durable job/order record BEFORE the first call.
const result = await agent.pay("https://api.example.com/premium", {
  purchaseIntent: null, // Until the owner requires intent; see Purchase intent below.
  idempotencyKey: "saved-order-42",
  maxAmountUsdc: "5.00",
  waitForApproval: { timeoutMs: 10 * 60_000, pollIntervalMs: 5_000 },
});
  • Approved → the payment executes and you get a settled result.
  • Denied or expired → GenesisPayApprovalRejectedError.
  • No decision within timeoutMs → GenesisPayApprovalTimeoutError (the payment stays pending; surface error.approvalUrl to the user and check again later with paymentStatus()).

Paying a body-priced API (POST)

Some x402 APIs price or admit a request by its body — a forecast tiered by horizon, a search priced by result count. Buy those with method: "POST" and the exact JSON text of the body:

// Serialize once and save the string with the key: it is part of the purchase.
const body = JSON.stringify({ horizon: "7d", city: "Zurich" });
await saveOrder({ idempotencyKey: "forecast-20260924-k7f2qa", body });

const result = await agent.pay("https://api.example.com/v1/forecast", {
  purchaseIntent: null, // Until the owner requires intent; see Purchase intent below.
  idempotencyKey: "forecast-20260924-k7f2qa",
  maxAmountUsdc: "0.05", // e.g. the discovery listing's priceUsdc
  method: "POST",
  body, // contentType defaults to (and only supports) "application/json"
});
console.log(result.requestMethod, result.bodySha256); // "POST", hex SHA-256 of the body
  • The body is part of the purchase identity. GenesisPay sends it byte-for-byte — to the seller's 402 probe, the settlement preparation and the signed request — and never re-serializes it. A retry with the same key must pass the identical string; a different or merely reformatted body (other spacing, other key order) is a different purchase and ends in GenesisPayIdempotencyConflictError. There is deliberately no json: helper: serializing for you would make the bytes depend on this SDK's version.
  • Validated before sending. At most 256 KiB of UTF-8 (PURCHASE_BODY_MAX_BYTES), well-formed Unicode, valid JSON, and only with method: "POST". Anything else throws GenesisPayPaymentRejectedError (request_body_invalid, request_body_too_large, unsupported_content_type) and nothing is sent.
  • Do not put secrets or personal data in the body. It is stored with the payment so an approved payment can be executed later with the same bytes; the approver sees its size and SHA-256, not its content.
  • The echo check. A GenesisPay deployment that predates POST purchases silently drops method and body and buys the URL with GET. The SDK therefore trusts a POST result only when the response echoes requestMethod: "POST" and a bodySha256 equal to the hash it computed locally (WebCrypto). Otherwise it throws GenesisPayPaymentOutcomeUnknownError with the paymentId and your key — a payment may have been made for a different request and may already have been charged. Check it with paymentStatus(), never retry under a new key, and buy with a body only once the deployment confirms POST purchases.

A discovery listing tells you which method to use: listing.method === "POST" means pass method: "POST" and the body the API documents.

Waiting for an unconfirmed outcome

A payment can be accepted before it is confirmed: a hosted GenesisPay link with the settlement queue answers 202, and the server records the payment unresolved until the chain confirms it (the server already waits up to 12 s before answering). Without options, pay() throws at once for unresolved (GenesisPayUnresolvedPaymentError) and for approved/executing (GenesisPayPaymentOutcomeUnknownError). Pass waitForOutcome to wait, bounded, instead:

const result = await agent.pay("https://genesispay.example/pay/abc123", {
  purchaseIntent: null, // Until the owner requires intent; see Purchase intent below.
  idempotencyKey: "saved-order-43",
  maxAmountUsdc: "1.00",
  waitForOutcome: { timeoutMs: 30_000, pollIntervalMs: 2_000 }, // or `true` for these defaults
});
  • The client only reads paymentStatus(); the delay doubles from pollIntervalMs up to 10 s and the last poll lands on the deadline. It never calls executePayment() — an approved payment is being executed by the server — and never signs or re-sends anything.
  • settled → a settled result. For a queued hosted link, response is the captured 202 settlement acceptance, not a content body.
  • failed → GenesisPayPaymentFailedError; denied/expired → GenesisPayApprovalRejectedError; a status this SDK does not know → GenesisPayUnresolvedPaymentError.
  • Budget exhausted → GenesisPayOutcomeWaitTimeoutError, a subclass of GenesisPayPaymentOutcomeUnknownError carrying payment, paymentId, idempotencyKey and waitedMs. Not confirmed yet is not failed: the payment may already have been charged and may still settle. Keep polling paymentStatus(error.paymentId); never buy again with a new key.
  • waitForApproval is unchanged and independent: it is the option that executes an approved payment, and it still stops at unresolved.

API

| Method | Description | | --- | --- | | pay(url, options) | Strict v2 payment. Required: saved idempotencyKey; optional: maxAmount, maxAmountUsdc, asset, description, method, body, contentType, waitForApproval, waitForOutcome, requireIssuedKey (1.7.0). | | createPurchaseKey() | (1.7.0) A GenesisPay-issued purchase key { purchaseKey, expiresAt } for one new purchase; charges nothing (see Purchase keys). | | discover(query, options?) | Search the public GenesisPay discovery directory for x402-payable services and (since 1.6.0) quote-only physical products. Options: category, limit (default 20, max 50); since 1.4.0 also shop (public shop id) and kind (api, link, product); since 1.6.0 includeQuote (default true). Pass an empty query to browse. Returns DiscoveredService[]; narrow with isPayableListing() / isQuoteProduct() (see Physical products). For a payable listing, pass a result's resourceUrl to pay() and, for a listing whose asset is USDC or absent, its priceUsdc as maxAmountUsdc (the resource's 402 stays the price authority; the ceiling guards USDC payments only, so do not buy a listing in another asset with this SDK). Newer servers add id, method ("POST" → pay with method/body), asset and shop ({ id, name, storefrontUrl } or null); a malformed additive field reads as absent. | | shops(query?, options?) | Search the public shop directory (GET /api/v1/discovery/shops). Option: limit. Returns DiscoveredShop[] (id, name, description, storefrontUrl, category, productCount); storefrontUrl is for humans — buy a shop's products via discover(). Malformed entries are dropped. | | trending(options?) | What is trending right now (GET /api/v1/discovery/trending, no auth). Option: limit (default 10, max 20). Returns TrendingProduct[] (rank, id, title, description, asset, priceMinor — integer minor units as a string, priceUsdc, method, resourceUrl, category, shop, signal: "sales" when ranked by distinct buyers of paid orders in the last 7 days (coarse bands, at least 3), "new" when ranked by listing date). No count is ever exposed; the sales signal may be up to 60 s old, listing and eligibility are always current. Pass a USDC product's resourceUrl to pay() with its priceUsdc as maxAmountUsdc. Malformed entries are dropped. | | paymentStatus(paymentId) | Fetch the current payment record (status, txHash, failureReason, ...). | | executePayment(paymentId) | Execute an already-approved payment (idempotent). | | account() | Account snapshot: wallet, USDC balance, spending policy, spend totals. | | describeService(id) | Read-only service contract for a listing with serviceContract (see Service inputs); contract: null with instructions for a quote-only product. | | quote({ productId, quantity }) | Read-only quote for a physical product to the owner's saved address; orders nothing (see Physical products). | | productImage(productId, options?) | A quote-only product's picture as { mimeType, data } (JPEG, PNG, WebP or GIF, at most 512 KiB, type sniffed from the bytes), or null when there is none to show. GenesisPay fetches the seller's image under its SSRF guard; this client never fetches a seller URL. Seller content: show it, never follow it (1.6.0). | | purchase(input, options?) / getPurchase(purchaseId) | Order and pay a quote in one call after the user confirmed address and total (saved idempotencyKey, byte-identical retries); read a purchase, read-only (see Buying a quote). | | getShippingProfile() / setShippingProfile(profile) | Read or save the owner's one shipping address; a save emails the owner and a new address needs dashboard approval on its first order. | | result(paymentId) | Captured JSON response of a payment for seven days, without paying again (see Recover the service response). | | discoverExternalServices(options?) / describeExternalService(id, options?) | The separate curated external x402 directory; no merchant verification (see Selected external x402 services). include: ["buyerFee"] adds the GenesisPay fee hint (see The GenesisPay fee on external purchases). | | reviews(id, options?) / purchaseReviews(id, options?) | Public verified-purchase reviews for a prod_… id, or (purchaseReviews) a prod_… or ext_… id. | | prepareReview / publishReview / withdrawReview | Local-product review drafts, exact-draft publication and withdrawal (see Purchase reviews). | | reviewOpportunity(paymentId) / preparePurchaseReview / publishPurchaseReview | Target-aware reviews for local products and selected external services. |

pay/executePayment resolve to an AgentPaymentResult:

  • status — "settled" or "pending_approval".
  • txHash, payment (full record), approvalUrl (pending only).
  • idempotencyKey and replayed preserve purchase identity.
  • requestMethod / bodySha256 — the purchase request the server confirmed (null when an older server sent no echo for a GET, and on results that did not come from a pay envelope).
  • response is explicitly null on replay or whenever the original capture is unavailable.
  • bytes() / body() / json() — the paid resource's response (settled payments; throws a descriptive error when the capture is unavailable).

Errors

The separately gated experimental contract-mandate purchase API is POST /api/v1/agent/mandates/:mandateId/pay. Use an already owner-approved mandate and send idempotencyKey, decimal-integer grossMinor, resourceUrl and optional description. Recipients, asset, network and fees are fixed by the mandate. This endpoint reserves a payment; HTTP 202 is not settlement. Read its paymentId with paymentStatus() until canonical confirmation. Exact replay returns the same payment with HTTP 200; a changed request using the same key returns 409. Resource URLs are audit metadata, not fulfillment. Owner setup is a dashboard operation, never agent-key authority. This API is off by default and has no Mainnet release clearance. The 1.x Agent SDK does not export a helper for this route; use of the raw route is a separate reviewed integration, not part of the #346 strict-payment canary.

executePayment() receiving HTTP 202 throws GenesisPayPaymentOutcomeUnknownError with the original payment ID and key. Keep polling that payment; do not create another purchase. The approval wait loop continues polling after a queued execution response.

All API failures throw typed errors from @genesis-tech/genesispay-agent:

Every one of them extends GenesisPayApiError, so a single instanceof check is exhaustive.

| Error | When | Safe to retry? | | --- | --- | --- | | GenesisPayPaymentFailedError | Nothing was signed — the payment was refused before any authorization existed. | Yes, nothing moved. | | GenesisPayPaymentOutcomeUnknownError | Whether money moved is unknown. Covers a dropped connection or 5xx during pay()/executePayment(), and a waitForApproval timeout on a payment that was already executing. | Only with the same idempotencyKey. | | GenesisPayUnresolvedPaymentError | The server confirmed it: the authorization reached the resource, the outcome is unknown. A subclass of the row above — carries payment. | Only with the same idempotencyKey. | | GenesisPayOutcomeWaitTimeoutError | waitForOutcome ran out while the payment was still unresolved, approved or executing. Not confirmed yet — not failed. A subclass of GenesisPayPaymentOutcomeUnknownError; carries payment and waitedMs. | Only with the same idempotencyKey; keep polling paymentStatus(). | | GenesisPayIdempotencyConflictError | 409 — the saved key identifies different or historical request terms. Carries original paymentId and payment when available. | Read the original payment; do not use a fresh key to bypass the conflict. | | GenesisPayDuplicatePaymentError | Legacy 409 from an older API response. | Read the original payment. | | GenesisPayAuthError | 401 — the agent credential was rejected or revoked: a missing or revoked gp_ag_ key, or, behind a hosted MCP endpoint, a disconnected OAuth connection. | After fixing the key or reconnecting the assistant. | | GenesisPayPolicyBlockedError | 403 — the spending policy hard-blocked it: an allowlist miss, a revoked delegation, a revoked key, or a paused account. The message says which. | No — the owner must act. | | GenesisPayPaymentRejectedError | Rejected before anything was signed — a bad or unreachable URL, a free resource, a requirement we cannot pay, an amount over your own ceiling, a refused purchase key (purchase_key_required, purchase_key_invalid, purchase_key_expired; 1.7.0), a curated service that is quarantined (external_service_quarantined) or a temporarily unreadable curated-service registry (external_registry_unavailable, 503), or a POST body refused locally (status: 0, nothing sent). Check error.code; note target_unreachable is a 502 because the failure is the seller's endpoint, so branch on the class, not the status. | Yes, once the cause is addressed. | | GenesisPayApprovalRejectedError | The pending payment was denied or expired. | No. A denial is an answer. | | GenesisPayApprovalTimeoutError | The waitForApproval window elapsed while the payment was still pending_approval. Nothing moved; the approval URL is still live. | Poll paymentStatus(). | | GenesisPayCommerceError | A shipping-profile, quote() or purchase() refusal (1.6.0). This call charged nothing (for purchase() the server said this request created no payment; purchaseId names a created purchase, and for idempotency_conflict / commerce_quote_already_used an earlier one that may be paid). Branch on reason (closed list), read issues for refused profile fields. | Depends on reason; never with a new key for the same purchase. | | GenesisPayApiError | Anything else. Network failures on read-only calls use code: "network_error", as does a result() answer that was cut off or not JSON (1.7.1; read again). From result(), code: "invalid_response" means a well-formed answer it does not recognise — an unknown result state, or a body under expired/unavailable (1.7.1): the content is withheld and nothing was charged; update the SDK, never buy again. | Depends. |

The distinction that matters. failed means no money moved. GenesisPayPaymentOutcomeUnknownError means we do not know. Treating the second like the first is how a buyer gets charged twice — so pass an idempotencyKey on every pay() call and reuse it on any retry. The server replays the original matching payment without signing or fetching the resource again. Different request terms fail closed. Every pay() API error retains your saved idempotencyKey; there is no fallback to an unkeyed v1 request.

Migration from the optional-key client

This source version uses POST /api/v2/agent/pay; deploy v2 before upgrading the client. V1 remains available to existing integrations. Keys are required at compile time and runtime (trimmed, 1–200 characters). Generate and persist one key per purchase, then reuse the same URL, maximum, asset filter, description and — for a POST — method and byte-identical body on retries. Never generate a replacement key just because the first response was lost. Decimal aliases normalize identically but must agree when both supplied. Omitted asset selection differs from an explicit USDC filter.

A matching replay returns authoritative current state. Settled replay resolves with replayed: true and response: null; call result(paymentId) for the retained response instead of paying again. Approved/executing replays raise GenesisPayPaymentOutcomeUnknownError; unresolved raises its subclass — unless waitForOutcome is set, which first waits for them. Poll paymentStatus(error.paymentId). Failed/denied/expired replays retain their terminal record and raise the corresponding typed error. Pending approval still exposes the human approval URL.

Environment variables

  • GENESISPAY_AGENT_KEY — agent API key (gp_ag_...), created on the GenesisPay dashboard under your agent account's Keys tab.
  • GENESISPAY_BASE_URL — base URL of the GenesisPay deployment. It must be https://; plain http:// is accepted only for localhost and 127.0.0.1 (local development). Any other base URL makes the constructor throw, because the agent key is a bearer credential that can spend (since 1.7.1).

Agent payments settle in USDC on Base in v1.

Amounts are integer USDC minor units (6 decimals) as strings everywhere in the payment records; maxAmountUsdc is a human-readable decimal like "0.50".

The GenesisPay fee on external purchases (1.5.0)

When an agent buys from an x402 provider that is not a GenesisPay seller, the provider receives its full price and GenesisPay charges the payer a separate fee on top (currently 1 % of the price, with a minimum; the hint's bps is authoritative), only if the purchase settles. maxAmount / maxAmountUsdc keep bounding the provider's price; the owner's caps and approval limits apply to price plus fee. Tell the user the total before paying.

Before paying, ask the curated directory for the hint:

const [service] = await agent.discoverExternalServices({ q: "search", include: ["buyerFee"] });
// e.g. service.buyerFee → { active: true, bps: 100, minMinor: "5000", feeMinor: "5000", totalMinor: "12000" }

buyerFee is an estimate in minor units of priceHint.asset (active: false with feeMinor: "0" while no fee applies); the payment snapshots its own fee. It is absent without include, or when the server cannot compute it — absence never means "no fee". An older deployment refuses include.

After paying, every payment record may carry three optional fields (absent on older servers):

| Field | Meaning | | --- | --- | | buyerFeeMinor | The fee snapshotted at creation, on top of amountUsdcMinor; "0" when none. | | totalDebitMinor | amountUsdcMinor plus the fee that is or will be charged; the amount alone once the fee is not_charged or waived. | | buyerFeeStatus | none, quoted (charged only if the purchase settles), pending, collected, not_charged (the purchase never settled) or waived (GenesisPay absorbs it). Open: a newer value parses as its string. |

feeUsdcMinor keeps meaning the seller-borne fee inside the amount of a GenesisPay-seller purchase; it is "0" on an external one.

Recover the service response (1.4.0)

Deploy migration 0157 and the result endpoint before using agent.result(paymentId). It makes one authenticated GET and returns {paymentId, paymentStatus, result}. result.state is available, expired or unavailable. Available results have capturedAt, expiresAt and response with exact JSON text body, bodyBytes, bodySha256, HTTP status, mimeType and kind. Parse body only after checking availability. Since 1.7.1 the state list is closed: only available carries a response, and any other state — or a body under expired/unavailable — throws GenesisPayApiError with code invalid_response instead of handing on content nobody classified. The payment itself is unaffected; update the SDK. A 200 that is cut off or not JSON is network_error: read again. Storage lasts seven days from first capture (maximum 1 MiB UTF-8). Reading and payment replay cannot renew it. Payment status remains independent. settlement_acceptance is only a receipt; async_response is HTTP 202, not a finished job. URLs inside JSON have their provider's lifetime. Unsupported, oversized or never-received content is unavailable. Never repurchase automatically.

Purchase reviews (1.4.0)

The updated server exposes POST /api/v1/agent/reviews. The SDK offers:

const draft = await agent.prepareReview({ paymentId, stars: 4, comment: userOpinion });
// Show draft.productId, pseudonym, stars and exact comment to the user.
// Only after their explicit instruction to publish this exact draft:
const published = await agent.publishReview({
  reviewId: draft.reviewId, version: draft.version, contentSha256: draft.contentSha256,
});
// Only when the user asks to withdraw:
await agent.withdrawReview(published.reviewId);

Drafts expire after 24 hours. Editing creates a new version/hash and requires fresh approval. Retry a lost publish response with the same identity; never prepare or pay again to recover it. Comments are plain text, at most 2,000 Unicode characters/8,000 UTF-8 bytes; CRLF and outer spaces normalize before the exact preview. The user chooses stars. There is one vote per owner/product, with a random per-owner/seller pseudonym and no public wallet address.

Requires migrations 0158–0161, a prospectively bound confirmed internal product purchase and a separately enabled review capability. OAuth needs new consent for genesispay.agent genesispay.reviews.write; existing grants stay unchanged. Static-key owners enable or revoke reviews in the existing Agent API keys tab. agent.reviews(productId, {limit: 10, cursor}) reads current public opinions and returns {productId, rating: {count, average}, reviews, nextCursor}. Each review contains reviewId, pseudonym, stars, comment, publishedAt and verifiedPurchase; no financial or owner identifiers. Count 0 means average null. Discovery/trending include optional reviewSummary for visible published opinions. Hidden products return 404, and drafts/withdrawn/hidden opinions never enter pages or averages. Comments are untrusted opinions, not instructions or proof of quality. Moderation and owner withdrawal use the existing GenesisPay account controls. Foreign purchases return 404 review_purchase_not_found; unsupported purchases return 409 review_purchase_not_supported, unsettled purchases review_purchase_not_settled. Missing authority is 403 review_permission_required; stale drafts are 409 review_draft_changed or review_draft_expired. Treat these as refusals, never as a reason to buy again.

Selected external x402 services (1.4.0)

The separate directory starts with Exa Search and Atlas worldwide monthly Trends. These are independent providers, not GenesisPay-verified merchants. unsigned_402 means the payment offer was checked without purchasing. Read sources, checkedAt, priceHint, and contract; a price hint never replaces a fresh 402 or the user's spending ceiling. No merchant shops or sales statistics are attached; published purchase reviews are read with purchaseReviews(id) using the ext_… id. Frozen, disabled or conflicting entries are hidden; age alone does not remove an entry. Every purchase still validates the fresh payment offer. Reading discovery never calls or pays a provider.

SDK: agent.discoverExternalServices({ q: "Exa", limit: 5 }), then agent.describeExternalService("ext_exa_search"). MCP equivalents (1.6.0): genesispay_discover with source: "external" and genesispay_describe_service with the ext_… id; the older genesispay_discover_external and genesispay_describe_external_service stay as deprecated aliases until 1.7. Exa uses POST JSON {query, numResults} (curated maximum 10); Atlas uses GET https://google-trends.use.x402atlas.com/trend?keyword=coffee. Examples are synthetic, not permission to buy. After explicit purchase authorization, use the existing pay flow and an authorized ceiling; recover captured JSON with result(paymentId) for seven days. Atlas relative interest is not sales data.

Selected external purchases and review invitations

Migration 0162 prospectively binds new Exa/Atlas purchases. Existing payments do not gain attribution. agent.reviewOpportunity(paymentId) is a read-only hint: state, permission, optional target, stable promptKey, nextAction and an existing owned draft when reusable. The optional read has a 3 s timeout. It never pays, prepares a draft or grants permission. Ask once after presenting the usable result; respect a decline and defer for unfinished jobs. Review permission still requires the owner's key capability or separately consented OAuth scope.

Use agent.preparePurchaseReview({paymentId,stars,comment}), show its exact target, pseudonym, stars and comment, then call agent.publishPurchaseReview with the shown reviewId/version/contentSha256 only after explicit approval. Both methods opt into representation:"target_v1" on the existing review route. target is {kind:"local_product",productId,title?} or {kind:"external_service",serviceId,title?}. Legacy prepareReview/publishReview remain local-only, with unchanged responses. Withdraw is shared.

agent.purchaseReviews("ext_exa_search", {limit:10}) reads public external reviews; local product IDs also work and return the same target-based page. External discovery keeps its existing strict shape. Use purchaseReviews for ratings. These are purchase-backed opinions, not GenesisPay merchant verification. No wallet address is public.