Skip to main content

React Integration

payid-react is the React package for PAY.ID, built on wagmi. It provides 20+ hooks for payments, rule management, and QR codes — handling all contract calls, wallet signatures, and WASM initialization automatically.


Install

npm install payid-react payid wagmi viem @tanstack/react-query ethers
# or
bun add payid-react payid wagmi viem @tanstack/react-query ethers

For QR image rendering (optional — payload string is always available):

npm install qrcode
npm install --save-dev @types/qrcode

Provider Setup

payid-react requires WagmiProvider and QueryClientProvider as parents. The PayIDProvider resolves contract addresses per chain automatically.

// main.tsx
import { WagmiProvider, createConfig, http } from 'wagmi'
import { hardhat } from 'wagmi/chains'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { PayIDProvider } from 'payid-react'

const wagmiConfig = createConfig({
chains: [hardhat],
transports: { [hardhat.id]: http() },
})

const queryClient = new QueryClient()

export default function Root() {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
{/* Optional: override contract addresses per chain */}
<PayIDProvider
contracts={{
31337: {
ruleAuthority: '0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44',
ruleItemERC721: '0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f',
combinedRuleStorage: '0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE',
payIDVerifier: '0x59b670e9fA9D0A427751Af201D676719a970857b',
payWithPayID: '0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1',
},
}}
ipfsGateway="https://gateway.pinata.cloud/ipfs/"
>
<App />
</PayIDProvider>
</QueryClientProvider>
</WagmiProvider>
)
}

Step-by-Step: Build a Complete Checkout Page

This walks you through adding PAY.ID to a React app from scratch.

1. Install & wrap your app

bun add payid-react payid wagmi viem @tanstack/react-query ethers

Wrap your app (see Provider Setup →):

<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<PayIDProvider contracts={{ [chainId]: { ...contractAddresses } }}>
<App />
</PayIDProvider>
</QueryClientProvider>
</WagmiProvider>

2. Add wallet connect (any wagmi connector)

import { useConnect, useAccount } from 'wagmi'
import { injected } from 'wagmi/connectors'

function ConnectButton() {
const { connect } = useConnect()
const { address, isConnected } = useAccount()

if (isConnected) return <span>{address?.slice(0, 8)}...</span>
return <button onClick={() => connect({ connector: injected() })}>Connect Wallet</button>
}

3. Add a payment button (payer side)

import { usePayIDFlow } from 'payid-react'

function PayButton({ receiver }: { receiver: `0x${string}` }) {
const { execute, status, isPending, isSuccess, error, txHash } = usePayIDFlow()

return (
<button
onClick={() => execute({ receiver, asset: USDC_ADDRESS, amount: 50_000_000n, payId: 'pay.id/store' })}
disabled={isPending || isSuccess}
>
{isSuccess ? `✅ Paid! TX: ${txHash?.slice(0, 10)}...` : isPending ? 'Processing...' : 'Pay 50 USDC'}
</button>
)
}

4. Check merchant subscription status

import { useSubscription } from 'payid-react'
import { useAccount } from 'wagmi'

function SubscriptionBadge() {
const { address } = useAccount()
const { data: sub } = useSubscription(address)

if (!sub?.isActive) return <span style={{ color: 'red' }}>⚠️ No active subscription</span>
return <span style={{ color: 'green' }}>✅ Subscribed — {sub.logicalRuleCount}/{sub.maxSlots} slots used</span>
}

5. Set up merchant rules (one-time)

import { useSubscribe, useCreateRule, useActivateRule, useRegisterCombinedRule } from 'payid-react'

// Follow the 4-step merchant setup from Quick Start →
// subscribe() → createRule() → activateRule() → registerCombinedRule()

That's all you need for a fully working checkout. The rest of this page covers every available hook in detail.


Hooks Overview

Payment Hooks (Payer side)

HookReturnsDescription
usePayIDFlow(){ execute, status, isPending, isSuccess, decision, txHash, error, reset }Complete payment flow — rules + proof + submit
usePayNative(){ pay, hash, isPending, isConfirming, isSuccess, error }Low-level native token payment (ETH, MATIC, A0GI, etc.)
usePayERC20(){ pay, hash, isPending, isConfirming, isSuccess, error }Low-level ERC20 payment

QR Hooks (Merchant side)

HookReturnsDescription
usePayIDQR(){ generate, payload, qrDataUrl, status, isPending, isReady, error, reset }Generate signed QR for customers to scan

