Sign InGet Started
Back to Blog

From wallet connect to entitlement, in two React hooks

useTokenPayment picks the stablecoin and the allowance. usePayment fetches the signed quote, submits it, and confirms the transaction.

October 13, 2026•PayXor Team

A checkout in a Web3 app is a short list of on-chain steps, and most of the bugs sit in the gaps between them. The wallet has to be on the right chain. The token has to be one your app accepts. The allowance has to cover the price. The quote has to be signed by your backend. Then the contract has to record an entitlement you can read back.

@payxor/react runs that sequence on top of a viem wallet client. Two hooks cover the payment itself: useTokenPayment for the token, and usePayment for the quote and the transaction.

A glass card on a dark blue field, with a wallet linked by a line of light to three stablecoin discs.

What you wire up

Install the React package with the SDK and the shared types. react, react-dom, and viem are peer dependencies. Wallet connection stays yours: wagmi, RainbowKit, or any provider that can hand you a viem wallet client.

pnpm add @payxor/react @payxor/sdk @payxor/types

From the dashboard you need an app id, a product id, and the API URL. Both ids are bytes32 hex strings. Pass the API URL explicitly. Production is https://api.payxor.xyz.

The hooks take the same objects you already read from wagmi: useAccount() for the address, useChainId() for the chain, usePublicClient() to wait for receipts, and useWalletClient() for the signer. They build a PayXorClient only when that wallet client and the API URL are both present, so the stablecoin list stays empty until the wallet is connected.

The sequence the hooks run

The payment flow is the same whether you call the SDK yourself or let the hooks do it:

  1. Load the product so the UI knows the price and the mode.
  2. Load the stablecoins that app accepts on the connected chain.
  3. Read the payer's balance and allowance for the selected token.
  4. Approve the PayXor contract when the allowance is short.
  5. Request a signed quote. Your backend prices it and signs it with EIP-712.
  6. Submit that quote with pay.
  7. Wait for the receipt, then confirm the transaction with the backend.
  8. Read the entitlement on-chain before you unlock anything in the UI.
A row of frosted-glass icons joined by a cyan line, from a tag and coins through a shield, a sealed document, and blocks.

useTokenPayment owns the stablecoin list, the balance, the allowance, and the approval. usePayment owns the quote, the payment transaction, and the backend confirmation. Entitlement hooks cover the last read. The hook never invents a price. For how quotes are signed, and why a payer cannot edit the amount, see Backend-Authoritative Pricing.

A buy button

useTokenPayment calls getAppSupportedStablecoins(appId, chainId) and selects the first token until the user picks another. It loads the product price through getProductInfo, then the balance and allowance for that token. needsApproval is true when the allowance is below the price. approve() sends an ERC-20 approval for the maximum uint256 and waits for that transaction to mine.

usePayment does not approve. Call it after the allowance covers the price, and pass the selected token address.

import { useTokenPayment, usePayment, DropdownBuyButton } from "@payxor/react";
import { useAccount, useChainId, usePublicClient, useWalletClient } from "wagmi";

function BuyButton({ appId, productId }: { appId: string; productId: string }) {
  const { address } = useAccount();
  const chainId = useChainId();
  const publicClient = usePublicClient();
  const { data: walletClient } = useWalletClient();

  const {
    stablecoins,
    selectedToken,
    setSelectedToken,
    needsApproval,
    hasSufficientBalance,
    approve,
    approving,
  } = useTokenPayment({
    appId,
    productId,
    address,
    chainId,
    publicClient,
    walletClient,
    apiUrl: "https://api.payxor.xyz",
  });

  const { executePayment, loading, status, error } = usePayment({
    appId,
    productId,
    tokenAddress: selectedToken,
    address,
    chainId,
    publicClient,
    walletClient,
    apiUrl: "https://api.payxor.xyz",
  });

  return (
    <DropdownBuyButton
      stablecoins={stablecoins}
      selectedToken={selectedToken}
      onSelect={setSelectedToken}
      needsApproval={needsApproval}
      disabled={!hasSufficientBalance}
      onApprove={approve}
      onBuy={executePayment}
      loading={loading}
      approving={approving}
    />
  );
}

