Skip to main content

Advanced Usage 🚀

Level up your PAY.ID game! These are the pro patterns for production-grade integrations.

New to PAY.ID? Start with Simple Usage → first.


1️⃣ Complex Rule Patterns

Nested Rules (VIP vs Normal Paths) 🎯

Create different payment paths for different user types:

const VIP_RULE = {
id: 'vip_or_normal',
logic: 'OR' as const,
rules: [
{
// VIP path: high amount + KYC3 + low risk
id: 'vip_path',
logic: 'AND' as const,
rules: [
{ if: { field: 'tx.amount', op: '>=', value: '1000000000' } }, // >= 1000 USDC
{ if: { field: 'oracle.kycLevel', op: '==', value: '3' } },
{ if: { field: 'risk.score', op: '<=', value: '30' } },
],
},
{
// Normal path: 5-500 USDC + ID + KYC1 + moderate risk
id: 'normal_path',
logic: 'AND' as const,
rules: [
{ if: { field: 'tx.amount', op: 'between', value: ['5000000', '500000000'] } }, // 5-500 USDC
{ if: { field: 'oracle.country', op: '==', value: 'ID' } },
{ if: { field: 'oracle.kycLevel', op: '>=', value: '1' } },
{ if: { field: 'risk.score', op: '<=', value: '70' } },
],
},
],
}

What this does: VIP users can send big amounts (1000+ USDC) if they have KYC3 and low risk. Normal users have smaller limits (5-500 USDC) with relaxed requirements. Smart, right?


Time-Based Rules (Business Hours) ⏰

Only allow payments during work hours:

const BUSINESS_HOURS_RULE = {
id: 'business_hours',
if: {
field: 'env.timestamp',
op: 'between',
value: ['9:00', '17:00'],
transform: 'hour',
},
message: 'Payments only allowed during business hours (9 AM - 5 PM)',
}

Weekday-Only Rule 📅

No weekend payments:

const WEEKDAY_RULE = {
id: 'weekday_only',
if: {
field: 'env.timestamp',
op: 'in',
value: [1, 2, 3, 4, 5], // Mon-Fri
transform: 'day',
},
message: 'Payments only allowed on weekdays',
}

Cross-Field Validation (Daily Limit) 💰

Track spending across all payments:

const DAILY_LIMIT_RULE = {
id: 'daily_limit',
if: {
field: 'tx.amount',
op: '<=',
value: '$state.dailyLimit',
},
message: 'Exceeds daily spending limit',
}

2️⃣ Attestation-Gated Payments (EAS) 🔐

Require EAS attestations before payment — perfect for KYC requirements:

// Server-side with Context V2
import { buildContextV2 } from 'payid/context'

const contextV2 = await buildContextV2({
baseContext: {
tx: { sender: payer, receiver: merchant, asset: USDC, amount: '50000000', chainId: 1 },
},
oracle: {
issuer: oracleSigner,
data: {
kycLevel: '2',
attestationUID: '0xabc123...', // EAS attestation UID
},
},
})

// Rule checks for attestation
const ATTESTATION_RULE = {
id: 'kyc_attestation',
if: { field: 'oracle.attestationUID', op: 'exists' },
message: 'KYC attestation required',
}

// Payment requires attestationUIDs
await payid.evaluateAndProve({
context: contextV2,
authorityRule: ATTESTATION_RULE,
attestationUIDs: ['0xabc123...'],
// ... other params
})

On-chain verification happens automatically via PayIDVerifier.requireAllowed() — pretty cool, right?


3️⃣ Context V2 with Server-Signed Data 📝

Complete Context V2 Structure

Here's the full Context V2 structure with all the bells and whistles:

import { buildContextV2 } from 'payid/context'

const contextV2 = await buildContextV2({
baseContext: {
tx: { sender, receiver, asset, amount, chainId },
},
// Environment timestamp signed by server
env: {
issuer: envSigner,
data: { timestamp: Math.floor(Date.now() / 1000) },
},
// State tracking (spending limits)
state: {
issuer: stateSigner,
data: {
spentToday: '150000000', // 150 USDC spent today
dailyLimit: '500000000', // 500 USDC daily limit
period: 'DAY',
},
},
// External oracle data (KYC, country, etc.)
oracle: {
issuer: oracleSigner,
data: {
kycLevel: '2',
country: 'ID',
riskScore: '25',
},
},
// Risk scoring
risk: {
issuer: riskSigner,
data: {
score: 25,
category: 'LOW',
modelHash: '0x123...',
},
},
})

Trusted Issuers Setup 🔑

Tell PAY.ID which signers you trust:

const payid = createPayIDServer({
signer: serverSigner,
trustedIssuers: new Set([
'0xEnvSignerAddress...',
'0xStateSignerAddress...',
'0xOracleSignerAddress...',
'0xRiskSignerAddress...',
]),
})