Merchant Setup Hooks (Write — one-time setup)

HookActionDescription
useSubscribe()subscribe(priceInWei)Subscribe to unlock rule slots
useCreateRule()createRule({ ruleHash, uri })Register a rule definition on-chain
useActivateRule()activateRule(ruleId)Mint the Rule NFT
useCreateRuleVersion()createRuleVersion({ parentRuleId, newHash, newUri })Create a new version of an existing rule
useExtendRuleExpiry()extendRuleExpiry({ tokenId, newExpiry, priceInWei })Extend Rule NFT expiry
useRegisterCombinedRule()registerCombinedRule({ ruleSetHash, ruleNFTs, tokenIds, version })Register as active payment policy
useDeactivateCombinedRule()deactivate()Deactivate your active policy

Read Hooks

HookDataDescription
useSubscription(address?)SubscriptionInfo | undefinedSubscription status, slots, expiry
useSubscriptionPrice()bigint | undefinedCurrent subscription price from oracle
useMyRules()RuleDefinition[]Your Rule NFTs (connected wallet)
useRules(options?)RuleDefinition[]All rules with optional filters
useRule(ruleId?)RuleDefinition | undefinedSingle rule by ID
useRuleCount()bigintTotal number of rules created
useRuleExpiry(tokenId?)bigintUnix expiry timestamp for a Rule NFT
useActiveCombinedRule(owner?)CombinedRule | undefinedActive payment policy for any address
useActiveCombinedRuleByDirection(owner?, direction)CombinedRule | undefinedActive policy by direction (INBOUND/OUTBOUND)
useAllCombinedRules(options?)CombinedRule[]All registered combined rules
useMyRuleSets()RuleSet[]Your registered rule sets
useOwnerRuleSets(owner?)RuleSet[]Rule sets for any address
useVerifyDecision(decision?, signature?)booleanVerify a Decision Proof on-chain
useNonceUsed(payer?, nonce?)booleanCheck if a nonce was already used

usePayIDFlow — Full Payment Flow (Payer)

The simplest way to pay. Handles every step: loading rules from chain + IPFS, evaluating, signing proof, ERC20 approval, and submitting the transaction.

import { usePayIDFlow } from 'payid-react'
import { parseUnits, zeroAddress } from 'viem'

const USDC_ADDRESS = '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d'

function CheckoutButton({ merchantAddress }: { merchantAddress: `0x${string}` }) {
const { execute, reset, status, isPending, isSuccess, error, decision, denyReason, txHash } =
usePayIDFlow()

const handlePay = () => {
execute({
receiver: merchantAddress,
asset: USDC_ADDRESS,
amount: parseUnits('50', 6), // 50 USDC
payId: 'pay.id/merchant',
// Optional: pass custom context fields (merged into tx/env/state)
context: { state: { spentToday: '0', period: new Date().toISOString().slice(0, 10) } },
})
}

return (
<div>
<button onClick={handlePay} disabled={isPending}>
{status === 'idle' && 'Pay 50 USDC'}
{status === 'fetching-rule' && 'Loading rules...'}
{status === 'evaluating' && 'Checking rules...'}
{status === 'proving' && 'Sign proof in wallet...'}
{status === 'approving' && 'Approve USDC...'}
{status === 'awaiting-wallet' && 'Confirm payment...'}
{status === 'confirming' && 'Confirming on chain...'}
{status === 'success' && '✅ Paid!'}
{status === 'denied' && '❌ Denied'}
{status === 'error' && 'Retry'}
</button>

{status === 'denied' && <p>Reason: {denyReason}</p>}
{status === 'success' && <p>TX: {txHash}</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}

{(status === 'success' || status === 'error' || status === 'denied') && (
<button onClick={reset}>Reset</button>
)}
</div>
)
}

Native Token Payment

Pass zeroAddress as the asset for the chain's native token (ETH on Ethereum, MATIC on Polygon, A0GI on 0G, etc.):

execute({
receiver: merchantAddress,
asset: zeroAddress, // address(0) = native token
amount: parseUnits('0.01', 18), // 0.01 native token
payId: 'pay.id/merchant',
})

Flow Status Reference