DropdownBuyButton is unstyled. Pass className, slotClassNames, and buttonProps to match your app. An empty stablecoins array renders the empty state: the wallet is still disconnected, or the connected chain has no token enabled for this app.

executePayment stops with "Please connect your wallet" when the address, chain, or client is missing, and with "Please select a payment token" when no token is selected. Put status and error next to the button. The status moves through "Fetching quote...", "Executing payment...", and "Waiting for transaction confirmation...".

Approval is its own transaction

When the allowance is short, the payer signs twice. The first transaction calls approve on the token. The second calls PayXor with the signed quote. The hooks keep those signatures separate, so a failed approval never submits a quote.

A cyan key entering a glass lock beside coins flowing into a vault.

The approval amount is the maximum uint256, so a later purchase of the same token on the same chain does not ask again. To approve the exact price instead, call approveToken on a PayXorClient from @payxor/sdk with that amount. useTokenPayment does not take an approval cap.

hasSufficientBalance is the balance counterpart of needsApproval. Disable the buy action when balance is below productAmount. The payment transaction is what reverts if the balance is short.

Receipt products can take a custom amount in the token's smallest unit, and only when that product was created with custom amounts enabled. Pass the same amount string to both hooks so the allowance check and the quote use one number. Session, feature, and pass products use the price stored on the product.

What happens inside executePayment

With a token selected, executePayment asks the API for a quote bound to that payer, chain, product, and token. The response is the quote plus sigBackend. The hook passes both to client.pay and stores the hash as soon as the wallet submits it.

Pass a publicClient. The hook then polls getTransactionReceipt for up to 60 seconds. A revert throws. A timeout throws and tells the payer to check the explorer. After a successful receipt, it calls confirmTransaction(txHash, quote.quoteId) so the backend can mark the quote paid. Treat the on-chain entitlement as the record of access. onSuccess receives the hash, onError receives the Error, and getExplorerUrl(hash, chainId) builds the explorer link. reset() clears loading, status, error, and the hash.

Read the entitlement before you unlock the UI

A mined transaction and an open screen are two different moments. Session products expire. Feature unlocks stay. The React package can poll the contract for you.

An hourglass of cyan light beside a key resting in an open lock.

useSessionStatus calls isSessionActive(address, appId, productId, chainId) and repeats every 5 seconds for as long as the screen is mounted. isActive is true, false, or null while the wallet or the chain is missing.

useFeatureStatus takes an entitlementId, the bytes32 stored on the product, rather than the product id. It calls isFeatureUnlocked and stops polling once the feature is unlocked.

import { useSessionStatus, useFeatureStatus } from "@payxor/react";

const session = useSessionStatus({
  appId,
  productId,
  address,
  chainId,
  walletClient,
  apiUrl: "https://api.payxor.xyz",
});

const feature = useFeatureStatus({
  appId,
  entitlementId,
  address,
  chainId,
  walletClient,
  apiUrl: "https://api.payxor.xyz",
});

Passes and receipts stay on the SDK: client.hasPass(payer, appId, passId, chainId) and client.verifyReceipt(txHash, quoteId, chainId). The React package does not ship hooks for those two checks.

Try it on a testnet first

PayXor has stablecoin addresses for Ethereum, Arbitrum, Base, Polygon, Avalanche, Optimism, BNB Chain, and zkSync, and for Sepolia, Base Sepolia, Arbitrum Sepolia, Polygon Mumbai, Optimism Sepolia, Avalanche Fuji, BNB Testnet, and zkSync Sepolia. The selector still shows only the tokens enabled for your app on the connected chain. A supported network can return an empty list.

Create the app and the product from the dashboard, copy the ids into the component above, and run approval and payment once with test funds. The documentation places these hooks next to the SDK and the API reference.