Skip to main content

Quick Start

Choose your integration path:

Path A — React FrontendPath B — Node.js Backend
Packagepayid-reactpayid
Who signs proofsUser's wallet (MetaMask, etc.)Your server wallet
Use casedApps, checkout pages, marketplacesAPIs, bots, ERC-4337 bundlers
ComplexitySimple — one hookModerate — handle context + signing
Two Roles in PAY.ID

Receiver (Merchant) — sets up payment rules once. Payer (Customer) — goes through the payment flow every time they pay.


Path A — React Quick Start

Prerequisites

Step 1 — Install Packages

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

Step 2 — Wrap Your App with Providers

This setup goes in your root file (main.tsx, _app.tsx, or layout.tsx):

Choose a testnet

Pick one from Contract Addresses →. This example uses Arbitrum Sepolia.

// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { WagmiProvider, createConfig, http } from 'wagmi'
import { injected, metaMask } from 'wagmi/connectors'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { PayIDProvider } from 'payid-react'
import App from './App'
import type { Chain } from 'viem'

// Example: Arbitrum Sepolia (chain 421614)
const arbitrumSepolia = {
id: 421614,
name: 'Arbitrum Sepolia',
nativeCurrency: { decimals: 18, name: 'Ether', symbol: 'ETH' },
rpcUrls: {
default: { http: ['https://sepolia-rollup.arbitrum.io/rpc'] },
public: { http: ['https://sepolia-rollup.arbitrum.io/rpc'] },
},
blockExplorers: {
default: { name: 'Arbitrum Sepolia Explorer', url: 'https://sepolia.arbiscan.io' },
},
} as const satisfies Chain

// Or use any other supported chain: Sepolia, Base Sepolia, Polygon Amoy, 0G Galileo, etc.
// See: https://docs.pay.id/network/contracts-address

const wagmiConfig = createConfig({
chains: [arbitrumSepolia],
connectors: [injected(), metaMask()],
transports: { [arbitrumSepolia.id]: http() },
})

const queryClient = new QueryClient()

// Contract addresses for your chosen chain
// Get addresses from: https://docs.pay.id/network/contracts-address
const CONTRACT_ADDRESSES = {
[arbitrumSepolia.id]: {
ruleAuthority: '0x44a50e4B7051C7155C28271bA9eacFd71ee571a8',
ruleItemERC721: '0xD3897D0ba0F219835b000992B21e56e8C44C7715',
combinedRuleStorage: '0xF674A5738D4f70006a9d3C541A0CF149E284a182',
payIDVerifier: '0x8FeCc22437Ab5Bc53805B2ebe8b861A2F3177737',
payWithPayID: '0x73c8B8f359AC2A16a8962e16842B8e7A1773024f',
vindexRegistry: '0xa7448AEc914074e19C0bC2259E6e1FAe695aCb0f',
// Optional contracts (include if you use these features):
aiAgentRegistry: '0xf5cf5cb577118e1a0993e69eb373C47A242C01D3',
aiAgentRuleManager: '0x45024b9dB494C66f1B2E43F910664D6f4E261D6C',
attestationVerifier: '0x0a83AEbdEeb392328F133b056b63946a3212FB60',
agentPayID: '0xa0c23E005f5D627dB73024385828c5682e63F364',
mockAgentRegistry: '0xBbE64FEa1a16b47a91c94272Bd6909A53b28E83C',
payWithPayIDBatch: '0x7031d36feeE7022cE7563b88bAc16698c73eAF02',
recurringPayments: '0x432dBA247F2F61fEc5DEe1F84E3855d44e9925D6',
},
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<PayIDProvider contracts={CONTRACT_ADDRESSES}>
<App />
</PayIDProvider>
</QueryClientProvider>
</WagmiProvider>
)

Step 3 — Add a Wallet Connect Button

payid-react works with any wagmi-compatible wallet connector. Add one from wagmi's supported connectors. Example with MetaMask:

// WalletButton.tsx
import { useConnect, useAccount, useDisconnect } from 'wagmi'
import { injected } from 'wagmi/connectors'

