Tempo enables users to pay transaction fees in any USD-denominated TIP-20 stablecoin. This eliminates the need to acquire and manage a separate gas token, simplifying the payment experience.
Supported Fee Tokens
Any TIP-20 token with currency() == "USD" can be used as a fee token, including:
pathUSD (0x20C0000000000000000000000000000000000000) - Tempo’s native stablecoin
USDC - Circle’s USD Coin
USDT - Tether USD
Any other USD-denominated TIP-20 token with sufficient Fee AMM liquidity
All TIP-20 stablecoins use 6 decimals (matching USDC). One token unit = 1 microdollar (μUSD) = $0.000001.
Setting Your Fee Token Preference
Users specify which token they want to pay fees in by calling the FeeManager precompile:
interface IFeeManager {
function setUserToken ( address token ) external ;
}
Using Cast
cast send 0xfeeC000000000000000000000000000000000000 \
"setUserToken(address)" \
0x123...ABC \
--rpc-url https://rpc.moderato.tempo.xyz \
--private-key $PRIVATE_KEY
Replace 0x123...ABC with your preferred TIP-20 stablecoin address.
Using viem
import { createWalletClient , http } from 'viem' ;
import { moderato } from '@tempoxyz/sdk/chains' ;
const client = createWalletClient ({
chain: moderato ,
transport: http ()
});
const FEE_MANAGER = '0xfeeC000000000000000000000000000000000000' ;
await client . writeContract ({
address: FEE_MANAGER ,
abi: [{
name: 'setUserToken' ,
type: 'function' ,
inputs: [{ name: 'token' , type: 'address' }],
outputs: [],
stateMutability: 'nonpayable'
}],
functionName: 'setUserToken' ,
args: [ '0x123...ABC' ] // Your preferred stablecoin
});
Using Foundry
contract SetFeeToken is Script {
function run () external {
address FEE_MANAGER = 0xfeeC000000000000000000000000000000000000 ;
address USDC = 0x123 ...ABC;
vm. startBroadcast ();
IFeeManager (FEE_MANAGER). setUserToken (USDC);
vm. stopBroadcast ();
}
}
Your fee token preference persists across all future transactions until you change it. You only need to set it once.
Fee Token Requirements
To use a token as a fee payment method, it must:
Be a TIP-20 token deployed via the TIP20Factory
Be USD-denominated : ITIP20(token).currency() must return "USD"
Have sufficient balance in your account to cover gasLimit × baseFee
Allow FeeManager transfers : Your TIP-403 policy must authorize both:
You as a sender
The FeeManager as a recipient
Have Fee AMM liquidity (if validator prefers a different token)
Checking Your Balance
cast call $TOKEN_ADDRESS "balanceOf(address)" $YOUR_ADDRESS
Checking TIP-403 Authorization
The FeeManager checks authorization during collectFeePreTx:
// From FeeManager.sol:47-52
uint64 policyId = ITIP20 (userToken). transferPolicyId ();
if (
! TIP403_REGISTRY. isAuthorizedSender (policyId, user) ||
! TIP403_REGISTRY. isAuthorizedRecipient (policyId, address ( this ))
) {
revert ITIP20. PolicyForbids ();
}
If your transfer policy blocks either you as a sender or the FeeManager as a recipient, the transaction will revert during validation.
How Fee Payment Works
1. Fee Token Resolution
Before executing a transaction, the protocol resolves which token will be used:
User's preferred token: (from setUserToken)
↓
Validator's preferred token: (from setValidatorToken)
↓
Fee AMM liquidity check: (if conversion needed)
↓
Resolved fee token: (final choice)
2. Maximum Fee Calculation
The protocol calculates the maximum fee the user might pay:
maxFee = gasLimit × baseFee
Example :
Gas limit: 500,000 gas
Base fee: 2,000 aUSD/gas = 2 × 10⁻¹⁵ USD/gas
Max fee (aUSD): 500,000 × 2,000 = 1,000,000,000 aUSD = 10⁹ aUSD
Max fee (μUSD): 10⁹ / 10¹² = 1,000 μUSD = 1,000 token units
Max fee (USD): $0.001 (0.1 cents)
The user must have at least 1,000 token units of their preferred stablecoin.
3. Pre-Transaction Collection
The FeeManager calls collectFeePreTx to lock the maximum fee:
function collectFeePreTx (
address user ,
address userToken ,
uint256 maxAmount
) external
This:
Transfers maxAmount from user to FeeManager
Checks Fee AMM liquidity (if conversion needed)
Reverts if insufficient balance or liquidity
See FeeManager.sol:38-60.
4. Transaction Execution
The transaction runs normally, consuming gas based on actual operations.
5. Post-Transaction Settlement
After execution, collectFeePostTx settles the actual fee:
function collectFeePostTx (
address user ,
uint256 maxAmount ,
uint256 actualUsed ,
address userToken
) external
This:
Calculates refund: maxAmount - actualUsed
Returns excess tokens to user
Converts actualUsed via Fee AMM (if needed)
Credits validator with converted tokens
See FeeManager.sol:62-87.
Example :
Locked: 1,000 USDC (max fee)
Actual gas: 400,000 gas
Actual fee: 800 USDC
Refund: 200 USDC (returned to user)
Conversion: 800 USDC → 798.4 pathUSD (0.3% spread)
Validator gets: 798.4 pathUSD
Fee Conversion via AMM
When your fee token differs from the validator’s preferred token, the Fee AMM automatically converts:
Conversion Rate
outputAmount = inputAmount × 0.9970
This provides a 0.3% conversion spread .
Example :
You pay: 1,000 USDC
Conversion spread: 1,000 × 0.003 = 3 USDC
Validator receives: 1,000 × 0.9970 = 997 pathUSD
See Fee AMM for details on conversion mechanics and liquidity.
Insufficient Liquidity
If the Fee AMM lacks sufficient liquidity for the conversion, the transaction validation fails:
if (pools[poolId].reserveValidatorToken < amountOutNeeded) {
revert InsufficientLiquidity ();
}
In this case, you must either:
Switch fee tokens to one with liquidity (e.g., pathUSD)
Wait for the pool to be rebalanced
Provide liquidity yourself to the relevant pool
pathUSD always has the best liquidity because it’s the default token for most validators. If conversion fails, try switching to pathUSD.
Cost Examples
Assuming base fee = 2,000 aUSD/gas:
Simple Transfer
Operation : Send 100 USDC to a friend
Gas used: 50,000 gas
Fee (USDC): 50,000 × 2,000 / 10¹² = 100 μUSD = 100 USDC token units
Fee (USD): $0.0001 (0.01 cents)
No conversion needed if validator also prefers USDC.
Transfer to New Address
Operation : Send 100 USDT to a new recipient (creates their balance storage)
Gas used: 300,000 gas
Fee (USDT): 300,000 × 2,000 / 10¹² = 600 μUSD = 600 USDT token units
Fee (USD): $0.0006 (0.06 cents)
The extra 250,000 gas covers the new storage slot creation.
With Conversion
Operation : Send 100 USDC, but validator prefers pathUSD
Gas used: 50,000 gas
Base fee: 100 USDC token units
Conversion: 100 → 99.7 pathUSD (validator receives)
Total cost: 100 USDC from your balance
Spread cost: 0.3 USDC (0.3% conversion fee)
You pay the same 100 USDC, but the validator receives 99.7 pathUSD after conversion.
Smart Contract Interaction
Operation : Call a DEX swap function
Gas limit: 800,000 gas
Gas used: 650,000 gas (actual)
Max locked: 1,600 μUSD
Actual fee: 1,300 μUSD
Refund: 300 μUSD (returned immediately)
Fee Token Introspection
Smart contracts can query which fee token is being used:
interface IFeeManager {
function getFeeToken () external view returns ( address );
}
contract FeeAware {
IFeeManager constant FEE_MANAGER = IFeeManager ( 0xfeeC000000000000000000000000000000000000 );
address constant PATH_USD = 0x20C0000000000000000000000000000000000000 ;
function doSomething () external {
address feeToken = FEE_MANAGER. getFeeToken ();
if (feeToken == PATH_USD) {
// User is paying in pathUSD
} else {
// User is paying in a different stablecoin
}
}
}
This enables fee-aware contract logic, such as:
Adjusting pricing based on fee token
Routing decisions that minimize total costs
Event logging for fee analytics
See TIP-1007 for the full specification.
Best Practices
For Users
Set fee token once : Your preference persists, no need to set it repeatedly
Use pathUSD for lowest costs : Native token typically has best liquidity and no conversion spread
Maintain buffer balance : Keep 2-3x expected fees to handle gas price spikes
Check authorization : Ensure your TIP-403 policy allows FeeManager transfers
For Validators
Set preferred token : Use setValidatorToken to receive fees in your chosen stablecoin
Provide liquidity : Add liquidity to (userToken → yourToken) pools to enable conversions
Monitor collection : Call distributeFees periodically to claim accumulated fees
Consider pathUSD : Default token simplifies liquidity management
Validators cannot change their fee token during the same block they’re producing. Attempting to call setValidatorToken from the block coinbase address reverts with CANNOT_CHANGE_WITHIN_BLOCK.
For Developers
Estimate gas accurately : Use eth_estimateGas to avoid over-locking user funds
Handle conversion failures : Catch InsufficientLiquidity and suggest alternative fee tokens
Display costs in dollars : Convert microdollars to human-readable amounts
Support multiple tokens : Let users choose their preferred stablecoin
Common Issues
”Insufficient Balance”
Problem : Transaction reverts during fee validation
Solutions :
Check balance: cast call $TOKEN "balanceOf(address)" $YOUR_ADDRESS
Ensure you have enough for both transfer amount AND gas fees
Remember: max fee is locked, not actual fee
”PolicyForbids”
Problem : TIP-403 policy blocks the fee transfer
Solutions :
Verify you’re authorized sender: Check your address against token’s policy
Verify FeeManager is authorized recipient: Check 0xfeeC...0000 against policy
If using a restricted token, contact the issuer
”InsufficientLiquidity”
Problem : Fee AMM lacks liquidity for conversion
Solutions :
Switch to pathUSD (best liquidity)
Wait for pool rebalancing
Provide liquidity to the pool
Check current pool reserves: FeeAMM.getPool(yourToken, validatorToken)
”CANNOT_CHANGE_WITHIN_BLOCK”
Problem : Validator trying to change fee token while producing a block
Solution : Wait until the next block, then call setValidatorToken
Transaction Lifecycle Example
Here’s a complete example of paying fees in USDC:
Setup
import { createTempoClient } from '@tempoxyz/sdk' ;
const client = createTempoClient ({
transport: http ( 'https://rpc.moderato.tempo.xyz' )
});
const USDC = '0x...' ;
const FEE_MANAGER = '0xfeeC000000000000000000000000000000000000' ;
// One-time: Set USDC as fee token
await client . writeContract ({
address: FEE_MANAGER ,
abi: feeManagerAbi ,
functionName: 'setUserToken' ,
args: [ USDC ]
});
Transaction Submission
// Send a transfer (fee paid in USDC)
const hash = await client . transfer ({
token: USDC ,
to: '0x456...' ,
amount: 100_000_000 n // 100 USDC
});
const receipt = await client . waitForTransactionReceipt ({ hash });
What Happened Behind the Scenes
Validation :
- Resolved fee token: USDC
- Calculated max fee: 500,000 gas × 2,000 aUSD/gas = 1,000 USDC units
- Checked balance: ✓ (you have 100,001,000 USDC)
- Checked AMM liquidity: ✓ (sufficient for conversion)
Pre-execution :
- Locked 1,000 USDC from your balance
- Your balance: 100,001,000 → 100,000,000 USDC (1,000 locked in FeeManager)
Execution :
- Actual gas used: 50,000 gas
- Actual fee: 100 USDC units
Post-execution :
- Refund: 1,000 - 100 = 900 USDC (returned to you)
- Conversion: 100 USDC → 99.7 pathUSD (sent to validator)
- Your final balance: 100,000,900 - 100,000,000 (transfer) = 99,900,900 USDC
Validator collection :
- Validator's collected fees: 99.7 pathUSD
- Validator calls distributeFees to claim
Next Steps
Fee AMM Learn how stablecoin conversion works
Gas Pricing Understand gas cost calculations
Fee System Overview High-level overview of fee mechanics
TIP-1007 Fee token introspection specification