StatusWhat's happening
idleReady to start
fetching-ruleReading merchant's active rule hash from chain
evaluatingRunning rules through WASM engine
provingGenerating EIP-712 proof — wallet signature requested
approvingWaiting for ERC20 approve TX (auto-triggered if allowance is insufficient)
awaiting-walletWaiting for user to confirm payment TX
confirmingTX submitted, waiting for block confirmation
successPayment confirmed on-chain ✅
deniedRules rejected this payment ❌
errorSomething went wrong — check error field

Loading States

usePayIDFlow includes isPending which is true during all active states (fetching-rule → evaluating → proving → approving → awaiting-wallet → confirming). Use this to disable buttons and show spinners:

<button onClick={handlePay} disabled={isPending}>
{isPending ? (
<>
<Loader2 className="animate-spin w-4 h-4" />
Processing...
</>
) : 'Pay 50 USDC'}
</button>

For skeleton loaders on initial data load, use the isLoading state from wagmi hooks like useBalance:

const { data: balance, isLoading: balanceLoading } = useBalance({ address })

{balanceLoading ? <Skeleton className="h-10 w-40" /> : <div>{balance?.formatted}</div>}

Multi-Chain Native Currency Support

payid-react automatically detects the chain's native currency symbol. Use useChains and useChainId from wagmi to get the current chain's native token:

import { useChains, useChainId } from 'wagmi'

function TokenDisplay() {
const chainId = useChainId()
const chains = useChains()
const currentChain = chains.find(c => c.id === chainId)
const nativeSymbol = currentChain?.nativeCurrency.symbol ?? 'ETH'
const nativeName = currentChain?.nativeCurrency.name ?? 'Ethereum'

return (
<div>
<span>{nativeName}</span>
<span>{nativeSymbol}</span>
</div>
)
}

This works across all supported chains:

  • Ethereum → ETH
  • Polygon → MATIC
  • 0G Newton → A0GI
  • Lisk Sepolia → LISK
  • etc.

usePayIDQR — QR Code Generator (Merchant)

Merchants use this hook to generate a signed QR code that customers scan to pay. The QR encodes a SessionPolicyV2 — the merchant's payment constraints signed with EIP-712.

import { usePayIDQR } from 'payid-react'
import { parseUnits } from 'viem'

const USDC_ADDRESS = '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d'

function MerchantQR() {
const { generate, reset, status, isPending, isReady, payload, qrDataUrl, error } =
usePayIDQR()

const handleGenerate = () => {
generate({
payId: 'pay.id/my-shop',
allowedAsset: USDC_ADDRESS,
maxAmount: parseUnits('100', 6), // max 100 USDC per scan
expiresAt: Math.floor(Date.now() / 1000) + 3600, // valid 1 hour
})
}

return (
<div>
<button onClick={handleGenerate} disabled={isPending}>
{isPending ? 'Generating...' : 'Generate QR'}
</button>

{/* qrDataUrl is available if 'qrcode' package is installed */}
{qrDataUrl && <img src={qrDataUrl} alt="Scan to pay" width={256} />}

{/* payload is always available — use with any QR library */}
{payload && !qrDataUrl && (
// Example with react-qr-code (npm install react-qr-code)
<div style={{ background: 'white', padding: 16 }}>
<p style={{ fontFamily: 'monospace', wordBreak: 'break-all', fontSize: 12 }}>
{payload}
</p>
</div>
)}

{error && <p style={{ color: 'red' }}>{error}</p>}
{isReady && <button onClick={reset}>New QR</button>}
</div>
)
}

On the payer side, after scanning:

import { decodeSessionPolicyV2QR } from 'payid/sessionPolicy'

const policy = decodeSessionPolicyV2QR(scannedQRString)

// Pass to usePayIDFlow via sessionPolicyV2 context field
execute({
receiver: policy.receiver as `0x${string}`,
asset: policy.allowedAsset as `0x${string}`,
amount: BigInt(policy.maxAmount),
payId: policy.payId,
})

QR Status Reference

StatusWhat's happening
idleReady
signingMerchant wallet is signing the SessionPolicyV2
encodingEncoding policy to payid-v2:... string
renderingRendering QR image (only if qrcode is installed)
readyQR is ready — payload and optionally qrDataUrl available
errorFailed — check error

Merchant Write Hooks

useSubscribe — Subscribe to unlock 3 rule slots

import { useSubscribe } from 'payid-react'
import { useReadContract } from 'wagmi'
import { usePayIDContext } from 'payid-react'