export function WalletButton() {
const { connect } = useConnect()
const { address, isConnected } = useAccount()
const { disconnect } = useDisconnect()

if (isConnected) return (
<div>
<span>{address?.slice(0, 6)}...{address?.slice(-4)}</span>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
)

return (
<button onClick={() => connect({ connector: injected() })}>
Connect Wallet
</button>
)
}

Step 4 — Make a Payment (Payer)

Use the usePayIDFlow hook. It handles everything: loading rules, evaluating, signing proof, ERC20 approval, and submitting the transaction.

Use testnet token addresses

On testnets, use mock USDC or native tokens. Check your testnet's documentation for token addresses.

// CheckoutButton.tsx
import { usePayIDFlow } from 'payid-react'

const USDC_ADDRESS = '0x...' // Use testnet USDC address for your chain

export 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: 50_000_000n, // 50 USDC (6 decimals)
payId: 'pay.id/my-store',
})
}

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: <a href={`https://etherscan.io/tx/${txHash}`}>{txHash?.slice(0, 10)}...</a></p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
)
}

Step 5 — Create Your Merchant Rules (Receiver)

One-time setup

You only need to do this once per merchant. After setup, just update rules as needed.

Before anyone can pay you, set up your payment policy using the merchant hooks:

Upload rule to IPFS first

Before creating a rule, upload your rule JSON to IPFS (or 0G Storage) to get a URI.

// MerchantSetup.tsx
import { useSubscribe, useCreateRule, useActivateRule, useRegisterCombinedRule } from 'payid-react'
import { keccak256, toBytes } from 'viem'

// Your payment rule — only accept USDC between 10–500
const MY_RULE = {
id: 'store_policy',
logic: 'AND' as const,
rules: [
{ id: 'usdc_only', if: { field: 'tx.asset', op: '==', value: 'YOUR_USDC_ADDRESS' } },
{ id: 'min_10', if: { field: 'tx.amount', op: '>=', value: '10000000' } },
{ id: 'max_500', if: { field: 'tx.amount', op: '<=', value: '500000000' } },
],
}

export function MerchantSetup() {
const { subscribe, isPending: subscribing, isSuccess: subscribed } = useSubscribe()
const { createRule, isPending: creating, isSuccess: created } = useCreateRule()
const { activateRule, isPending: activating, isSuccess: activated } = useActivateRule()
const { registerCombinedRule, isPending: registering, isSuccess: registered } = useRegisterCombinedRule()

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{/* Step 1: Subscribe to get a rule slot */}
<button onClick={() => subscribe(100_000_000_000_000n)} disabled={subscribing}>
{subscribed ? '✅ Subscribed' : subscribing ? 'Subscribing...' : '1. Subscribe (0.0001 ETH)'}
</button>

{/* Step 2: Create the rule definition on-chain */}
<button
onClick={() => createRule({
ruleHash: keccak256(toBytes(JSON.stringify(MY_RULE))),
uri: 'ipfs://YOUR_RULE_CID', // ← Upload MY_RULE to IPFS first
})}
disabled={creating || !subscribed}
>
{created ? '✅ Rule Created' : creating ? 'Creating...' : '2. Create Rule'}
</button>

{/* Step 3: Activate (mint the NFT) — use ruleId from Step 2 */}
<button onClick={() => activateRule(1n)} disabled={activating || !created}>
{activated ? '✅ Rule NFT Minted' : activating ? 'Activating...' : '3. Activate Rule NFT'}
</button>

{/* Step 4: Register as active payment policy */}
<button
onClick={() => registerCombinedRule({
ruleSetHash: keccak256(toBytes('my-store-v1')),
ruleNFTs: ['0xRuleItemERC721Address'],
tokenIds: [1n],
version: 1n,
})}
disabled={registering || !activated}
>
{registered ? '✅ Policy Active!' : registering ? 'Registering...' : '4. Activate Policy'}
</button>
</div>
)
}
Storage Options

PAY.ID supports two storage backends for rule metadata:

Option A — IPFS (Traditional):