4️⃣ Session Policies (QR Codes) 📱

Generate Session Policy

Create a payment policy that customers can scan:

import { createSessionPolicyV2, encodeSessionPolicyV2QR } from 'payid/sessionPolicy'

const policy = await createSessionPolicyV2({
receiver: merchantAddress,
ruleSetHash: activeRuleSetHash,
ruleAuthority: COMBINED_RULE_STORAGE,
allowedAsset: USDC_ADDRESS,
maxAmount: parseUnits('100', 6), // Max 100 USDC per scan
expiresAt: Math.floor(Date.now() / 1000) + 3600, // Valid 1 hour
payId: 'pay.id/my-shop',
chainId: 31337,
verifyingContract: PAYID_VERIFIER,
signer: merchantSigner,
})

const qrString = encodeSessionPolicyV2QR(policy)
// → "payid-v2:eyJ2ZXJzaW9uIjoicGF5aWQuc2Vzc2lvbi5wb2xpY3kudjIi..."

Decode and Use Session Policy

When someone scans your QR, decode it and use the policy:

import { decodeSessionPolicyV2QR } from 'payid/sessionPolicy'

const policy = decodeSessionPolicyV2QR(scannedQRString)

// Use with usePayIDFlow
execute({
receiver: policy.receiver as `0x${string}`,
asset: policy.allowedAsset as `0x${string}`,
amount: BigInt(policy.maxAmount),
payId: policy.payId,
sessionPolicyV2: policy, // Pass session policy
})

5️⃣ ERC-4337 Account Abstraction 🦊

Build UserOperation from Proof

Use PAY.ID with smart contract wallets:

import { createPayIDServer } from 'payid/server'

const payid = createPayIDServer({ signer: bundlerSigner })

const { proof } = await payid.evaluateAndProve({ /* ... */ })

// Build ERC-4337 UserOperation
const userOp = await payid.buildUserOperation({
targetContract: PAY_WITH_PAYID,
proof,
attestationUIDs: [],
sender: smartAccountAddress,
nonce: 0n,
})

// Send to bundler
await bundler.sendUserOperation(userOp)

6️⃣ Custom State Tracking 📊

Track Spending Limits

Keep track of how much users spend on your server:

// Server maintains state
const userState = {
spentToday: 0n,
dailyLimit: 500_000_000n, // 500 USDC
period: new Date().toISOString().slice(0, 10), // YYYY-MM-DD
}

// On each payment
userState.spentToday += amount

// Sign and include in context
const stateContext = await signState(userState, stateSigner)

const contextV2 = await buildContextV2({
baseContext: { tx: { /* ... */ } },
state: { issuer: stateSigner, data: userState },
})

Rule Enforces Limit

The rule checks the state and blocks overspending:

const DAILY_LIMIT_RULE = {
id: 'daily_limit',
logic: 'AND' as const,
rules: [
{ if: { field: 'state.spentToday', op: '<=', value: '$state.dailyLimit' } },
{ if: { field: 'state.period', op: '==', value: new Date().toISOString().slice(0, 10) } },
],
}

7️⃣ Risk Scoring Integration 🎯

Risk-Based Payment Limits

Adjust payment limits based on user risk score:

const RISK_BASED_RULE = {
id: 'risk_limits',
logic: 'OR' as const,
rules: [
{
// Low risk: up to 1000 USDC
id: 'low_risk',
logic: 'AND' as const,
rules: [
{ if: { field: 'risk.score', op: '<=', value: '30' } },
{ if: { field: 'tx.amount', op: '<=', value: '1000000000' } },
],
},
{
// Medium risk: up to 100 USDC
id: 'medium_risk',
logic: 'AND' as const,
rules: [
{ if: { field: 'risk.score', op: 'between', value: ['31', '70'] } },
{ if: { field: 'tx.amount', op: '<=', value: '100000000' } },
],
},
{
// High risk: up to 10 USDC
id: 'high_risk',
logic: 'AND' as const,
rules: [
{ if: { field: 'risk.score', op: '>', value: '70' } },
{ if: { field: 'tx.amount', op: '<=', value: '10000000' } },
],
},
],
}

8️⃣ Directional Rules (INBOUND/OUTBOUND) ↔️

Have different rules for receiving vs sending:

import { useActiveCombinedRuleByDirection } from 'payid-react'

// Get INBOUND rule (receiving payments)
const inboundPolicy = useActiveCombinedRuleByDirection(merchantAddress, 0)

// Get OUTBOUND rule (sending payments)
const outboundPolicy = useActiveCombinedRuleByDirection(userAddress, 1)

