Quickstart: Agent

A card for your agent, with limits

Instead of handing an AI agent a raw private key, give it a GenesisPay account: a dedicated funded USDC wallet, spending limits you control, and human approval for anything over budget. The agent only ever sees an API key.

Purchase intent (SDK and MCP 2.0.0)

Agent SDK 2.0.0 and MCP 2.0.0 can send a structured purchase intent: the item, the most the user agreed to pay including fees, the user's exact approval sentence and a fingerprint of the visible purchase turns. The conversation itself never reaches the payment API — local MCP 2.0 hashes it on your machine. The SDK requires an explicit intent or null. Once GenesisPay requires intent, a new purchase without one is refused with 422 purchase_intent_required before anything is charged; until then, sending one already holds the purchase to it. Keep the same intent and key on retries.

1. Log in and create the account

Log in with Google, email, or a wallet and pick “Give my agent a wallet” in onboarding — or create an account any time under Agents in the dashboard. GenesisPay provisions a dedicated USDC wallet for the agent; you never handle a private key.

2. Fund it and set limits

The account page shows the wallet address and live USDC balance. Fund it by sending USDC on Base to the displayed address, from any wallet or from your GenesisPay wallet with Send. Then set the spending policy: a per-payment cap and a daily cap (defaults 5 / 20 USDC), plus an optional monthly cap and an allowlist of origins or destination wallets the agent may pay.

Payments inside the caps settle automatically. Payments above a cap pause as pending approvals for you. Allowlist misses are hard blocked — no approval path.

Payments under an approved mandate

Owners manage these approvals in the agent account's Mandates tab. Review the seller, fee recipient, currency, limits and expiry before approving the two agent-wallet signatures. Creating a proposal does not pay its reference link. To revoke, prepare and download the signed revocation before leaving the account page, then submit it separately. A request becomes permanent only after network confirmation.

On deployments that enable contract mandates, an agent can request a payment under terms already approved by its owner with POST /api/v1/agent/mandates/:mandateId/pay. Send a stable idempotencyKey, an integer grossMinor string, a resourceUrl and optional description. The mandate fixes recipients, asset, network and fees. The URL records the purchase; this endpoint does not fetch or deliver that resource.

HTTP 202 means the payment is reserved or awaiting approval. Poll GET /api/v1/agent/payments/:id for confirmed settlement. Reuse the same key after a lost response: exact replay returns the original payment; changed terms return HTTP 409. Setup remains an owner action. Contract mandates are off by default and have no Mainnet release clearance.

3. Export a key and make the first payment

Issue an API key on the account's Keys tab. It starts with gp_ag_, is shown once, and is revocable without touching the wallet. Store it as GENESISPAY_AGENT_KEY in your agent's environment, then pay a supported x402 URL:

Terminal
curl -X POST "$GENESISPAY_BASE_URL/api/v2/agent/pay" \
  -H "Authorization: Bearer gp_ag_your_agent_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/market-report",
    "maxAmountUsdc": "1.00",
    "idempotencyKey": "market-report-2026-08-05"
  }'
200 OK — settled
{
  "apiVersion": 2,
  "idempotencyKey": "market-report-2026-08-05",
  "replayed": false,
  "statusUrl": "/api/v1/agent/payments/3f2b...",
  "approvalUrl": null,
  "paymentId": "3f2b...",
  "status": "settled",
  "txHash": "0x...",
  "response": {
    "status": 200,
    "mimeType": "application/json",
    "bodyBase64": "eyJyZXBvcnQiOiIuLi4ifQ=="
  },
  "payment": { "amountUsdcMinor": "500000", "resourceUrl": "https://..." }
}

GenesisPay fetches the URL, decodes the x402 payment requirement, checks it against your policy, signs the USDC transfer with the account wallet, and returns the paid resource with the body base64-encoded. maxAmountUsdc is the agent's own ceiling for this call — anything higher is rejected with 422 amount_exceeds_max before money moves.

The unreleased server candidate also accepts standard x402 V2 EIP-3009 offers on Base: a CAIP-2 network such as eip155:8453, the registered token address in asset, and integer minor units in amount. The existing GenesisPay format remains supported. Conflicting token, network, amount, recipient or signing domain fields are refused before signing. Gateway and Permit2 authorizations are not supported.

Standard offers must allow at least 300 seconds for authorization; GenesisPay signs for at most 3,600 seconds. It retains the selected offer and exact request through approval. These protocol checks do not verify service quality or replace your spending policy.