function SubscribeButton() {
const { contracts } = usePayIDContext()
const { subscribe, isPending, isSuccess } = useSubscribe()

// Fetch current subscription price from oracle
const { data: price } = useReadContract({
address: contracts.ruleItemERC721,
abi: RuleItemERC721ABI,
functionName: 'subscriptionPriceETH',
})

return (
<button onClick={() => price && subscribe(price as bigint)} disabled={isPending}>
{isPending ? 'Subscribing...' : isSuccess ? '✅ Subscribed' : 'Subscribe'}
</button>
)
}

useCreateRule + useActivateRule — Mint a Rule NFT

import { useCreateRule, useActivateRule, useRuleCount } from 'payid-react'
import { keccak256, toBytes } from 'viem'

function CreateRuleFlow() {
const { createRule, isPending: isCreating, isSuccess: created } = useCreateRule()
const { activateRule, isPending: isActivating } = useActivateRule()
const { data: ruleCount } = useRuleCount()

const ruleJson = JSON.stringify({
id: 'usdc_only',
if: { field: 'tx.asset', op: 'in', value: ['USDC', 'USDT'] },
message: 'Only stablecoins accepted',
})

const handleCreate = () => {
createRule({
ruleHash: keccak256(toBytes(ruleJson)),
uri: 'ipfs://Qm...', // upload to IPFS first
})
}

const handleActivate = () => {
if (ruleCount !== undefined) {
activateRule(ruleCount as bigint) // activates the latest rule
}
}

return (
<div>
<button onClick={handleCreate} disabled={isCreating}> Create Rule </button>
<button onClick={handleActivate} disabled={isActivating}>Activate NFT</button>
</div>
)
}

useExtendRuleExpiry — Renew before expiry

import { useExtendRuleExpiry } from 'payid-react'

function RenewRule({ tokenId }: { tokenId: bigint }) {
const { extendRuleExpiry, isPending } = useExtendRuleExpiry()

const handleRenew = () => {
extendRuleExpiry({
tokenId,
newExpiry: BigInt(Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60), // +30 days
priceInWei: 100_000_000_000_000n, // 0.0001 ETH fallback; use oracle price in production
})
}

return (
<button onClick={handleRenew} disabled={isPending}>
{isPending ? 'Renewing...' : 'Renew 30 days'}
</button>
)
}

useRegisterCombinedRule — Activate payment policy

import { useRegisterCombinedRule } from 'payid-react'
import { keccak256, toBytes } from 'viem'

function RegisterPolicy({ tokenIds, ruleNFTAddress }: {
tokenIds: bigint[]
ruleNFTAddress: `0x${string}`
}) {
const { registerCombinedRule, isPending } = useRegisterCombinedRule()

const handleRegister = () => {
const ruleSetHash = keccak256(toBytes('my-policy-v1')) // deterministic hash of your rules

registerCombinedRule({
ruleSetHash,
ruleNFTs: Array(tokenIds.length).fill(ruleNFTAddress),
tokenIds,
version: 1n,
})
}

return (
<button onClick={handleRegister} disabled={isPending}>
{isPending ? 'Registering...' : 'Activate Policy'}
</button>
)
}

Read Hooks

useSubscription — Check subscription status

import { useSubscription } from 'payid-react'
import { useAccount } from 'wagmi'

function SubscriptionStatus() {
const { address } = useAccount()
const { data: sub } = useSubscription(address)

if (!sub) return null

return (
<div>
<p>Status: {sub.isActive ? '✅ Active' : '❌ Expired'}</p>
<p>Slots: {sub.logicalRuleCount} / {sub.maxSlots}</p>
<p>Expires: {new Date(Number(sub.expiry) * 1000).toLocaleDateString()}</p>
</div>
)
}

useSubscriptionPrice — Get dynamic subscription price

Fetches the current subscription price from the Chainlink oracle. Use this instead of hardcoding prices.

import { useSubscriptionPrice, useSubscribe } from 'payid-react'
import { parseEther } from 'viem'

function SubscribeButton() {
const { data: subPrice } = useSubscriptionPrice()
const { subscribe, isPending } = useSubscribe()

// Use dynamic price with fallback
const price = subPrice ? (subPrice as bigint) : parseEther('0.001')

return (
<button onClick={() => subscribe(price)} disabled={isPending}>
{isPending ? 'Subscribing...' : `Subscribe (~${Number(price) / 1e18} ETH)`}
</button>
)
}

