Skip to content
DexterDocs
x402 SDK / Browser Payments
Sections

Browser Payments

useX402Payment sends a request, handles an x402 challenge with the connected wallet, and returns the API response. Your component still needs to handle rejected payments and unsuccessful HTTP responses.

Before You Start

Use Node.js 22 or newer to build a React 18 or 19 app with an existing wallet connection, a paid API URL, and USDC on the network that API accepts. The seller must allow your app's origin through CORS, including payment headers. To build the API, see Accept a Payment.

The hook in @dexterai/x402@6.0.0 has no maxAmountAtomic option. Show the API's price before the user clicks and review the wallet's payment request. For a programmatic price ceiling, use the lower-level payAndFetch options.

npm install @dexterai/x402@6.0.0 @dexterai/vault@0.43.4 @solana/web3.js@1 viem@2

Keep your wallet provider mounted above the payment component. Connecting a wallet remains part of your app's existing flow.

Display a Paid Response

Pass your connected Solana wallet and endpoint into this component. The wallet must expose publicKey and signTransaction; an address alone cannot authorize payment.

  1. The client requests the resource.
  2. The API returns HTTP 402 and its payment requirements.
  3. The client authorizes payment and retries with its proof.
  4. The API asks the facilitator to verify and settle the payment.
  5. The facilitator returns the settlement result.
  6. After successful settlement, the API returns the resource and receipt.
PayButton.tsx
'use client';

import { useState } from 'react';
import { useX402Payment } from '@dexterai/x402/react';
import type { SolanaWallet } from '@dexterai/x402/adapters';

type Props = {
  endpoint: string;
  solana: SolanaWallet | undefined;
};

export function PayButton({ endpoint, solana }: Props) {
  const [body, setBody] = useState('');
  const [failure, setFailure] = useState('');
  const { fetch: paidFetch, isLoading, transactionUrl, reset } =
    useX402Payment({ wallets: { solana } });

  async function request() {
    setBody('');
    setFailure('');
    reset();
    try {
      const response = await paidFetch(endpoint, { method: 'GET' });
      const text = await response.text();
      if (!response.ok) {
        setFailure(`API returned HTTP ${response.status}. Check payment status before retrying.`);
        return;
      }
      setBody(text);
    } catch (error) {
      setFailure(error instanceof Error ? error.message : 'Request failed');
    }
  }

  return (
    <div>
      <button
        type="button"
        onClick={request}
        disabled={!solana?.publicKey || isLoading}
      >
        {isLoading ? 'Request in progress...' : 'Pay and fetch'}
      </button>
      {failure ? <p role="alert">{failure}</p> : null}
      {body ? <pre>{body}</pre> : null}
      {transactionUrl ? <a href={transactionUrl}>View payment</a> : null}
    </div>
  );
}

A successful call displays the API's response body. A transaction link appears when the payment response includes a transaction identifier. An endpoint that serves the request without a payment challenge can return a body with no transaction link.

For a Solana wallet adapter, build the prop from its connected publicKey and signTransaction. The SDK declares a generic signing method, so the cast belongs at this boundary:

wallet.ts
import type { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
import type { SolanaWallet } from '@dexterai/x402/adapters';

type SignTransaction = <T extends Transaction | VersionedTransaction>(
  transaction: T,
) => Promise<T>;

export function toPaymentWallet(
  publicKey: PublicKey | null,
  signTransaction: SignTransaction | undefined,
): SolanaWallet | undefined {
  if (!publicKey || !signTransaction) return undefined;
  return {
    publicKey,
    signTransaction: signTransaction as SolanaWallet['signTransaction'],
  };
}

Payment State

Hook fieldUse
fetch(input, init?)Make the request; rejects when the payment flow fails.
isLoadingDisable duplicate submissions while the request runs.
errorError from the latest failed payment flow. Catch the returned promise too.
transactionId, transactionUrl, transactionNetworkPayment details reported by the response.
balances, refreshBalances()Read and refresh connected wallet balances.
reset()Clear displayed payment state. An in-flight request continues.

The hook's status describes the payment flow. Check response.ok separately to decide whether the requested API operation succeeded. A server error or a lost response can occur after payment; reconcile before retrying.

EVM Wallets

Pass an EvmWallet under wallets.evm. It needs the connected address and an EIP-712 signTypedData implementation. With wagmi, obtain the signing client from useWalletClient(); useAccount() supplies connection information only.

The same response and error handling applies. When both wallets are connected, set preferredNetwork to the API's CAIP-2 network if your app needs to select a particular chain.

If the Request Fails

SymptomCheck
The button stays disabledThe wallet connection has a public key and transaction signer.
The browser blocks the requestInspect the CORS preflight. The API must accept your origin and payment request headers.
The wallet reports insufficient fundsCheck the accepted asset and network, then refresh the balance.
Payment is visible but the body is missingPreserve the transaction identifier and request details. Ask the seller to reconcile delivery before paying again.

Last Updated:

On this page