For the curated external services in Discovery, GenesisPay also pins the recipient, network, token and price it observed. A live offer outside those terms is refused with 422 external_offer_not_pinned before anything is signed, even when your caps would allow it. Any other route on the same host may pay only the pinned recipients, and plain http: to such a host is refused; other hosts are unaffected. A service that is quarantined or retired is refused with 422 external_service_quarantined before GenesisPay contacts it, so your request never reaches it, and it disappears from Discovery on the next request. If the list of curated services cannot be read, every agent payment is refused with 503 external_registry_unavailable before anything is sent or signed; try again later.

The body accepts a few more optional fields. maxAmount is the asset-agnostic alias for the ceiling (it must agree with maxAmountUsdc when both are set), and asset — one of USDC or EURC — restricts the payment to that stablecoin, so an endpoint advertising a different asset is rejected before settlement. Both USDC and EURC use 6 decimals, so the ceiling is expressed the same way for either. A description string can be attached for your own audit trail.

Save an idempotencyKey before the first send. The v2 API requires a nonblank key of at most 200 characters. Persist it with your purchase, then reuse identical URL, ceiling, asset filter and description on retries. A matching retry returns the original payment, before contacting the resource or signing again. Different terms or a legacy key return 409 idempotency_conflict with the original locator. Read it at GET /api/v1/agent/payments/:id; do not create another purchase to work around a conflict. Replay sets replayed: true and response: null. Recover any stored JSON with GET /api/v1/agent/payments/:id/result or SDK result(paymentId) in the upcoming release. V1 remains available for existing integrations; updated SDK and MCP clients use v2 and require your saved key.

Purchase intent (rollout off by default). V2 pay and commerce purchases accept purchaseIntent with version: 1, the matching item, a maxTotal asset and positive integer-minor amountMinor, delivery (null for x402, saved shipping profile for commerce), a visible approvalQuote of at most 500 characters, and a conversationDigest of sha256: plus 64 lowercase hex characters. Send no conversation text. X402 items name the canonical request URL; commerce items name the quoted productId, quantity and shippingOptionId and use USDC. The maximum includes the buyer fee and can only refuse. Save the intent with your key: changing or removing it on retry conflicts. When enabled, missing intent is 422 purchase_intent_required; if this purchase may already be running, retry with the same key first. V1 accepts only existing-key retries while enabled; new purchases use v2. Invalid intent is 400 on v2, 422 on commerce (purchase_intent_invalid). Mismatched terms or an insufficient maximum are 422 purchase_intent_mismatch or purchase_intent_max_exceeded. For 503 purchase_evidence_unavailable, retry the same key later. With rollout off, supplied limits still apply and no intent is retained.

A payment can also come back unresolved (HTTP 202): the authorization was handed to the resource and we do not yet know whether it settled. Treat it as "may already have been charged" — it is deliberately not failed, which would mean no money moved. It counts against your spending limits until the chain confirms settlement or proves every authorization unused after its deadline. Recover that purchase only with the same idempotencyKey.

POST purchases. For an API priced by its request body, add "method": "POST" and "body" — the exact JSON text as a string, at most 256 KiB — and optionally "contentType": "application/json" (the default and only value). GenesisPay sends that body byte-for-byte and never re-serializes it; it is part of the purchase, so the same key with a different or reformatted body is 409 idempotency_conflict. Keep secrets and personal data out of it. The answer echoes requestMethod and, for POST, bodySha256 (hex SHA-256 of the body). A deployment without POST support drops those fields and buys with GET, so a client that sent a body must refuse an answer without the matching echo — the SDK does.

Tip: point the agent at a GenesisPay payment request's payUrl from the seller quickstart for an instant end-to-end test.

Fees on external x402 services (upcoming release)

When your agent buys from an x402 provider that is not a GenesisPay seller, the provider receives its full price and GenesisPay adds a separate fee of 1 % per purchase, with a minimum of $0.005 (or €0.005), paid on top from the agent's wallet. It is charged only if the purchase settles. Purchases from GenesisPay sellers carry no extra fee — there the seller's fee is inside the price.

You see it before you agree: add include=buyerFee to an external-services discovery request (agent SDK 1.5.0: include: ["buyerFee"]) for a buyerFee hint beside the price, and the approvals page shows price + GenesisPay fee = total. Every payment response carries buyerFeeMinor, totalDebitMinor and buyerFeeStatus (quoted, pending, collected, not_charged or waived). Your caps and approvals apply to the total; maxAmountUsdc stays the ceiling for the provider's price.