Pricing logic:

  • The contract uses Chainlink ETH/USD oracle for dynamic pricing
  • Formula: (subscriptionUsdCents * 1e24) / (ethUsdPrice * 100)
  • Falls back to 0.0001 ether if oracle is stale or unavailable
  • Price is calculated on-chain, ensuring consistency across all clients

useMyRules — Your Rule NFTs

import { useMyRules } from 'payid-react'

function MyRules() {
const { data: rules = [], isLoading } = useMyRules()

if (isLoading) return <p>Loading...</p>

return (
<ul>
{rules.map(rule => (
<li key={rule.ruleId.toString()}>
Rule #{rule.ruleId.toString()}{' '}
{rule.active ? `✅ Active (tokenId: ${rule.tokenId})` : '⏳ Not activated'}
</li>
))}
</ul>
)
}

useActiveCombinedRule — Merchant's active policy

import { useActiveCombinedRule } from 'payid-react'

function MerchantPolicy({ merchantAddress }: { merchantAddress: `0x${string}` }) {
const { data: policy } = useActiveCombinedRule(merchantAddress)

if (!policy) return <p>No active policy — all payments allowed</p>

return (
<div>
<p>Policy hash: {policy.hash.slice(0, 10)}...</p>
<p>Version: {policy.version.toString()}</p>
<p>Rules: {policy.ruleRefs.length}</p>
</div>
)
}

useRuleExpiry — Check when a rule expires

import { useRuleExpiry } from 'payid-react'

function RuleExpiryBadge({ tokenId }: { tokenId: bigint }) {
const { data: expiry } = useRuleExpiry(tokenId)

if (!expiry) return null

const expiryDate = new Date(Number(expiry) * 1000)
const daysLeft = Math.floor((expiryDate.getTime() - Date.now()) / 86_400_000)

return (
<span style={{ color: daysLeft < 7 ? 'orange' : 'green' }}>
Expires {expiryDate.toLocaleDateString()} ({daysLeft}d left)
</span>
)
}

usePayIDContext — Access resolved addresses

import { usePayIDContext } from 'payid-react'

function ContractInfo() {
const { contracts, chainId, ipfsGateway } = usePayIDContext()

return (
<pre>{JSON.stringify({ chainId, ...contracts }, null, 2)}</pre>
)
}

Additional Write Hooks

useCreateRuleVersion — Create a new version of an existing rule

import { useCreateRuleVersion } from 'payid-react'
import { keccak256, toBytes } from 'viem'

function UpdateRule({ parentRuleId }: { parentRuleId: bigint }) {
const { createRuleVersion, isPending, isSuccess } = useCreateRuleVersion()

const updatedRule = { id: 'usdc_only_v2', if: { field: 'tx.asset', op: '==', value: USDC_ADDRESS } }

return (
<button
onClick={() => createRuleVersion({
parentRuleId,
newHash: keccak256(toBytes(JSON.stringify(updatedRule))),
newUri: 'ipfs://NEW_CID',
})}
disabled={isPending}
>
{isSuccess ? '✅ Updated' : isPending ? 'Updating...' : 'Update Rule'}
</button>
)
}

useDeactivateCombinedRule — Remove your active payment policy

import { useDeactivateCombinedRule } from 'payid-react'

function DeactivateButton() {
const { deactivate, isPending, isSuccess } = useDeactivateCombinedRule()

return (
<button onClick={() => deactivate()} disabled={isPending}>
{isSuccess ? '✅ Deactivated' : isPending ? 'Deactivating...' : 'Deactivate Policy'}
</button>
)
}
warning

Deactivating removes your active payment policy. All payments to you will pass without rule checking until you register a new combined rule.


Read Hooks (Detailed)

useRules — All rules with filters

import { useRules } from 'payid-react'
import { useAccount } from 'wagmi'

function AllRules() {
const { address } = useAccount()
const { data: myRules, isLoading } = useRules({
onlyActive: true, // filter to only active rules
creator: address, // filter to only rules created by this address
})

if (isLoading) return <p>Loading...</p>

return (
<ul>
{myRules.map(rule => (
<li key={rule.ruleId.toString()}>
Rule #{rule.ruleId.toString()} — hash: {rule.ruleHash.slice(0, 10)}...
{' | '}tokenId: {rule.tokenId.toString()}
{' | '}expires: {new Date(Number(rule.expiry) * 1000).toLocaleDateString()}
</li>
))}
</ul>
)
}

