Pay-As-You-Verify (PAYV)

PAYV is an alternative to a monthly plan: instead of your app paying a subscription, a small fee in USDC is paid on-chain each time your app reuses a user's existing INFRA verification. It uses the open x402 payment protocol on Base, so web3 apps, wallets and AI agents can pay per verification with no card on file. Becoming verified is always free for the user; PAYV only charges for reuse. Payment is never consent: the user must have approved your app through INFRA Connect first.

When to use it#

  • Your app is web3-native and would rather settle per verification in USDC than hold a card subscription.
  • You want the user (or a sponsor) to pay for their own verification reuse, for example on an exchange, dApp or marketplace.
  • Your volume is spiky or low, so per-use pricing beats a monthly plan.

How billing works#

  • An app is either on a subscription plan or on PAYV, never both. PAYV apps aren't subject to subscription limits.
  • PAYV is opt-in per app and enabled on request: email engineering@infraidentity.com with your client_id. Where it isn't enabled, every PAYV endpoint returns 503 PAYV_DISABLED.
  • The fee, token and network are set by INFRA and returned in every quote (amount_display, asset_symbol, accepts[0].network). Don't hard-code them.
  • Test apps settle on Base Sepolia; live apps settle on Base.

How it works#

  1. Get consent with INFRA Connect — Redeem the user's one-time Connect code (see INFRA Connect). That records their approval for your app and returns their user_id. PAYV only works for users who have an active Connect approval covering the scopes you ask for.
  2. Ask for a quote — POST /payv/verification with the user_id and scopes, without payment. INFRA checks consent and your verification policy, then answers 402 Payment Required with a payment_request_id and the x402 payment requirements.
  3. Pay — The payer's wallet signs the payment requirements with any x402 client (for example Coinbase's x402 libraries). The payer can be the user or your app sponsoring the fee, unless you turn on strict payer mode.
  4. Retry with the payment — POST the same request with payment_request_id and the signed payment in the X-PAYMENT header. INFRA verifies and settles it on-chain, then returns the user's verified data and the transaction hash.

Ask for a quote#

const auth = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;

// 1) Ask for a quote (no X-PAYMENT header)
const quote = await fetch('https://api.infraidentity.com/api/v1/payv/verification', {
  method: 'POST',
  headers: {
    'Authorization': auth,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(), // optional: safe retries
  },
  body: JSON.stringify({
    infra_user_id: userId,                    // from INFRA Connect
    scopes: ['profile:basic', 'kyc:status'],
  }),
});

// 402 Payment Required
{
  "success": false,
  "error": { "code": "PAYMENT_REQUIRED", "message": "Payment is required to reuse this verification. …" },
  "payment_request_id": "9b2f…",
  "amount_display": "0.01",
  "asset_symbol": "USDC",
  "payer_requirement": "any",           // or "linked_wallet" in strict payer mode
  "accepts": [{
    "scheme": "exact",
    "network": "base",
    "maxAmountRequired": "10000",       // atomic units (USDC has 6 decimals)
    "resource": "/api/v1/payv/verification",
    "description": "INFRA verification reuse fee",
    "mimeType": "application/json",
    "payTo": "0x…",
    "maxTimeoutSeconds": 600,
    "asset": "0x…"
  }]
}

Pay and receive the data#

// 2) Have the payer's wallet sign accepts[0] with an x402 client,
//    then send the base64 payment payload in X-PAYMENT.
const result = await fetch('https://api.infraidentity.com/api/v1/payv/verification', {
  method: 'POST',
  headers: {
    'Authorization': auth,
    'Content-Type': 'application/json',
    'X-PAYMENT': paymentHeader,            // base64 x402 PaymentPayload
  },
  body: JSON.stringify({
    infra_user_id: userId,
    scopes: ['profile:basic', 'kyc:status'],
    payment_request_id: '9b2f…',           // from the 402 response
  }),
});

// 200 OK
{
  "success": true,
  "data": {
    "payment_request_id": "9b2f…",
    "status": "consumed",
    "tx_hash": "0x…",
    "replayed": false,
    "profile": { "first_name": "Ada", "last_name": "Okafor", "email": "ada@example.com", "country": "NG" },
    "kyc": { "kyc_level": "level_2", "status": "verified", "verified_at": "2026-10-01T09:12:00Z", "provider": "persona" }
  }
}

// Check a payment later
// GET /api/v1/payv/payment-requests/:id  →  status, amount_display, expires_at, tx_hash, failure_reason

Strict payer mode and wallet linking#

By default anyone may pay: the user or your app. Set payer_must_be_user: true in your app's verification policy to require that the paying wallet is linked to the user's INFRA account; quotes then say payer_requirement: "linked_wallet". Users can link a wallet in their INFRA account, or you can start the link from inside your dApp. If an unlinked wallet pays, nothing is charged and you get 402 PAYER_NOT_LINKED with wallet_link_url.

// Link a wallet from inside your dApp (strict payer mode)
// 1. Start: returns a message for the wallet to sign
POST /api/v1/payv/wallet-links
{ "payment_request_id": "9b2f…", "address": "0xUserWallet…" }
→ 201 { "challenge_id": "…", "message": "…", "chain_id": 8453, "expires_at": "…" }

// 2. Submit the wallet's personal_sign signature
POST /api/v1/payv/wallet-links/:challenge_id/signature
{ "payment_request_id": "9b2f…", "signature": "0x…" }
→ 200 { "status": "awaiting_user" }   // the user approves in their INFRA account

// 3. Poll until linked, then retry the same payment
GET /api/v1/payv/wallet-links/:challenge_id?payment_request_id=9b2f…
→ 200 { "status": "linked" }        // or "denied" if the user declines

Errors#

Code Status Meaning
CONSENT_REQUIRED 403 The user hasn't approved your app for these scopes. Get a Connect code from them first.
VERIFICATION_POLICY_NOT_SATISFIED 403 The user doesn't meet your verification policy (see policy_failures). No payment is requested.
PAYV_NOT_ENABLED_FOR_APP 400 Your app is on a subscription plan, not PAYV.
INVALID_SCOPES 400 PAYV accepts profile:basic and kyc:status.
PAYMENT_REQUEST_EXPIRED 410 The quote expired (10 minutes). Ask for a new one.
PAYMENT_ALREADY_CONSUMED 409 That payment request has already been used.
WRONG_CHAIN / WRONG_ASSET / WRONG_RECIPIENT / INSUFFICIENT_AMOUNT 402 The signed payment doesn't match the quote.
PAYER_NOT_LINKED 402 Strict payer mode: link the paying wallet, then retry. Nothing was charged.
PAYV_DISABLED 503 PAYV isn't enabled on this deployment.

Notes#

  • Consent is checked again on the paid retry: if the user revoked in between, the request is refused before anything is charged.
  • Only the data covered by the granted scopes is returned. Document images are never available through PAYV.
  • Every paid and failed attempt is recorded in INFRA's audit log with the transaction hash, without personal data.

This page as Markdown: /payv.md · All docs in one file: /llms-full.txt