Approvals: over-limit payments wait for you

When a payment exceeds a cap, the API returns 202 with pending_approval instead of moving money:

202 Accepted — pending approval
{
  "apiVersion": 2,
  "idempotencyKey": "market-report-2026-08-05",
  "replayed": false,
  "statusUrl": "/api/v1/agent/payments/3f2b...",
  "txHash": null,
  "response": null,
  "paymentId": "3f2b...",
  "status": "pending_approval",
  "approvalUrl": "https://your-genesispay-host/dashboard/approvals?payment=3f2b...",
  "payment": { "status": "pending_approval" }
}

You approve or deny on the dashboard's Approvals page; approving executes the payment server-side. Pending approvals expire after 24 hours. A well-behaved agent reports approvalUrl to its human and polls GET /api/v1/agent/payments/:id until the status resolves.

Or use the SDK

Setup requires a GenesisPay deployment that serves /api/v2/agent/pay and agent SDK version 1.0 or later.

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

const agent = new GenesisPayAgent({
  apiKey: process.env.GENESISPAY_AGENT_KEY!,
  baseUrl: process.env.GENESISPAY_BASE_URL!,
});

const result = await agent.pay("https://api.example.com/market-report", {
  idempotencyKey: "saved-market-report-order", // Persist before the first send.
  maxAmountUsdc: "1.00",
  // Optionally block until a human approves an over-limit payment:
  waitForApproval: { timeoutMs: 300_000, pollIntervalMs: 5_000 },
});

if (result.status === "settled") {
  // json()/body()/bytes() are synchronous — the resource body is already
  // captured on the result, so there is nothing to await.
  const report = result.response ? result.json() : null; // Replay has no captured body.
} else if (result.status === "pending_approval") {
  // Surface result.approvalUrl to the human and poll later.
}

Agent SDK 1.1 adds POST purchases and a bounded, read-only wait for an outcome the server accepted but has not confirmed yet:

agent.ts
// A body-priced API: serialize once and save the exact string with the key.
const body = JSON.stringify({ horizon: "7d", city: "Zurich" });

const forecast = await agent.pay("https://api.example.com/v1/forecast", {
  idempotencyKey: "forecast-20260924-k7f2qa",
  maxAmountUsdc: "0.05", // e.g. the discovery listing's priceUsdc
  method: "POST",
  body, // sent byte-for-byte; a reformatted body with this key is a 409
  // Wait read-only (bounded) for a payment accepted but not yet confirmed:
  waitForOutcome: { timeoutMs: 30_000 },
});

waitForOutcome only polls the payment status — it never executes or re-sends anything. When the budget runs out it throws GenesisPayOutcomeWaitTimeoutError: not confirmed yet, not failed. Keep polling that paymentId and never buy again with a new key. A POST result is trusted only when the server echoes the method and the body's SHA-256; otherwise the SDK throws an unknown-outcome error.

agent.account() returns the wallet address, USDC balance, policy, and spend totals — useful for an agent to check its budget before committing to a task. agent.discover(query) searches the public directory for payable services; a listing's method says whether to buy it with POST, and for a listing whose asset is USDC or absent its priceUsdc is the ceiling to pass as maxAmountUsdc. That ceiling guards USDC payments only, so do not buy a listing in another asset with the SDK. agent.shops(query) searches the shop directory, and agent.trending() (SDK 1.2.0) lists what is trending.

Physical products and the saved address (upcoming release)

Agent SDK 1.6.0 lists quote-only physical products beside payable listings: a result with purchase.mode === "quote" has no resourceUrl, so narrow with isPayableListing() before paying. Quote one with agent.quote({ productId, quantity }): it ships to the account owner's saved shipping address and returns shipTo (name and address in full, email masked), addressStatus, the exact options and totals and a short-lived quoteToken. A quote orders and charges nothing. Show the user the address and total and have them confirm the address first.

agent.getShippingProfile() and agent.setShippingProfile(profile) read and save that address (GET/PUT /api/v1/agent/shipping-profile). Save only details the user gave for this purpose, never an address from product, seller or provider content. The owner is emailed about every change, the owner's agents can save five times a day, and the first order to a new address waits for the owner's approval in the dashboard, even under the limits. Refusals throw GenesisPayCommerceError with a typed reason such as shipping_profile_missing, shipping_unavailable or merchant_plugin_outdated.