useAllCombinedRules — All policies on chain

import { useAllCombinedRules } from 'payid-react'

function AllPolicies() {
const { data: policies, isLoading, refetch } = useAllCombinedRules({ onlyActive: true })

return (
<div>
<button onClick={() => refetch()}>Refresh</button>
{policies.map(p => (
<div key={p.hash}>
<strong>{p.owner.slice(0, 8)}...</strong>
{' — '}v{p.version.toString()}{p.ruleRefs.length} rule(s)
</div>
))}
</div>
)
}

useVerifyDecision — Check a proof on-chain

import { useVerifyDecision, useNonceUsed } from 'payid-react'

function ProofStatus({ decision, signature, payer, nonce }: {
decision: Decision
signature: `0x${string}`
payer: `0x${string}`
nonce: `0x${string}`
}) {
const { data: isValid } = useVerifyDecision(decision, signature)
const { data: nonceUsed } = useNonceUsed(payer, nonce)

return (
<div>
<p>Proof valid: {isValid ? '✅' : '❌'}</p>
<p>Nonce used: {nonceUsed ? '⚠️ Already used' : '✅ Fresh'}</p>
</div>
)
}

TypeScript Types Reference

All types are exported from payid-react:

import type {
PayIDContracts,
PayIDContextValue,
RuleDefinition,
RuleSet,
RuleRef,
CombinedRule,
SubscriptionInfo,
PayIDFlowParams,
PayIDFlowResult,
PayIDFlowStatus,
PayIDQRParams,
PayIDQRResult,
PayIDQRStatus,
} from 'payid-react'
import { RuleDirection } from 'payid-react'

Key Types

// Contract addresses for one chain
interface PayIDContracts {
ruleAuthority: Address // RuleAuthority contract
ruleItemERC721: Address // Rule NFT contract
combinedRuleStorage: Address // CombinedRuleStorage contract
payIDVerifier: Address // PayIDVerifier contract
payWithPayID: Address // PayWithPayID contract
}

// Rule NFT data
interface RuleDefinition {
ruleId: bigint
ruleHash: Hash
uri: string // ipfs://CID
creator: Address
rootRuleId: bigint
version: number
deprecated: boolean
active: boolean
tokenId: bigint // 0 if not yet activated
expiry: bigint // unix timestamp, 0 = no expiry
}

// Active payment policy on-chain
interface CombinedRule {
hash: Hash
owner: Address
version: bigint
active: boolean
ruleRefs: RuleRef[] // array of { ruleNFT, tokenId }
direction?: RuleDirection
}

// Subscription status
interface SubscriptionInfo {
expiry: bigint
isActive: boolean
logicalRuleCount: number // current active rule count
maxSlots: number // 1 without sub, 3 with sub
}

// usePayIDFlow params
interface PayIDFlowParams {
receiver: Address
asset: Address // token address, zeroAddress for ETH
amount: bigint
payId: string
context?: Record<string, unknown> // extra context fields
attestationUIDs?: Hash[]
ruleAuthorityAddress?: Address
/** Token decimals (e.g. 6 for USDC, 18 for ETH). Required for USD oracle injection. */
tokenDecimals?: number
/** Chainlink AggregatorV3 address for token/USD. If set, oracle.txValueUsd is auto-injected into context. */
tokenPriceOracle?: Address
/** Minimum USD value (8 decimals) for on-chain oracle guard. e.g. 45_00000000 = $45.00 */
minUsdValue?: bigint
}

// RuleDirection for directional rules
enum RuleDirection {
INBOUND = 0, // payments coming IN to the owner
OUTBOUND = 1, // payments going OUT from the owner
}

Tips

No ready() needed in React. WASM loads lazily inside usePayIDFlow and usePayIDQR — no manual initialization needed.

Never cache a proof. usePayIDFlow generates a fresh proof every time. Proofs expire after ~5 minutes (300 seconds by default).

Check subscription before creating rules. Use useSubscription(address) to verify the merchant has active slots before calling useCreateRule.

Preview receiver's policy before paying. Use useActiveCombinedRule(receiver) to show the merchant's rules to the payer before they click pay.

Works with all wagmi connectors. MetaMask, WalletConnect, Coinbase Wallet, Safe, and any other connector work automatically.

ETH vs ERC20. Pass zeroAddress (from viem) as asset for native ETH payments. Pass the token contract address for ERC20.