Skip to main content

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:

  1. Off-chain: SDK fetches Chainlink price → computes oracle.txValueUsd → WASM rule engine evaluates
  2. On-chain: PayWithPayID.payERC20WithOracleGuard spot-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 units
  • tokenPrice: 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:

  1. Queries Chainlink latestRoundData
  2. Computes usdValue = (amount * price) / 10^(decimals + 8)
  3. Reverts with BELOW_USD_MINIMUM if 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 (not tx.amountUsd)
  • usePayIDFlow auto-injects oracle context when tokenPriceOracle is provided
  • On-chain guard (payERC20WithOracleGuard) requires contract redeployment
  • Fallback to mock addresses on localhost for development