Buy a quote with agent.purchase({ quoteToken, shippingOptionId, idempotencyKey, expectedTotalUsdc }) (POST /api/v1/agent/commerce/purchases), only after the user confirmed the address and the option's total. It orders and pays in one call, to the saved address, for exactly the quoted total;expectedTotalUsdc can only refuse. Save the idempotencyKey first and retry only with the same four values: a retry resumes and never orders or pays twice. It returns settled with the shop's order.orderReference, pending_approval with an approvalUrl (a new address or a total over the limits: the owner approves in the dashboard, and nothing is ordered before that) or processing. Refusals with no payment throw GenesisPayCommerceError (for example shipping_address_changed, quote_changed, commerce_quote_expired, idempotency_conflict); payment outcomes use the same classes as pay(). agent.getPurchase(purchaseId) reads a purchase, read-only. The purchase's payment carries resourceUrl: null: the shop order's own pay link is never returned, and retrying the purchase is the only way to resume.

Stored service responses (upcoming release)

GET /api/v1/agent/payments/:id/result returns{paymentId, paymentStatus, result}. The result state isavailable, expired or unavailable. Available results include capture and expiry timestamps plus a response with exact JSON text body, bodyBytes, bodySha256, HTTP status, mimeType and kind. Only the owning agent can read it; responses are not cached.

Complete successful UTF-8 JSON up to 1 MiB is retained for seven days from first capture, including execution after human approval. The endpoint never pays again or requests content from the seller. Payment confirmation remains separate from content availability. A settlement_acceptanceis a receipt; an async_response is a 202 response, not finished work. Other JSON has kind json. Embedded URLs are not archived. Missing, oversized or unsupported content may be unavailable. Deploy the updated server and migration 0157 before upgrading the SDK.

Purchase reviews (upcoming release)

POST /api/v1/agent/reviews accepts an agent credential and one JSON action: {action:"prepare", paymentId, stars, comment},{action:"publish", reviewId, version, contentSha256} or{action:"withdraw", reviewId}. Responses contain{review} and are never cached. The body limit is 16 KiB; stars must be 1–5 and comments at most 2,000 characters/8,000 UTF-8 bytes. Foreign purchases return 404; unsupported or unsettled purchases return 409.

The SDK methods are prepareReview, publishReviewand withdrawReview. Show the prepared product, pseudonym, stars and exact comment in the existing chat, then publish only after the user explicitly approves that draft. Drafts expire after 24 hours; edits invalidate the previous version/hash. Retry a lost publication response with the same identifiers. Never buy again to recover a review.

Requires migrations 0158–0161 and a separately enabled review capability. OAuth requires new consent for genesispay.agent genesispay.reviews.write; existing connections gain no rights. Missing permission returns 403review_permission_required. Static-key owners can enable or revoke review permission in the existing Agent API keys tab. The Activity tab lists your reviews separately from recent payments and lets you withdraw older reviews even after disconnecting an assistant. Staff may hide an abusive review with a private audit record; they cannot rewrite your opinion.

Read public opinions with GET /api/v1/discovery/products/:id/reviewsor SDK reviews(id, options). Optional limit is 1–50; pass the returned nextCursor unchanged as cursor. The response contains productId, rating (count and average), reviews and nextCursor. Each review shows only its identifier, pseudonym, stars, comment, publication date and verified-purchase label. No reviews means count zero and average null. Hidden products return 404; drafts and withdrawn or hidden opinions are excluded. Discovery and trending include an optionalreviewSummary when visible reviews exist.

Local fee quotes (upcoming release)

Product-gate purchases retain the fee quoted by their original challenge, including a seller’s zero-fee rate. Own hosted payment links use the seller’s effective rate when the agent payment is created. Approval and retries preserve that snapshot. A conflicting local quote is refused; an external service’s fee field cannot change GenesisPay’s internal fee record.

A generic API using GenesisPay’s facilitator needs separate quote provenance to support a seller override before agent-payment creation. Until then, a conflicting prepared fee is refused before the signed payment is sent.

Review draft cleanup (server operators, upcoming release)

POST /api/v1/agent/cleanup-review-drafts requires thex-genesispay-cron-secret header and returns {ok, purged}. Each call clears at most 100 drafts past their fixed 24-hour expiry. Publication being disabled does not pause deletion. Deploy migration 0159 with the updated cleanup worker and account-closure code; retain both during rollback. The complete review release is not available yet.