// Register directional rule
await registerCombinedRule({
ruleSetHash: keccak256(toBytes('inbound-policy')),
ruleNFTs: [ruleNFTAddress],
tokenIds: [1n],
version: 1n,
direction: 0, // INBOUND
})

9️⃣ Batch Payments 📦

Pay multiple people at once:

import { usePayNative } from 'payid-react'

function BatchPay({ receivers }: { receivers: `0x${string}`[] }) {
const { pay, isPending } = usePayNative()

const handleBatchPay = async () => {
for (const receiver of receivers) {
await pay({
decision: proofForReceiver,
signature: signatureForReceiver,
attestationUIDs: [],
})
}
}

return <button onClick={handleBatchPay} disabled={isPending}>Batch Pay</button>
}

🔟 Multi-Chain Deployment 🌐

Configure Multiple Chains

Deploy across multiple networks:

import { hardhat, polygon, base, mainnet } from 'wagmi/chains'

const wagmiConfig = createConfig({
chains: [hardhat, polygon, base, mainnet],
transports: {
[hardhat.id]: http('http://127.0.0.1:8545'),
[polygon.id]: http(),
[base.id]: http(),
[mainnet.id]: http(),
},
})

const CONTRACT_ADDRESSES = {
[hardhat.id]: { /* localhost addresses */ },
[polygon.id]: { /* polygon addresses */ },
[base.id]: { /* base addresses */ },
[mainnet.id]: { /* mainnet addresses */ },
}

Chain-Specific Rules

Different chains, different rules:

const chainId = useChainId()

// Different rules per chain
const RULES_BY_CHAIN = {
1: ETHEREUM_RULES, // Mainnet rules
137: POLYGON_RULES, // Polygon rules
8453: BASE_RULES, // Base rules
31337: DEV_RULES, // Localhost rules
}

const activeRules = RULES_BY_CHAIN[chainId] || DEFAULT_RULES

1️⃣1️⃣ Error Handling & Retry Logic 🔄

Don't let network errors ruin the experience — retry automatically:

const { execute, status, error, reset } = usePayIDFlow()

const handlePayWithRetry = async () => {
let retries = 3
while (retries > 0) {
try {
await execute({ receiver, asset, amount, payId })
break // Success
} catch (e) {
retries--
if (retries === 0) throw e
await new Promise(r => setTimeout(r, 1000)) // Wait 1s
reset()
}
}
}

1️⃣2️⃣ Subscription Management 💳

Extend Rule Expiry

Keep your rules active by renewing:

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
})
}

return <button onClick={handleRenew} disabled={isPending}>Renew 30 Days</button>
}

Check Subscription Status

Show users their subscription info:

import { useSubscription } from 'payid-react'

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

if (!sub?.isActive) {
return <span style={{ color: 'red' }}>⚠️ Subscription Expired</span>
}

const daysLeft = Math.floor((Number(sub.expiry) - Date.now() / 1000) / 86400)

return (
<span>
{sub.logicalRuleCount}/{sub.maxSlots} slots • {daysLeft} days left
</span>
)
}

1️⃣3️⃣ Custom IPFS Gateway 🌐

<PayIDProvider
contracts={CONTRACT_ADDRESSES}
ipfsGateway="https://my-custom-ipfs-gateway.com/ipfs/"
>
<App />
</PayIDProvider>

1️⃣4️⃣ Decision Proof Verification ✅

Verify a proof on-chain:

import { useVerifyDecision } from 'payid-react'

function VerifyProof({ decision, signature }: { decision: any, signature: string }) {
const { data: isValid, isLoading } = useVerifyDecision(decision, signature)

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

return <p>{isValid ? '✅ Valid Proof' : '❌ Invalid Proof'}</p>
}

1️⃣5️⃣ Nonce Management 🔒

Check if a nonce was already used (prevents replay attacks):

import { useNonceUsed } from 'payid-react'

function CheckNonce({ payer, nonce }: { payer: `0x${string}`, nonce: string }) {
const { data: used } = useNonceUsed(payer, nonce)

if (used) {
return <p style={{ color: 'red' }}>⚠️ Nonce already used - possible replay attack</p>
}

return <p>✅ Nonce is fresh</p>
}

Advanced Patterns Summary 📚

PatternUse Case
Nested RulesVIP vs normal payment paths
Time-Based RulesBusiness hours, weekday-only
Attestation-GatedKYC-required payments
Context V2Server-signed oracle data
Session PoliciesQR code payments
ERC-4337Account abstraction
State TrackingSpending limits
Risk ScoringDynamic limits based on risk
Directional RulesSeparate INBOUND/OUTBOUND policies
Batch PaymentsMultiple recipients
Multi-ChainDeploy across networks
Retry LogicHandle transient failures
SubscriptionRule slot management

Next Steps 🎯


You're now a PAY.ID pro! 🎉