Multi-Token Pricing
PAY.ID supports USD-equivalent pricing across multiple tokens, allowing rules to check payment values in USD regardless of the token used.
Overview
Instead of checking raw token amounts (which vary by token decimals), you can now check the USD equivalent of any payment. This enables:
- USD-based spending limits - e.g., "Max $35 per payment" regardless of whether it's USDC, USDT, or ETH
- Multi-token minimum thresholds - e.g., "Minimum $10 payment" across all supported tokens
- Cross-token daily limits - e.g., "Daily limit of $1000 across all tokens"
Supported Tokens
Currently supported via Chainlink Price Feeds:
- USDC/USD - Circle USD Coin
- USDT/USD - Tether USD
- DAI/USD - MakerDAO
- WBTC/USD - Wrapped Bitcoin
- LINK/USD - Chainlink
- UNI/USD - Uniswap
- ETH/USD - Native Ethereum
How It Works (Hybrid Approach)
PAY.ID uses a hybrid off-chain + on-chain model for USD value rules:
- Off-chain: SDK fetches Chainlink price → computes
oracle.txValueUsd→ WASM rule engine evaluates - On-chain:
PayWithPayID.payERC20WithOracleGuardspot-checks the same math as defense-in-depth
1. Token Price Oracles
PAY.ID uses Chainlink Price Feeds to get real-time token prices in USD:
import { TOKEN_PRICE_ORACLES } from '@/constants/oracles';
const usdcPriceOracle = TOKEN_PRICE_ORACLES[chainId]['USDC/USD'];
const usdtPriceOracle = TOKEN_PRICE_ORACLES[chainId]['USDT/USD'];
2. USD Equivalent Calculation
The USD equivalent is calculated using the formula:
txValueUsd = (tokenAmount × tokenPrice) / (10^tokenDecimals × 10^8)
Where:
tokenAmount: Raw token amount in token unitstokenPrice: Token price in USD (8 decimals, Chainlink standard)tokenDecimals: Token decimals (e.g., 6 for USDC, 18 for ETH)
SDK helper:
import { computeTxValueUsd } from 'payid';
const txValueUsd = computeTxValueUsd(amount, decimals, priceInUsd); // 8-decimal USD
3. Context Extension
The computed value is injected into the oracle namespace before rule evaluation:
interface OracleContext {
txValueUsd: string; // USD equivalent (8 decimals)
txValueUsdFormatted: string; // e.g. "$45.00"
tokenPrice: string; // Raw oracle price
}
Usage Examples
Example 1: USD-Based Spending Limit
Create a rule that limits payments to $35 USD regardless of token:
{
"if": {
"field": "oracle.txValueUsd",
"op": "<=",
"value": 3500000000
},
"message": "Payment exceeds $35 USD limit"
}
What this means:
- 35 USDC = $35 USD ✅
- 0.01 ETH (at $3500) = $35 USD ✅
- 35 USDT = $35 USD ✅
- 100 USDC = $100 USD ❌ (rejected)
Example 2: Multi-Token Minimum Threshold
Require minimum $10 USD payment:
{
"if": {
"field": "oracle.txValueUsd",
"op": ">=",
"value": 1000000000
},
"message": "Payment below $10 USD minimum"
}
Example 3: Cross-Token $45 Minimum
The classic "send any token but min $45" rule:
{
"id": "cross_token_min_45",
"if": {
"field": "oracle.txValueUsd",
"op": ">=",
"value": 4500000000
},
"message": "Min $45 required. You sent ${oracle.txValueUsd|div:100000000} USD-worth of {tx.asset}"
}
Frontend Integration (usePayIDFlow)
usePayIDFlow automatically injects oracle.txValueUsd when you pass tokenPriceOracle and tokenDecimals:
import { usePayIDFlow } from 'payid-react';
const { execute } = usePayIDFlow();
await execute({
receiver: '0xReceiver...',
asset: '0xUSDC...',
amount: 50_000_000n, // 50 USDC (6 decimals)
payId: 'pay.id/alice',
tokenDecimals: 6,
tokenPriceOracle: '0x...', // Chainlink USDC/USD feed
minUsdValue: 45_00000000n, // optional: on-chain guard
});
With Token Config Helper
import { getTokenConfig, getTokenPriceOracle } from '@/constants/tokens';
const token = getTokenConfig(chainId, 'USDC');
const oracle = getTokenPriceOracle(chainId, 'USDC');
await execute({
receiver: '0x...',
asset: token.address,
amount: parseUnits('50', token.decimals),
payId: 'pay.id/alice',
tokenDecimals: token.decimals,
tokenPriceOracle: oracle,
minUsdValue: 45_00000000n,
});
On-Chain Oracle Guard
For defense-in-depth, PayWithPayID includes payERC20WithOracleGuard:
function payERC20WithOracleGuard(
Decision calldata d,
bytes calldata sig,
bytes32[] calldata attestationUIDs,
address tokenPriceOracle,
uint256 minUsdValue, // 8 decimals
uint8 tokenDecimals
) external;
If tokenPriceOracle is set and minUsdValue > 0, the contract:
- Queries Chainlink
latestRoundData - Computes
usdValue = (amount * price) / 10^(decimals + 8) - Reverts with
BELOW_USD_MINIMUMif under threshold
SDK Helpers
import { computeTxValueUsd, formatUsdValue } from 'payid';
const usdValue = computeTxValueUsd(
1_000_000n, // 1 USDC
6, // USDC decimals
100_000_000n // $1.00 price (8 decimals)
); // → 100_000_000n ($1.00)
formatUsdValue(usdValue); // "$1.00"
Oracle Addresses
Oracle addresses are configured per chain in @/constants/oracles.ts:
export const TOKEN_PRICE_ORACLES: Record<number, Record<string, `0x${string}`>> = {
1: {
'USDC/USD': '0x8fFfF9545Ff14a92c8329c339F71d3f3Ea8eD444', // Mainnet
'USDT/USD': '0x3E7d1eAB13ad0104d2750B8863b489D65364e32D', // Mainnet
'DAI/USD': '0xAed0c38402a5d19df6E4c03f4322c2963490d050', // Mainnet
'WBTC/USD': '0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c', // Mainnet
'LINK/USD': '0x2c1d072e956AFFc0D435Cb7D389FaE8fC5B9cB2D', // Mainnet
'UNI/USD': '0xD71eCFF9422D8057F9692e8DdA2B7549aCf96663', // Mainnet
},
11155111: {
'USDC/USD': '0xA2F78d2358df4E29c589B9C1Ada1ddc062c1Ec4e', // Sepolia
'USDT/USD': '0x20955D69f13E1e2c3c1f9BfF2Ad5C807485A4f0e', // Sepolia
'DAI/USD': '0x14866185B1962A69b940F2c2953A6C3747525b43', // Sepolia
'WBTC/USD': '0x7e87009ceF4D986fC2719BfF2adEA5565dfB9C56', // Sepolia
'LINK/USD': '0xc59E36313BAa28Cf64E2863425DCc5b33Ae4C8f3', // Sepolia
'UNI/USD': '0xD71eCFF9422D8057F9692e8DdA2B7549aCf96663', // Sepolia
},
31337: {
'USDC/USD': '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // Mock for local dev
'USDT/USD': '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // Mock for local dev
'DAI/USD': '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // Mock for local dev
'WBTC/USD': '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // Mock for local dev
'LINK/USD': '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // Mock for local dev
'UNI/USD': '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // Mock for local dev
},
};
Notes
- USD values are stored with 8 decimals (Chainlink standard)
- Rule engine uses
oracle.txValueUsd(nottx.amountUsd) usePayIDFlowauto-injects oracle context whentokenPriceOracleis provided- On-chain guard (
payERC20WithOracleGuard) requires contract redeployment - Fallback to mock addresses on localhost for development