# Upload rule JSON to Pinata at https://app.pinata.cloud
# Use the returned CID as: uri = 'ipfs://YOUR_CID'

Option B — 0G Storage (Decentralized):

import { uploadToZGStorage } from '@/lib/zgStorage'
// Upload returns a root hash — use as uri: `0g://<rootHash>`
const result = await uploadToZGStorage(JSON.stringify(MY_RULE), signer)
const uri = `0g://${result.rootHash}`

See Create Rule NFT → for the full script.

That's it for the React path! → Full React Integration → for all 20+ hooks.


Path B — Node.js Quick Start

For backend servers, bots, or scripts.

Step 1 — Install

npm install payid ethers
# or
bun add payid ethers

Step 2 — Initialize the SDK

// server.ts
import { createPayIDServer } from 'payid/server'
import { ethers } from 'ethers'

const provider = new ethers.JsonRpcProvider(process.env.RPC_URL)
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)

const payid = createPayIDServer({
signer,
// Optional: storage config for fetching rules
// zgStorageIndexer: 'https://indexer-storage-testnet-turbo.0g.ai', // for 0G Storage
})

Step 3 — Evaluate and Generate Proof

import { ethers } from 'ethers'

// Load merchant's active rule set from chain + IPFS
// (see examples/client.md for full loading code)
const authorityRule = { version: '1', logic: 'AND', rules: ruleConfigs }

const { result, proof } = await payid.evaluateAndProve({
context: {
tx: {
sender: payerAddress,
receiver: merchantAddress,
asset: usdcAddress,
amount: '50000000', // 50 USDC (string)
chainId: 1,
},
env: { timestamp: Math.floor(Date.now() / 1000) },
},
authorityRule,
payId: 'pay.id/my-store',
payer: payerAddress,
receiver: merchantAddress,
asset: usdcAddress,
amount: 50_000_000n, // bigint
verifyingContract: process.env.PAYID_VERIFIER!,
ruleAuthority: process.env.COMBINED_RULE_STORAGE!,
chainId: 1,
blockTimestamp: Math.floor(Date.now() / 1000),
ttlSeconds: 300,
})

if (result.decision === 'REJECT') {
throw new Error(`Payment rejected: ${result.reason ?? result.code}`)
}

console.log('Proof ready. Send to client:', {
payload: proof!.payload,
signature: proof!.signature,
})

Step 4 — Submit Payment On-chain

On the client side (or backend if custodial), submit the proof to the contract:

import PayWithPayIDAbi from './abi/PayWithPayID.json'

const payContract = new ethers.Contract(
process.env.PAY_WITH_PAYID!,
PayWithPayIDAbi.abi,
payerWallet
)

// ERC20 payment
const tx = await payContract.getFunction('payERC20').send(
proof!.payload,
proof!.signature,
[], // attestationUIDs (empty for client mode)
)
await tx.wait()

// Native token payment — pass value:
const tx = await payContract.getFunction('payNative').send(
proof!.payload,
proof!.signature,
[],
{ value: 50_000_000n }
)

Full Server Guide → for Context V2, KYC, and rate limits.


Summary of Flows

RECEIVER SETUP (done once):
1. subscribe() → get a rule slot (ETH fee, 30 days)
2. createRule(hash, uri) → register rule definition on-chain
3. activateRule(ruleId) → mint Rule NFT (proof of ownership)
4. registerCombinedRule(...) → set as active payment policy

PAYER FLOW (every payment):
1. getActiveRuleOf(receiver) → fetch receiver's active rule hash
2. loadRulesFromIPFS(refs) → get rule JSON configs
3. evaluateAndProve(...) → evaluate rules + generate signed proof
4. approve(contract, amount) → ERC20 spending approval (if needed)
5. payERC20(payload, sig,[]) → submit to blockchain
// or payNative(payload, sig,[]) for native token (ETH, MATIC, etc.)

Next Steps

GoalGuide
All React hooksReact Integration →
Understand RulesRule Basics →
KYC / rate limits on backendServer Mode →
QR code paymentsReact Integration → usePayIDQR →
Deploy to testnetContract Addresses →