# Tempo Docs Documentation for the Tempo network and protocol specifications # Integrating Tempo: getting started Use this section when you are connecting an existing app, wallet, contract, bridge, or infrastructure service to Tempo. These pages cover the practical edges of integration: chain configuration, RPC endpoints, faucet funding, wallet support, EVM compatibility, predeploys, contract verification, and ecosystem resources. Tempo Mainnet has been live since March 18, 2026. Use mainnet for production integrations and Tempo Wallet. Use the separate Moderato testnet only for development and faucet-funded examples. Tempo is EVM-compatible and targets the **Osaka** EVM hard fork. Most Ethereum tooling works as expected, with Tempo-specific differences documented where they matter. ## Connect and Fund * [Connect to Tempo](https://tempo.xyz/developers/docs/quickstart/connection-details) — Add Tempo chain configuration, RPC endpoints, explorer links, and wallet connection details. * [Get Testnet Funds](https://tempo.xyz/developers/docs/quickstart/faucet) — Fund test wallets so you can deploy contracts, send payments, and exercise integration flows. * [Verify Contracts](https://tempo.xyz/developers/docs/quickstart/verify-contracts) — Verify deployed contracts and make them easier to inspect, debug, and share. ## App and Wallet Readiness * [Wallet Integration](https://tempo.xyz/developers/docs/quickstart/wallet-developers) — Support Tempo in wallets with stablecoin-native fees, chain metadata, and transaction behavior. * [Tempo EVM Compatibility](https://tempo.xyz/developers/docs/quickstart/evm-compatibility) — Understand the Tempo-specific behavior that differs from default Ethereum assumptions. * [System Contracts and Predeploys](https://tempo.xyz/developers/docs/quickstart/predeployed-contracts) — Find Tempo predeploys and system contracts used by integrations and protocol features. * [Token Lists](https://tempo.xyz/developers/docs/quickstart/tokenlist) — Use Tempo token lists to discover supported assets and present stablecoins correctly. ## Bridges and Ecosystem * [Migrate an ERC-20 to TIP-20](https://tempo.xyz/developers/docs/guide/issuance/migrate-erc20-to-tip20) — Choose native issuance, a bridge adapter, full migration, or an external lifecycle manager for an existing ERC-20. * [LayerZero Bridge](https://tempo.xyz/developers/docs/guide/bridge-layerzero) — Bridge supported assets to and from Tempo using LayerZero. * [Relay Bridge](https://tempo.xyz/developers/docs/guide/bridge-relay) — Use Relay to move assets between Tempo and other supported networks. * [Tempo Ecosystem](https://tempo.xyz/developers/docs/ecosystem) — Find bridges, wallets, analytics, infrastructure, compliance, and orchestration partners. # Getting funds on Tempo: wallet, faucet & bridge Tempo Wallet uses Tempo Mainnet. Funds added through Tempo Wallet or a mainnet bridge are production assets. pathUSD in Tempo Wallet is live mainnet pathUSD. For development on the separate Moderato testnet, use the faucet described below. ## Tempo Wallet [Tempo Wallet](https://wallet.tempo.xyz) is Tempo's web-based passkey wallet. You can onramp with fiat or bridge from other chains using the flow below. **Interactive demo: Add Funds** 1. Sign in with tempo 2. Deposit to tempo wallet ### With the CLI Install the [Tempo CLI](https://tempo.xyz/developers/docs/cli/wallet) and log in to your wallet: ```bash curl -fsSL https://tempo.xyz/install | bash tempo wallet login ``` Then fund your wallet: ```bash tempo wallet fund ``` ### With an agent Paste this into your agent to set up Tempo Wallet and add funds: :::code-group ```bash [Claude Code] claude -p "Read https://tempo.xyz/SKILL.md and fund my Tempo Wallet" ``` ```bash [Amp] amp --execute "Read https://tempo.xyz/SKILL.md and fund my Tempo Wallet" ``` ```bash [Codex CLI] codex exec "Read https://tempo.xyz/SKILL.md and fund my Tempo Wallet" ``` ::: ## Bridge Use one of these supported bridges to move assets to Tempo: When you bridge USDC with LayerZero (Stargate), it appears on Tempo as USDC.e. Stargate does not charge its transfer fee on the direct route between Ethereum and Tempo, but other routes can vary, so check the quote before sending. * **[LayerZero (Stargate)](https://tempo.xyz/developers/docs/guide/bridge-layerzero)**: Bridge USDC to and from Tempo via Stargate. See the [full bridging guide](https://tempo.xyz/developers/docs/guide/bridge-layerzero) for contract addresses, code examples, and EndpointDollar details. * **[Chainlink CCIP](https://docs.chain.link/ccip/directory/mainnet/chain/tempo-mainnet)**: Transfer supported tokens to and from Tempo. Check the CCIP Directory for current tokens, lanes, fees, and contract configuration. * **[Squid](https://app.squidrouter.com/)**: Swap and bridge assets to Tempo in one flow. * **[Relay](https://relay.link/)**: Bridge assets to Tempo with low fees. * **[Across](https://app.across.to/)**: Bridge assets to Tempo quickly with competitive fees. * **[Bungee](https://tempo.xyz/developers/docs/guide/bridge-bungee)**: Swap and bridge assets to and from Tempo using Bungee Deposit. See the [full Bungee guide](https://tempo.xyz/developers/docs/guide/bridge-bungee) for quote, transaction, and status examples. ## Testnet funds For development and testing, use the [Tempo Faucet](https://tempo.xyz/developers/docs/quickstart/faucet). The faucet provides `pathUSD`, `alphaUSD`, `betaUSD`, and `thetaUSD` test stablecoins. # How to send a stablecoin payment on Tempo Send stablecoin payments between accounts on Tempo. Payments can include optional memos for reconciliation and tracking. :::warning[Confirm delivery, not just transaction success] On post-T6 networks, a blocked TIP-20 `transfer` / `transferFrom` still succeeds, but credits `ReceivePolicyGuard` at `0xB10C000000000000000000000000000000000000` instead of the intended receiver. * Before marking a payment, withdrawal, or deposit delivered, confirm the `Transfer` recipient is the intended receiver or the master wallet for its virtual address. * Index `ReceivePolicyGuard.TransferBlocked` so redirected funds can be surfaced and claimed later. See [Configure Receive Policies](https://tempo.xyz/developers/docs/guide/payments/configure-receive-policies). ::: ## Send payment demo By the end of this guide you will be able to send payments on Tempo with an optional memo. **Interactive demo: Send a Payment** 1. Connect 2. Add funds 3. Send payment Source: [tempoxyz/examples/tree/main/examples/payments](https://github.com/tempoxyz/examples/tree/main/examples/payments) ## Send payment implementation steps ::::steps ### Set up Wagmi for Tempo payments Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: * [Connection details](https://tempo.xyz/developers/docs/quickstart/connection-details) * [TypeScript SDK](https://tempo.xyz/developers/docs/sdk/typescript) * [Wallet integration](https://tempo.xyz/developers/docs/quickstart/wallet-developers) ### Add testnet stablecoin funds¹ Before you can send a payment, you need to fund your account. In this guide you will be sending `AlphaUSD` (`0x20c000…0001`). The built-in Tempo testnet faucet funds accounts with `AlphaUSD`. **Interactive demo: Add Funds** 1. Add funds :::code-group ```tsx twoslash [AddFunds.ts] // @noErrors import { Hooks } from 'wagmi/tempo' import { useConnection } from 'wagmi' function AddFunds() { const { address } = useConnection() const { mutate, isPending } = Hooks.faucet.useFundSync() return ( ) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: :::warning ¹ It is important to note that the `addFunds` Hook only works on testnets as a convenience feature to get started quickly. For production, you will need to onramp & fund your account manually. ::: ### Add TIP-20 transfer logic Now that you have `AlphaUSD` you are ready to add logic to send a payment with an optional memo. **Interactive demo: Send Payment** 1. Add funds 2. Send payment :::code-group ```tsx twoslash [SendPaymentWithMemo.tsx] import { Hooks } from 'wagmi/tempo' import { parseUnits, stringToHex, pad } from 'viem' // @noErrors function SendPaymentWithMemo() { const sendPayment = Hooks.token.useTransferSync() // [!code hl] return (
{ event.preventDefault() const formData = new FormData(event.target as HTMLFormElement) const recipient = (formData.get('recipient') || '0x0000000000000000000000000000000000000000') as `0x${string}` const memo = (formData.get('memo') || '') as string sendPayment.mutate({ // [!code hl] amount: parseUnits('100', 6), // [!code hl] to: recipient, // [!code hl] token: '0x20c0000000000000000000000000000000000001', // [!code hl] memo: memo ? pad(stringToHex(memo), { size: 32 }) : undefined, // [!code hl] }) // [!code hl] } }>
{/* [!code hl] */}
) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Display the payment receipt Now that you can send a payment, you can display the transaction receipt on success. :::code-group ```tsx twoslash [SendPaymentWithMemo.tsx] import { Hooks } from 'wagmi/tempo' import { parseUnits, stringToHex, pad } from 'viem' // @noErrors function SendPaymentWithMemo() { const sendPayment = Hooks.token.useTransferSync() return ( <> {/* ... your payment form ... */} {sendPayment.data && ( // [!code ++] {/* [!code ++] */} View receipt {/* [!code ++] */} {/* [!code ++] */} )} {/* [!code ++] */} ) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Next steps after sending a payment Now that you have made a payment you can * **[Accept a payment](https://tempo.xyz/developers/docs/guide/payments/accept-a-payment)** to receive payments in your application * Learn about [Tempo Transactions](https://tempo.xyz/developers/docs/protocol/transactions) for batching, sponsorship, scheduling, and more * Send a payment [with a specific fee token](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) :::: ## Stablecoin payment recipes ### Basic TIP-20 transfer Send a payment using the standard `transfer` function: #### Viem :::code-group ```ts twoslash [example.ts] // @noErrors import { parseUnits } from 'viem' import { client } from './viem.config' const { receipt } = await client.token.transferSync({ amount: parseUnits('100', 6), // 100 tokens (6 decimals) // [!code hl] to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', // [!code hl] token: '0x20c0000000000000000000000000000000000001', // AlphaUSD // [!code hl] }) ``` ```ts twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi ```tsx twoslash // @noErrors import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' function SendPayment() { const { mutate, isPending } = Hooks.token.useTransferSync() // [!code hl] return ( ) } ``` #### Rust :::code-group ```rust [example.rs] use alloy::{ primitives::{address, U256}, providers::ProviderBuilder, }; use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let token = ITIP20::new( // [!code hl] address!("0x20c0000000000000000000000000000000000001"), // AlphaUSD // [!code hl] &provider, // [!code hl] ); // [!code hl] let receipt = token // [!code hl] .transfer( // [!code hl] address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"), // [!code hl] U256::from(100_000_000), // 100 tokens (6 decimals) // [!code hl] ) // [!code hl] .send() // [!code hl] .await? // [!code hl] .get_receipt() // [!code hl] .await?; // [!code hl] println!("Transfer successful: {:?}", receipt.transaction_hash); Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from eth_abi import encode from pytempo import Call, TempoTransaction from provider import w3, account TRANSFER_SELECTOR = bytes.fromhex("a9059cbb") ALPHA_USD = "0x20c0000000000000000000000000000000000001" data = TRANSFER_SELECTOR + encode( # [!code hl] ["address", "uint256"], # [!code hl] ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", 100_000_000], # [!code hl] ) # [!code hl] tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=100_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=( Call.create(to=ALPHA_USD, data="0x" + data.hex()), # [!code hl] ), ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func buildTransferData(to common.Address, amount *big.Int) []byte { data := make([]byte, 68) // 4 (selector) + 32 (address) + 32 (uint256) data[0], data[1], data[2], data[3] = 0xa9, 0x05, 0x9c, 0xbb copy(data[16:36], to.Bytes()) amount.FillBytes(data[36:68]) return data } func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) recipient := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(100_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( // [!code hl] alphaUSD, // [!code hl] big.NewInt(0), // [!code hl] buildTransferData(recipient, big.NewInt(100_000_000)), // [!code hl] ). // [!code hl] Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast erc20 transfer \ 0x20c0000000000000000000000000000000000001 \ 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb \ 100000000 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # [!code hl] ``` #### Solidity ```solidity import {ITIP20} from "tempo-std/interfaces/ITIP20.sol"; contract PaymentSender { ITIP20 public token; function sendPayment(address recipient, uint256 amount) external { token.transfer(recipient, amount); // [!code hl] } } ``` ### Transfer with memo Include a memo for payment reconciliation and tracking. The memo is a 32-byte value that can store payment references, invoice IDs, order numbers, or any other metadata. #### Viem :::code-group ```ts twoslash [example.ts] // @noErrors import { parseUnits, stringToHex, pad } from 'viem' import { client } from './viem.config' const invoiceId = pad(stringToHex('INV-12345'), { size: 32 }) // [!code hl] const { receipt } = await client.token.transferSync({ amount: parseUnits('100', 6), to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', token: '0x20c0000000000000000000000000000000000001', memo: invoiceId, // [!code hl] }) ``` ```ts twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi ```tsx twoslash // @noErrors import { Hooks } from 'wagmi/tempo' import { parseUnits, stringToHex, pad } from 'viem' function SendPaymentWithMemo() { const { mutate, isPending } = Hooks.token.useTransferSync() return ( ) } ``` #### Rust :::code-group ```rust [example.rs] use alloy::{ primitives::{address, B256, U256}, providers::ProviderBuilder, }; use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let token = ITIP20::new( address!("0x20c0000000000000000000000000000000000001"), &provider, ); let receipt = token .transferWithMemo( // [!code hl] address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"), U256::from(100_000_000), B256::left_padding_from("INV-12345".as_bytes()), // [!code hl] ) .send() .await? .get_receipt() .await?; println!("Transfer successful: {:?}", receipt.transaction_hash); Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from eth_abi import encode from pytempo import Call, TempoTransaction from provider import w3, account TRANSFER_WITH_MEMO_SELECTOR = bytes.fromhex("76a8ee59") ALPHA_USD = "0x20c0000000000000000000000000000000000001" memo = b"INV-12345" + b"\x00" * 23 # right-pad to 32 bytes # [!code hl] data = TRANSFER_WITH_MEMO_SELECTOR + encode( # [!code hl] ["address", "uint256", "bytes32"], # [!code hl] ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", 100_000_000, memo], # [!code hl] ) # [!code hl] tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=100_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=( Call.create(to=ALPHA_USD, data="0x" + data.hex()), ), ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func buildTransferWithMemoData(to common.Address, amount *big.Int, memo [32]byte) []byte { data := make([]byte, 100) // 4 + 32 + 32 + 32 data[0], data[1], data[2], data[3] = 0x76, 0xa8, 0xee, 0x59 copy(data[16:36], to.Bytes()) amount.FillBytes(data[36:68]) copy(data[68:100], memo[:]) return data } func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) recipient := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") var memo [32]byte // [!code hl] copy(memo[:], []byte("INV-12345")) // [!code hl] tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(100_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( // [!code hl] alphaUSD, // [!code hl] big.NewInt(0), // [!code hl] buildTransferWithMemoData(recipient, big.NewInt(100_000_000), memo), // [!code hl] ). // [!code hl] Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast send \ 0x20c0000000000000000000000000000000000001 \ "transferWithMemo(address,uint256,bytes32)" \ 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb \ 100000000 \ $(cast --format-bytes32-string "INV-12345") \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # [!code hl] ``` #### Solidity ```solidity import {ITIP20} from "tempo-std/interfaces/ITIP20.sol"; contract PaymentSender { ITIP20 public token; function sendPaymentWithMemo( address recipient, uint256 amount, bytes32 invoiceId ) external { token.transferWithMemo(recipient, amount, invoiceId); // [!code hl] } } ``` ### Batch payment transactions Send multiple payments in a single transaction using batch transactions: #### Viem :::code-group ```ts twoslash [example.ts] // @noErrors import { encodeFunctionData, parseUnits } from 'viem' import { Abis } from 'viem/tempo' import { client } from './viem.config' const token = '0x20c0000000000000000000000000000000000001' as const const payments = [ { to: '0x742d35cc6634c0532925a3b844bc9e7595f0bebb', amount: parseUnits('100', 6) }, { to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', amount: parseUnits('50', 6) }, ] as const const calls = payments.map(({ to, amount }) => ({ // [!code hl] to: token, // [!code hl] data: encodeFunctionData({ // [!code hl] abi: Abis.tip20, // [!code hl] functionName: 'transfer', // [!code hl] args: [to, amount], // [!code hl] }), // [!code hl] })) // [!code hl] await client.sendTransaction({ calls }) // [!code hl] ``` ```ts twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi ```tsx twoslash // @noErrors import { useSendTransactionSync } from 'wagmi' import { encodeFunctionData, parseUnits } from 'viem' import { Abis } from 'viem/tempo' function BatchPayment() { const { sendTransactionSync, isPending } = useSendTransactionSync() const tokenABI = Abis.tip20 const token = '0x20c0000000000000000000000000000000000001' return ( ) } ``` #### Rust :::code-group ```rust [example.rs] use alloy::{ primitives::{address, Address, U256}, providers::{Provider, ProviderBuilder}, sol_types::SolCall, }; use tempo_alloy::{ TempoNetwork, contracts::precompiles::ITIP20, primitives::transaction::Call, rpc::TempoTransactionRequest, }; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let recipient1 = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"); let recipient2 = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); let token_address: Address = address!("0x20c0000000000000000000000000000000000001"); let calls = vec![ // [!code hl] Call { // [!code hl] to: token_address.into(), // [!code hl] input: ITIP20::transferCall { // [!code hl] to: recipient1, // [!code hl] amount: U256::from(100_000_000), // [!code hl] } // [!code hl] .abi_encode() // [!code hl] .into(), // [!code hl] value: U256::ZERO, // [!code hl] }, // [!code hl] Call { // [!code hl] to: token_address.into(), // [!code hl] input: ITIP20::transferCall { // [!code hl] to: recipient2, // [!code hl] amount: U256::from(50_000_000), // [!code hl] } // [!code hl] .abi_encode() // [!code hl] .into(), // [!code hl] value: U256::ZERO, // [!code hl] }, // [!code hl] ]; // [!code hl] let pending = provider .send_transaction(TempoTransactionRequest { calls, ..Default::default() }) .await?; let tx_hash = pending.tx_hash(); println!("Batch transaction sent: {tx_hash:?}"); Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from eth_abi import encode from pytempo import Call, TempoTransaction from provider import w3, account TRANSFER_SELECTOR = bytes.fromhex("a9059cbb") ALPHA_USD = "0x20c0000000000000000000000000000000000001" def build_transfer(to: str, amount: int) -> str: data = TRANSFER_SELECTOR + encode( ["address", "uint256"], [to, amount] ) return "0x" + data.hex() tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=200_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=( # [!code hl] Call.create( # [!code hl] to=ALPHA_USD, # [!code hl] data=build_transfer("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", 100_000_000), # [!code hl] ), # [!code hl] Call.create( # [!code hl] to=ALPHA_USD, # [!code hl] data=build_transfer("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 50_000_000), # [!code hl] ), # [!code hl] ), # [!code hl] ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func buildTransferData(to common.Address, amount *big.Int) []byte { data := make([]byte, 68) data[0], data[1], data[2], data[3] = 0xa9, 0x05, 0x9c, 0xbb copy(data[16:36], to.Bytes()) amount.FillBytes(data[36:68]) return data } func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) recipient1 := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") recipient2 := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(200_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall(alphaUSD, big.NewInt(0), buildTransferData(recipient1, big.NewInt(100_000_000))). // [!code hl] AddCall(alphaUSD, big.NewInt(0), buildTransferData(recipient2, big.NewInt(50_000_000))). // [!code hl] Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Batch transaction sent: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast batch-send \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --call "0x20c0000000000000000000000000000000000001::transfer(address,uint256):0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb,100000000" \ --call "0x20c0000000000000000000000000000000000001::transfer(address,uint256):0x70997970C51812dc3A010C7d01b50e0d17dc79C8,50000000" # [!code hl] ``` #### Solidity ```solidity import {ITIP20} from "tempo-std/interfaces/ITIP20.sol"; contract BatchPaymentSender { ITIP20 public token; struct Payment { address recipient; uint256 amount; } function batchPay(Payment[] calldata payments) external { for (uint256 i = 0; i < payments.length; i++) { token.transfer(payments[i].recipient, payments[i].amount); // [!code hl] } } } ``` Pass every encoded call together so Tempo executes all transfers in one all-or-nothing transaction. ### Index payment events When you send a payment, the token contract emits events: * **Transfer**: Standard ERC-20 transfer event * **TransferWithMemo**: Additional event with memo (if using `transferWithMemo`) You can filter these events to track payments in your off-chain systems. ## Stablecoin payment best practices ### Payment loading states Users should see a loading state when the payment is being processed. You can use the `isPending` property from the `useTransferSync` hook to show pending state to the user. ### Payment error handling If an error unexpectedly occurs, you can display an error message to the user by using the `error` property from the `useTransferSync` hook. ```tsx import { Hooks } from 'wagmi/tempo' function SendPayment() { const sendPayment = Hooks.token.useTransferSync() return ( <> {/* ... your paymentform ... */} {sendPayment.error &&
Error: {sendPayment.error.message}
} ) } ``` ## Stablecoin payment learning resources * [TypeScript SDK](https://tempo.xyz/developers/docs/sdk/typescript) — Learn more about the TypeScript SDK * [Transactions](https://tempo.xyz/developers/docs/protocol/transactions) — Learn more about transactions on Tempo # Stablecoin payments: integration guide Send and receive payments using stablecoins on Tempo. Start with core payment flows, then add reconciliation, receive controls, fee preferences, sponsorship, and higher-throughput transaction patterns. Use the [Tempo API](https://tempo.xyz/developers/docs/api) in your application backend to monitor account and transaction activity, deliver webhook updates, and sponsor transaction fees. Use [receive policies](https://tempo.xyz/developers/docs/guide/payments/configure-receive-policies) when an account needs to filter inbound TIP-20 transfers or mints by token and sender. ## Core Payment Flows * [Send a Payment](https://tempo.xyz/developers/docs/guide/payments/send-a-payment) — Send stablecoin payments between accounts with optional memos for reconciliation. * [Accept a Payment](https://tempo.xyz/developers/docs/guide/payments/accept-a-payment) — Accept payments from users and integrate payment flows into your application. * [Attach a Transfer Memo](https://tempo.xyz/developers/docs/guide/payments/transfer-memos) — Attach 32-byte references to TIP-20 transfers for payment reconciliation. * [Use Virtual Addresses](https://tempo.xyz/developers/docs/guide/payments/virtual-addresses) — Generate one TIP-20 deposit address per customer without sweep transactions. ## Payment Controls * [Configure Receive Policies](https://tempo.xyz/developers/docs/guide/payments/configure-receive-policies) — Filter inbound TIP-20 transfers and mints by token and sender, then handle blocked receipts. * [Pay Fees in Any Stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) — Configure users to pay transaction fees in any supported stablecoin. * [Sponsor User Fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) — Sponsor transaction fees for your users to enable gasless transactions. * [Send Parallel Transactions](https://tempo.xyz/developers/docs/guide/payments/send-parallel-transactions) — Submit multiple transactions in parallel using nonce keys. # Stablecoin issuance using the TIP-20 standard Create and manage your own stablecoin on Tempo. Launch the token first, then configure fees, roles, supply controls, and transfer policies. ## Launch a Stablecoin * [Create a Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/create-a-stablecoin) — Create your own stablecoin using TIP-20 tokens with built-in compliance features. * [Migrate an ERC-20 to TIP-20](https://tempo.xyz/developers/docs/guide/issuance/migrate-erc20-to-tip20) — Map an existing OpenZeppelin ERC-20 to native TIP-20 issuance, bridging, roles, burns, decimals, and compliance policies. * [Mint Stablecoins](https://tempo.xyz/developers/docs/guide/issuance/mint-stablecoins) — Mint new tokens to increase supply and distribute your stablecoin. * [Use Your Stablecoin for Fees](https://tempo.xyz/developers/docs/guide/issuance/use-for-fees) — Enable users to pay transaction fees using your stablecoin. ## Operate a Stablecoin * [Manage Your Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/manage-stablecoin) — Manage roles, permissions, supply caps, and transfer policies for your stablecoin. # Exchange stablecoins on the Tempo DEX Trade between stablecoins on Tempo's enshrined decentralized exchange (DEX). Start with quote-token behavior, then execute swaps, provide liquidity, or manage the fee-liquidity path for a stablecoin. ## Stablecoin Exchange Paths * [Understand pathUSD](https://tempo.xyz/developers/docs/protocol/exchange/quote-tokens#pathusd) — Learn about pathUSD, the root quote token that powers stablecoin interoperability. * [Execute Stablecoin Swaps](https://tempo.xyz/developers/docs/guide/stablecoin-dex/executing-swaps) — Execute swaps between stablecoins. * [Provide Stablecoin Liquidity](https://tempo.xyz/developers/docs/guide/stablecoin-dex/providing-liquidity) — Provide liquidity by placing limit orders or flip orders in the orderbook. * [Manage Fee Liquidity](https://tempo.xyz/developers/docs/guide/stablecoin-dex/managing-fee-liquidity) — Keep fee pools balanced so users can pay transaction fees with your stablecoin. # Make agentic payments with the Machine Payments Protocol Make agentic payments using the [Machine Payments Protocol](https://mpp.dev) (MPP). MPP adds inline payments to any HTTP endpoint — agents, apps, or humans pay as part of their request, and the server verifies payment before returning the response. ## Try an agentic payment See the full payment flow in action. The terminal creates an ephemeral wallet, funds it with testnet USDG, and makes a paid request to fetch a photo. The interactive terminal creates a test wallet, funds it, and makes a paid request. ## MPP payment flow A client requests a paid resource, the server responds with `402` and a `Challenge` describing the price. The client pays, retries with a `Credential` transaction, and the server returns the resource with a `Receipt`. ```mermaid sequenceDiagram participant Client participant Server Client->>Server: (1) GET /resource Server-->>Client: (2) 402 Payment Required + Challenge Note over Client: (3) Client fulfills payment Client->>Server: (4) GET /resource + Credential Note over Server: (5) Server verifies payment Server-->>Client: (6) 200 OK + Receipt ``` 1. **Request** — Any HTTP method (`GET`, `POST`, etc.) 2. **Challenge** — `402` with `WWW-Authenticate: Payment` header describing amount, currency, and recipient 3. **Pay** — Client signs a transaction or fulfills payment off-chain 4. **Retry** — Client re-sends with `Authorization: Payment` header containing the Credential 5. **Deliver** — Server verifies, returns `200` with `Payment-Receipt` header ## Why Tempo for agentic payments Tempo's transaction model is designed for agentic payments using MPP: * **~500ms finality** — Deterministic confirmation fast enough for synchronous request/response flows * **Sub-cent fees** — Low enough for micropayments and per-request billing * **Lower repeated channel lifecycle costs** — T7 adds payer-scoped storage credits for MPP payment channels. The credited reopen path, `open_new_channel_with_storage_credit`, is 60,225 gas in the channel-reserve gas snapshot. * **Fee sponsorship** — Servers can cover gas on behalf of clients so they only need stablecoins * **2D and expiring nonces** — Parallel nonce lanes prevent payment transactions from blocking other account activity * **High throughput** — Supports the on-chain settlement volume that payment channels generate at scale ## MPP payment intents on Tempo Two [intents](https://mpp.dev/protocol#payment-intents) are available on Tempo: | | **Charge** | **Session** | |---|---|---| | **Pattern** | One-time payment per request | Continuous pay-as-you-go | | **Latency** | ~500ms (on-chain confirmation) | Near-zero (off-chain vouchers) | | **Best for** | Single API calls, content access, one-off purchases | LLM APIs, metered services, usage-based billing | | **On-chain cost** | Per request | Amortized across many requests | ## Agentic payment use cases * **Paid APIs** — Charge per request without API keys, billing accounts, or signup flows. * **MCP tools** — Monetize tool calls served through the Model Context Protocol. Agents pay per call without OAuth or account setup. * **Digital content** — Charge per access for articles, data feeds, or media without subscription paywalls. ## Get started with MPP on Tempo * [Client quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/client) — Handle payment-gated resources automatically * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Discover services and make paid requests from a terminal or AI agent * [Discover MPP services](https://tempo.xyz/developers/docs/guide/machine-payments/discover-services) — Find paid APIs through the mpp.dev directory, catalog API, and MCP server * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Add payment gating to your HTTP endpoints * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Charge per request with on-chain settlement * [Accept pay-as-you-go payments](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) — Session-based billing with off-chain vouchers ## MPP SDKs and tools | Tool | Package | Install | |-----|---------|---------| | CLI | [`tempo request`](https://tempo.xyz/developers/docs/cli/request) | `curl -fsSL https://tempo.xyz/install \| bash` | | TypeScript | [`mppx`](https://github.com/wevm/mppx) | `npm install mppx viem` | | Python | [`pympp`](https://github.com/tempoxyz/pympp) | `pip install pympp` | | Rust | [`mpp-rs`](https://github.com/tempoxyz/mpp-rs) | `cargo add mpp` | See the [full SDK documentation](https://mpp.dev/sdk) for API reference and advanced usage. ## Learn more about MPP * [MPP documentation](https://mpp.dev) — Full protocol docs, SDK reference, and guides * [IETF specs](https://paymentauth.org/) — Normative protocol specification * [Protocol overview](https://mpp.dev/protocol) — Challenges, Credentials, Receipts, and transports # Connect to Tempo Zones on testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Tempo Zones let you keep balances and transfers inside a private execution environment while still using the public Tempo chain when funds enter or leave. The important thing to remember is that most zone flows settle in stages: a public or zone transaction lands first, then the private balance update appears shortly after. ![Tempo Zones overview](/developers/learn/zones/diagram-overview.svg) ## Before you start * Use a Tempo passkey account in the demo so the page can authorize private zone reads. * Keep some `pathUSD` on the public chain if you want to try deposits, source-zone top-ups, routed sends, swaps, or withdrawals. * Expect deposits, routed sends, routed swaps, and withdrawals to complete asynchronously rather than in a single balance update. These guides cover the current zone connection setup plus the baseline workflows used in the demos: deposits through `Actions.zone.depositSync(...)` and `Actions.zone.encryptedDepositSync(...)`, in-zone transfers, same-token routed sends through `Actions.zone.requestWithdrawalSync(...)`, routed swaps, direct withdrawals, and authenticated withdrawals through `Actions.zone.requestVerifiableWithdrawalSync(...)`. The deposit guide's demo lets you switch between plaintext and encrypted deposits, and the withdrawal guide lets you switch between standard and authenticated withdrawals, while keeping the transaction flow on the upstream `viem` zone actions. ## Choose the right guide * **Connect to a zone** if you want the Zone A and Zone B RPC URLs, chain IDs, and a minimal `viem` client setup. * **Deposit to a zone** if you want to move `pathUSD` from your public balance into `Zone A`. * **Send tokens within a zone** if you want to transfer `pathUSD` between private accounts without leaving `Zone A`. * **Send tokens across zones** if you want to leave `Zone A` with `pathUSD` and arrive in `Zone B` with the same token. * **Swap across zones** if you want to leave `Zone A` with `pathUSD` and arrive in `Zone B` with `betaUSD`. * **Withdraw from a zone** if you want to move `pathUSD` back from `Zone A` to your public balance. - [Connect to a zone](https://tempo.xyz/developers/docs/guide/private-zones/connect-to-a-zone) — Get the Zone A and Zone B RPC details and start with a minimal viem client setup. - [Deposit to a zone](https://tempo.xyz/developers/docs/guide/private-zones/deposit-to-a-zone) — Move pathUSD from your public balance into Zone A and confirm the private balance update. - [Send tokens within a zone](https://tempo.xyz/developers/docs/guide/private-zones/send-tokens-within-a-zone) — Send pathUSD between private accounts inside Zone A without returning to the public chain. - [Send tokens across zones](https://tempo.xyz/developers/docs/guide/private-zones/send-tokens-across-zones) — Route pathUSD out of Zone A and into Zone B without changing the token. - [Swap stablecoins across zones](https://tempo.xyz/developers/docs/guide/private-zones/swap-across-zones) — Withdraw from Zone A, swap on the public chain, and land in Zone B as betaUSD. - [Withdraw from a zone](https://tempo.xyz/developers/docs/guide/private-zones/withdraw-from-a-zone) — Withdraw stablecoins from a zone back to your public balance. # Tempo developer tools and SDKs Use this section when you need the implementation surface for Tempo: libraries, command-line tools, APIs, wallet integration, RPC references, and query access to network data. If you are integrating a product, start with the TypeScript SDKs and Tempo API. If you are operating infrastructure or debugging low-level behavior, start with the CLI and RPC reference. ## Build with SDKs * [Tempo SDKs](https://tempo.xyz/developers/docs/sdk) — Use TypeScript, Go, Foundry, Python, and Rust tooling for Tempo applications. * [Tempo CLI](https://tempo.xyz/developers/docs/cli) — Manage wallets, make requests, download binaries, and run node-related commands. * [Tempo API](https://tempo.xyz/developers/docs/api) — Use JSON-RPC, fee sponsorship, and indexed data through the Tempo API. * [Tempo Wallet](https://tempo.xyz/developers/docs/wallet) — Integrate the Tempo Wallet, recipes, references, and agent-oriented wallet flows. ## Operate and Inspect * [JSON-RPC API](https://tempo.xyz/developers/docs/protocol/rpc) — Browse Tempo RPC methods, request shapes, and protocol-level API behavior. * [Indexer API](https://tempo.xyz/developers/docs/api/indexer-api) — Query Tempo blocks, transactions, logs, token balances, and decoded events through SQL. * [Fee Payer API](https://tempo.xyz/developers/docs/api/fee-payer) — Sponsor Tempo transaction fees through the hosted Relay API. * [Use Tempo with AI](https://tempo.xyz/developers/docs/guide/using-tempo-with-ai) — Install MCP, plugins, and docs context so coding agents can work with Tempo accurately. # Tempo protocol: specifications and reference Use this section when you need the technical reference for Tempo itself. These pages are written for implementers, auditors, wallet teams, infrastructure operators, and anyone building against protocol-level behavior rather than a single product workflow. If you are building an app, start with the guides in **Build on Tempo** and come here when you need exact semantics, ABI details, or protocol rationale. For private execution internals, start with Tempo Zones and the [zone accounts specification](https://tempo.xyz/developers/docs/protocol/zones/accounts), which defines private balance, allowance, and account-scoped access rules. ## Core Primitives * [TIP-20 Tokens](https://tempo.xyz/developers/docs/protocol/tip20/overview) — Stablecoin-native token behavior, metadata, fees, memos, policies, and liquidity routing. * [Tempo Policies (TIP-403)](https://tempo.xyz/developers/docs/protocol/tip403/overview) — TIP-403 policy checks, receive policies, access controls, and registry behavior. * [Transaction Fees](https://tempo.xyz/developers/docs/protocol/fees) — Stablecoin fee payment, fee accounting, and the fee AMM used for conversion. * [Tempo Transactions](https://tempo.xyz/developers/docs/protocol/transactions) — Tempo transaction type, batching, scheduling, fee sponsorship, and account keychains. ## Network Systems * [Blockspace](https://tempo.xyz/developers/docs/protocol/blockspace/overview) — Consensus, finality, block format, payment lanes, and throughput-oriented protocol design. * [Stablecoin DEX](https://tempo.xyz/developers/docs/protocol/exchange) — Enshrined stablecoin exchange, quote tokens, swaps, liquidity, and exchange balances. * [Zones](https://tempo.xyz/developers/docs/protocol/zones) — Private zone architecture, accounts, bridging, RPC, execution, gas, and proving. * [Network Upgrades](https://tempo.xyz/developers/docs/protocol/upgrades/t8) — Track upgrade specifications, migration notes, and release-level protocol changes. ## Specifications and Source * [TIPs](https://tips.sh/) — Browse Tempo Improvement Proposals for standards, process changes, and protocol history. * [GitHub Repository](https://github.com/tempoxyz/tempo) — Read the Tempo source implementation and follow protocol development in the open. # Run a Tempo node: validator, RPC, and standby Run Tempo infrastructure when you need direct network access, dedicated RPC capacity, validator operations, or lower-level visibility into network behavior. Most teams should start with [running RPC and standby nodes](https://tempo.xyz/developers/docs/guide/node/rpc). Validator operation requires coordination with the Tempo team and includes a separate [validator failover](https://tempo.xyz/developers/docs/guide/node/validator-failover) path for backup infrastructure. ## RPC Node Path * [System Requirements](https://tempo.xyz/developers/docs/guide/node/system-requirements) — Hardware, operating system, and runtime requirements for Tempo node software. * [Installation](https://tempo.xyz/developers/docs/guide/node/installation) — Download, install, and prepare the Tempo node binary for supported platforms. * [Run RPC and Standby Nodes](https://tempo.xyz/developers/docs/guide/node/rpc) — Configure and run nodes that serve JSON-RPC traffic or stay ready for validator failover. * [Node Security](https://tempo.xyz/developers/docs/guide/node/security) — Harden node operations, network exposure, key handling, and runtime configuration. ## Validator Operations * [Validator Overview](https://tempo.xyz/developers/docs/guide/node/validator) — Understand validator responsibilities and the operational path before onboarding. * [Validator Onboarding](https://tempo.xyz/developers/docs/guide/node/validator-setup) — Set up validator software and prepare to participate in coordinated network operations. * [Monitoring](https://tempo.xyz/developers/docs/guide/node/validator-monitoring) — Monitor validator health, status, failover, and lifecycle operations. * [Troubleshooting](https://tempo.xyz/developers/docs/guide/node/validator-troubleshooting) — Debug validator issues, common failure modes, and operational questions. ## Releases * [Upgrade Cadence](https://tempo.xyz/developers/docs/guide/node/upgrade-cadence) — Understand how Tempo node upgrades are planned, communicated, and rolled out. * [Network Upgrades](https://tempo.xyz/developers/docs/guide/node/network-upgrades) — Track upgrade instructions, release notes, and node operator actions. * [Changelog](https://tempo.xyz/developers/docs/changelog) — Review recent changes relevant to node operators and protocol users. # Using Tempo with AI through the MCP server Tempo publishes documentation, Markdown pages, MCP tools, and agent plugins so AI coding agents can work with Tempo using current project context instead of guesses. Tempo Mainnet has been live since March 18, 2026. Agent wallet workflows use mainnet, and pathUSD in Tempo Wallet is a live mainnet asset. Moderato and its faucet-issued pathUSD are reserved for explicitly testnet development workflows. Use this page when you want Claude, Codex, Cursor, Amp, or another MCP-compatible client to search Tempo and related documentation, read complete pages, and install reusable Tempo workflows. ## Connect to Tempo's MCP server The Tempo MCP server gives agents programmatic access to Tempo documentation and related developer resources. ### Claude ```bash claude mcp add --transport http tempo https://mcp.tempo.xyz ``` ### Codex ```bash codex mcp add tempo --url https://mcp.tempo.xyz ``` ### Cursor [Install in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=tempo\&config=eyJ1cmwiOiJodHRwczovL21jcC50ZW1wby54eXoifQ%3D%3D) To open Cursor and automatically add the Tempo MCP server, click install. Alternatively, add the following to your `~/.cursor/mcp.json` file. To learn more, see the Cursor [documentation](https://docs.cursor.com/context/model-context-protocol). ```json { "mcpServers": { "tempo": { "url": "https://mcp.tempo.xyz" } } } ``` ### Amp ```bash amp mcp add --transport http tempo https://mcp.tempo.xyz ``` ### Manual ```json { "mcpServers": { "tempo": { "url": "https://mcp.tempo.xyz" } } } ``` ## MCP tools The MCP endpoint exposes these tools to connected agents: | Tool | Purpose | Required arguments | | --- | --- | --- | | `search` | Search docs. | `query` | | `find_pages` | Find matching page URLs from a source index. | `source`, `query` | | `read_page` | Read one cleaned documentation page. | `source` plus `path` or `url` | | `code` | Run multi-step documentation lookups. | `code` | ### MCP feedback MCP clients can also post feedback directly to `https://tempo.xyz/developers/api/feedback`: ```json { "source": "mcp", "sentiment": "negative", "message": "The read_page result for /guide/payments is missing fee-token setup.", "toolName": "read_page", "relatedResource": "/guide/payments", "client": "codex" } ``` ### Try the MCP server Use the interactive web page to try the Tempo MCP server. ## Install Tempo plugins The Tempo plugin installs a complete agent integration: the Tempo MCP server, workflow skills for using Tempo APIs and docs, and editor metadata that helps agents discover the right Tempo tools without manual setup. :::code-group ```bash [Codex] codex plugin marketplace add tempoxyz/docs --ref main codex plugin add docs@tempo ``` ```bash [Claude] claude plugin marketplace add tempoxyz/docs claude plugin install docs@tempo ``` ::: :::warning[Legacy Codex marketplace name] If Codex reports that the plugin is missing after finding this marketplace as `docs`, replace that pre-rename registration once, then rerun the install: ```bash codex plugin marketplace remove docs codex plugin marketplace add tempoxyz/docs --ref main codex plugin add docs@tempo ``` ::: ## Install the Mercator plugin Mercator adds paid API discovery and execution through the Mercator MCP server. The installer installs the CLI, configures detected MCP clients, and bootstraps the local Mercator plugin: ```bash curl -fsSL https://mercator.tempo.xyz/downloads/latest/install.sh | sh ``` To install the plugin directly from the Tempo marketplace, add the public `tempoxyz/docs` source and select `mercator@tempo`: :::code-group ```bash [Codex] codex plugin marketplace add tempoxyz/docs --ref main codex plugin add mercator@tempo ``` ```bash [Claude] claude plugin marketplace add tempoxyz/docs claude plugin install mercator@tempo --scope user --yes ``` ::: ## Tempo Docs ### Docs skill Install the Tempo Docs skill to give AI coding agents access to Tempo documentation, related documentation sources, and examples: ```bash npx skills add tempoxyz/docs ``` Once installed, the agent uses it automatically when relevant tasks are detected. ### Read docs as Markdown Every page on this site is available as plain Markdown — append `.md` to any URL: ``` https://tempo.xyz/developers/docs/quickstart/integrate-tempo.md ``` For LLM consumption, two [`llms.txt`](https://llmstxt.org/) files are served at the root: | URL | Contents | | --- | --- | | [`/llms.txt`](https://tempo.xyz/developers/llms.txt) | Concise index of all pages with titles and descriptions | | [`/llms-full.txt`](https://tempo.xyz/developers/llms-full.txt) | Complete documentation in a single file | # Partners Tempo works with partners across stablecoin issuance, wallets and custody, compliance tooling, fraud monitoring, interoperability protocols, analytics and monitoring, orchestration, ramps, and infrastructure. The ecosystem is designed to support production payment workloads from day one, with issuers across regions, broad local currency support, and infrastructure partners for developers building on Tempo. * [Explore Tempo Ecosystem](https://tempo.xyz/developers/docs/ecosystem) — See infrastructure partners including issuers, wallets, orchestration and ramps, compliance tooling, custodians, and more. * [Get In Touch](https://tempo.xyz/contact) — Get in touch with the Tempo team if you'd like to be connected with an ecosystem partner or become one yourself. # Build on Tempo Use this section when you are designing payment experiences or payment infrastructure on Tempo. The guides here focus on what your product needs to do: move stablecoins, issue assets, exchange liquidity, isolate activity in private zones, or let agents pay for services. Start with **Make Payments** if you are not sure where to begin. It covers the core transfer flow and introduces the payment primitives used throughout the rest of the docs. Use the [Tempo API](https://tempo.xyz/developers/docs/api) when your application backend needs indexed payment activity, webhooks, asset route or exchange quotes, fee sponsorship, or integration management. Use [Agentic Payments](https://tempo.xyz/developers/docs/guide/machine-payments) when agents, APIs, or services need to pay per request. Use [Private Zones](https://tempo.xyz/developers/docs/guide/private-zones) when your product needs isolated execution, private transfers, or zone bridging flows. ## Core Build Paths * [Make Payments](https://tempo.xyz/developers/docs/guide/payments) — Send and receive stablecoin payments, attach memos, sponsor fees, and handle parallel transactions. * [Issue Stablecoins](https://tempo.xyz/developers/docs/guide/issuance) — Create, mint, and manage stablecoins using TIP-20 tokens. * [Exchange Stablecoins](https://tempo.xyz/developers/docs/guide/stablecoin-dex) — Use Tempo's enshrined stablecoin exchange for swaps and fee-liquidity workflows. * [Agentic Payments](https://tempo.xyz/developers/docs/guide/machine-payments) — Accept one-time, pay-as-you-go, and streamed payments through the Machine Payments Protocol. * [Private Zones](https://tempo.xyz/developers/docs/guide/private-zones) — Connect to zones, deposit funds, send tokens privately, bridge, swap, and withdraw. ## Production Essentials * [Get Funds](https://tempo.xyz/developers/docs/guide/getting-funds) — Fund wallets and test payment flows before moving to production integrations. * [Use Tempo Transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction) — Batch calls, sponsor fees, schedule execution, and build richer payment flows. * [Pay Fees in Any Stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) — Use Tempo's fee system to pay transaction fees with supported stablecoins. * [Sponsor User Fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) — Let your app or service pay transaction fees on behalf of users. * [Configure Receive Policies](https://tempo.xyz/developers/docs/guide/payments/configure-receive-policies) — Control which tokens and senders an account can receive for TIP-20 transfers. # How to receive stablecoin payments Accept stablecoin payments in your application. Learn how to receive payments, verify transactions, and reconcile payments using memos. ## Receive stablecoin payments Payments are automatically credited to the recipient's address when a transfer is executed. You don't need to do anything special to "accept" a payment, it happens automatically onchain. In this basic receiving demo you can see the balances update after you add funds to your account, using the `getBalance` and `watchEvent` calls documented below. **Interactive demo: Receive a Payment** 1. Connect 2. Add funds Source: [tempoxyz/examples/tree/main/examples/payments](https://github.com/tempoxyz/examples/tree/main/examples/payments) ## Verify stablecoin payments Check if a payment has been received by querying the token balance or listening for transfer events: ### Check receiver token balance #### Viem :::code-group ```ts [example.ts] import { client } from './viem.config' const balance = await client.token.getBalance({ account: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', token: '0x20c0000000000000000000000000000000000001', // AlphaUSD }) console.log('Balance:', balance.formatted) ``` ```ts [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::{primitives::address, providers::ProviderBuilder}; use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) .await?; let balance = ITIP20::new( // [!code focus] address!("0x20c0000000000000000000000000000000000001"), // AlphaUSD // [!code focus] &provider, // [!code focus] ) // [!code focus] .balanceOf(address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb")) // [!code focus] .call() // [!code focus] .await?; // [!code focus] println!("Balance: {balance:?}"); // [!code focus] Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from web3 import Web3 from eth_abi import encode from provider import w3 token_address = "0x20c0000000000000000000000000000000000001" # AlphaUSD account_address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb" # balanceOf(address) selector: 0x70a08231 calldata = "0x70a08231" + encode(["address"], [account_address]).hex() # [!code hl] result = w3.eth.call({"to": token_address, "data": calldata}) # [!code hl] balance = int.from_bytes(result, "big") # [!code hl] print(f"Balance: {balance}") ``` ```python [provider.py] from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "fmt" "log" "math/big" "strings" ) func main() { c := newClient() ctx := context.Background() token := "0x20c0000000000000000000000000000000000001" account := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb" // balanceOf(address) — ABI-encoded eth_call // [!code hl] calldata := "0x70a08231" + fmt.Sprintf("%064s", strings.TrimPrefix(account, "0x")) // [!code hl] resp, err := c.SendRequest(ctx, "eth_call", map[string]interface{}{ // [!code hl] "to": token, // [!code hl] "data": calldata, // [!code hl] }, "latest") // [!code hl] if err != nil { log.Fatal(err) } balance := new(big.Int) // [!code hl] balance.SetString(strings.TrimPrefix(resp.Result.(string), "0x"), 16) // [!code hl] fmt.Printf("Balance: %s\n", balance) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast call 0x20c0000000000000000000000000000000000001 \ "balanceOf(address)(uint256)" \ 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb \ --rpc-url $TEMPO_RPC_URL ``` ### Listen for TIP-20 transfer events #### Viem :::code-group ```ts [example.ts] import { client } from './viem.config' // Watch for incoming transfers const unwatch = client.watchEvent({ address: '0x20c0000000000000000000000000000000000001', event: { type: 'event', name: 'Transfer', inputs: [ { name: 'from', type: 'address', indexed: true }, { name: 'to', type: 'address', indexed: true }, { name: 'value', type: 'uint256' }, ], }, onLogs: (logs) => { // [!code focus] logs.forEach((log) => { // [!code focus] console.log('Received payment:', { // [!code focus] from: log.args.from, // [!code focus] amount: log.args.value, // [!code focus] }) // [!code focus] }) // [!code focus] }, // [!code focus] }) ``` ```ts [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::{primitives::address, providers::ProviderBuilder}; use futures::StreamExt; use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) .await?; // Watch for incoming transfers // [!code focus] let mut transfers = ITIP20::new( // [!code focus] address!("0x20c0000000000000000000000000000000000001"), // [!code focus] &provider, // [!code focus] ) // [!code focus] .Transfer_filter() // [!code focus] .watch() // [!code focus] .await? // [!code focus] .into_stream(); // [!code focus] while let Some(Ok((payment, _))) = transfers.next().await { // [!code focus] println!("Received payment: {payment:?}") // [!code focus] } // [!code focus] Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] import json from web3 import Web3 from provider import w3 token_address = "0x20c0000000000000000000000000000000000001" # AlphaUSD transfer_event_abi = { # [!code focus] "anonymous": False, # [!code focus] "name": "Transfer", # [!code focus] "type": "event", # [!code focus] "inputs": [ # [!code focus] {"indexed": True, "name": "from", "type": "address"}, # [!code focus] {"indexed": True, "name": "to", "type": "address"}, # [!code focus] {"indexed": False, "name": "value", "type": "uint256"}, # [!code focus] ], # [!code focus] } # [!code focus] contract = w3.eth.contract( # [!code focus] address=Web3.to_checksum_address(token_address), # [!code focus] abi=[transfer_event_abi], # [!code focus] ) # [!code focus] # Get historical Transfer events # [!code focus] events = contract.events.Transfer().get_logs(from_block="latest") # [!code focus] for event in events: # [!code focus] print(f"Transfer: {event.args['from']} -> {event.args['to']}: {event.args['value']}") # [!code focus] ``` ```python [provider.py] from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "fmt" "log" "github.com/ethereum/go-ethereum/crypto" ) func main() { c := newClient() ctx := context.Background() token := "0x20c0000000000000000000000000000000000001" // Transfer(address,address,uint256) event topic // [!code focus] transferTopic := crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) // [!code focus] resp, err := c.SendRequest(ctx, "eth_getLogs", map[string]interface{}{ // [!code focus] "fromBlock": "0x0", // [!code focus] "toBlock": "latest", // [!code focus] "address": token, // [!code focus] "topics": []interface{}{transferTopic.Hex()}, // [!code focus] }) // [!code focus] if err != nil { log.Fatal(err) } logs, _ := resp.Result.([]interface{}) // [!code focus] for _, entry := range logs { // [!code focus] l := entry.(map[string]interface{}) // [!code focus] topics := l["topics"].([]interface{}) // [!code focus] fmt.Printf("Transfer: %s -> %s (data: %s)\n", topics[1], topics[2], l["data"]) // [!code focus] } // [!code focus] } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast logs \ --address 0x20c0000000000000000000000000000000000001 \ "Transfer(address indexed, address indexed, uint256)" \ --rpc-url $TEMPO_RPC_URL ``` ## Payment Reconciliation with Memos If payments include memos (invoice IDs, order numbers, etc.), you can reconcile them automatically: ### Viem :::code-group ```ts [example.ts] import { client } from './viem.config' // Watch for TransferWithMemo events const unwatch = client.watchEvent({ address: '0x20c0000000000000000000000000000000000001', event: { type: 'event', name: 'TransferWithMemo', inputs: [ { name: 'from', type: 'address', indexed: true }, { name: 'to', type: 'address', indexed: true }, { name: 'value', type: 'uint256' }, { name: 'memo', type: 'bytes32', indexed: true }, ], }, onLogs: (logs) => { // [!code focus] logs.forEach((log) => { // [!code focus] const invoiceId = log.args.memo // [!code focus] // Mark invoice as paid in your database // [!code focus] markInvoiceAsPaid(invoiceId, log.args.value) // [!code focus] }) // [!code focus] }, // [!code focus] }) ``` ```ts [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: ### Rust :::code-group ```rust [example.rs] use alloy::{primitives::address, providers::ProviderBuilder}; use futures::StreamExt; use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) .await?; let mut transfers = ITIP20::new( // [!code focus] address!("0x20c0000000000000000000000000000000000001"), // [!code focus] &provider, // [!code focus] ) // [!code focus] .TransferWithMemo_filter() // [!code focus] .watch() // [!code focus] .await? // [!code focus] .into_stream(); // [!code focus] while let Some(Ok((transfer, _))) = transfers.next().await { // [!code focus] let invoice_id = transfer.memo; // [!code focus] println!("Transfer received with memo: {invoice_id:?}"); // [!code focus] } // [!code focus] Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: ### Python :::code-group ```python [example.py] from web3 import Web3 from provider import w3 token_address = "0x20c0000000000000000000000000000000000001" # AlphaUSD transfer_memo_abi = { # [!code focus] "anonymous": False, # [!code focus] "name": "TransferWithMemo", # [!code focus] "type": "event", # [!code focus] "inputs": [ # [!code focus] {"indexed": True, "name": "from", "type": "address"}, # [!code focus] {"indexed": True, "name": "to", "type": "address"}, # [!code focus] {"indexed": False, "name": "value", "type": "uint256"}, # [!code focus] {"indexed": True, "name": "memo", "type": "bytes32"}, # [!code focus] ], # [!code focus] } # [!code focus] contract = w3.eth.contract( # [!code focus] address=Web3.to_checksum_address(token_address), # [!code focus] abi=[transfer_memo_abi], # [!code focus] ) # [!code focus] events = contract.events.TransferWithMemo().get_logs(from_block="latest") # [!code focus] for event in events: # [!code focus] invoice_id = event.args["memo"] # [!code focus] print(f"Transfer with memo {invoice_id.hex()}: {event.args['value']}") # [!code focus] ``` ```python [provider.py] from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) ``` ::: ### Go :::code-group ```go [main.go] package main import ( "context" "fmt" "log" "github.com/ethereum/go-ethereum/crypto" ) func main() { c := newClient() ctx := context.Background() token := "0x20c0000000000000000000000000000000000001" // TransferWithMemo(address,address,uint256,bytes32) event topic // [!code focus] memoTopic := crypto.Keccak256Hash( // [!code focus] []byte("TransferWithMemo(address,address,uint256,bytes32)"), // [!code focus] ) // [!code focus] resp, err := c.SendRequest(ctx, "eth_getLogs", map[string]interface{}{ // [!code focus] "fromBlock": "0x0", // [!code focus] "toBlock": "latest", // [!code focus] "address": token, // [!code focus] "topics": []interface{}{memoTopic.Hex()}, // [!code focus] }) // [!code focus] if err != nil { log.Fatal(err) } logs, _ := resp.Result.([]interface{}) // [!code focus] for _, entry := range logs { // [!code focus] l := entry.(map[string]interface{}) // [!code focus] topics := l["topics"].([]interface{}) // [!code focus] fmt.Printf("Transfer: %s -> %s (memo: %s, data: %s)\n", // [!code focus] topics[1], topics[2], topics[3], l["data"]) // [!code focus] } // [!code focus] } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: ### Cast ```bash $ cast logs \ --address 0x20c0000000000000000000000000000000000001 \ "TransferWithMemo(address indexed, address indexed, uint256, bytes32 indexed)" \ --rpc-url $TEMPO_RPC_URL ``` ## Smart Contract Integration If you're building a smart contract that accepts payments: ```solidity contract PaymentReceiver { ITIP20 public token; mapping(bytes32 => bool) public paidInvoices; event PaymentReceived( address indexed payer, uint256 amount, bytes32 indexed invoiceId ); function receivePayment( address payer, uint256 amount, bytes32 invoiceId ) external { require(!paidInvoices[invoiceId], "Invoice already paid"); // Transfer tokens from payer to this contract token.transferFrom(payer, address(this), amount); paidInvoices[invoiceId] = true; emit PaymentReceived(payer, amount, invoiceId); } } ``` ## Payment Verification Best Practices 1. **Verify onchain**: Always verify payments onchain before marking orders as paid 2. **Use memos**: Request memos from payers to link payments to invoices or orders 3. **Check confirmations**: Wait for transaction finality (~1 second on Tempo) before processing 4. **Handle edge cases**: Account for partial payments, refunds, and failed transactions ## Cross-Stablecoin Payments If you need to accept payments in a specific stablecoin but receive a different one, use the exchange to swap: ```ts // User sends USDG, but you need USDT // Swap USDG to USDT using the exchange const { receipt } = await client.dex.sellSync({ tokenIn: usdgAddress, tokenOut: usdtAddress, amountIn: receivedAmount, minAmountOut: receivedAmount * 99n / 100n, // 1% slippage }) ``` ## Next steps for accepting payments * **[Send a payment](https://tempo.xyz/developers/docs/guide/payments/send-a-payment)** to learn how to send payments * Learn more about [Exchange](https://tempo.xyz/developers/docs/guide/stablecoin-dex) for cross-stablecoin payments # Configure Receive Policies Receive policies let a receiver define which [TIP-20](https://tempo.xyz/developers/docs/protocol/tip20/overview) tokens it accepts and which senders may send those tokens to it. This page is intentionally about the builder flow rather than the full spec. It covers when to configure a policy, what happens when delivery is blocked, and what operators need to watch. If a receive policy blocks an inbound transfer or mint, the TIP-20 call still succeeds. The funds are credited to `ReceivePolicyGuard` instead of the receiver, and `ReceivePolicyGuard` records a receipt that can be claimed later by the configured recovery authority. The token's [TIP-403](https://tempo.xyz/developers/docs/protocol/tip403/spec) policy checks still run first and still revert on failure. Only a receive-policy failure redirects delivery to `ReceivePolicyGuard`. For the full protocol flow, claim rules, burn rules, and event signatures, see the [Receive Policies protocol overview](https://tempo.xyz/developers/docs/protocol/tip403/receive-policies). ## When to configure a receive policy Configure a receive policy when an address should reject some incoming TIP-20 tokens or some incoming senders. For example, receive policies are useful when: * a regulated entity that only wants to receive from addresses held by individuals it has KYC'ed * an orchestrator or exchange that only wants specific tokens sent to its deposit addresses ## Understand receive policy behavior If an address has no receive policy, all transfers and mints are allowed by the receive-policy layer. | Result | Transaction result | Where the funds go | |---|---|---| | Token-level checks fail | The call reverts | Funds are not delivered | | Receive policy allows | The call succeeds | Receiver is credited normally | | Receive policy blocks | The call succeeds | `ReceivePolicyGuard` is credited and records a receipt | The receive-policy check applies to `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `systemTransferFrom`, `mint`, and `mintWithMemo`. It does not apply to `approve`, `permit`, or `burn`. It also does not affect fee deposits or refunds through `transfer_fee_pre_tx` or `transfer_fee_post_tx`, TIP-20 rewards, or internal balances. For `transferFrom`, the spender's allowance is consumed even when the receiver's policy blocks delivery, because the funds still move out of the `from` account. ## Configure the policy :::steps ### Create or choose a sender policy A receive policy uses `senderPolicyId` to decide which senders are allowed. The sender policy can be: * built-in policy `0`, which rejects all * built-in policy `1`, which allows all * an existing simple TIP-403 `WHITELIST` or `BLACKLIST` policy * a newly created simple TIP-403 `WHITELIST` or `BLACKLIST` policy `COMPOUND` policies are not valid for `senderPolicyId`. ### Create or choose a token filter A receive policy uses `tokenFilterId` to decide which TIP-20 tokens are allowed. Token filters use existing TIP-403 policy data and membership sets, where members are interpreted as TIP-20 token addresses. The token filter can use built-in policy `0`, built-in policy `1`, or a simple TIP-403 `WHITELIST` or `BLACKLIST` policy. `COMPOUND` policies are not valid for `tokenFilterId`. Receive policies do not add a dedicated token-filter interface. Token filters are managed through the existing TIP-403 policy interface. ### Choose a recovery authority The recovery authority controls who may claim receipts for future blocked transfers or mints: | Recovery authority | Who can claim | |---|---| | `address(0)` | the originator of the transfer or mint | | the receiver's address | the receiver | | another nonzero address | that address | Changing the recovery authority affects future receipts only. Existing receipts keep the recovery authority captured when they were created. Nonzero recovery authorities cannot be `ReceivePolicyGuard`, [virtual addresses](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses), or system precompile addresses that cannot initiate calls. ### Set the receive policy Call `setReceivePolicy(senderPolicyId, tokenFilterId, recoveryAuthority)` from the address being configured. If the receiver is using virtual addresses, configure the policy on the resolved master address. A virtual address must not call `setReceivePolicy(...)`. ::: ## Monitor blocked receipts `ReceivePolicyGuard` lives at `0xB10C000000000000000000000000000000000000`. When delivery is blocked, the token balance is credited to that address. `ReceivePolicyGuard` stores a keyed amount for the blocked receipt. The receipt includes the token, recovery authority, originator, recipient, timestamp, nonce, blocked reason, inbound kind, and memo. `ReceivePolicyGuard` does not enumerate receipts onchain. Claimers need the receipt bytes, usually by indexing `TransferBlocked` events. The regular TIP-20 events still happen: * a blocked transfer emits the regular `Transfer` event with `ReceivePolicyGuard` as the recipient, then `TransferBlocked(...)` * a blocked mint emits the regular `Transfer` and `Mint` events with `ReceivePolicyGuard` as the recipient, then `TransferBlocked(...)` Index these events to track policy changes and receipt lifecycle: | Event | Purpose | |---|---| | `ReceivePolicyUpdated` | A receive policy changed for an address | | `TransferBlocked` | A transfer or mint was blocked and a receipt was created | | `ReceiptClaimed` | A receipt was consumed and funds were released | | `ReceiptBurned` | A receipt was consumed and funds were burned | `TransferBlocked.receipt` is the ABI-encoded receipt witness. The spec requires that this receipt be directly usable as the `receipt` argument to `balanceOf`, `claim`, and `burnBlockedReceipt`. ## Claim blocked receipts A claim is the normal recovery path for blocked transfers or mints. It consumes one full receipt and releases the full amount to one destination. Partial claims are not supported. Only the authorized claimer may call `claim(...)`: * if the receipt's `recoveryAuthority` is `address(0)`, only the receipt's `originator` may claim * otherwise, only the receipt's nonzero `recoveryAuthority` may claim Changing a receiver's recovery authority does not change who can claim older receipts. Claim paths have additional policy checks. See the [protocol overview](https://tempo.xyz/developers/docs/protocol/tip403/receive-policies#claims) before implementing recovery flows. ## Burn blocked receipts Burning is an issuer-only path for blocked receipts. `burnBlockedReceipt(...)` consumes one full receipt and burns the stored amount from `ReceivePolicyGuard`. The caller must hold `BURN_BLOCKED_ROLE` for the token. A receipt is burnable only when its policy subject is currently unauthorized as a sender under the token's TIP-403 policy. Burn eligibility has additional policy checks. See the [protocol overview](https://tempo.xyz/developers/docs/protocol/tip403/receive-policies#burns) before implementing issuer burn flows. ## Virtual-address behavior If `to` is a virtual address, it is resolved to its master address before checks run. Receive-policy checks use the master address. If resolution fails, the operation reverts as before. If the transfer or mint is blocked, the receipt is recorded for the master address and preserves the original `to` for attribution. ## Learn more about receive policies * [Receive policies specification](https://tips.sh/1028) — The approved specification for address-level receive policies. * [Protocol Overview](https://tempo.xyz/developers/docs/protocol/tip403/receive-policies) — Technical reference for receive-policy evaluation, guard receipts, claims, and events. * [Accept a Payment](https://tempo.xyz/developers/docs/guide/payments/accept-a-payment) — Verify incoming payments and listen for transfer events. * [Use Virtual Addresses](https://tempo.xyz/developers/docs/guide/payments/virtual-addresses) — Understand how virtual addresses affect deposit attribution. # Attach a Transfer Memo Attach 32-byte references to [TIP-20](https://tempo.xyz/developers/docs/protocol/tip20/overview) transfers for payment reconciliation. Use memos to link onchain transactions to your internal records—customer IDs, invoice numbers, or any identifier that helps you match payments to your database. ## Transfer memo demo **Interactive demo: Transfer with Memo** 1. Connect 2. Add funds 3. Send payment with memo Source: [tempoxyz/examples/tree/main/examples/payments](https://github.com/tempoxyz/examples/tree/main/examples/payments) ## Transfer memo implementation steps ::::steps ### Set up your project for transfer memos Ensure you have Wagmi configured with Tempo: * [Connection details](https://tempo.xyz/developers/docs/quickstart/connection-details) * [TypeScript SDK](https://tempo.xyz/developers/docs/sdk/typescript) * [Wallet integration](https://tempo.xyz/developers/docs/quickstart/wallet-developers) ### Send a transfer with memo Use `transferWithMemo` to attach a reference to your payment. The memo is a 32-byte value that gets emitted in the `TransferWithMemo` event. :::code-group ```tsx twoslash [SendWithMemo.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { parseUnits, toHex } from 'viem' import { useConnection } from 'wagmi' export function SendWithMemo() { const { address } = useConnection() const transfer = Hooks.token.useTransferSync() const handleSend = () => { transfer.mutate({ token: '0x20c0000000000000000000000000000000000001', to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', amount: parseUnits('100', 6), memo: toHex('INV-12345', { size: 32 }), }) } return ( ) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Watch for transfers with memos Listen for `TransferWithMemo` events to reconcile incoming payments. The memo is indexed, so you can filter by specific values. :::code-group ```tsx twoslash [WatchMemos.tsx] // @noErrors import { useWatchContractEvent } from 'wagmi' import { fromHex } from 'viem' import { Abis } from 'viem/tempo' export function WatchMemos({ depositAddress }: { depositAddress: `0x${string}` }) { useWatchContractEvent({ address: '0x20c0000000000000000000000000000000000001', abi: Abis.TIP20, eventName: 'TransferWithMemo', onLogs: (logs) => { for (const log of logs) { if (log.args.to === depositAddress) { const memo = fromHex(log.args.memo, 'string').replace(/\0/g, '') console.log(`Received ${log.args.value} with memo: ${memo}`) } } }, }) return
Watching for deposits...
} ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: :::: ## Transfer memo recipes ### Exchange deposit reconciliation As an exchange, use a single master hot wallet for all customer deposits. Customers include their customer ID as the memo, and you credit their account by parsing the event. ```ts import { Actions } from 'viem/tempo' import { parseUnits, stringToHex, pad } from 'viem' // Customer deposits with their customer ID await Actions.token.transferSync(walletClient, { token: tokenAddress, to: exchangeHotWallet, amount: parseUnits('500', 6), memo: pad(stringToHex('CUST-12345'), { size: 32 }), }) ``` ### Payroll batch payments Start with the [Viem batch payment setup](https://tempo.xyz/developers/docs/guide/payments/send-a-payment#batch-payment-transactions), then encode `transferWithMemo` for each employee ID. ```ts import { Abis } from 'viem/tempo' import { encodeFunctionData, parseUnits, stringToHex, pad } from 'viem' import { client } from './viem.config' const calls = employees.map(emp => ({ to: tokenAddress, data: encodeFunctionData({ abi: Abis.tip20, functionName: 'transferWithMemo', args: [emp.wallet, parseUnits(emp.salary, 6), pad(stringToHex(emp.id), { size: 32 })] }) })) await client.sendTransaction({ calls }) ``` ### Refund address in memo Include a refund address in the memo so the recipient knows where to send funds if a reversal is needed. ```ts import { Actions } from 'viem/tempo' import { parseUnits, stringToHex, pad } from 'viem' const refundMemo = pad(stringToHex('REFUND 0x742d35Cc6634C0532925a3b8'), { size: 32 }) await Actions.token.transferSync(walletClient, { token: tokenAddress, to: merchantAddress, amount: parseUnits('100', 6), memo: refundMemo, }) ``` ## Transfer memo best practices ### Use consistent memo formats Establish a naming convention for your memos (e.g., `CUST-{id}`, `INV-{number}`, `REFUND-{id}`) to make parsing and filtering reliable across your system. ### Keep memos under 32 bytes Memos are `bytes32` values. Use `toHex(string, { size: 32 })` to convert strings—if your string exceeds 32 bytes, it will be truncated. For longer references, store the full data offchain and use a hash or short ID as the memo. ### Index memos for efficient queries The `TransferWithMemo` event has `memo` as an indexed parameter. Use `getLogs` with the `args` filter to query transactions by memo without scanning all events. ```ts import { parseAbiItem, stringToHex, pad } from 'viem' const logs = await client.getLogs({ address: tokenAddress, event: parseAbiItem('event TransferWithMemo(address indexed from, address indexed to, uint256 value, bytes32 indexed memo)'), args: { memo: pad(stringToHex('INV-12345'), { size: 32 }) }, }) ``` ## Transfer memo learning resources * [Send a Payment](https://tempo.xyz/developers/docs/guide/payments/send-a-payment) — Complete guide to sending stablecoin payments with optional memos * [Accept a Payment](https://tempo.xyz/developers/docs/guide/payments/accept-a-payment) — Watch for incoming payments and integrate reconciliation flows * [TIP-20 Specification](https://tempo.xyz/developers/docs/protocol/tip20/spec) — Full API reference for memo methods and events # Use virtual addresses for deposits Virtual addresses let you issue a distinct deposit address for each customer without creating a separate onchain TIP-20 balance for each one. The deposit is attributed to the virtual address, but the balance is credited directly to the registered master wallet. This page is intentionally about the operator flow rather than the spec. In preview environments the demo may run against pre-release infrastructure, but the flow is the same one operators will use on public testnet. ## How virtual address deposits work ```mermaid sequenceDiagram participant Sender participant TIP20 as TIP-20 participant Registry as Virtual registry participant Master as Registered wallet Sender->>TIP20: transfer(virtualAddress, amount) TIP20->>Registry: resolve(masterId) Registry-->>TIP20: master wallet TIP20->>Master: credit balance Note over TIP20: emits Transfer(sender → virtual, amount) Note over TIP20: emits Transfer(virtual → master, amount) ``` The important behavior is: * the sender pays the **virtual address** * TIP-20 resolves that address to the registered **master wallet** * the **master wallet** receives the balance * the virtual address still appears in events, so you can attribute the deposit correctly ## Virtual address live demo This walkthrough shows the full flow: 1. sign in with a passkey and get a Tempo address 2. register a master id for that address 3. send `pathUSD` from a second address to a virtual address derived from that master id 4. confirm that the balance lands in the registered wallet ### Fast demo Use a docs-managed master with a pre-mined valid salt so you can skip the wait and jump straight to the forwarding flow. **Interactive demo: Virtual addresses** 1. Virtual addresses fast demo ### Real registration Use `VirtualMaster.mineSaltAsync` to register a master id for the passkey account you create in the demo. :::info Mining a virtual-address salt can take 30+ seconds depending on your browser, hardware, and available worker parallelism. ::: **Interactive demo: Virtual addresses** 1. Virtual addresses live demo ## What to verify in virtual address deposits When the demo succeeds, you should see all of the following: * the passkey wallet is shown as the registered master wallet * the virtual address is distinct from the master wallet * the sender transfers `pathUSD` to the virtual address * the master wallet balance increases * the virtual address TIP-20 balance remains `0` * the receipt shows the expected two-hop `Transfer` events ## Derive deposit addresses offchain Once a master is registered, operators derive virtual addresses offchain from the `masterId` and their own customer tag. :::code-group ```ts [virtualAddress.ts] import { VirtualAddress } from 'ox/tempo' const virtualAddress = VirtualAddress.from({ masterId, userTag: '0x000000000001', }) ``` ::: In practice, the `userTag` is the operator's internal routing value for a customer, account, or payment reference. ## Operational notes A few things matter in production: * virtual forwarding applies only to **TIP-20** transfer and mint paths * `balanceOf(virtualAddress)` stays `0`; use events and your own `userTag` mapping for attribution * policy checks apply to the **resolved master wallet**, not the literal virtual address * avoid using virtual addresses in reward protocols (lending pools, DEX rewards, etc) unless explicitly supported, as they can't track or hold funds ## Learn more about TIP-20 virtual addresses * [Virtual addresses overview](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses) — Start with the conceptual model for routing, attribution, and treasury operations. * [Virtual address specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) — Read the full protocol definition, including derivation rules, transfer paths, and invariants. * [T3 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t3) — See when virtual addresses activate and what else ships with T3. # Pay Fees in Any Stablecoin Configure users to pay transaction fees in any supported stablecoin. Tempo's flexible fee system allows users to pay fees with the same token they're using, eliminating the need to hold a separate gas token. ## Fee-token payment demo By the end of this guide you will be able to pay fees in any stablecoin on Tempo. **Interactive demo: Pay Fees in Any Stablecoin** 1. Connect 2. Add funds 3. Pay with fee token Source: [tempoxyz/examples/tree/main/examples/payments](https://github.com/tempoxyz/examples/tree/main/examples/payments) ## Quick fee-token snippet Using a custom fee token is as simple as passing a `feeToken` attribute to mutable actions like `useTransferSync`, `useSendTransactionSync`, and more. ### Wagmi ```tsx twoslash // @noErrors import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' const sendPayment = Hooks.token.useTransferSync() sendPayment.mutate({ amount: parseUnits('100', 6), feeToken: betaUsd, // [!code ++] to: '0x0000000000000000000000000000000000000000', token: alphaUsd, }) ``` ### Viem ```tsx twoslash // @noErrors import { parseUnits } from 'viem' import { client } from './viem.config' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' const receipt = await client.token.transferSync({ amount: parseUnits('100', 6), feeToken: betaUsd, // [!code ++] to: '0x0000000000000000000000000000000000000000', token: alphaUsd, }) ``` ### Rust :::code-group ```rust [example.rs] use alloy::{ primitives::{address, U256}, providers::Provider, sol_types::SolCall, }; use tempo_alloy::{ contracts::precompiles::ITIP20, primitives::transaction::Call, rpc::TempoTransactionRequest, }; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); let beta_usd = address!("0x20c0000000000000000000000000000000000002"); let calls = vec![Call { to: alpha_usd.into(), input: ITIP20::transferCall { to: address!("0x0000000000000000000000000000000000000000"), amount: U256::from(100_000_000), } .abi_encode() .into(), value: U256::ZERO, }]; let pending = provider .send_transaction(TempoTransactionRequest { calls, fee_token: Some(beta_usd), // [!code ++] ..Default::default() }) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: ### Python ```python from pytempo import TempoTransaction from pytempo.contracts import TIP20, ALPHA_USD, BETA_USD tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=100_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), fee_token=BETA_USD, # [!code ++] calls=( TIP20(ALPHA_USD).transfer( to="0x0000000000000000000000000000000000000000", amount=100_000_000, ), ), ) ``` ### Go ```go alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") betaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000002") tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). SetNonce(nonce). SetGas(100_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetFeeToken(betaUSD). // [!code ++] AddCall(alphaUSD, big.NewInt(0), buildTransferData(recipient, big.NewInt(100_000_000))). Build() ``` ### Cast ```bash $ cast send \ 0x20c0000000000000000000000000000000000001 \ "transfer(address,uint256)" \ 0x0000000000000000000000000000000000000000 \ 100000000 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.fee-token 0x20c0000000000000000000000000000000000002 # [!code ++] ``` ### Solidity :::info The fee token for a given transaction cannot be set from Solidity — it is a transaction-level parameter handled by the signing SDK. However, you can configure a **default** fee token for an account using [`setUserToken`](#set-a-default-user-fee-token), which will apply to all future transactions unless explicitly overridden at submission. ::: ## Fee-token implementation steps ### Wagmi ::::steps ### Set up Wagmi for fee tokens Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: * [Connection details](https://tempo.xyz/developers/docs/quickstart/connection-details) * [TypeScript SDK](https://tempo.xyz/developers/docs/sdk/typescript) * [Wallet integration](https://tempo.xyz/developers/docs/quickstart/wallet-developers) ### Add testnet fee-token funds¹ Before you can pay fees in a token of your choice, you need to fund your account. In this guide you will be sending `AlphaUSD` (`0x20c000…0001`) and paying fees in `BetaUSD` (`0x20c000…0002`). The built-in Tempo testnet faucet includes `AlphaUSD` and `BetaUSD` when funding. **Interactive demo: Add Funds** 1. Add funds :::code-group ```tsx twoslash [AddFunds.ts] // @noErrors import { Hooks } from 'wagmi/tempo' import { useConnection } from 'wagmi' function AddFunds() { const { address } = useConnection() const { mutate, isPending } = Hooks.faucet.useFundSync() return ( ) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: :::warning ¹ It is important to note that the `addFunds` Hook only works on testnets as a convenience feature to get started quickly. For production, you will need to onramp & fund your account manually. ::: ### Add custom fee-token logic Now that you have `AlphaUSD` to send and `BetaUSD` to pay fees with, you can add a form that allows users to select a fee token and send a payment. After this step, your users can send payments with a specified fee token by clicking the "Send Payment" button! **Interactive demo: Pay Fees in Any Stablecoin** 1. Add funds 2. Pay with fee token Source: [tempoxyz/examples/tree/main/examples/payments](https://github.com/tempoxyz/examples/tree/main/examples/payments) :::code-group ```tsx twoslash [PayWithFeeToken.tsx] import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' const thetaUsd = '0x20c0000000000000000000000000000000000003' const pathUsd = '0x20c0000000000000000000000000000000000000' // @noErrors function PayWithFeeToken() { const sendPayment = Hooks.token.useTransferSync() const metadata = Hooks.token.useGetMetadata({ token: alphaUsd, }) return (
{ event.preventDefault() const formData = new FormData(event.target as HTMLFormElement) const recipient = (formData.get('recipient') || '0x0000000000000000000000000000000000000000') as `0x${string}` const feeToken = (formData.get('feeToken') || alphaUsd) as `0x${string}` sendPayment.mutate({ // [!code hl] amount: parseUnits('100', metadata.data?.decimals ?? 6), // [!code hl] to: recipient, // [!code hl] token: alphaUsd, // [!code hl] feeToken: feeToken, // [!code hl] }) // [!code hl] } }>
) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Display the fee-token receipt Now that users can send payments with a specified fee token, you can link to the transaction receipt. :::code-group ```tsx twoslash [PayWithFeeToken.tsx] import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' const thetaUsd = '0x20c0000000000000000000000000000000000003' const pathUsd = '0x20c0000000000000000000000000000000000000' // @noErrors function PayWithFeeToken() { const sendPayment = Hooks.token.useTransferSync() const metadata = Hooks.token.useGetMetadata({ token: alphaUsd, }) return ( <> {/* ... your payment form ... */} {sendPayment.data && ( // [!code ++] {/* [!code ++] */} View receipt {/* [!code ++] */} {/* [!code ++] */} )} {/* [!code ++] */} ) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Next fee-token steps Now that you have made a payment using a desired fee token, you can: * Follow a guide on how to [sponsor user fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) to enable gasless transactions * Learn more about [transaction fees](https://tempo.xyz/developers/docs/protocol/fees) :::: ### Viem ::::steps ### Set up a Viem client for fee tokens First, we will set up a Viem client configured with Tempo. ```ts twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` :::info For simplicity of the guide, this example uses a Private Key (Secp256k1) account instead of Passkeys (WebAuthn). ::: ### Add testnet fee-token funds¹ Before you can pay fees in a token of your choice, you need to fund your account. In this guide you will be sending `AlphaUSD` (`0x20c000…0001`) and paying fees in `BetaUSD` (`0x20c000…0002`). The built-in Tempo testnet faucet includes `AlphaUSD` and `BetaUSD` when funding. **Interactive demo: Add Funds** 1. Add funds :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' await client.faucet.fundSync({ account: client.account, }) ``` ```tsx twoslash [viem.config.ts] filename="viem.config.ts" // @noErrors import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: ### Add custom fee-token logic Now that you have `AlphaUSD` to send and `BetaUSD` to pay fees with, you can now add logic to send a payment with a specified fee token. **Interactive demo: Pay Fees in Any Stablecoin** 1. Add funds 2. Pay with fee token Source: [tempoxyz/examples/tree/main/examples/payments](https://github.com/tempoxyz/examples/tree/main/examples/payments) :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const receipt = await client.token.transferSync({ amount: parseUnits('100', 6), feeToken: betaUsd, // [!code hl] to: '0x0000000000000000000000000000000000000000', token: alphaUsd, }) ``` ```tsx twoslash [viem.config.ts] filename="viem.config.ts" // @noErrors import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: :::: ### Rust :::info For Rust integration, refer to the [Quick Snippet](#quick-fee-token-snippet) above and the [Set user fee token](#set-a-default-user-fee-token) below. ::: ### Python :::info For Python integration, refer to the [Quick Snippet](#quick-fee-token-snippet) above and the [Set user fee token](#set-a-default-user-fee-token) below. ::: ### Go :::info For Go integration, refer to the [Quick Snippet](#quick-fee-token-snippet) above and the [Set user fee token](#set-a-default-user-fee-token) below. ::: ### Cast :::info For Cast integration, refer to the [Quick Snippet](#quick-fee-token-snippet) above and the [Set user fee token](#set-a-default-user-fee-token) below. ::: ### Solidity :::info For Solidity integration, refer to the [Quick Snippet](#quick-fee-token-snippet) above and the [Set user fee token](#set-a-default-user-fee-token) below. ::: ## Set a default user fee token You can also set a persistent default fee token for an account, so users don't need to specify `feeToken` on every transaction. Learn more about fee token preferences [here](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#fee-token-preferences). ### Wagmi ```tsx twoslash // @noErrors // @errors: 2307 import { Hooks } from 'wagmi/tempo' const { data: result, mutate } = Hooks.fee.useSetUserTokenSync() // Call `mutate` in response to user action (e.g. button click, form submission) mutate({ token: '0x20c0000000000000000000000000000000000001', }) console.log('Transaction hash:', result.receipt.transactionHash) // @log: Transaction hash: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ``` ### Viem ```ts twoslash // @noErrors // @errors: 2307 import { client } from './viem.config' const { receipt } = await client.fee.setUserTokenSync({ token: '0x20c0000000000000000000000000000000000001', }) console.log('Transaction hash:', receipt.transactionHash) // @log: Transaction hash: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ``` ### Rust :::code-group ```rust [example.rs] use alloy::primitives::address; use tempo_alloy::contracts::precompiles::IFeeManager; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let fee_manager = IFeeManager::new( address!("0xFEEc000000000000000000000000000000000000"), &provider, ); let receipt = fee_manager .setUserToken( // [!code hl] address!("0x20c0000000000000000000000000000000000001"), // [!code hl] ) // [!code hl] .send() .await? .get_receipt() .await?; println!("Transaction hash: {:?}", receipt.transaction_hash); Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: ### Python :::code-group ```python [example.py] from pytempo import TempoTransaction from pytempo.contracts import FeeManager, ALPHA_USD from provider import w3, account tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=100_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=( FeeManager.set_user_token(ALPHA_USD), # [!code hl] ), ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) account = Account.from_key("0x...") ``` ::: ### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) feeManager := common.HexToAddress("0xFEEc000000000000000000000000000000000000") token := common.HexToAddress("0x20c0000000000000000000000000000000000001") // setUserToken(address) selector: 0xe7897444 data := make([]byte, 36) // [!code hl] data[0], data[1], data[2], data[3] = 0xe7, 0x89, 0x74, 0x44 // [!code hl] copy(data[16:36], token.Bytes()) // [!code hl] tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). SetNonce(nonce). SetGas(100_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall(feeManager, big.NewInt(0), data). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: ### Cast ```bash $ cast send \ 0xFEEc000000000000000000000000000000000000 \ "setUserToken(address)" \ 0x20c0000000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # [!code hl] ``` ### Solidity ```solidity import {StdPrecompiles} from "tempo-std/StdPrecompiles.sol"; StdPrecompiles.TIP_FEE_MANAGER.setUserToken(0x20c0000000000000000000000000000000000001); // [!code hl] ``` ## Fee-token learning resources * [Transaction Fees](https://tempo.xyz/developers/docs/protocol/fees) — Learn more about transaction fees on Tempo # Sponsor user fees with `feePayer` Enable gasless transactions by sponsoring transaction fees for your users. Tempo's native fee sponsorship allows applications to pay fees on behalf of users, improving UX and removing friction from payment flows. ## Fee sponsorship demo **Interactive demo: Sponsor User Fees** 1. Connect 2. Add funds 3. Send relayer sponsored payment ## Fee sponsorship implementation steps ::::steps ### Set up the fee payer service #### Hosted See the [Fee Payer API guide](https://tempo.xyz/developers/docs/api/fee-payer) to set up authenticated production (and sandbox) sponsorship. #### Self-hosted Use the [Relay & Fee Payer Handler](https://tempo.xyz/developers/docs/server/relay-handler) to run a fee payer service with your own sponsorship policy. :::info For testing on Tempo testnet, use the public fee payer endpoint at `https://sponsor.moderato.tempo.xyz` without an API key. ::: ### Configure your client to use the fee payer service :::code-group ```ts twoslash [Tempo Wallet] // @noErrors import { tempoModerato } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet({ feePayer: 'https://sponsor.moderato.tempo.xyz', // [!code focus] })], chains: [tempoModerato], multiInjectedProviderDiscovery: false, transports: { [tempoModerato.id]: http(), }, }) ``` ```ts twoslash [WebAuthn + Other] // @noErrors import { tempoModerato } from 'viem/chains' import { withRelay } from 'viem/tempo' import { createConfig, http } from 'wagmi' import { webAuthn } from 'wagmi/tempo' export const config = createConfig({ connectors: [webAuthn({ authUrl: '/auth' })], chains: [tempoModerato], multiInjectedProviderDiscovery: false, transports: { [tempoModerato.id]: withRelay( // [!code focus] http(), // [!code focus] http('https://sponsor.moderato.tempo.xyz'), // [!code focus] ), // [!code focus] }, }) ``` ::: ### Sponsor your user's transactions Set `feePayer: true` to request sponsorship from the configured fee payer service. For more details on how to send a transaction, see the [Send a payment](https://tempo.xyz/developers/docs/guide/payments/send-a-payment) guide. :::code-group ```tsx twoslash [SendSponsoredPayment.tsx] filename="SendSponsoredPayment.tsx" import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' // @noErrors function SendSponsoredPayment() { const sendPayment = Hooks.token.useTransferSync() // [!code hl] const metadata = Hooks.token.useGetMetadata({ token: alphaUsd, }) return (
{ event.preventDefault() const formData = new FormData(event.target as HTMLFormElement) const recipient = (formData.get('recipient') || '0x0000000000000000000000000000000000000000') as `0x${string}` sendPayment.mutate({ // [!code hl] amount: parseUnits('100', metadata.data.decimals), // [!code hl] feePayer: true, // [!code focus] to: recipient, // [!code hl] token: alphaUsd, // [!code hl] }) // [!code hl] } }>
) } ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempoModerato } from 'viem/chains' import { withRelay } from 'viem/tempo' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [ tempoWallet({ feePayer: { precedence: 'user-first', url: 'https://sponsor.moderato.tempo.xyz', }, }), ], chains: [tempoModerato], multiInjectedProviderDiscovery: false, transports: { [tempoModerato.id]: http(), }, }) ``` ::: ### Next steps for fee sponsorship Now that you've implemented fee sponsorship, you can: * Learn more about the [Tempo Transaction](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#fee-payer-signature-details) type and fee payer signature details * Use Tempo Transactions to sponsor multiple calls in a single signed operation * Learn how to [Pay Fees in Any Stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) :::: ## Fee sponsorship best practices 1. **Set sponsorship limits**: Implement daily or per-user limits to control costs 2. **Monitor expenses**: Track sponsorship costs regularly to stay within budget 3. **Consider selective sponsorship**: Only sponsor fees for specific operations or user segments 4. **Educate users**: Clearly communicate when fees are being sponsored ### Fee sponsorship security considerations * **Transaction-specific**: Fee payer signatures are tied to specific transactions * **No delegation risk**: Fee payer can't execute arbitrary transactions * **Balance checks**: Network verifies fee payer has sufficient balance * **Signature validation**: Both signatures must be valid ## Fee sponsorship learning resources * [Fee Specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee) — Learn more about fees and how they work on Tempo * [TempoTransaction Spec](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction) — Technical specification for TempoTransactions and fee payer signature details * [Pay Fees in Any Stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) — Understand fee token selection and how to pay with different stablecoins # Send parallel transactions using expiring nonces Tempo enables concurrent transaction execution through its [expiring nonce](https://tempo.xyz/developers/docs/guide/tempo-transaction#expiring-nonces) system. Unlike traditional sequential nonces that require transactions to be processed one at a time, expiring nonces allow multiple transactions to be submitted simultaneously without nonce conflicts. Each transaction uses an independent nonce that automatically expires after a set time window, enabling true parallel execution. ## Parallel transaction demo By the end of this guide you will understand how to send parallel payments using expiring nonces under-the-hood. **Interactive demo: Send Parallel Payments** 1. Connect 2. Add funds 3. Send parallel payments ## Parallel transaction implementation steps ::::steps ### Set up Wagmi for parallel transactions Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: * [Connection details](https://tempo.xyz/developers/docs/quickstart/connection-details) * [TypeScript SDK](https://tempo.xyz/developers/docs/sdk/typescript) * [Wallet integration](https://tempo.xyz/developers/docs/quickstart/wallet-developers) ### Send concurrent transactions with nonce keys To send multiple transactions in parallel, simply batch them together. [Expiring nonces](https://tempo.xyz/developers/docs/guide/tempo-transaction#expiring-nonces) are attached to each transaction automatically. :::code-group ```ts twoslash [example.ts] // @noErrors import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const { mutate: transfer } = Hooks.token.useTransferSync() // Send both transfers in parallel. // [!code focus] const [receipt1, receipt2] = await Promise.all([ // [!code focus] transfer.mutate({ // [!code focus] amount: parseUnits('100', 6), // [!code focus] to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // [!code focus] token: alphaUsd, // [!code focus] }), // [!code focus] transfer.mutate({ // [!code focus] amount: parseUnits('50', 6), // [!code focus] to: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', // [!code focus] token: alphaUsd, // [!code focus] }), // [!code focus] ]) // [!code focus] console.log('Transaction 1:', receipt1.transactionHash) // [!code focus] console.log('Transaction 2:', receipt2.transactionHash) // [!code focus] ``` ```tsx twoslash [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: :::: ## Parallel transaction learning resources * [Expiring Nonces](https://tempo.xyz/developers/docs/guide/tempo-transaction#expiring-nonces) — Learn more about expiring nonces that power concurrent transactions. * [Transactions](https://tempo.xyz/developers/docs/protocol/transactions) — Learn more about Tempo Transactions and their properties. # How to create a TIP-20 stablecoin Create your own stablecoin on Tempo using [TIP-20 Tokens](https://tempo.xyz/developers/docs/protocol/tip20/overview). TIP-20 tokens are designed specifically for payments with built-in compliance features, role-based permissions, and integration with Tempo's payment infrastructure. ## Stablecoin creation demo By the end of this guide, you will be able to create a stablecoin on Tempo. **Interactive demo: Create a Stablecoin** 1. Connect 2. Add funds 3. Create token Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) ## Stablecoin creation steps ::::steps ### Set up Wagmi for stablecoin creation Ensure that you have set up your project with Wagmi, a Tempo chain config, and a wallet connector: * [Connection details](https://tempo.xyz/developers/docs/quickstart/connection-details) * [TypeScript SDK](https://tempo.xyz/developers/docs/sdk/typescript) * [Wallet integration](https://tempo.xyz/developers/docs/quickstart/wallet-developers) ### Add testnet funds for deployment¹ Before we send off a transaction to deploy our stablecoin to the Tempo testnet, we need to make sure our account is funded with a stablecoin to cover the transaction fee. As we have configured our project to use `AlphaUSD` (`0x20c000…0001`) as the [default fee token](https://tempo.xyz/developers/docs/quickstart/evm-compatibility#consideration-1-setting-a-user-default-fee-token), we will need to add some `AlphaUSD` to our account. Luckily, the built-in Tempo testnet faucet supports funding accounts with `AlphaUSD`. **Interactive demo: Add Funds** 1. Add funds :::code-group ```tsx twoslash [AddFunds.ts] // @noErrors import { Hooks } from 'wagmi/tempo' import { useConnection } from 'wagmi' export function AddFunds() { const { address } = useConnection() const addFunds = Hooks.faucet.useFund() return ( ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: :::warning ¹ It is important to note that the `addFunds` Hook only works on testnets as a convenience feature to get started quickly. For production, you will need to onramp & fund your account manually. ::: ### Add stablecoin form fields Now that we have some funds to cover the transaction fee in our account, we can create a stablecoin. Let's create a new component and add some input fields for the **name** and **symbol** of our stablecoin, as shown in the demo. **Interactive demo: Create Form** 1. Add funds 2. Create token :::code-group ```tsx twoslash [CreateStablecoin.tsx] // @noErrors export function CreateStablecoin() { return (
{ event.preventDefault() const formData = new FormData(event.target as HTMLFormElement) const name = formData.get('name') as string const symbol = formData.get('symbol') as string }} >
) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Add stablecoin deployment logic Now that we have some input fields, we need to add some logic to handle the submission of the form to create the stablecoin. After this step, your users will be able to create a stablecoin by clicking the "Create" button! Tokens can also carry an optional on-chain [`logoURI`](https://tempo.xyz/developers/docs/protocol/tip20/spec#logo-uri) that wallets and explorers read directly from the token contract. It's set on the token contract and is independent of this creation flow; for the recommended format, use a square, rasterized PNG or WebP (max 256 bytes; `https`, `http`, `ipfs`, or `data` scheme). :::warning The `currency` field is **immutable** after token creation and affects fee payment eligibility, DEX routing, and quote token pairing. See [Currency Declaration](https://tempo.xyz/developers/docs/protocol/tip20/overview#currency-declaration) for guidelines on choosing the right value. **Only `USD` stablecoins can be used to pay transaction fees on Tempo.** ::: **Interactive demo: Create Form** 1. Create token :::code-group ```tsx twoslash [CreateStablecoin.tsx] import { Hooks } from 'wagmi/tempo' // [!code ++] // @noErrors export function CreateStablecoin() { const create = Hooks.token.useCreateSync() // [!code ++] return (
{ event.preventDefault() const formData = new FormData(event.target as HTMLFormElement) const name = formData.get('name') as string const symbol = formData.get('symbol') as string create.mutate({ // [!code ++] name, // [!code ++] symbol, // [!code ++] currency: 'USD', // [!code ++] }) // [!code ++] }} >
) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Add stablecoin creation success state Now that users can submit the form and create a stablecoin, let's add a basic success state to display the name of the stablecoin and a link to the transaction receipt. :::code-group ```tsx twoslash [CreateStablecoin.tsx] import { Hooks } from 'wagmi/tempo' // @noErrors export function CreateStablecoin() { const create = Hooks.token.useCreateSync() return (
{ event.preventDefault() const formData = new FormData(event.target as HTMLFormElement) const name = formData.get('name') as string const symbol = formData.get('symbol') as string create.mutate({ name, symbol, currency: 'USD', }) }} >
{create.data && ( // [!code ++]
{/* [!code ++] */} {create.data.name} created successfully! {/* [!code ++] */} {/* [!code ++] */} View receipt {/* [!code ++] */} {/* [!code ++] */}
{/* [!code ++] */} )} {/* [!code ++] */}
) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Next steps after creating a stablecoin Now that you have created your first stablecoin, you can now: * [Add your token to the Token List](https://tempo.xyz/developers/docs/quickstart/tokenlist#adding-a-new-token) so it appears in wallets, explorers, and other apps on Tempo * learn the [Best Practices](#stablecoin-creation-best-practices) below * follow a guide on how to [mint](https://tempo.xyz/developers/docs/guide/issuance/mint-stablecoins) and [more](https://tempo.xyz/developers/docs/guide/issuance/manage-stablecoin) with your stablecoin. :::: ## Stablecoin creation best practices ### Stablecoin creation loading state When the user is creating a stablecoin, we should show loading state to indicate that the process is happening. We can use the `isPending` property from the `useCreateSync` hook to show pending state to the user on our "Create" button. ```tsx ``` ### Stablecoin creation error handling If an error unexpectedly occurs, we should display an error message to the user. We can use the `error` property from the `useCreateSync` hook to show error state to the user. ```tsx twoslash // @noErrors export function CreateStablecoin() { // ... if (create.error) // [!code ++] return
Error: {create.error.message}
{/* [!code ++] */} // ... } ``` ## Stablecoin creation learning resources * [TIP-20 Tokens](https://tempo.xyz/developers/docs/protocol/tip20/overview) — Learn more about TIP-20 tokens on Tempo # Migrate an ERC-20 to TIP-20 Choose how to bring an OpenZeppelin-based ERC-20 to Tempo, then map its deployment, roles, burn flow, decimals, custom logic, and compliance controls to a native TIP-20 token. Quick choice: * New asset: deploy a native TIP-20. * Existing canonical asset: use a bridge adapter. * Full migration: retire or lock legacy supply, then mint TIP-20. * Custom issuance/redemption: use an external lifecycle manager. Use this guide if your current token uses: * `ERC20` or `ERC20Upgradeable` * `AccessControl` or `AccessControlEnumerable` * `Pausable` * `Burnable` * `ERC20Wrapper` * A proxy pattern such as UUPS or Transparent Proxy * A manager, minter, bridge, or tokenization engine around the token ## 1. Choose your Tempo path | Path | Use when | Tempo pattern | |---|---|---| | Native TIP-20 launch | New issuance on Tempo | Deploy TIP-20 through `TIP20Factory` | | ERC-20 migration | Existing supply should move to Tempo | Burn or retire ERC-20 supply, then mint TIP-20 | | Bridge adapter | ERC-20 remains canonical on another chain | Lock or burn ERC-20, then mint TIP-20 on Tempo through a bridge adapter | | Lifecycle manager | Issuance has subscriptions, redemptions, caps, or oracle pricing | Keep business logic outside TIP-20 and grant the manager `ISSUER_ROLE`; enforce limits in the manager | ## 2. Review the recommended deployment flow ```mermaid sequenceDiagram participant Issuer as Issuer ops participant Admin as TIP-20 admin wallet participant Factory as TIP20Factory participant Token as Native TIP-20 token participant Operator as Issuer wallet / manager / adapter participant PolicyAdmin as Simple-policy admin / sync signer participant Registry as TIP-403 registry participant Vault as Treasury or customer vault opt Compliance required Note over PolicyAdmin,Registry: Prepare policy before token deployment PolicyAdmin->>Registry: Create or select policy end Note over Issuer,Token: One-time token deployment Issuer->>Factory: createToken(name, symbol, currency, quoteToken, admin, salt, logoURI) Factory-->>Token: Create native TIP-20 token Factory->>Token: Grant DEFAULT_ADMIN_ROLE to Admin opt Compliance required Note over Admin,Token: Attach the prepared policy before token operations Admin->>Token: changeTransferPolicyId(policyId) end Note over Admin,Operator: One-time or occasional role setup Admin->>Token: grantRole(ISSUER_ROLE, Operator) Admin->>Token: grantRole(PAUSE_ROLE, pause wallet) Admin->>Token: grantRole(UNPAUSE_ROLE, unpause wallet) opt Compliance required Note over PolicyAdmin,Registry: Ongoing compliance maintenance PolicyAdmin->>Registry: Add, remove, allow, or block wallets end Note over Operator,Vault: Ongoing issuance operations Operator->>Token: mint(Vault, amount) Token->>Token: Check ISSUER_ROLE, pause state, and supply cap Token->>Registry: Authorize Vault as mint recipient Token-->>Vault: TIP-20 balance increases ``` `TIP20Factory.createToken` does not accept a policy ID. If compliance is required, prepare the policy first, deploy the token, and attach the policy before minting or transferring. A new TIP-20 otherwise starts on the always-allow policy. ## 3. Complete the implementation checklist Before production deployment, confirm: * TIP-20 token is deployed through `TIP20Factory`. * Token name, symbol, currency, quote token, and logo are correct. * Admin address is controlled by the issuer or approved operator. * `ISSUER_ROLE` is granted only to the intended issuer wallet, manager, or bridge adapter. * Native role queries use `hasRole(account, role)`, and each role's administrator is confirmed with `getRoleAdmin(role)`. * `PAUSE_ROLE` and `UNPAUSE_ROLE` are assigned. * Supply cap is set deliberately. Every TIP-20 starts at `type(uint128).max`; prefer the lowest practical cap and increase it as issuance grows. * Transfer policy is configured if compliance is required. * If using a compound policy, sender, recipient, and mint-recipient checks are tested independently. * Existing ERC-20 supply is burned, locked, or reconciled if this is a migration or bridge. * Decimal conversion is tested. * Mint and burn flows are tested. * Mint tests cover the supply cap, paused state, and TIP-403 mint-recipient authorization. * Pause tests confirm that transfers, minting, normal burns, and `burnBlocked` are blocked while role and configuration administration remain available. * Transfer and transferFrom are tested. * Custody and treasury wallets are funded and labeled. * Tokenlist and explorer metadata are ready. * The issuer has an operational runbook for role changes, pausing, minting, burning, and reconciliation. ### Review mutable and immutable settings | Setting | Initial value | Can it change? | |---|---|---| | Token address | Deterministically derived from factory caller and salt | No | | Name and symbol | Set at creation | No | | Decimals | `6` | No | | Currency | Set at creation | No | | Supply cap | `type(uint128).max` | Yes, but not below current total supply | | Quote token | Set at creation | Yes, through the staged quote-token update flow | | Transfer policy | Always-allow policy (`1`) | Yes, by the TIP-20 admin | | Logo URI | Set or left empty at creation | Yes, by the TIP-20 admin | | Roles and role administrators | Admin receives `DEFAULT_ADMIN_ROLE` | Yes | | Pause state | Unpaused | Yes, through `PAUSE_ROLE` and `UNPAUSE_ROLE` | ### Choose the currency by unit-price behavior Set `currency` to the asset or unit of account that **one token unit is designed to remain approximately 1:1 with**. The deciding question is how the price of one token unit behaves, not whether the token generates yield. | Token behavior | Example | `currency` | |---|---|---| | USD stablecoin that remains near $1 | USDC | `USD` | | Rebasing USD yield token whose balance grows while each unit remains near $1 | Rebasing USD yield token | `USD` | | Wrapped token that remains 1:1 with BTC | WBTC or cbBTC | `BTC` | | Accumulating token whose balance stays fixed while its redemption value rises | PRIME or cbETH | Its own asset identifier, such as `PRIME` or `cbETH` | | Non-rebasing wrapper around a rebasing asset | Wrapped yield token | Its own asset identifier, unless one wrapper unit remains near 1:1 with the underlying denomination | Do not set `currency = USD` merely because an accumulating token is USD-backed or reports its value in USD. Classifying a TIP-20 as USD makes it eligible for infrastructure designed for near-par USD assets: * The [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) converts between a user's fee token and a validator's preferred fee token at fixed near-par rates. If one token unit is worth more than $1, that conversion would misprice it. * The [Stablecoin DEX](https://tempo.xyz/developers/docs/protocol/exchange) is market-priced, but it is designed for USD-classified pairs and currently limits orders to a ±2% range around parity. An accumulating token can move outside that range as its redemption value grows. Because `currency` is immutable, confirm the token's unit-price behavior before deployment. See [Currency Declaration](https://tempo.xyz/developers/docs/protocol/tip20/overview#currency-declaration) for the full selection rules. ## 4. Inspect the current ERC-20 Before deploying on Tempo, identify the current token architecture. | Item | What to check | |---|---| | Token address | Current ERC-20 or proxy address | | Implementation | Proxy implementation, if upgradeable | | Metadata | `name`, `symbol`, `decimals` | | Supply | `totalSupply`, current holder or treasury vault | | Roles | Admin, minter, burner, pauser, upgrader | | Mint flow | Who can create supply and through what contract | | Burn flow | Who can destroy supply and whether `burnFrom` is used | | Pause flow | Who can pause and unpause | | Compliance | Allowlist, blocklist, sanctions, KYC, transfer hooks | | Bridge | Source adapter, destination adapter, replay protection | | Oracle | NAV, FX, redemption price, or yield calculation | ## 5. Map OpenZeppelin patterns to TIP-20 Map by capability, not by contract name. | OpenZeppelin pattern | Common behavior | Tempo mapping | |---|---|---| | `ERC20` / `ERC20Upgradeable` | Core transfer and allowance surface | Native TIP-20 transfer and allowance surface | | `IERC20Metadata` | `name`, `symbol`, `decimals` | Set name, symbol, and currency at TIP-20 creation | | `AccessControl` | Role-based permissions | TIP-20 native roles | | `Pausable` | Pause state and pause checks | `PAUSE_ROLE` and `UNPAUSE_ROLE` | | `ERC20Burnable` | `burn` and often `burnFrom` | An `ISSUER_ROLE` holder can burn only its own TIP-20 balance; TIP-20 does not expose `burnFrom` | | `ERC20Wrapper` | Deposit underlying ERC-20, mint wrapped ERC-20 | Use an external adapter that mints TIP-20 | | Proxy upgrade pattern | Upgradeable implementation and storage | Not portable to TIP-20 | ## 6. Map roles | Current role or control | Tempo role | |---|---| | `DEFAULT_ADMIN_ROLE` or owner | TIP-20 `DEFAULT_ADMIN_ROLE` for role and configuration administration; grant `ISSUER_ROLE` separately for minting | | `MINTER_ROLE` | `ISSUER_ROLE` | | `BURNER_ROLE` | `ISSUER_ROLE` for normal burns | | `PAUSER_ROLE` | `PAUSE_ROLE` plus `UNPAUSE_ROLE` | | Compliance admin | TIP-403 simple-policy admin | | Bridge operator | Bridge adapter with `ISSUER_ROLE`; enforce limits in the adapter | | Lifecycle manager | Manager contract with `ISSUER_ROLE`; enforce limits in the manager | Important: `Pausable` does not define who can pause. That authority usually comes from `AccessControl`, `Ownable`, or a custom manager. On TIP-20, pause and unpause are separate permissions. `ISSUER_ROLE` is not natively scoped by amount, destination, or operation. If an issuer needs those limits, its wallet, manager, or adapter must enforce them before calling TIP-20. ## 7. Handle decimals OpenZeppelin ERC-20 tokens commonly use 18 decimals unless overridden. TIP-20 uses 6 decimals. Before migration or integration: * Define the conversion rule. * Decide how to handle dust. * Update bridge and migration scripts. * Update reporting and accounting. * Confirm wallet and explorer display. * Test mint, burn, transfer, and reconciliation in smallest units. Example: ```text 1.000000 TIP-20 unit = 1 token 1 TIP-20 token = 1,000,000 base units ``` ## 8. Move custom logic outside the token TIP-20 is native. Do not port custom ERC-20 methods into the token. | Current custom logic | Tempo location | |---|---| | Transfer hooks | TIP-403 policy, maintained by an issuer compliance sync service | | Allowlist or blocklist | TIP-403 simple or compound policy | | Sanctions checks | TIP-403 policy, maintained by an issuer compliance sync service | | Subscription flow | Lifecycle manager | | Redemption flow | Lifecycle manager | | Fee logic | Lifecycle manager | | Rate limits | Lifecycle manager | | Oracle pricing | External oracle or manager-facing oracle | | Bridge accounting | Bridge adapter | ## 9. Map compliance hooks to TIP-403 Many issuer ERC-20s enforce compliance inside token transfer hooks such as `_beforeTokenTransfer` or `_update`. Common checks include: * allowlisted sender * allowlisted recipient * blocked sender or recipient * sanctions screening * KYC or identity registry membership * transfer-agent or operator eligibility On Tempo, keep the token native and move transfer eligibility into TIP-403 policy. A TIP-20 token points to one `transferPolicyId`. Do not model compliance as attaching both an allowlist policy and a blocklist policy directly to the token. Use a simple whitelist or blacklist when the same rule applies to transfer senders, transfer recipients, and mint recipients. Use a compound policy when those three rules need to differ. ### Simple vs compound policies | Policy type | Use when | |---|---| | Simple whitelist | The same allowlist applies to transfer-sender, transfer-recipient, and mint-recipient checks | | Simple blacklist | The same blocklist applies to transfer-sender, transfer-recipient, and mint-recipient checks | | Compound policy | Sender, recipient, and mint-recipient rules need to differ | For simple whitelist or blacklist policies, the flow is shorter: create the simple policy, update its wallet entries, and set the token's `transferPolicyId` to that simple policy ID. Use a compound policy only when sender, recipient, and mint-recipient rules need to differ. A compound policy references three simple policies: | Compound policy component | Used for | |---|---| | `senderPolicyId` | Transfer senders and blocked-account burn eligibility | | `recipientPolicyId` | Transfer recipients | | `mintRecipientPolicyId` | Mint recipients | For compound policies, the TIP-20 admin attaches the compound policy initially: ```text TIP20.changeTransferPolicyId(compoundPolicyId) ``` The TIP-20 admin can later change `transferPolicyId` to another policy. Day to day, the issuer updates the underlying simple policies as wallets become allowed, blocked, or ineligible. ### Separate policy and token responsibilities Each TIP-403 simple policy has one admin address. A compliance sync service must operate through that address; the service is not a parallel native authority. | Responsibility | Owner | |---|---| | Select which policy the token uses | TIP-20 admin | | Maintain allowlist, blocklist, KYC, or sanctions entries | Simple-policy admin address, which may be operated by a compliance sync service | | Mint and burn normal supply | Issuer wallet, lifecycle manager, or bridge adapter with `ISSUER_ROLE` | | Pause or unpause token operations | `PAUSE_ROLE` / `UNPAUSE_ROLE` holders | ### Follow the compliance flow ```mermaid sequenceDiagram participant Compliance as Issuer compliance system participant Updater as Simple-policy admin / sync signer participant Registry as TIP-403 registry participant Admin as TIP-20 admin participant Token as Native TIP-20 token participant Issuer as Issuer / lifecycle manager participant Sender participant Recipient Note over Updater,Registry: One-time policy setup Updater->>Registry: Create sender policy Updater->>Registry: Create recipient policy Updater->>Registry: Create mint recipient policy Updater->>Registry: Create compound policy(senderPolicyId, recipientPolicyId, mintRecipientPolicyId) Note over Admin,Token: One-time policy attachment Admin->>Token: changeTransferPolicyId(compoundPolicyId) Note over Compliance,Registry: Ongoing compliance maintenance Compliance->>Updater: Wallet eligibility changes Updater->>Registry: Update simple policy entries Note over Sender,Recipient: Ongoing transfer checks Sender->>Token: transfer(Recipient, amount) Token->>Registry: Check senderPolicyId for Sender Token->>Registry: Check recipientPolicyId for Recipient Registry-->>Token: allow or reject Token-->>Recipient: Balance increases if allowed Note over Issuer,Recipient: Ongoing mint checks Issuer->>Token: mint(Recipient, amount) Token->>Token: Check ISSUER_ROLE, not paused, and totalSupply + amount <= supplyCap Token->>Registry: Check mintRecipientPolicyId for Recipient Registry-->>Token: allow or reject ``` Minting succeeds only when the caller holds `ISSUER_ROLE`, the token is not paused, the resulting supply does not exceed the supply cap, and TIP-403 authorizes the mint recipient. The recipient can also configure an account-level receive policy, which the issuer does not control. If that policy blocks a transfer or mint, the call succeeds but delivery is redirected to `ReceivePolicyGuard` as a claimable receipt. Issuers should monitor this outcome when reconciling mints and transfers. See [Receive policies](https://tempo.xyz/developers/docs/protocol/tip403/receive-policies). ### Map ERC-20 compliance patterns | Current ERC-20 compliance pattern | Tempo mapping | |---|---| | `_beforeTokenTransfer` allowlist check | Simple whitelist or compound policy component | | `_update` allowlist check | Simple whitelist or compound policy component | | Blocklist check | Simple blacklist or compound policy component | | Sanctions-list contract | Simple blacklist maintained by issuer compliance sync service | | Identity or KYC registry | Simple whitelist maintained by issuer compliance sync service | | Different sender and recipient rules | Compound policy | | Mint recipient eligibility | Compound policy `mintRecipientPolicyId` | | Operator or spender compliance | Lifecycle manager or bridge adapter check | | Compliance admin | TIP-403 simple-policy admin | ## 10. Avoid incompatible ERC-20 assumptions * Do not deploy a custom ERC-20 when the target is native TIP-20. * Do not assume proxy storage or upgrade logic carries over. * Do not grant issuer permissions to broad operational wallets without a reason. * Use TIP-20's native role-query methods; do not assume OpenZeppelin argument ordering. * Do not ignore 18-to-6 decimal conversion. * Do not put compliance or subscription logic inside the token. ## 11. Consult the operational reference ### Query and administer native roles TIP-20 exposes native methods for querying and administering roles: ```solidity hasRole(address account, bytes32 role) getRoleAdmin(bytes32 role) setRoleAdmin(bytes32 role, bytes32 adminRole) ``` Unlike OpenZeppelin's `hasRole(role, account)`, TIP-20's `hasRole` takes the account first: `hasRole(account, role)`. `DEFAULT_ADMIN_ROLE` is `bytes32(0)`. Multiple accounts may hold it. It governs role and token administration, but it does not itself permit minting; an account or contract must hold `ISSUER_ROLE` to mint. Initially, every role is governed by `DEFAULT_ADMIN_ROLE`. `setRoleAdmin(role, adminRole)` can later change which role is authorized to grant and revoke that role. Use `getRoleAdmin(role)` to inspect the current relationship before changing role assignments. ### Apply pause semantics Since the T3 upgrade, a paused TIP-20 blocks: * transfers * minting * normal burns * `burnBlocked` Administration remains available while paused. `transferFeePreTx` and `transferFeePostTx` are system-only entrypoints used by Tempo protocol precompiles, not issuer-callable operations. As a protocol invariant, `transferFeePreTx` respects pause while `transferFeePostTx` remains available so fee settlement can complete. ## Related references * [OpenZeppelin ERC-20 contracts](https://docs.openzeppelin.com/contracts/5.x/api/token/erc20) * [OpenZeppelin access control](https://docs.openzeppelin.com/contracts/5.x/access-control) * [TIP-20 tokens specification](https://tempo.xyz/developers/docs/protocol/tip20/spec) # How to mint TIP-20 stablecoins Create new tokens by minting them to a specified address. Minting increases the total supply of your stablecoin. ## Stablecoin minting steps ::::steps ### Create the stablecoin to mint Before you can mint tokens, you need to create a stablecoin. Follow the [Create a Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/create-a-stablecoin) guide to deploy your token. Once you've created your token, you can proceed to grant the issuer role and mint tokens. ### Grant the stablecoin issuer role Assign the issuer role to the address that will mint tokens. Minting requires the **`ISSUER_ROLE`**. **Interactive demo: Grant Issuer Role** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [GrantIssuerRole.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { useQueryClient } from '@tanstack/react-query' // @noErrors export function GrantIssuerRole() { const queryClient = useQueryClient() const tokenAddress = '0x...' // Your token address const issuerAddress = '0x...' // Address to grant the issuer role const grant = Hooks.token.useGrantRolesSync({ // [!code hl] mutation: { // [!code hl] onSettled() { // [!code hl] queryClient.refetchQueries({ queryKey: ['hasRole'] }) // [!code hl] }, // [!code hl] }, // [!code hl] }) // [!code hl] const handleGrantIssuer = async () => { // [!code hl] await grant.mutate({ // [!code hl] token: tokenAddress, // [!code hl] roles: ['issuer'], // [!code hl] to: issuerAddress, // [!code hl] feeToken: '0x20c0000000000000000000000000000000000001', // [!code hl] }) // [!code hl] } // [!code hl] return ( {/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Mint stablecoins to a recipient Now that the issuer role is granted, you can mint tokens to any address. **Interactive demo: Mint Tokens** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles 5. Mint token Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [MintToken.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { useConnection } from 'wagmi' import { parseUnits, pad, stringToHex } from 'viem' import { useQueryClient } from '@tanstack/react-query' // @noErrors export function MintToken() { const { address } = useConnection() const queryClient = useQueryClient() const tokenAddress = '0x...' // Your token address const [recipient, setRecipient] = React.useState('') const [memo, setMemo] = React.useState('') const { data: metadata } = Hooks.token.useGetMetadata({ // [!code hl] token: tokenAddress, // [!code hl] }) // [!code hl] const mint = Hooks.token.useMintSync({ // [!code hl] mutation: { // [!code hl] onSettled() { // [!code hl] queryClient.refetchQueries({ queryKey: ['getBalance'] }) // [!code hl] }, // [!code hl] }, // [!code hl] }) // [!code hl] const handleMint = () => { // [!code hl] if (!tokenAddress || !recipient || !metadata) return // [!code hl] mint.mutate({ // [!code hl] amount: parseUnits('100', metadata.decimals), // [!code hl] to: recipient as `0x${string}`, // [!code hl] token: tokenAddress, // [!code hl] memo: memo ? pad(stringToHex(memo), { size: 32 }) : undefined, // [!code hl] feeToken: '0x20c0000000000000000000000000000000000001', // [!code hl] }) // [!code hl] } // [!code hl] return ( <>
setRecipient(e.target.value)} placeholder="0x..." />
setMemo(e.target.value)} placeholder="INV-12345" />
{/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ```solidity [Solidity] TIP20 token = TIP20(0x20c0000000000000000000000000000000000004); // Mint 1,000 tokens to the treasury (USD has 6 decimals) address treasuryAddress = 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef; token.mint(treasuryAddress, 1_000_000_000); // Mint with a memo for tracking token.mintWithMemo( treasuryAddress, 1_000_000_000, keccak256("Q1_2024_TREASURY_ALLOCATION") ); ``` ```rust [Rust] use alloy::{ primitives::{address, keccak256, U256}, providers::ProviderBuilder, }; use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) .await?; let token = ITIP20::new( // [!code focus] address!("0x20c0000000000000000000000000000000000004"), // [!code focus] &provider, // [!code focus] ); // [!code focus] let treasury_address = address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); // [!code focus] // Mint 1,000 tokens to the treasury (USD has 6 decimals) token // [!code focus] .mint(treasury_address, U256::from(1_000_000_000)) // [!code focus] .send() // [!code focus] .await? // [!code focus] .get_receipt() // [!code focus] .await?; // [!code focus] // Mint with a memo for tracking token // [!code focus] .mintWithMemo( // [!code focus] treasury_address, // [!code focus] U256::from(1_000_000_000), // [!code focus] keccak256("Q1_2024_TREASURY_ALLOCATION"), // [!code focus] ) // [!code focus] .send() // [!code focus] .await? // [!code focus] .get_receipt() // [!code focus] .await?; // [!code focus] println!("Tokens minted successfully"); // [!code focus] Ok(()) } ``` ::: :::: ## Stablecoin minting recipes ### Burn stablecoins To decrease supply, you can burn tokens from your own balance. Burning requires the **`ISSUER_ROLE`** and sufficient balance in the caller's account. **Interactive demo: Burn Your Token** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles 5. Mint token 6. Burn token Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [BurnToken.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { useConnection } from 'wagmi' import { parseUnits, pad, stringToHex } from 'viem' import { useQueryClient } from '@tanstack/react-query' // @noErrors export function BurnToken() { const { address } = useConnection() const queryClient = useQueryClient() const tokenAddress = '0x...' // Your token address const [memo, setMemo] = React.useState('') const { data: metadata } = Hooks.token.useGetMetadata({ // [!code hl] token: tokenAddress, // [!code hl] }) // [!code hl] const burn = Hooks.token.useBurnSync({ // [!code hl] mutation: { // [!code hl] onSettled() { // [!code hl] queryClient.refetchQueries({ queryKey: ['getBalance'] }) // [!code hl] }, // [!code hl] }, // [!code hl] }) // [!code hl] const handleBurn = () => { // [!code hl] if (!tokenAddress || !address || !metadata) return // [!code hl] burn.mutate({ // [!code hl] amount: parseUnits('100', metadata.decimals), // [!code hl] token: tokenAddress, // [!code hl] memo: memo ? pad(stringToHex(memo), { size: 32 }) : undefined, // [!code hl] feeToken: '0x20c0000000000000000000000000000000000001', // [!code hl] }) // [!code hl] } // [!code hl] return ( <>
setMemo(e.target.value)} placeholder="INV-12345" />
{/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ```solidity [Solidity] TIP20 token = TIP20(0x20c0000000000000000000000000000000000004); // Burn 100 tokens from your own balance token.burn(100_000_000); // Burn with a memo for tracking token.burnWithMemo(100_000_000, keccak256("REDEMPTION_Q1_2024")); ``` ```rust [Rust] use alloy::{ primitives::{address, keccak256, U256}, providers::ProviderBuilder, }; use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) .await?; let token = ITIP20::new( // [!code focus] address!("0x20c0000000000000000000000000000000000004"), // [!code focus] &provider, // [!code focus] ); // [!code focus] // Burn 100 tokens from your own balance token // [!code focus] .burn(U256::from(100_000_000)) // [!code focus] .send() // [!code focus] .await? // [!code focus] .get_receipt() // [!code focus] .await?; // [!code focus] // Burn with a memo for tracking token // [!code focus] .burnWithMemo( // [!code focus] U256::from(100_000_000), // [!code focus] keccak256("REDEMPTION_Q1_2024"), // [!code focus] ) // [!code focus] .send() // [!code focus] .await? // [!code focus] .get_receipt() // [!code focus] .await?; // [!code focus] println!("Tokens burned successfully"); // [!code focus] Ok(()) } ``` ::: ## Stablecoin minting best practices ### Monitor stablecoin supply caps If your token has a supply cap set, any `mint()` or `mintWithMemo()` call that would exceed the cap will revert with `SupplyCapExceeded()`. You must either: * Burn tokens to reduce total supply below the cap * Increase the supply cap (requires `DEFAULT_ADMIN_ROLE`) * Remove the cap entirely by setting it to `type(uint256).max` Use [`getMetadata`](https://viem.sh/tempo/actions/token.getMetadata) to check your token's total supply before minting. ### Separate stablecoin roles Assign the issuer role to dedicated treasury or minting addresses separate from your admin address. This enhances security by limiting the privileges of any single address. ## Stablecoin minting learning resources * [TIP-20 Tokens](https://tempo.xyz/developers/docs/protocol/tip20/overview) — Learn more about TIP-20 tokens on Tempo * [Manage Your Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/manage-stablecoin) — Learn about role-based access control and token management # Use Your Stablecoin for Fees Enable users to pay transaction fees using your stablecoin. Tempo supports flexible fee payment options, allowing users to pay fees in any stablecoin they hold. ## Fee-token stablecoin demo **Interactive demo: Use Your Stablecoin for Fees** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles 5. Mint token 6. Mint fee AMM liquidity 7. Pay with issued token Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) ## Fee-token stablecoin setup steps ::::steps ### Create your fee-token stablecoin First, create and mint your stablecoin by following the [Create a Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/create-a-stablecoin) guide. ### Add Fee AMM liquidity Before users can pay fees with your token, you need to provide liquidity in the Fee AMM between your token and a liquid quote token, or directly against validator payout tokens where you want explicit direct-route coverage. To determine which validator tokens are needed, sample recent blocks and check the miner's preferred fee token using `getValidatorToken` on the FeeManager contract. For example, on Moderato testnet, validators accept fees in pathUSD and AlphaUSD. On mainnet, this token mix is different and subject to change. Add liquidity to your token's fee pool: :::code-group ```tsx twoslash [TypeScript] // @noErrors import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' import { useConnection } from 'wagmi' const { address } = useConnection() const yourToken = '0x...' // Your issued token address const validatorToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD on testnet const mintFeeLiquidity = Hooks.amm.useMintSync() // [!code hl] // Add 100 AlphaUSD of liquidity to the fee pool // [!code hl] mintFeeLiquidity.mutate({ // [!code hl] feeToken: validatorToken, to: address, userTokenAddress: yourToken, validatorTokenAddress: validatorToken, validatorTokenAmount: parseUnits('100', 6), }) // [!code hl] ``` ```solidity [Solidity] IFeeAMM feeAmm = IFeeAMM(TIP_FEE_AMM_ADDRESS); address yourToken = 0x20c0000000000000000000000000000000000004; // Your issued token address validatorToken = 0x20c0000000000000000000000000000000000001; // AlphaUSD // Add 100 AlphaUSD of liquidity to the fee pool feeAmm.mint( yourToken, validatorToken, 100_000_000, // 100 tokens (6 decimals) address(this) ); ``` ::: You can also check your token's fee pool liquidity at any time: :::code-group ```tsx twoslash [TypeScript] // @noErrors import { Hooks } from 'wagmi/tempo' const { data: pool } = Hooks.amm.usePool({ userToken: yourToken, validatorToken: '0x20c0000000000000000000000000000000000001', // AlphaUSD on testnet }) const hasLiquidity = pool && pool.reserveValidatorToken > 0n ``` ```solidity [Solidity] IFeeAMM feeAmm = IFeeAMM(TIP_FEE_AMM_ADDRESS); address yourToken = 0x20c0000000000000000000000000000000000004; // Your issued token address validatorToken = 0x20c0000000000000000000000000000000000001; // AlphaUSD // Get pool reserves (uint256 reserveValidator, uint256 reserveUser) = feeAmm.getReserves(yourToken, validatorToken); // Check if there's sufficient liquidity require(reserveValidator > 0, "No liquidity available for fee conversion"); ``` ::: If the pool has no liquidity (`reserveValidatorToken == 0`), you'll need to add liquidity to the fee pool before users can pay fees with your token. See the [Create a Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/create-a-stablecoin) guide for instructions on minting fee AMM liquidity. ### Send a payment using your stablecoin as the fee token Your users can send payments using your issued stablecoin as the fee token: :::code-group ```tsx twoslash [PayWithIssuedToken.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { useConnection } from 'wagmi' import { parseUnits, pad, stringToHex, isAddress } from 'viem' // @noErrors export function PayWithIssuedToken() { const { address } = useConnection() const [recipient, setRecipient] = React.useState('') const [memo, setMemo] = React.useState('') const feeToken = '0x...' // Your issued token address const paymentToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD const { data: paymentBalance, refetch: paymentBalanceRefetch } = // [!code hl] Hooks.token.useGetBalance({ // [!code hl] account: address, // [!code hl] token: paymentToken, // [!code hl] }) // [!code hl] const { data: feeTokenBalance, refetch: feeTokenBalanceRefetch } = // [!code hl] Hooks.token.useGetBalance({ // [!code hl] account: address, // [!code hl] token: feeToken, // [!code hl] }) // [!code hl] const sendPayment = Hooks.token.useTransferSync({ // [!code hl] mutation: { // [!code hl] onSettled() { // [!code hl] paymentBalanceRefetch() // [!code hl] feeTokenBalanceRefetch() // [!code hl] }, // [!code hl] }, // [!code hl] }) // [!code hl] const isValidRecipient = recipient && isAddress(recipient) const handleTransfer = () => { // [!code hl] if (!isValidRecipient) return // [!code hl] sendPayment.mutate({ // [!code hl] amount: parseUnits('100', 6), // [!code hl] to: recipient as `0x${string}`, // [!code hl] token: paymentToken, // [!code hl] memo: memo ? pad(stringToHex(memo), { size: 32 }) : undefined, // [!code hl] feeToken, // Pay fees with your issued token // [!code hl] }) // [!code hl] } // [!code hl] return ( <>
setRecipient(e.target.value)} placeholder="0x..." />
setMemo(e.target.value)} placeholder="INV-12345" />
{/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ```solidity [Solidity] ITIP20 token = ITIP20(0x20c0000000000000000000000000000000000001); // AlphaUSD address yourToken = 0x20c0000000000000000000000000000000000004; // Your issued stablecoin address recipient = 0xbeefcafe54750903ac1c8909323af7beb21ea2cb; // Send payment using your token for fees IFeeManager feeManager = IFeeManager(TIP_FEE_MANAGER_ADDRESS); feeManager.setTransactionFeeToken(yourToken); token.transfer(recipient, 100_000_000); ``` ```rust [Rust] use alloy::{ primitives::{address, U256}, providers::ProviderBuilder, }; use tempo_alloy::{ TempoNetwork, contracts::precompiles::{ITIPFeeAMM, TIP_FEE_MANAGER_ADDRESS}, }; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) .await?; // Your issued token let your_token = address!("0x20c0000000000000000000000000000000000004"); // [!code focus] // AlphaUSD let validator_token = address!("0x20c0000000000000000000000000000000000001"); // [!code focus] let fee_amm = ITIPFeeAMM::new( // [!code focus] TIP_FEE_MANAGER_ADDRESS, // [!code focus] &provider, // [!code focus] ); // [!code focus] let recipient = address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); // [!code focus] // Add 100 AlphaUSD of liquidity to the fee pool fee_amm // [!code focus] .mint( // [!code focus] your_token, // [!code focus] validator_token, // [!code focus] U256::from(100_000_000), // [!code focus] recipient, // [!code focus] ) // [!code focus] .send() // [!code focus] .await? // [!code focus] .get_receipt() // [!code focus] .await?; // [!code focus] println!("Fee liquidity added successfully"); // [!code focus] Ok(()) } ``` ::: Users can set your stablecoin as their default fee token at the account level, or specify it for individual transactions. Learn more about [how users pay fees in different stablecoins](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin). :::: ## How stablecoin fee payments work When users pay transaction fees with your stablecoin, Tempo's fee system automatically handles the conversion if validators prefer a different token. The [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) ensures seamless fee payments across all supported stablecoins. Users can select your stablecoin as their fee token through: * **Account-level preference**: Set as default for all transactions * **Transaction-level preference**: Specify for individual transactions * **Automatic selection**: When directly interacting with your token contract Learn more about [how users pay fees in different stablecoins](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) and the complete [fee token preference hierarchy](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#fee-token-preferences). ## Benefits of stablecoin fee tokens * **User convenience**: Users can pay fees with the same token they're using * **Liquidity**: Encourages users to hold your stablecoin * **Flexibility**: Works seamlessly with Tempo's fee system ## Stablecoin fee-token best practices ### Monitor pool liquidity Regularly check your token's fee pool reserves to ensure users can consistently pay fees with your stablecoin. Low liquidity can prevent transactions from being processed. ### Maintain adequate reserves Keep sufficient validator token reserves in your fee pool to handle expected transaction volume. Consider your user base size and typical transaction frequency when determining reserve levels. As fees accrue in your token, the pool will run low on validator tokens and need to be rebalanced. Use [`rebalanceSwap`](https://tempo.xyz/developers/docs/guide/stablecoin-dex/managing-fee-liquidity#rebalance-pools) to replenish validator token reserves when they become depleted. ### Test before launch Before promoting fee payments with your token, thoroughly test the flow on testnet: 1. Add liquidity to the fee pool 2. Verify users can set your token as their fee preference 3. Execute test transactions with various gas costs 4. Monitor that fee conversions work correctly ## Next steps for stablecoin fee tokens * [Manage Your Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/manage-stablecoin) — Manage roles, supply caps, and transfer policies * [Managing Fee Liquidity](https://tempo.xyz/developers/docs/guide/stablecoin-dex/managing-fee-liquidity) — Add and remove liquidity to enable fee conversions * [Fee Tokens](https://tempo.xyz/developers/docs/protocol/fees/spec-fee) — Complete fee token specification # Managing your TIP-20 stablecoin Configure your stablecoin's permissions, supply limits, and compliance policies after deployment. This guide covers granting roles to manage token operations, setting supply caps, configuring transfer policies, and controlling token transfers through pause/unpause functionality. TIP-20 tokens use a role-based access control system that allows you to delegate different administrative functions to different addresses. For detailed information about the role system, see the [TIP-20 specification](https://tempo.xyz/developers/docs/protocol/tip20/spec#tip-20-roles). ## Stablecoin management steps In this guide, we'll walk through how to assign and check the **`issuer`** role, but the process is identical for other roles like `pause`, `unpause`, `burnBlocked`, and `defaultAdmin`. ::::steps ### Set up stablecoin management Before you can manage roles on your stablecoin, you need to create one. Follow the [Create a Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/create-a-stablecoin) guide to deploy your token. Once you've created your token, you can proceed to grant roles to specific addresses. ### Grant stablecoin roles to an address Assign roles to specific addresses to delegate token management capabilities. **Interactive demo: Grant Roles to an Address** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [GrantRoles.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { useQueryClient } from '@tanstack/react-query' // @noErrors export function GrantRoles() { const queryClient = useQueryClient() const tokenAddress = '0x...' // Your token address const treasuryAddress = '0x...' // Address to grant the issuer role const grant = Hooks.token.useGrantRolesSync({}) // [!code hl] const handleGrantIssuer = async () => { // [!code hl] await grant.mutate({ // [!code hl] token: tokenAddress, // [!code hl] roles: ['issuer'], // [!code hl] to: treasuryAddress, // [!code hl] }) // [!code hl] } // [!code hl] return ( {/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Check stablecoin roles for an address Use `hasRole` to verify whether an address has been granted a specific role. :::code-group ```tsx twoslash [GrantRoles.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { useQueryClient } from '@tanstack/react-query' // @noErrors export function GrantRoles() { const queryClient = useQueryClient() const tokenAddress = '0x...' // Your token address const treasuryAddress = '0x...' // Address to grant the issuer role // Grant the issuer role const grant = Hooks.token.useGrantRolesSync({ mutation: { // [!code ++] onSettled() { // [!code ++] queryClient.refetchQueries({ queryKey: ['hasRole'] }) // [!code ++] }, // [!code ++] }, // [!code ++] }) const handleGrantIssuer = async () => { await grant.mutate({ token: tokenAddress, roles: ['issuer'], to: treasuryAddress, }) } const { data: hasIssuerRole } = Hooks.token.useHasRole({ // [!code ++] account: treasuryAddress, // [!code ++] token: tokenAddress, // [!code ++] role: 'issuer', // [!code ++] }) // [!code ++] return (
{hasIssuerRole !== undefined && ( // [!code ++]
{/* [!code ++] */} Treasury {hasIssuerRole ? 'has' : 'does not have'} the issuer role // [!code ++]
{/* [!code ++] */} )} // [!code ++]
) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Revoke the stablecoin issuer role Revoke roles from addresses when you need to remove their permissions. **Interactive demo: Revoke Issuer Role** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles 5. Revoke token roles Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [RevokeRoles.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { useQueryClient } from '@tanstack/react-query' // @noErrors export function RevokeRoles() { const queryClient = useQueryClient() const tokenAddress = '0x...' // Your token address const treasuryAddress = '0x...' // Address to grant/revoke the issuer role // Grant the issuer role const grant = Hooks.token.useGrantRolesSync({ mutation: { onSettled() { queryClient.refetchQueries({ queryKey: ['hasRole'] }) }, }, }) const handleGrantIssuer = async () => { await grant.mutate({ token: tokenAddress, roles: ['issuer'], to: treasuryAddress, }) } // Check if the treasury has the issuer role const { data: hasIssuerRole } = Hooks.token.useHasRole({ account: treasuryAddress, token: tokenAddress, role: 'issuer', }) // Revoke the issuer role // [!code ++] const revoke = Hooks.token.useRevokeRolesSync({ // [!code ++] mutation: { // [!code ++] onSettled() { // [!code ++] queryClient.refetchQueries({ queryKey: ['hasRole'] }) // [!code ++] }, // [!code ++] }, // [!code ++] }) // [!code ++] const handleRevokeIssuer = async () => { // [!code ++] await revoke.mutate({ // [!code ++] token: tokenAddress, // [!code ++] roles: ['issuer'], // [!code ++] from: treasuryAddress, // [!code ++] }) // [!code ++] } // [!code ++] return (
{/* [!code ++] */} {hasIssuerRole !== undefined && (
Treasury {hasIssuerRole ? 'has' : 'does not have'} the issuer role
)}
) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: :::: ## Stablecoin management recipes ### Set the stablecoin logo Set or update the token's on-chain [`logoURI`](https://tempo.xyz/developers/docs/protocol/tip20/spec#logo-uri) so wallets and explorers can read the icon directly from the token contract via `logoURI()`. This requires the **`DEFAULT_ADMIN_ROLE`** and is done by calling `setLogoURI(string newLogoURI)` on the token. For the recommended format, use a square, rasterized PNG or WebP (max 256 bytes; `https`, `http`, `ipfs`, or `data` scheme). ### Set the stablecoin supply cap Limit the maximum total supply of your token. Setting supply caps requires the **`DEFAULT_ADMIN_ROLE`**. The new cap cannot be less than the current total supply. **Interactive demo: Set Supply Cap** 1. Connect 2. Add funds 3. Create or load token 4. Set supply cap Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [SetSupplyCap.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' // @noErrors export function SetSupplyCap() { const tokenAddress = '0x...' // Your token address const { data: metadata, refetch: refetchMetadata } = // [!code hl] Hooks.token.useGetMetadata({ token: tokenAddress }) // [!code hl] const setSupplyCap = Hooks.token.useSetSupplyCapSync({ // [!code hl] mutation: { onSettled: () => refetchMetadata() }, // [!code hl] }) // [!code hl] const handleSetSupplyCap = () => { // [!code hl] setSupplyCap.mutate({ // [!code hl] token: tokenAddress, // [!code hl] supplyCap: parseUnits('1000', metadata?.decimals || 6), // [!code hl] }) // [!code hl] } // [!code hl] return ( {/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Configure stablecoin transfer policies Control who can send and receive your stablecoin for compliance and regulatory requirements. Setting transfer policies requires the **`DEFAULT_ADMIN_ROLE`**. Transfer policies can be: * **Always allow**: Anyone can send/receive (default) * **Always reject**: Nobody can send/receive * **Whitelist**: Only authorized addresses can send/receive * **Blacklist**: Blocked addresses cannot send/receive Learn more about configuring transfer policies in the [TIP-403 specification](https://tempo.xyz/developers/docs/protocol/tip403/spec). **Interactive demo: Create and Link Transfer Policy** 1. Connect 2. Add funds 3. Create or load token 4. Create token policy 5. Link token policy Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [CreateTokenPolicy.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' // @noErrors export function CreateTokenPolicy() { const tokenAddress = '0x...' // Your token address const createPolicy = Hooks.policy.useCreateSync({ // [!code hl] mutation: { // [!code hl] onSuccess(result) { // [!code hl] // Store policyId for the next step // [!code hl] console.log('Policy ID:', result.policyId) // [!code hl] }, // [!code hl] }, // [!code hl] }) // [!code hl] const handleCreatePolicy = async () => { // [!code hl] await createPolicy.mutateAsync({ // [!code hl] addresses: [ // [!code hl] '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', // [!code hl] ], // [!code hl] type: 'blacklist', // [!code hl] }) // [!code hl] } // [!code hl] return ( {/* [!code hl] */} ) } ``` ```tsx twoslash [LinkTokenPolicy.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' // @noErrors export function LinkTokenPolicy() { const tokenAddress = '0x...' // Your token address const policyId = 1n // Policy ID from previous step const linkPolicy = Hooks.token.useChangeTransferPolicySync() // [!code hl] const handleLinkPolicy = async () => { // [!code hl] await linkPolicy.mutateAsync({ // [!code hl] policyId, // [!code hl] token: tokenAddress, // [!code hl] }) // [!code hl] } // [!code hl] return ( {/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Pause and unpause stablecoin transfers Temporarily halt all token transfers during emergency situations or maintenance windows. Pausing transfers requires the **`PAUSE_ROLE`**. Unpausing transfers requires the **`UNPAUSE_ROLE`**. **Interactive demo: Pause and Unpause Your Token** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles 5. Pause unpause transfers Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [PauseUnpauseTransfers.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' // @noErrors export function PauseUnpauseTransfers() { const tokenAddress = '0x...' // Your token address const { data: metadata, refetch: refetchMetadata } = // [!code hl] Hooks.token.useGetMetadata({ token: tokenAddress }) // [!code hl] const pause = Hooks.token.usePauseSync({ // [!code hl] mutation: { onSettled: () => refetchMetadata() }, // [!code hl] }) // [!code hl] const unpause = Hooks.token.useUnpauseSync({ // [!code hl] mutation: { onSettled: () => refetchMetadata() }, // [!code hl] }) // [!code hl] const paused = metadata?.paused || false // [!code hl] const handleToggle = () => { // [!code hl] if (paused) { // [!code hl] unpause.mutate({ // [!code hl] token: tokenAddress, // [!code hl] }) // [!code hl] } else { // [!code hl] pause.mutate({ // [!code hl] token: tokenAddress, // [!code hl] }) // [!code hl] } // [!code hl] } // [!code hl] const isProcessing = pause.isPending || unpause.isPending // [!code hl] return ( {/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ### Use the stablecoin burn-blocked role The Burn Blocked role allows your team to burn tokens from blocked or frozen addresses. This is useful for regulatory compliance when you need to remove tokens from addresses that violate terms of service or legal requirements. **Interactive demo: Create and Link Transfer Policy** 1. Connect 2. Add funds 3. Create or load token 4. Grant token roles 5. Mint token 6. Create token policy 7. Link token policy 8. Burn token blocked Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) :::code-group ```tsx twoslash [BurnBlocked.tsx] import React from 'react' import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' // @noErrors export function BurnBlocked() { const tokenAddress = '0x...' // Your token address const blockedAddress = '0x...' // The blocked address to burn tokens from const { data: metadata } = Hooks.token.useGetMetadata({ // [!code hl] token: tokenAddress, // [!code hl] }) // [!code hl] const burnBlocked = Hooks.token.useBurnBlockedSync() // [!code hl] const handleBurnBlocked = async () => { // [!code hl] if (!metadata) return // [!code hl] await burnBlocked.mutateAsync({ // [!code hl] token: tokenAddress, // [!code hl] from: blockedAddress, // [!code hl] amount: parseUnits('100', metadata.decimals), // [!code hl] }) // [!code hl] } // [!code hl] return ( {/* [!code hl] */} ) } ``` ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ chains: [tempo], connectors: [tempoWallet()], transports: { [tempo.id]: http(), }, }) ``` ::: ## Stablecoin management best practices ### Stablecoin role separation Use different addresses for different roles to enhance security. For example, assign the issuer role to your treasury address for minting, and the pause role to your security team for emergency controls. ### Stablecoin event monitoring Monitor onchain events for role changes, mints, burns, and administrative actions to maintain visibility into token operations and detect unauthorized activities. ### Stablecoin emergency procedures Ensure pause and unpause roles are assigned to trusted addresses and that your team has documented procedures for responding to security incidents requiring token transfers to be halted. ## Stablecoin management learning resources * [Role-Based Access Control](https://tempo.xyz/developers/docs/protocol/tip20/spec#tip-20-roles) — Learn about the role-based access control system and all available roles * [Transfer Policies (TIP-403)](https://tempo.xyz/developers/docs/protocol/tip403/spec) — Learn how to configure transfer policies for compliance requirements # Managing Fee Liquidity The Fee AMM converts transaction fees between stablecoins when users pay in a different token than the validator prefers. This guide shows you how to add and remove liquidity to enable fee conversions. The Fee AMM also supports [multihop FeeAMM routing](https://tips.sh/1033): one fee conversion can route through two pools when a direct pair between the user's fee token and the validator's payout token doesn't exist or has insufficient liquidity. The pool mechanics for adding and removing liquidity are the same either way, but a single conversion may reserve and consume liquidity from two pools you provide to instead of one. Browse current pool reserves and route availability in the [FeeAMM explorer view](https://explore.tempo.xyz/fee-amm). **Interactive demo: Manage Fee Liquidity** 1. Connect 2. Add funds 3. Create or load token 4. Mint fee AMM liquidity 5. Check fee AMM pool 6. Burn fee AMM liquidity Source: [tempoxyz/examples/tree/main/examples/issuance](https://github.com/tempoxyz/examples/tree/main/examples/issuance) ## Fee liquidity management steps ::::steps ### Check pool reserves Before adding liquidity, check the current pool reserves to understand the pool state. :::code-group ```tsx twoslash [ManageFeeLiquidity.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits } from 'viem' const userToken = '0x20c0000000000000000000000000000000000002' // BetaUSD const validatorToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD function ManageFeeLiquidity() { const { data: pool } = Hooks.amm.usePool({ // [!code hl] userToken, // [!code hl] validatorToken, // [!code hl] }) // [!code hl] return (
User token reserves: {formatUnits(pool?.reserveUserToken ?? 0n, 6)}
Validator token reserves: {formatUnits(pool?.reserveValidatorToken ?? 0n, 6)}
) } ``` ```ts twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Add liquidity Add validator token to the pool to receive LP tokens representing your share. The first liquidity provider to a new pool must burn 1,000 units of liquidity. This costs approximately 0.002 USD and prevents attacks on pool reserves. Learn more in the [Fee AMM specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm). :::code-group ```tsx twoslash [ManageFeeLiquidity.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useConnection } from 'wagmi' const userToken = '0x20c0000000000000000000000000000000000002' // BetaUSD const validatorToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD function ManageFeeLiquidity() { const { address } = useConnection() const { data: pool } = Hooks.amm.usePool({ userToken, validatorToken, }) const mintLiquidity = Hooks.amm.useMintSync() // [!code ++] return (
User token reserves: {formatUnits(pool?.reserveUserToken ?? 0n, 6)}
Validator token reserves: {formatUnits(pool?.reserveValidatorToken ?? 0n, 6)}
{/* [!code ++] */}
) } ``` ```ts twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Check your LP balance View your LP token balance to see your share of the pool. :::code-group ```tsx twoslash [ManageFeeLiquidity.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useConnection } from 'wagmi' const userToken = '0x20c0000000000000000000000000000000000002' // BetaUSD const validatorToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD function ManageFeeLiquidity() { const { address } = useConnection() const { data: pool } = Hooks.amm.usePool({ userToken, validatorToken, }) const { data: balance } = Hooks.amm.useLiquidityBalance({ // [!code ++] address, // [!code ++] userToken, // [!code ++] validatorToken, // [!code ++] }) // [!code ++] const mintLiquidity = Hooks.amm.useMintSync() return (
LP token balance: {formatUnits(balance ?? 0n, 6)}
{/* [!code ++] */}
User token reserves: {formatUnits(pool?.reserveUserToken ?? 0n, 6)}
Validator token reserves: {formatUnits(pool?.reserveValidatorToken ?? 0n, 6)}
) } ``` ```ts twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Remove liquidity Burn LP tokens to withdraw your share of pool reserves plus accumulated fees. :::code-group ```tsx twoslash [ManageFeeLiquidity.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useConnection } from 'wagmi' const userToken = '0x20c0000000000000000000000000000000000002' // BetaUSD const validatorToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD function ManageFeeLiquidity() { const { address } = useConnection() const { data: pool } = Hooks.amm.usePool({ userToken, validatorToken, }) const { data: balance } = Hooks.amm.useLiquidityBalance({ address, userToken, validatorToken, }) const mintLiquidity = Hooks.amm.useMintSync() const burnLiquidity = Hooks.amm.useBurnSync() // [!code ++] return (
LP token balance: {formatUnits(balance ?? 0n, 6)}
User token reserves: {formatUnits(pool?.reserveUserToken ?? 0n, 6)}
Validator token reserves: {formatUnits(pool?.reserveValidatorToken ?? 0n, 6)}
{/* [!code ++] */}
) } ``` ```ts twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: :::: ## Fee liquidity recipes ### Monitor pool utilization Track fee swap activity to understand pool utilization and revenue. :::code-group ```tsx twoslash [MonitorSwaps.tsx] // @noErrors import * as React from 'react' import { Hooks } from 'wagmi/tempo' import { formatUnits } from 'viem' const userToken = '0x20c0000000000000000000000000000000000002' // BetaUSD const validatorToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD function MonitorSwaps() { const [swaps, setSwaps] = React.useState([]) Hooks.amm.useWatchFeeSwap({ // [!code hl] userToken, // [!code hl] validatorToken, // [!code hl] onLogs(logs) { // [!code hl] for (const log of logs) { // [!code hl] setSwaps((prev) => [...prev, { // [!code hl] amountIn: formatUnits(log.args.amountIn, 6), // [!code hl] amountOut: formatUnits(log.args.amountOut, 6), // [!code hl] revenue: formatUnits(log.args.amountIn * 30n / 10000n, 6), // [!code hl] }]) // [!code hl] } // [!code hl] }, // [!code hl] }) // [!code hl] return (
{swaps.map((swap, i) => (
Swap: {swap.amountIn} → {swap.amountOut} (LP revenue: {swap.revenue})
))}
) } ``` ```ts twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Rebalance pools You can rebalance pools by swapping validator tokens for accumulated user tokens at a fixed rate. Rebalancing restores validator token reserves and enables continued fee conversions. Learn more [here](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm#swap-mechanisms). :::code-group ```tsx twoslash [RebalancePool.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useConnection } from 'wagmi' const userToken = '0x20c0000000000000000000000000000000000002' // BetaUSD const validatorToken = '0x20c0000000000000000000000000000000000001' // AlphaUSD function RebalancePool() { const { address } = useConnection() const { data: pool } = Hooks.amm.usePool({ userToken, validatorToken, }) const rebalance = Hooks.amm.useRebalanceSwapSync() // [!code hl] return (
User token reserves: {formatUnits(pool?.reserveUserToken ?? 0n, 6)}
Validator token reserves: {formatUnits(pool?.reserveValidatorToken ?? 0n, 6)}
{ /* [!code hl] */ }
) } ``` ```ts twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ## Fee liquidity best practices ### Monitor pool reserves Regularly check pool reserves to ensure sufficient liquidity for fee conversions. Low reserves can prevent transactions from being processed. Add liquidity when: * Transaction rates increase for a given `userToken` * Reserve levels drop below expected daily volume * Multiple validators begin preferring the same token ### Maintain adequate reserves As an issuer, keep sufficient validator token reserves to handle expected transaction volume. Consider your anticipated fee conversion volume when determining reserve levels. For new token pairs, provide the entire initial amount in the validator token. The pool naturally accumulates user tokens as fees are paid. ### Deploy liquidity strategically Focus liquidity on pools with: * High transaction volume and frequent fee conversions * New stablecoins that need initial bootstrapping * Validator tokens preferred by multiple validators ## Fee liquidity learning resources * [Fee AMM Specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) — Complete technical specification of the Fee AMM protocol * [Fee AMM Overview](https://tempo.xyz/developers/docs/protocol/fees/fee-amm) — Learn how the Fee AMM enables flexible fee payments * [Use Your Stablecoin for Fees](https://tempo.xyz/developers/docs/guide/issuance/use-for-fees) — Enable users to pay fees using your stablecoin # Executing swaps on the Tempo Stablecoin DEX Execute swaps between stablecoins on the exchange. Swaps execute immediately against existing orders in the orderbook, providing instant liquidity for cross-stablecoin payments. By the end of this guide you will be able to execute swaps, get price quotes, and manage slippage protection. **Interactive demo: Execute a Swap** 1. Connect 2. Add funds 3. Make swaps Source: [tempoxyz/examples/tree/main/examples/exchange](https://github.com/tempoxyz/examples/tree/main/examples/exchange) ## Swap implementation steps ::::steps ### Set up your swap client Ensure that you have set up your client by following the [guide](https://tempo.xyz/developers/docs/sdk/typescript). ### Get a price quote Before executing a swap, get a quote to see the expected price. :::code-group ```tsx twoslash [Buy.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Buy() { const amount = parseUnits('10', 6) // How much AlphaUSD do I need to spend to receive 10 BetaUSD? // [!code hl] const { data: quote } = Hooks.dex.useBuyQuote({ // [!code hl] tokenIn: alphaUsd, // [!code hl] tokenOut: betaUsd, // [!code hl] amountOut: amount, // [!code hl] }) // [!code hl] return
Quote: {formatUnits(quote, 6)}
} ``` ```tsx twoslash [Sell.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Sell() { const amount = parseUnits('10', 6) // How much BetaUSD will I receive for 10 AlphaUSD? // [!code hl] const { data: quote } = Hooks.dex.useSellQuote({ // [!code hl] tokenIn: alphaUsd, // [!code hl] tokenOut: betaUsd, // [!code hl] amountIn: amount, // [!code hl] }) // [!code hl] return
Quote: {formatUnits(quote, 6)}
} ``` ::: ### Calculate slippage tolerance Set appropriate slippage based on your quote to protect against unfavorable price movements. :::code-group ```tsx twoslash [Buy.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Buy() { const amount = parseUnits('10', 6) // How much AlphaUSD do I need to spend to receive 10 BetaUSD? const { data: quote } = Hooks.dex.useBuyQuote({ tokenIn: alphaUsd, tokenOut: betaUsd, amountOut: amount, }) // [!code ++] Calculate 0.5% slippage tolerance const slippageTolerance = 0.005 // [!code ++] const maxAmountIn = quote // [!code ++] ? quote * BigInt(Math.floor((1 + slippageTolerance) * 1000)) / 1000n // [!code ++] : 0n // [!code ++] return (
Quote: {formatUnits(quote, 6)}
Max input (0.5% slippage): {formatUnits(maxAmountIn, 6)}
{/* [!code ++] */}
) } ``` ```tsx twoslash [Sell.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Sell() { const amount = parseUnits('10', 6) // How much BetaUSD will I receive for 10 AlphaUSD? const { data: quote } = Hooks.dex.useSellQuote({ tokenIn: alphaUsd, tokenOut: betaUsd, amountIn: amount, }) // [!code ++] Calculate 0.5% slippage tolerance const slippageTolerance = 0.005 // [!code ++] const minAmountOut = quote // [!code ++] ? quote * BigInt(Math.floor((1 - slippageTolerance) * 1000)) / 1000n // [!code ++] : 0n // [!code ++] return (
Quote: {formatUnits(quote, 6)}
Min output (0.5% slippage): {formatUnits(minAmountOut, 6)}
{/* [!code ++] */}
) } ``` ::: ### Approve stablecoin spend To execute a swap, you need to approve the Stablecoin DEX contract to spend the token you're using to fund the swap. :::code-group ```tsx twoslash [Buy.tsx] // @noErrors import { Actions, Addresses } from 'viem/tempo' // [!code ++] import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useSendCallsSync } from 'wagmi' // [!code ++] const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Buy() { const amount = parseUnits('10', 6) // How much AlphaUSD do I need to spend to receive 10 BetaUSD? const { data: quote } = Hooks.dex.useBuyQuote({ tokenIn: alphaUsd, tokenOut: betaUsd, amountOut: amount, }) // Calculate 0.5% slippage tolerance const slippageTolerance = 0.005 const maxAmountIn = quote ? quote * BigInt(Math.floor((1 + slippageTolerance) * 1000)) / 1000n : 0n const sendCalls = useSendCallsSync() // [!code ++] return (
Quote: {formatUnits(quote, 6)}
Max input (0.5% slippage): {formatUnits(maxAmountIn, 6)}
{/* [!code ++] */}
) } ``` ```tsx twoslash [Sell.tsx] // @noErrors import { Actions, Addresses } from 'viem/tempo' // [!code ++] import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useSendCallsSync } from 'wagmi' // [!code ++] const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Sell() { const amount = parseUnits('10', 6) // How much BetaUSD will I receive for 10 AlphaUSD? const { data: quote } = Hooks.dex.useSellQuote({ tokenIn: alphaUsd, tokenOut: betaUsd, amountIn: amount, }) // Calculate 0.5% slippage tolerance const slippageTolerance = 0.005 const minAmountOut = quote ? quote * BigInt(Math.floor((1 - slippageTolerance) * 1000)) / 1000n : 0n const sendCalls = useSendCallsSync() // [!code ++] return (
Quote: {formatUnits(quote, 6)}
Min output (0.5% slippage): {formatUnits(minAmountOut, 6)}
{/* [!code ++] */}
) } ``` ::: ### Execute a swap Batch the token approval with the swap in a single transaction for better UX. :::code-group ```tsx twoslash [Buy.tsx] // @noErrors import { Actions, Addresses } from 'viem/tempo' import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useSendCallsSync } from 'wagmi' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Buy() { const amount = parseUnits('10', 6) // How much AlphaUSD do I need to spend to receive 10 BetaUSD? const { data: quote } = Hooks.dex.useBuyQuote({ tokenIn: alphaUsd, tokenOut: betaUsd, amountOut: amount, }) // Calculate 0.5% slippage tolerance const slippageTolerance = 0.005 const maxAmountIn = quote ? quote * BigInt(Math.floor((1 + slippageTolerance) * 1000)) / 1000n : 0n const sendCalls = useSendCallsSync() return (
Quote: {formatUnits(quote, 6)}
Max input (0.5% slippage): {formatUnits(maxAmountIn, 6)}
) } ``` ```tsx twoslash [Sell.tsx] // @noErrors import { Actions, Addresses } from 'viem/tempo' import { Hooks } from 'wagmi/tempo' import { formatUnits, parseUnits } from 'viem' import { useSendCallsSync } from 'wagmi' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Sell() { const amount = parseUnits('10', 6) // How much BetaUSD will I receive for 10 AlphaUSD? const { data: quote } = Hooks.dex.useSellQuote({ tokenIn: alphaUsd, tokenOut: betaUsd, amountIn: amount, }) // Calculate 0.5% slippage tolerance const slippageTolerance = 0.005 const minAmountOut = quote ? quote * BigInt(Math.floor((1 - slippageTolerance) * 1000)) / 1000n : 0n const sendCalls = useSendCallsSync() return (
Quote: {formatUnits(quote, 6)}
Min output (0.5% slippage): {formatUnits(minAmountOut, 6)}
) } ``` ::: :::: ## Stablecoin swap recipes ### Handling insufficient liquidity Quote requests will fail with an `InsufficientLiquidity` error if there isn't enough liquidity in the orderbook to satisfy the requested amount. Handle this error when fetching quotes: ```tsx import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' const betaUsd = '0x20c0000000000000000000000000000000000002' function Swap() { const amount = parseUnits('10', 6) const { data: quote, error } = Hooks.dex.useSellQuote({ tokenIn: alphaUsd, tokenOut: betaUsd, amountIn: amount, }) if (error) { if (error.message.includes('InsufficientLiquidity')) { return
Not enough liquidity available. Try a smaller amount.
} return
Error: {error.message}
} if (!quote) { return
Loading quote...
} return
Quote: {quote.toString()}
} ``` ## Stablecoin swap best practices ### Always get quotes before swapping Query the expected price before executing a swap to ensure you're getting a fair rate and to set appropriate slippage protection. ### Set appropriate slippage protection Use `minAmountOut` or `maxAmountIn` to protect against unfavorable price movements between quoting and execution. ## Stablecoin swap learning resources * [Exchange Balance](https://tempo.xyz/developers/docs/protocol/exchange/exchange-balance) — Learn about how the Stablecoin DEX interacts with your balances * [Swap Execution Details](https://tempo.xyz/developers/docs/protocol/exchange/executing-swaps) — Learn about the technical details of swap execution # How to provide liquidity on the Tempo DEX Add liquidity for a token pair by placing orders on the Stablecoin DEX. You can provide liquidity on the `buy` or `sell` side of the orderbook, with `limit` or `flip` orders. To learn more about order types see the [documentation on order types](https://tempo.xyz/developers/docs/protocol/exchange/providing-liquidity#order-types). In this guide you will learn how to place buy and sell orders to provide liquidity on the Stablecoin DEX orderbook. Active makers may pay less gas when they cancel eligible orders or have eligible orders filled, then later place new eligible orders. See the [T7 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t7#user-attributed-dex-savings) for the feature details. ## Liquidity provider demo **Interactive demo: Place an Order** 1. Connect 2. Add funds 3. Place order 4. Query order Source: [tempoxyz/examples/tree/main/examples/exchange](https://github.com/tempoxyz/examples/tree/main/examples/exchange) ## Liquidity provider steps ::::steps ### Set up your liquidity client Ensure that you have set up your client by following the [guide](https://tempo.xyz/developers/docs/sdk/typescript). ### Approve stablecoin spend To place an order, you need to approve the Stablecoin DEX contract to spend the order's "spend" token. :::info The code samples in this guide will place orders on the `AlphaUSD` / `pathUSD` pair. The "spend" token is the token that will be spent to place the order. * buying `AlphaUSD` spends `pathUSD` * selling `AlphaUSD` spends `AlphaUSD` ::: **Interactive demo: Approve Spend** 1. Connect 2. Approve spend :::code-group ```tsx twoslash [ApproveSpend.tsx] // @noErrors import { parseUnits } from 'viem' import { Addresses } from 'viem/tempo' import { Hooks } from 'wagmi/tempo' const pathUsd = '0x20c0000000000000000000000000000000000000' const alphaUsd = '0x20c0000000000000000000000000000000000001' function ApproveSpend(props: { orderType: 'buy' | 'sell' }) { const { orderType } = props // buying AlphaUSD requires we spend pathUSD // [!code hl] const spendToken = orderType === 'buy' ? pathUsd : alphaUsd // [!code hl] // [!code hl] const { mutate: approve } = Hooks.token.useApproveSync() // [!code hl] return ( ) } ``` ```ts [wagmi.config.ts] import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Place a liquidity order Once the spend is approved, you can place an order by calling the `place` action on the Stablecoin DEX. :::info In the code sample below, we use the `useSendCallsSync` hook to batch the approve and place order calls in a single transaction for efficiency. ::: **Interactive demo: Place Order** 1. Connect 2. Place order :::code-group ```tsx twoslash [PlaceOrder.tsx] // @noErrors import { Actions, Addresses } from 'viem/tempo' import { parseUnits } from 'viem' import { useSendCallsSync } from 'wagmi' // [!code hl] const pathUsd = '0x20c0000000000000000000000000000000000000' const alphaUsd = '0x20c0000000000000000000000000000000000001' function PlaceOrder(props: { orderType: 'buy' | 'sell' }) { const { orderType } = props // buying AlphaUSD requires we spend pathUSD const spendToken = orderType === 'buy' ? pathUsd : alphaUsd const sendCalls = useSendCallsSync() // [!code hl] return ( ) } ``` ```ts [wagmi.config.ts] import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### View liquidity order details After placing an order, you can query its details to see the current state, including the amount filled and remaining using [`Hooks.dex.useOrder`](https://wagmi.sh/tempo/hooks/dex.useOrder). **Interactive demo: View Order** 1. Connect 2. Add funds 3. Place order 4. Query order :::code-group ```tsx twoslash [QueryOrder.tsx] // @noErrors import { Hooks } from 'wagmi/tempo' const orderId = 123n const { data: order, refetch } = Hooks.dex.useOrder({ orderId, }) console.log('Type:', order?.isBid ? 'Buy' : 'Sell') console.log('Amount:', order?.amount.toString()) console.log('Remaining:', order?.remaining.toString()) console.log('Tick:', order?.tick) console.log('Is flip order:', order?.isFlip) ``` ```ts [wagmi.config.ts] import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: For more details on querying orders, see the [`Hooks.dex.useOrder`](https://wagmi.sh/tempo/hooks/dex.useOrder) documentation. :::: ## Liquidity provider recipes ### Cancel a liquidity order Cancel an order using its order ID. When you cancel an order, any remaining funds are credited to your exchange balance (not directly to your wallet). To move funds back to your wallet, you can [withdraw them to your wallet](https://tempo.xyz/developers/docs/protocol/exchange/exchange-balance#withdrawing-from-the-dex). **Interactive demo: Place and Cancel an Order** 1. Connect 2. Add funds 3. Place order 4. Cancel order Source: [tempoxyz/examples/tree/main/examples/exchange](https://github.com/tempoxyz/examples/tree/main/examples/exchange) :::code-group ```tsx twoslash [ManageOrder.tsx] // @noErrors import { Actions, Addresses } from 'viem/tempo' import { Hooks } from 'wagmi/tempo' import { parseUnits } from 'viem' import { useSendCallsSync } from 'wagmi' const pathUsd = '0x20c0000000000000000000000000000000000000' const alphaUsd = '0x20c0000000000000000000000000000000000001' function ManageOrder() { const sendCalls = useSendCallsSync() const cancelOrder = Hooks.dex.useCancelSync() // [!code hl] const placeOrder = () => { const calls = [ Actions.token.approve.call({ spender: Addresses.stablecoinDex, amount: parseUnits('100', 6), token: pathUsd, }), Actions.dex.place.call({ token: alphaUsd, amount: parseUnits('100', 6), type: 'buy', tick: 0, }), ] sendCalls.sendCallsSync({ calls }) } return ( <>
{ event.preventDefault() const formData = new FormData(event.target as HTMLFormElement) const orderId = BigInt(formData.get('orderId') as string) cancelOrder.mutate({ orderId }) // [!code hl] } }> {/* [!code hl] */}
) } ``` ```ts [wagmi.config.ts] import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Determine the quote token Each token has a designated quote token that it trades against on the DEX. For most stablecoins, this will be `pathUSD`. Use the `token.useGetMetadata` hook to retrieve a token's quote token. :::code-group ```ts twoslash [example.ts] // @errors: 2322 import { config } from './wagmi.config' declare module 'wagmi' { interface Register { config: typeof config } } // ---cut--- import { Hooks } from 'wagmi/tempo' const { data: metadata } = Hooks.token.useGetMetadata({ // [!code focus] token: '0x20c0000000000000000000000000000000000001', // AlphaUSD // [!code focus] }) // [!code focus] console.log('Token:', metadata?.symbol) // @log: Token: AlphaUSD console.log('Quote Token:', metadata?.quoteToken) // returns `pathUSD` address // @log: Quote Token: 0x20c0000000000000000000000000000000000000 ``` ```ts [wagmi.config.ts] filename="wagmi.config.ts" // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Place a flip order Flip orders automatically switch between buy and sell sides when filled, providing continuous liquidity. Use viem's [`dex.placeFlip`](https://viem.sh/tempo/actions/dex.placeFlip) to create a flip order call. A flip order can re-list on the opposite side at the same tick (`flipTick == tick`), which is useful for pegged or near-1:1 pairs. When it fills, the order keeps the same `orderId` and emits an `OrderFlipped` event instead of a new `OrderPlaced`, so a single flip strategy stays trackable under one ID across its full lifecycle. Indexers, SDKs, and contract code that follow flip orders should treat `OrderFlipped` as the latest active state — see the [flip-order indexing notes](https://tempo.xyz/developers/docs/protocol/exchange/providing-liquidity#flip-order-indexing). :::code-group ```tsx twoslash [PlaceFlipOrder.tsx] // @noErrors import { Actions, Addresses } from 'viem/tempo' import { parseUnits } from 'viem' import { useSendCallsSync } from 'wagmi' const pathUsd = '0x20c0000000000000000000000000000000000000' const alphaUsd = '0x20c0000000000000000000000000000000000001' function PlaceFlipOrder(props: { orderType: 'buy' | 'sell' }) { const { orderType } = props // buying AlphaUSD requires we spend pathUSD const spendToken = orderType === 'buy' ? pathUsd : alphaUsd const sendCalls = useSendCallsSync() return ( ) } ``` ```ts [wagmi.config.ts] import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: ### Place order at specific price Ticks represent prices relative to the quote token (usually pathUSD). The formula is: ``` tick = (price - 1) * 100_000 ``` For example, price $1.0000 → tick = 0, price $0.9990 → tick = -10, and price $1.0010 → tick = 10. Use the `Tick` utility to convert between prices and ticks: ```tsx import { Actions, Tick } from 'viem/tempo' // [!code hl] import { parseUnits } from 'viem' const alphaUsd = '0x20c0000000000000000000000000000000000001' // buy order at $0.9990 (tick: -10) // [!code hl] const buyCall = Actions.dex.place.call({ // [!code hl] token: alphaUsd, // [!code hl] amount: parseUnits('100', 6), // [!code hl] type: 'buy', // [!code hl] tick: Tick.fromPrice('0.9990'), // -10 // [!code hl] }) // [!code hl] // sell order at $1.0010 (tick: 10) // [!code hl] const sellCall = Actions.dex.place.call({ // [!code hl] token: alphaUsd, // [!code hl] amount: parseUnits('100', 6), // [!code hl] type: 'sell', // [!code hl] tick: Tick.fromPrice('1.0010'), // 10 // [!code hl] }) // [!code hl] ``` For more details including tick precision, limits, and calculation examples, see [Understanding Ticks](https://tempo.xyz/developers/docs/protocol/exchange/providing-liquidity#understanding-ticks). ## Liquidity provider best practices ### Batch liquidity calls You can batch the calls to approve spend and place the order in a single transaction for efficiency. See the [Tempo Transactions guide](https://tempo.xyz/developers/docs/guide/tempo-transaction) for more details. ## Liquidity learning resources * [Executing Swaps](https://tempo.xyz/developers/docs/guide/stablecoin-dex/executing-swaps) — Learn how to execute swaps and trade stablecoins on the DEX * [Protocol Exchange Specification](https://tempo.xyz/developers/docs/protocol/exchange/spec) — Deep dive into the Stablecoin DEX protocol specification * [Exchange Balance](https://tempo.xyz/developers/docs/protocol/exchange/exchange-balance) — Manage token balances on the exchange to optimize gas costs # Connect to a Tempo Zone on testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Use this page when you need the RPC endpoint and chain metadata for `Zone A` or `Zone B`. Account-scoped zone RPC methods require an `X-Authorization-Token` header signed by the Tempo account you are using. The interactive guides handle that for you automatically. If you are building your own integration, see the [Zone RPC specification](https://tempo.xyz/developers/docs/protocol/zones/rpc) for the token format and the list of scoped methods. ## Create a Viem client Use `zoneModerato(...)` from `viem/tempo/zones` so the client has the correct chain metadata for the zone you want to reach. ```ts import { createClient } from 'viem/tempo' import { http, zoneModerato } from 'viem/tempo/zones' const rpcUrl = 'https://rpc-zone-a.testnet.tempo.xyz' const zoneClient = createClient({ chain: zoneModerato(6), transport: http(rpcUrl), }) const blockNumber = await zoneClient.getBlockNumber() console.log(blockNumber) ``` ## Direct Connection Details ### Zone A | **Property** | **Value** | |-------------------|-------| | **Network Name** | Zone A | | **Zone ID** | `6` | | **Chain ID** | `4217000006` | | **HTTP URL** | `https://rpc-zone-a.testnet.tempo.xyz` | | **Portal Address** | `0x7069DeC4E64Fd07334A0933eDe836C17259c9B23` | | **Outbox Address** | `0x1c00000000000000000000000000000000000002` | ### Zone B | **Property** | **Value** | |-------------------|-------| | **Network Name** | Zone B | | **Zone ID** | `7` | | **Chain ID** | `4217000007` | | **HTTP URL** | `https://rpc-zone-b.testnet.tempo.xyz` | | **Portal Address** | `0x3F5296303400B56271b476F5A0B9cBF74350D6Ac` | | **Outbox Address** | `0x1c00000000000000000000000000000000000002` | Zones do not expose a public block explorer for private activity. Use authenticated RPC reads instead. # Deposit pathUSD to a Tempo Zone on testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Use this guide when you want to move `pathUSD` from your public Tempo balance into `Zone A`. You will submit a public-chain deposit first, then wait for `Zone A` to credit the net amount after fees. ![Zone contract architecture](/developers/learn/zones/diagram-deposit.svg) The deposit is accepted through `ZonePortal` on the public chain. You need private zone authorization to read the resulting zone balance, because those reads are only exposed to the authenticated account. ## Depositing pathUSD to Zone A By the end of this guide you will have deposited `pathUSD` into `Zone A` and confirmed the balance update. **Interactive demo: Deposit to Zone A** 1. Deposit to zone ## Code examples These snippets assume you already have a signed-in `rootClient` on the public chain and the usual token and zone constants in scope. Use the plaintext flow when revealing the recipient and memo is acceptable. Use the encrypted flow when only the zone sequencer should be able to read those fields. ### Plaintext ```ts import { parseUnits } from 'viem' import { Actions } from 'viem/tempo' const depositAmount = parseUnits('100', 6) const { receipt } = await Actions.zone.depositSync(rootClient, { account: rootClient.account, amount: depositAmount, token: pathUsd, zoneId: ZONE_A.id, }) console.log(receipt.blockNumber) ``` ### Encrypted ```ts import { parseUnits } from 'viem' import { Actions } from 'viem/tempo' const depositAmount = parseUnits('100', 6) const { receipt } = await Actions.zone.encryptedDepositSync(rootClient, { // [!code focus] account: rootClient.account, amount: depositAmount, token: pathUsd, zoneId: ZONE_A.id, }) console.log(receipt.blockNumber) ``` ## What Happens During a Deposit A zone deposit settles in two phases. First, you submit a public Tempo transaction depositing to the `ZonePortal`. The Zone Portal contract locks the token, deducts the deposit fee in the same token, and records the net deposit in its deposit queue. Later, the zone sequencer processes that queue and credits the recipient inside the zone. That means your public transaction receipt and your zone balance do not update at the same time. The Tempo transaction confirms that the deposit request was accepted. The zone balance changes only after the zone has processed that deposit, and it reflects the post-fee amount rather than the full amount you passed into `deposit(...)`. :::warning If you need a specific net amount inside the zone, account for the portal deposit fee first. The amount minted on the zone is `amount - depositFee`. ::: # Send tokens within a zone :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Use this guide when you want to send `pathUSD` from one private `Zone A` balance to another without moving funds back through the public chain. Zone tokens use the `TIP20` token interface, so once you authorize private reads for the session, an in-zone transfer looks much like a normal token transfer. ## Sending pathUSD within Zone A By the end of this guide you will have sent `25 pathUSD` inside `Zone A` and confirmed the updated balance. **Interactive demo: Send tokens within Zone A** 1. Send tokens within zone ## Code example This snippet assumes you already have a signed-in `rootClient` on the public chain and a derived `zoneAClient`. It shows the core zone transfer path; use the demo above when you want to watch the updated zone balance. ```ts import { parseUnits, type Address } from 'viem' import { Actions } from 'viem/tempo' const transferAmount = parseUnits('25', 6) const demoRecipient = '0xbeefcafe54750903ac1c8909323af7beb21ea2cb' as Address await zoneAClient.zone.signAuthorizationToken() const { receipt } = await Actions.token.transferSync(zoneAClient, { account: rootClient.account, amount: transferAmount, feeToken: pathUsd, to: demoRecipient, token: pathUsd, }) ``` # Send tokens across zones :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Use this guide when you want to move `pathUSD` from `Zone A` into `Zone B` without changing the token. The route still touches the public chain, so the confirmation happens in stages rather than as a single balance update. The flow uses `swapAndDepositRouter` on the public chain in same-token mode: withdraw from `Zone A`, skip the swap because the asset stays as `pathUSD`, then deposit that `pathUSD` into `Zone B`. ## Sending pathUSD from Zone A into Zone B By the end of this guide you will have sent **25 pathUSD** from **Zone A** into **Zone B** and confirmed the routed deposit. **Interactive demo: Send tokens across zones** 1. Send tokens across zones ## Code example This snippet assumes you already have a signed-in `rootClient` on the public chain plus `zoneAClient`, and the shared token, router, and portal constants used throughout the zone guides. It shows the core routed send submission path; use the demo above when you want to watch the routed deposit settle into Zone B. ```ts import { encodeAbiParameters, parseUnits } from 'viem' import { Actions } from 'viem/tempo' const transferAmount = parseUnits('25', 6) await zoneAClient.zone.signAuthorizationToken() const callbackData = encodeAbiParameters( [ { type: 'bool' }, { type: 'address' }, { type: 'address' }, { type: 'address' }, { type: 'bytes32' }, { type: 'uint128' }, ], [false, pathUsd, ZONE_B.portalAddress, rootClient.account.address, zeroBytes32, 0n], ) const { receipt } = await Actions.zone.requestWithdrawalSync(zoneAClient, { account: rootClient.account, amount: transferAmount, data: callbackData, feeToken: pathUsd, gas: routerCallbackGasLimit, timeout: zoneRpcSyncTimeout, to: swapAndDepositRouter, token: pathUsd, }) console.log(receipt.blockNumber) ``` ## What this routed send does The cross-zone transfer path looks like this: the token leaves `Zone A`, briefly lands on the public chain, and is deposited back into `Zone B` as the same asset. 1. Withdraws `pathUSD` from `Zone A` through `ZoneOutbox`. 2. Routes that withdrawal to `swapAndDepositRouter` on Tempo. 3. Skips the DEX swap because the input and output token are both `pathUSD`. 4. Deposits the routed `pathUSD` into `Zone B` through `ZonePortal`. The target deposit still pays the normal portal deposit fee, so the amount that arrives in `Zone B` is the routed `pathUSD` minus that fee. :::warning If the routed withdrawal fails on Tempo—for example because the callback reverts or the target deposit cannot be completed—the amount is bounced back to the withdrawal's `fallbackRecipient` inside `Zone A`. The fee is still paid to the sequencer. ::: # Swap across zones :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Use this guide when you want to leave `Zone A` with `pathUSD` and arrive in `Zone B` with `betaUSD` in one routed flow. The trade briefly touches the public chain, so the confirmation happens in stages rather than as a single balance update. The route uses `swapAndDepositRouter` on the public chain: withdraw from `Zone A`, swap on the Stablecoin DEX, then deposit the output token into `Zone B`. ![Cross-zone DEX swap flow](/developers/learn/zones/diagram-swap.svg) ## Swapping pathUSD from Zone A into betaUSD on Zone B By the end of this guide you will have swapped **25 pathUSD** from **Zone A** into **betaUSD** on **Zone B** and confirmed the routed deposit. ## What this swap does 1. Withdraws `pathUSD` from `Zone A`. 2. Routes it through the public chain and swaps it on the Stablecoin DEX. 3. Deposits the output token into `Zone B` through `ZonePortal`. 4. Lets you authorize private reads in `Zone B` so you can confirm the final `betaUSD` balance. **Interactive demo: Swap across zones** 1. Swap across zones ## Code example This snippet assumes you already have a signed-in `rootClient` on the public chain plus `zoneAClient`, and the shared token, router, and portal constants used throughout the zone guides. It shows the core routed swap submission path; use the demo above when you want to watch the output deposit settle into Zone B. ```ts import { encodeAbiParameters, parseUnits } from 'viem' import { Actions } from 'viem/tempo' const swapAmount = parseUnits('25', 6) await zoneAClient.zone.signAuthorizationToken() const routedWithdrawalFee = await zoneAClient.zone.getWithdrawalFee({ gas: routerCallbackGasLimit }) const quotedBetaOut = await rootClient.dex.getSellQuote({ amountIn: swapAmount, tokenIn: pathUsd, tokenOut: betaUsd, }) const minimumBetaOut = quotedBetaOut - quotedBetaOut / 100n const callbackData = encodeAbiParameters( [ { type: 'bool' }, { type: 'address' }, { type: 'address' }, { type: 'address' }, { type: 'bytes32' }, { type: 'uint128' }, ], [false, betaUsd, ZONE_B.portalAddress, rootClient.account.address, zeroBytes32, minimumBetaOut], ) const { receipt } = await Actions.zone.requestWithdrawalSync(zoneAClient, { account: rootClient.account, amount: swapAmount, data: callbackData, feeToken: pathUsd, gas: routerCallbackGasLimit, timeout: zoneRpcSyncTimeout, to: swapAndDepositRouter, token: pathUsd, }) console.log(receipt.blockNumber) ``` ## How Routed Zone Swaps Settle This guide's swap flow is asynchronous because the trade temporarily leaves the zone. The source token is withdrawn through `ZoneOutbox`, transferred to `SwapAndDepositRouter` on Tempo, optionally swapped on the Stablecoin DEX, and then deposited back through a `ZonePortal` as the output token. That routed deposit pays the normal portal deposit fee, so the amount that arrives on the zone is the post-fee output. :::warning If the routed withdrawal fails on Tempo - for example because the swap fails, the transfer fails, the router callback reverts, or the target deposit cannot be completed—the amount is bounced back to the withdrawal's `fallbackRecipient` inside the source zone. The fee is still paid to the sequencer, so a failed routed swap still results in fees for the sender. ::: # Withdraw pathUSD from a Tempo Zone on testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Use this guide when you want to move `pathUSD` out of `Zone A` and back to your public Tempo balance. ![Zone contract architecture](/developers/learn/zones/diagram-withdraw.svg) Direct withdrawals exit through `ZoneOutbox` on the zone chain. You submit the withdrawal request in the zone first, then wait for the public balance to increase after the batch settles. ## Withdrawing pathUSD from Zone A By the end of this guide you will have withdrawn `pathUSD` from `Zone A` and confirmed the balance update on the public chain. **Interactive demo: Withdraw from Zone A** 1. Withdraw from zone ## Code examples These snippets assume you already have a signed-in `rootClient` on the public chain, a derived `zoneAClient`, and the usual token constants in scope. Use the plaintext flow when normal withdrawal visibility is fine. Use the authenticated flow when the sender details should only be revealed to the holder of a `revealTo` public key. ### Plaintext ```ts import { parseUnits } from 'viem' import { Actions } from 'viem/tempo' const withdrawalAmount = parseUnits('100', 6) await zoneAClient.zone.signAuthorizationToken() const { receipt } = await Actions.zone.requestWithdrawalSync(zoneAClient, { account: rootClient.account, feeToken: pathUsd, amount: withdrawalAmount, token: pathUsd, to: rootClient.account.address, }) console.log(receipt.blockNumber) ``` ### Authenticated ```ts import { parseUnits } from 'viem' import { Actions } from 'viem/tempo' const withdrawalAmount = parseUnits('100', 6) const revealTo = '0x031dc147467e8f106eb22850fef549dc74b8f6634aeac554ebdd4ab896b67cdf68' // [!code focus] await zoneAClient.zone.signAuthorizationToken() const { receipt } = await Actions.zone.requestVerifiableWithdrawalSync(zoneAClient, { // [!code focus] account: rootClient.account, feeToken: pathUsd, amount: withdrawalAmount, revealTo, // [!code focus] token: pathUsd, to: rootClient.account.address, }) console.log(receipt.blockNumber) ``` ## What a Direct Withdrawal Does A direct withdrawal is the simplest way to exit a zone. You ask `ZoneOutbox` on the zone to burn the zone balance, include the request in the next withdrawal batch, and settle the amount back to a public Tempo address. Like deposits, withdrawals settle in phases. The request is accepted on the zone first, and the public balance changes later when the sequencer submits and processes the corresponding batch on Tempo. If Tempo-side processing fails, the withdrawal does not stay stuck in limbo. The protocol re-deposits the withdrawal amount back into the zone to the request's `fallbackRecipient`. The fee is still consumed. :::warning Even with `gasLimit: 0n`, a direct withdrawal can still fail on Tempo—for example because of token transfer or policy checks. In that case, the amount bounces back to `fallbackRecipient` on the zone instead of increasing the public balance. ::: # Client quickstart: build an MPP client Polyfill `fetch` to handle `402` responses. Your existing code works unchanged — payments happen in the background. ::::steps ### Install MPP client dependencies :::code-group ```bash [npm] npm install mppx viem ``` ```bash [pnpm] pnpm add mppx viem ``` ```bash [bun] bun add mppx viem ``` ::: ### Define the MPP payer account ```ts import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0xabc…123') ``` :::tip With Tempo, you can also use passkey or WebCrypto signing. ::: ### Create the MPP payment handler Call `Mppx.create` at startup. This polyfills `fetch` to automatically handle `402` payment challenges. ```ts import { privateKeyToAccount } from 'viem/accounts' import { Mppx, tempo } from 'mppx/client' const account = privateKeyToAccount('0xabc…123') Mppx.create({ methods: [tempo({ account })], }) ``` :::tip If you want to avoid polyfilling, use the bound `fetch` instead. ```ts const mppx = Mppx.create({ polyfill: false, methods: [tempo({ account })] }) const response = await mppx.fetch('https://api.example.com/resource') ``` ::: ### Request MPP-protected resources Use `fetch`. Payment happens when a server returns `402`. ```ts const response = await fetch('https://api.example.com/resource') ``` :::: ## Advanced MPP client patterns ### Use MPP with Wagmi You can inject a [Wagmi](https://wagmi.sh) connector into Mppx by passing the `getConnectorClient` function. :::code-group ```ts [example.ts] import { Mppx, tempo } from 'mppx/client' import { getConnectorClient } from 'wagmi/actions' import { config } from './config' Mppx.create({ methods: [tempo({ getClient: (parameters) => getConnectorClient(config, parameters), })], }) ``` ```ts [config.ts] import { createConfig, http } from 'wagmi' import { tempoModerato } from 'viem/chains' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempoModerato], transports: { [tempoModerato.id]: http(), }, }) ``` ::: ### Use per-request payer accounts Pass accounts on individual requests instead of at setup: ```ts import { privateKeyToAccount } from 'viem/accounts' import { Mppx, tempo } from 'mppx/client' const mppx = Mppx.create({ polyfill: false, methods: [tempo()] }) const response = await mppx.fetch('https://api.example.com/resource', { context: { account: privateKeyToAccount('0xabc…123'), } }) ``` ### Handle MPP payments manually Use `Mppx.create` for full control over the payment flow: * Present payment UI before paying * Implement custom retry logic * Handle credentials manually ```ts import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ polyfill: false, methods: [tempo()], }) const response = await fetch('https://api.example.com/resource') if (response.status === 402) { const credential = await mppx.createCredential(response, { account: privateKeyToAccount('0x...'), }) const paidResponse = await fetch('https://api.example.com/resource', { headers: { Authorization: credential }, }) } ``` ### Read MPP payment receipts On success, the server returns a `Payment-Receipt` header: ```ts import { Receipt } from 'mppx' const response = await fetch('https://api.example.com/resource') const receipt = Receipt.fromResponse(response) console.log(receipt.status) // success console.log(receipt.reference) // 0xtx789abc... ``` ## Next steps for MPP clients * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Add payment gating to your HTTP endpoints * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Charge per request with on-chain settlement * [Full SDK reference](https://mpp.dev/sdk/typescript/client/Mppx.create) — Complete mppx client API documentation # Agent quickstart: build an AI agent that pays for APIs The `tempo` CLI handles `402 Payment Required` responses the same way the client SDK does — but from a terminal, script, or AI agent, with zero integration code. ::::steps ### Install the Tempo CLI ```bash curl -fsSL https://tempo.xyz/install | bash ``` ### Log in to Tempo Wallet ```bash tempo wallet login tempo wallet whoami ``` `login` opens a browser flow that creates or connects a Tempo wallet. `whoami` confirms readiness and prints your address and balances. :::tip If your balance is zero, run `tempo wallet fund` before making requests. ::: ### Discover MPP services ```bash tempo wallet services --search ai tempo wallet services ``` The service directory shows endpoint URLs, HTTP methods, pricing, and request schemas — everything you need to construct a valid request. For MCP-capable agents, see [Discover MPP services](https://tempo.xyz/developers/docs/guide/machine-payments/discover-services) to connect the read-only services MCP server at `https://mpp.dev/mcp/services`. ### Preview the MPP request cost ```bash tempo request --dry-run -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` `--dry-run` validates the request and shows the payment cost without spending. ### Make an MPP paid request ```bash tempo request -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` `tempo request` sends the request, intercepts the `402` challenge, signs and submits the payment, and retries with the credential — all in one command. :::: ## Set up an AI agent Paste this into your agent to install Tempo's wallet and request skills: :::code-group ```txt [Claude Code] Read https://tempo.xyz/SKILL.md and set up tempo ``` ```txt [Amp] Read https://tempo.xyz/SKILL.md and set up tempo ``` ```txt [Codex] Read https://tempo.xyz/SKILL.md and set up tempo ``` ::: Once installed, the agent can discover services, preview costs, and make paid requests within scoped spending limits. ## Next steps for agentic payments * [Discover MPP services](https://tempo.xyz/developers/docs/guide/machine-payments/discover-services) — Connect agents to the mpp.dev catalog over MCP * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Add payment gating to your HTTP endpoints * [CLI reference](https://tempo.xyz/developers/docs/cli/wallet) — Complete tempo wallet and tempo request command reference * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Charge per request with on-chain settlement # Discover MPP services MPP service discovery helps agents find paid APIs before they make a request. Use it to rank services for a task, compare payment offers, inspect endpoint metadata, and build the next HTTP request. Discovery is advisory. The runtime `402 Payment Required` challenge from the target service is always the authoritative source of current payment terms. ## Discovery surfaces | Surface | URL | Use it for | | --- | --- | --- | | Web directory | [`https://mpp.dev/services`](https://mpp.dev/services) | Browse live services, categories, providers, endpoints, and examples. | | Public catalog API | [`https://mpp.dev/api/services`](https://mpp.dev/api/services) | Fetch the JSON catalog directly from scripts, CLIs, or custom agents. | | Services MCP | [`https://mpp.dev/mcp/services`](https://mpp.dev/mcp/services) | Let an MCP-capable agent rank services, inspect offers, get usage recipes, and fetch advisory OpenAPI summaries. | | Protocol reference | [`https://mpp.dev/advanced/discovery`](https://mpp.dev/advanced/discovery) | Learn how service providers publish discovery metadata. | Use this page as the agent-facing setup and recipe guide. Use `mpp.dev` for the live catalog, protocol reference, and service-provider discovery docs. The services MCP server is read-only. It does not register services, execute payments, sign transactions, or proxy paid API calls. ## Connect over MCP Use the Streamable HTTP endpoint: ```text https://mpp.dev/mcp/services ``` :::code-group ```bash [Claude Code] claude mcp add --transport http mpp-services https://mpp.dev/mcp/services ``` ```bash [Codex] codex mcp add mpp-services --url https://mpp.dev/mcp/services ``` ```bash [Amp] amp mcp add --transport http mpp-services https://mpp.dev/mcp/services ``` ```json [Cursor] { "mcpServers": { "mpp-services": { "url": "https://mpp.dev/mcp/services" } } } ``` ```json [Manual] { "mcpServers": { "mpp-services": { "url": "https://mpp.dev/mcp/services" } } } ``` ::: For AWS AgentCore, Bedrock agents, or another managed agent runtime, configure a remote MCP server with Streamable HTTP transport, no authentication, and the endpoint above. ## Smoke test with Inspector Use the MCP Inspector when you want to verify the server from the same environment as your agent: ```bash npx -y @modelcontextprotocol/inspector \ --cli \ --transport http \ --server-url https://mpp.dev/mcp/services ``` You can also smoke test with plain JSON-RPC: ```bash curl https://mpp.dev/mcp/services \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "my-agent", "version": "1.0.0" } } }' ``` Expected result: `serverInfo.name` is `mpp-services-mcp`, `instructions` mention that discovery is advisory, and `tools/list` includes `recommend_services`, `get_usage_recipe`, `search_offers`, and `get_catalog_status`. ## Recommended agent workflow ::::steps ### Rank services for the task Start with `recommend_services` when the agent has a task but not a provider. ```json { "name": "recommend_services", "arguments": { "task": "send a transactional email from an agent", "constraints": { "category": "ai", "method": "tempo", "limit": 5 } } } ``` The response includes ranked services, matched task terms, reasons, top payment offers, and suggested next MCP calls. ### Get a usage recipe After choosing a service, call `get_usage_recipe` to get payable endpoint candidates, target URLs, and the HTTP steps the agent should follow. ```json { "name": "get_usage_recipe", "arguments": { "service": "agentmail" } } ``` Use `route` when the agent already knows the endpoint: ```json { "name": "get_usage_recipe", "arguments": { "service": "agentmail", "route": "POST /v0/inboxes" } } ``` ### Inspect payment offers Use `search_offers` or `get_offers` when the agent needs endpoint-level payment terms. ```json { "name": "search_offers", "arguments": { "query": "web search", "category": "search", "method": "tempo", "dynamic": false, "limit": 10 } } ``` ### Inspect the API shape Fetch an advisory OpenAPI summary or registry-derived endpoint view before constructing the request body. ```json { "name": "get_openapi", "arguments": { "service": "agentmail" } } ``` ### Make the paid request Call the target service directly with an MPP-capable client such as [`tempo request`](https://tempo.xyz/developers/docs/cli/request). The service returns a `402` challenge if payment is required, and the client pays and retries. ```bash tempo request -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` :::warning The MCP server only helps with discovery and planning. The target service's runtime `402` challenge controls the final amount, currency, recipient, method, and credential flow. ::: :::: ## MPP service discovery example recipes | Agent task | Start with | Then call | | --- | --- | --- | | Send email or create an agent inbox | `recommend_services` with `task: "send email from an agent"` | `get_usage_recipe` for the selected service, then `get_openapi` | | Find an LLM or image model endpoint | `recommend_services` with `category: "ai"` | `search_offers` to compare fixed vs dynamic pricing | | Search the web or crawl pages | `recommend_services` with `category: "search"` | `get_usage_recipe` for the chosen search provider | | Map a `402` recipient back to services | `get_services_by_recipient` | `get_offers` and `get_service` for the matching provider | | Build custom catalog analytics | `get_facets` and `get_catalog_status` | Fetch [`https://mpp.dev/api/services`](https://mpp.dev/api/services) directly | ## Agent prompts for MPP discovery Give your agent a task and tell it to use the services MCP server first: ```txt Use the mpp-services MCP server to find a paid API for sending email from an agent. Rank options with recommend_services, inspect the best service with get_usage_recipe, then tell me the target endpoint and what the runtime 402 challenge will confirm. Do not execute payment. ``` ```txt Use the mpp-services MCP server to find AI model APIs that support Tempo payments. Compare endpoint offers, prefer active services with OpenAPI metadata, and summarize which route I should call with tempo request. Discovery is advisory. ``` ```txt Use the mpp-services MCP server to find web search or crawl APIs. Show the top three services, why each matched, whether pricing is fixed or dynamic, and the next MCP tool call you would make before constructing the HTTP request. ``` ## MCP tools for MPP service discovery | Tool | Purpose | | --- | --- | | `list_services` | List catalog services with id, name, URL, categories, integration, status, and description. | | `search_services` | Search services by text query and exact filters. | | `search_offers` | Search endpoint-level payment offers by task, method, currency, amount, recipient, and category. | | `recommend_services` | Rank paid API services for a natural-language agent task with optional exact constraints. | | `get_usage_recipe` | Turn a selected service into endpoint candidates, follow-up MCP calls, and target HTTP/`402` steps. | | `get_facets` | Discover valid filter values and counts before narrowing a search. | | `get_services_by_recipient` | Identify services that publish offers for a payment recipient. | | `get_catalog_status` | Inspect catalog version, source URL, cache age, refresh time, and service count. | | `get_service` | Fetch the full service record by id or name. | | `get_offers` | Fetch payment offers for one service, optionally filtered by route. | | `get_openapi` | Fetch a live OpenAPI summary when available, otherwise return the registry endpoint view. | ## Choosing a discovery surface Use the web directory when a human is exploring the catalog. Use the public API when you are building your own service browser, CLI, or analytics job. Use the MCP server when an agent needs tool-callable discovery inside its planning loop. For execution, call services directly with [`tempo request`](https://tempo.xyz/developers/docs/cli/request), the [MPP client quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/client), or another MPP-capable client. Discovery narrows the choice; the service's runtime `402` challenge confirms the exact payment terms. ## Next steps for MPP service discovery * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Use the Tempo CLI to discover services, preview costs, and make paid requests * [tempo request](https://tempo.xyz/developers/docs/cli/request) — Make HTTP requests that handle MPP payment automatically * [MPP discovery docs](https://mpp.dev/advanced/discovery) — Publish provider discovery metadata on mpp.dev # Server quickstart: add MPP payments to your server Plug MPP into any server framework to accept payments for protected resources. Use `mppx` middleware for your framework, or call `mppx/server` directly with the Fetch API. ## MPP server framework middleware Use the framework-specific middleware from `mppx` to integrate payment into your server. Each middleware handles the `402` challenge/credential flow and attaches receipts automatically. :::code-group ```ts [Next.js] import { Mppx, tempo } from 'mppx/nextjs' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) export const GET = mppx.charge({ amount: '0.1' }) (() => Response.json({ data: '...' })) ``` ```ts [Hono] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) app.get( '/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' }), ) ``` ```ts [Express] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) app.get( '/resource', mppx.charge({ amount: '0.1' }), (req, res) => res.json({ data: '...' })) ``` ::: :::tip You can override `currency` and `recipient` per call if different routes need different payment configurations. ```ts mppx.charge({ amount: '0.1', currency: '0x…', recipient: '0x…', }) ``` ::: ## Manual MPP server mode If you prefer full control over the payment flow, use `mppx/server` directly with the Fetch API. ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) export async function handler(request: Request) { const response = await mppx.charge({ amount: '0.1' })(request) // Payment required: send 402 response with challenge if (response.status === 402) return response.challenge // Payment verified: attach receipt and return resource return response.withReceipt(Response.json({ data: '...' })) } ``` :::info[Currency and recipient values] `currency` is the TIP-20 token contract address — [`0x20c0…`](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000?live=false) is PathUSD on Tempo. `recipient` is the address that receives payment. See the [Tempo payment method](https://mpp.dev/payment-methods/tempo) for supported tokens. ::: ## MPP payment realm The `realm` identifies your server in payment challenges and on-chain attribution. By default, mppx auto-detects it from environment variables (`HOSTNAME`, `VERCEL_URL`, etc.), but this can produce incorrect values in containerized environments where `HOSTNAME` is set to an internal identifier (e.g. a Kubernetes pod name). Set `realm` explicitly to your public domain: ```ts const mppx = Mppx.create({ realm: 'api.example.com', // [!code hl] methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) ``` You can also set the `MPP_REALM` environment variable instead of passing it in code. ## MPP push and pull modes Tempo charges support two transaction submission modes, determined by the client: * **`pull` mode (default)**: the client signs the transaction and sends the serialized transaction to the server. The server broadcasts it and verifies on-chain. This enables the server to sponsor gas fees via a `feePayer`. * **`push` mode**: the client builds, signs, and broadcasts the transaction itself (for example, via a browser wallet). It sends the transaction hash to the server, which verifies the payment by fetching the receipt. Your server handles both modes automatically — no configuration required. The server inspects the credential payload type (`transaction` for pull, `hash` for push) and verifies accordingly. ### MPP fee sponsorship To sponsor gas fees for pull-mode clients, pass a `feePayer` account to `tempo()`: ```ts import { Mppx, tempo } from 'mppx/server' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', feePayer: privateKeyToAccount('0x…'), recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) ``` When a pull-mode client submits a signed transaction, the server co-signs with the fee payer account before broadcasting. Push-mode clients pay their own gas, so `feePayer` is ignored for those requests. ## Test your MPP server After your server is running, test it with the `mppx` CLI: ```bash # Create an account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx /resource ``` :::tip Use `npx mppx --inspect` to debug your server's Challenge response without making any payments. ::: ## Next steps for MPP servers * [Client quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/client) — Handle payment-gated resources automatically * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — End-to-end guide with the charge intent * [Full SDK reference](https://mpp.dev/sdk/typescript/server/Mppx.create) — Complete mppx server API documentation # Accept one-time payments Build a payment-gated API that charges $0.01 per request using `mppx`. The server returns a random photo from [Picsum](https://github.com/DMarby/picsum-photos) behind a paywall. ## One-time payment server setup ::::steps ### Install one-time payment dependencies :::code-group ```bash [npm] npm install mppx viem ``` ```bash [pnpm] pnpm add mppx viem ``` ```bash [bun] bun add mppx viem ``` ::: ### Set up an `Mppx` charge instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) ``` ### Add a one-time payment route Add payment verification using `mppx.charge` as route middleware. The handler only runs after payment is verified. :::code-group ```ts [Next.js] import { Mppx, tempo } from 'mppx/nextjs' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) export const GET = mppx.charge({ amount: '0.01', description: 'Random stock photo' }) (async () => { const res = await fetch('https://picsum.photos/1024/1024') return Response.json({ url: res.url }) }) ``` ```ts [Hono] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) app.get( '/api/photo', mppx.charge({ amount: '0.01', description: 'Random stock photo' }), async (c) => { const res = await fetch('https://picsum.photos/1024/1024') return c.json({ url: res.url }) }, ) ``` ```ts [Express] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) app.get( '/api/photo', mppx.charge({ amount: '0.01', description: 'Random stock photo' }), async (req, res) => { const response = await fetch('https://picsum.photos/1024/1024') res.json({ url: response.url }) }, ) ``` ```ts [Fetch API] import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) Bun.serve({ async fetch(request) { const result = await mppx.charge({ amount: '0.01', description: 'Random stock photo', })(request) if (result.status === 402) return result.challenge const res = await fetch('https://picsum.photos/1024/1024') return result.withReceipt(Response.json({ url: res.url })) }, }) ``` ::: ### Test the one-time payment endpoint ```bash # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/photo ``` :::: ## Next steps for one-time payments * [Accept pay-as-you-go payments](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) — Session-based billing with payment channels * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Framework middleware reference * [Full charge reference](https://mpp.dev/payment-methods/tempo/charge) — Complete tempo.charge API documentation # Accept pay-as-you-go payments Use pay-as-you-go sessions when a customer should pay a small amount each time they use an API or service, without sending an on-chain transaction for every request. This guide uses a simple photo API as the example: the client opens one `mppx` session, then pays $0.01 each time it asks for a photo. `mppx` is the library that manages the MPP session; [Picsum](https://github.com/DMarby/picsum-photos) only supplies sample images for the demo. :::info Unlike [one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments), a session opens a payment channel once. Each later request is paid with a signed voucher that the server verifies off-chain. ::: ## How pay-as-you-go sessions work ```mermaid sequenceDiagram participant Client participant Server participant Tempo Client->>Tempo: (1) Deposit tokens Tempo-->>Client: Channel created Client->>Server: (2) Open credential Note over Server: Verify on-chain deposit Server-->>Client: 200 OK (session established) loop Per request Client->>Server: (3) Request + voucher Note over Server: ecrecover only Server-->>Client: 200 OK + Receipt end Note over Server: (4) Periodic settlement Server->>Tempo: settle(channelId, voucher) Client->>Server: (5) Close Server->>Tempo: close(channelId, voucher) Tempo-->>Client: Refund remaining deposit ``` 1. **Open** — Client deposits funds into an on-chain reserve contract, creating a payment channel 2. **Session** — Client signs EIP-712 vouchers with increasing cumulative amounts as service is consumed 3. **Top up** — If the channel runs low, the client deposits additional tokens without closing the channel 4. **Close** — Either party closes the channel, settling the final balance on-chain and refunding unused deposit :::info[T7 payment-channel savings] The [T7 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t7) can make repeated sessions cheaper for the same payer. When a payer closes or withdraws a finished channel, Tempo records a channel storage credit for that payer. If the same payer opens another channel later, Tempo can use that payer's credit. Other payers cannot use it. ::: The channel-reserve gas snapshot for the credited reopen path is: | Channel reserve snapshot | T7 gas | |--------------------------|-------:| | `open_new_channel_with_storage_credit` | 60,225 | This is a call-level gas number and excludes separate approval gas. It matters most for session services where the same payer opens, closes or withdraws, and later opens channels again. ## Pay-as-you-go server setup ::::steps ### Install pay-as-you-go dependencies :::code-group ```bash [npm] npm install mppx viem ``` ```bash [pnpm] pnpm add mppx viem ``` ```bash [bun] bun add mppx viem ``` ::: ### Set up an `Mppx` session instance Set up an `Mppx` instance with the `tempo` method. * `recipient` is the address where you receive payments. * `currency` is the token address for payments (in this case, `pathUSD`). ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) ``` ### Add a pay-as-you-go route Add payment verification using `mppx.session` as route middleware. The handler only runs after payment is verified. :::code-group ```ts [Next.js] import { Mppx, tempo } from 'mppx/nextjs' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) export const GET = mppx.session({ amount: '0.01', unitType: 'photo' }) (async () => { const res = await fetch('https://picsum.photos/200/200') return Response.json({ url: res.url }) }) ``` ```ts [Hono] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) app.get( '/api/sessions/photo', mppx.session({ amount: '0.01', unitType: 'photo' }), async (c) => { const res = await fetch('https://picsum.photos/200/200') return c.json({ url: res.url }) }, ) ``` ```ts [Express] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) app.get( '/api/sessions/photo', mppx.session({ amount: '0.01', unitType: 'photo' }), async (req, res) => { const response = await fetch('https://picsum.photos/200/200') res.json({ url: response.url }) }, ) ``` ```ts [Fetch API] import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', })], }) Bun.serve({ async fetch(request) { const result = await mppx.session({ amount: '0.01', unitType: 'photo', })(request) if (result.status === 402) return result.challenge const res = await fetch('https://picsum.photos/200/200') return result.withReceipt(Response.json({ url: res.url })) }, }) ``` ::: ### Test the pay-as-you-go endpoint ```bash # Create account funded with testnet tokens $ npx mppx account create # Make a paid request $ npx mppx http://localhost:3000/api/sessions/photo ``` :::: ## Pay-as-you-go client setup When using sessions from a client, set `maxDeposit` to enable automatic channel management. This is the maximum amount of tokens the client locks into the payment channel's reserve contract. Any unspent deposit is refunded when the channel closes. ```ts import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const mppx = Mppx.create({ methods: [tempo({ account: privateKeyToAccount('0x...'), maxDeposit: '1', // Lock up to 1 pathUSD per channel })], }) // Each fetch automatically manages the session lifecycle: // 1st request: opens channel on-chain, sends initial voucher // 2nd+ requests: sends off-chain vouchers (no on-chain tx) const res = await fetch('http://localhost:3000/api/sessions/photo') ``` * **`maxDeposit: '1'`** — Locks up to 1 pathUSD into the payment channel. At $0.01/photo, this covers up to 100 requests before the channel runs out. * The client handles the full session lifecycle automatically: channel open, voucher signing, and retry after `402` responses. * If the server sets `suggestedDeposit`, the client uses `min(suggestedDeposit, maxDeposit)`. ## Next steps for pay-as-you-go payments * [Accept streamed payments](https://tempo.xyz/developers/docs/guide/machine-payments/streamed-payments) — Per-token billing over Server-Sent Events * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Framework middleware reference * [Full session reference](https://mpp.dev/payment-methods/tempo/session) — Complete tempo.session API documentation # Accept streamed payments Build a payment-gated API that streams content word-by-word and charges $0.001 per word using `mppx` sessions with Server-Sent Events (SSE). :::info Streamed payments extend [pay-as-you-go sessions](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) with SSE. The server charges per token as content streams — if the channel balance runs out mid-stream, the client automatically sends a new voucher and the stream resumes. ::: ## How streamed payment sessions work ```mermaid sequenceDiagram participant Client participant Server participant Tempo Client->>Tempo: (1) Deposit tokens Tempo-->>Client: Channel created Client->>Server: (2) Open credential Note over Server: Verify on-chain deposit Server-->>Client: 200 OK (SSE stream begins) loop Per token streamed Server-->>Client: (3) SSE data event + charge Note over Server: ecrecover only end alt Channel balance low Server-->>Client: (4) payment-need-voucher event Client->>Server: New voucher Note over Server: Resume streaming end Note over Server: (5) Periodic settlement Server->>Tempo: settle(channelId, voucher) Client->>Server: (6) Close Server->>Tempo: close(channelId, voucher) Tempo-->>Client: Refund remaining deposit ``` 1. **Open** — Client deposits funds into an on-chain reserve contract, creating a payment channel 2. **Stream** — Server streams SSE events, calling `stream.charge()` per token to increment the voucher amount 3. **Top up** — If the channel runs low mid-stream, the server emits a `payment-need-voucher` event and the client automatically signs a new voucher 4. **Close** — Either party closes the channel, settling the final balance on-chain and refunding unused deposit ## Streamed payment server setup ::::steps ### Install streamed payment dependencies :::code-group ```bash [npm] npm install mppx viem ``` ```bash [pnpm] pnpm add mppx viem ``` ```bash [bun] bun add mppx viem ``` ::: ### Set up an `Mppx` streaming instance Set up an `Mppx` instance with `sse: true` to enable SSE support on the session method. ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', sse: true, })], }) ``` ### Add a streamed payment route The handler returns an async generator — each yielded value becomes one SSE event and is charged one tick ($0.001). If the channel balance runs out mid-stream, the server emits `event: payment-need-voucher` and pauses until the client sends a new voucher. :::code-group ```ts [Next.js] import { Mppx, tempo } from 'mppx/nextjs' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', sse: true, })], }) const poem = { title: 'The Road Not Taken', author: 'Robert Frost', lines: [ 'Two roads diverged in a yellow wood,', 'And sorry I could not travel both', 'And be one traveler, long I stood', 'And looked down one as far as I could', 'To where it bent in the undergrowth;', ], } export const GET = mppx.session({ amount: '0.001', unitType: 'word' }) (async () => { const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) return async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }) for (const word of words) { await stream.charge() yield word } } }) ``` ```ts [Hono] import { Hono } from 'hono' import { Mppx, tempo } from 'mppx/hono' const app = new Hono() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', sse: true, })], }) const poem = { title: 'The Road Not Taken', author: 'Robert Frost', lines: [ 'Two roads diverged in a yellow wood,', 'And sorry I could not travel both', 'And be one traveler, long I stood', 'And looked down one as far as I could', 'To where it bent in the undergrowth;', ], } app.get( '/api/sessions/poem', mppx.session({ amount: '0.001', unitType: 'word' }), async (c) => { const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) return async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }) for (const word of words) { await stream.charge() yield word } } }, ) ``` ```ts [Express] import express from 'express' import { Mppx, tempo } from 'mppx/express' const app = express() const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', sse: true, })], }) const poem = { title: 'The Road Not Taken', author: 'Robert Frost', lines: [ 'Two roads diverged in a yellow wood,', 'And sorry I could not travel both', 'And be one traveler, long I stood', 'And looked down one as far as I could', 'To where it bent in the undergrowth;', ], } app.get( '/api/sessions/poem', mppx.session({ amount: '0.001', unitType: 'word' }), async (req, res) => { const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) return async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }) for (const word of words) { await stream.charge() yield word } } }, ) ``` ```ts [Fetch API] import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', sse: true, })], }) const poem = { title: 'The Road Not Taken', author: 'Robert Frost', lines: [ 'Two roads diverged in a yellow wood,', 'And sorry I could not travel both', 'And be one traveler, long I stood', 'And looked down one as far as I could', 'To where it bent in the undergrowth;', ], } Bun.serve({ async fetch(request) { const result = await mppx.session({ amount: '0.001', unitType: 'word', })(request) if (result.status === 402) return result.challenge const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) return result.withReceipt(async function* (stream) { yield JSON.stringify({ title: poem.title, author: poem.author }) for (const word of words) { await stream.charge() yield word } }) }, }) ``` ::: ### Test the streamed payment endpoint ```bash # Create account funded with testnet tokens $ npx mppx account create # Stream a paid poem $ npx mppx http://localhost:3000/api/sessions/poem ``` :::: ## Streamed payment client setup Use `tempo.session()` from `mppx/client` to create a session manager. The `.sse()` method connects to the SSE endpoint and handles voucher renewal automatically — if the server requests a new voucher mid-stream, the client signs and sends one without interrupting the stream. ```ts import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const session = tempo.session({ account: privateKeyToAccount('0x...'), maxDeposit: '1', // Lock up to 1 pathUSD per channel }) // .sse() returns an async iterable of SSE data payloads const stream = await session.sse('http://localhost:3000/api/sessions/poem') for await (const word of stream) { process.stdout.write(word + ' ') } ``` * **`tempo.session()`** — Creates a session manager that handles the full channel lifecycle: open, voucher signing, and close. * **`.sse()`** — Connects to an SSE endpoint. Automatically sends new vouchers when the server emits `payment-need-voucher` events. * **`maxDeposit: '1'`** — Locks up to 1 pathUSD. At $0.001/word, this covers ~1,000 words before the channel needs a top-up. ## Next steps for streamed payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Charge per request with on-chain settlement * [Accept pay-as-you-go payments](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) — Session-based billing without streaming * [Full session reference](https://mpp.dev/payment-methods/tempo/session) — Complete tempo.session API documentation # Monetize your API with agentic payments Accept stablecoin payments for any HTTP endpoint. No signup flows, no billing accounts, no API key management. Your users — agents, apps, or humans — pay per request with stablecoins on Tempo, and you get paid instantly. ## The API monetization problem Monetizing an API today means building a billing system: user registration, API key provisioning, usage metering, invoicing, and payment collection. For most developers, the billing infrastructure is harder to build than the API itself. Stripe and similar tools help, but still require account creation and credit card onboarding from every customer. ## How MPP adds agentic API payments With MPP, you add a few lines of code to your HTTP endpoint and it becomes payment-gated. The server returns a `402` Challenge with the price, the client pays with a stablecoin transfer, and the server delivers the response with a `Receipt`. Settlement happens in ~500ms on Tempo. **Two billing models** are available out of the box: * **Tempo Charge** — One-time payment per request. Best for single API calls, content access, or discrete operations. * **Tempo Session** — Continuous pay-as-you-go via payment channels. Best for LLM APIs, metered services, or streamed responses. ## Add API payments in 5 minutes ::::steps ### Install the MPP SDK :::code-group ```bash [npm] npm install mppx viem ``` ```bash [pnpm] pnpm add mppx viem ``` ```bash [bun] bun add mppx viem ``` ::: ### Add MPP payment gating ```ts import { Mppx, tempo } from 'mppx/server' const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0000000000000000000000000000000000000', recipient: '0xYOUR_ADDRESS_HERE', })], }) export async function handler(request: Request) { const result = await mppx.charge({ amount: '0.01', description: 'API call', })(request) if (result.status === 402) return result.challenge // Your API logic here return result.withReceipt(Response.json({ data: '...' })) } ``` ### Test the paid API ```bash npx mppx account create npx mppx http://localhost:3000/api/endpoint ``` :::: ## Why Tempo for API monetization * **~500ms settlement** — Fast enough for synchronous request/response flows * **Sub-cent fees** — Charge $0.001 per request and still be profitable * **No customer onboarding** — Clients pay with a wallet, no signup required * **Fee sponsorship** — Cover gas fees for your customers so they only need stablecoins * **Any stablecoin** — Accept pathUSD, USDC.e, or any TIP-20 token ## Next steps for API monetization * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Step-by-step guide with framework examples * [Accept pay-as-you-go payments](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) — Session-based billing for metered services * [Accept streamed payments](https://tempo.xyz/developers/docs/guide/machine-payments/streamed-payments) — Pay-per-chunk for streaming responses * [Full MPP documentation](https://mpp.dev) — Protocol spec, SDK reference, and guides # Pay for AI models per request Give your agents access to any LLM — OpenAI, Anthropic, Gemini, DeepSeek, Mistral, and more — without managing API keys, billing accounts, or usage limits. MPP lets agents pay per token with stablecoins on Tempo, and the model provider gets paid instantly. ## The AI model payment problem Every LLM provider requires a separate API key, billing account, and credit card on file. For a single developer this is manageable. For a fleet of autonomous agents, it's a bottleneck: each agent needs its own credentials, each provider has different billing cycles, and rate limits are tied to account tiers rather than willingness to pay. ## How MPP enables paid AI model access With MPP, your agent holds a stablecoin balance on Tempo and pays per request. No signup, no API keys, no invoices. The agent discovers the model's price via the `402` Challenge, pays with a stablecoin transfer or session voucher, and gets the response — all in a single HTTP round-trip. **Tempo Sessions** are ideal for LLM access. The agent opens a payment channel once, then signs off-chain vouchers for each chunk of tokens received. The model provider verifies vouchers in microseconds — no blockchain calls during inference — and settles in batch later. This makes per-token billing practical without adding latency. ## AI model services you can pay for | Provider | Models | Service URL | |---|---|---| | OpenAI | GPT-4o, o3, DALL·E, Whisper | `openai.mpp.tempo.xyz` | | Anthropic | Claude Sonnet, Opus, Haiku | `anthropic.mpp.tempo.xyz` | | Google Gemini | Gemini, Veo video, image gen | `gemini.mpp.tempo.xyz` | | [DeepSeek](https://deepseek.mpp.paywithlocus.com) | DeepSeek-V3, R1 reasoning | `deepseek.mpp.paywithlocus.com` | | [Mistral](https://mistral.mpp.paywithlocus.com) | Large, Codestral, Pixtral | `mistral.mpp.paywithlocus.com` | | OpenRouter | 100+ models, unified API | `openrouter.mpp.tempo.xyz` | | Grok | xAI chat, search, code exec | `grok.mpp.tempo.xyz` | | [Perplexity](https://perplexity.mpp.paywithlocus.com) | Sonar search + grounding | `perplexity.mpp.paywithlocus.com` | ## Try paid AI model access with Tempo ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Call OpenAI with pay-per-request — no API key needed tempo request openai.mpp.tempo.xyz/v1/chat/completions \ -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}' ``` ## Prompt your agent to buy AI model access ``` Use openai.mpp.tempo.xyz to call GPT-4o via Tempo. Pay per request with stablecoins — no API key needed. ``` ## Next steps for AI model payments * [Accept pay-as-you-go payments](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) — Build session-based billing for your own LLM API * [Tempo Sessions](https://mpp.dev/payment-methods/tempo/session) — How payment channels enable per-token billing * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for web search and research Give your agents access to web search, content extraction, and multi-hop research — pay per query with stablecoins on Tempo. No API keys, no monthly plans, no usage caps. ## The web search payment problem Agents that research, summarize, or fact-check need reliable web search. But search APIs require developer accounts, billing setups, and API keys for every provider. Rate limits are tied to pricing tiers, and switching between providers means managing multiple integrations. ## How MPP enables paid web research With MPP, your agent pays per search query or page extraction in a single HTTP request. The agent discovers the price via the `402` Challenge, pays with a stablecoin transfer, and gets results — no signup required. Switch between search providers by changing the URL. **Tempo Charge** works well here: each search query is an independent request with a known price. The agent signs a TIP-20 transfer, the provider verifies it in ~500ms, and returns the results. ## Web research services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | [Parallel](https://parallelmpp.dev) | Web search, page extraction, multi-hop research | `parallelmpp.dev` | | Exa | AI-powered search, content retrieval, answers | `api.exa.ai` | | [Brave](https://brave.mpp.paywithlocus.com) | Web, news, images, videos, AI answers | `brave.mpp.paywithlocus.com` | | Firecrawl | Web scraping, crawling, structured extraction | `firecrawl.mpp.tempo.xyz` | | [Diffbot](https://diffbot.mpp.paywithlocus.com) | Article, product, and discussion extraction | `diffbot.mpp.paywithlocus.com` | | Oxylabs | Web scraping with geo-targeting and JS rendering | `oxylabs.mpp.tempo.xyz` | ## Try Parallel with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Search the web via Parallel tempo request parallelmpp.dev/search \ -d '{"query": "machine payments protocol"}' ``` ## Prompt your agent to buy web research ``` Use parallelmpp.dev to search the web via Tempo. Pay per query with stablecoins — no API key needed. ``` ## Next steps for web research payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated search API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for image, video, and audio generation Let your agents generate images, videos, audio, and speech — pay per generation with stablecoins on Tempo. Access 600+ models through fal.ai, or use OpenAI, Gemini, and Deepgram directly. ## The media generation payment problem Media generation APIs require separate accounts and API keys per provider. Costs vary wildly by model and resolution, making budgeting unpredictable. Agents that need to generate a product image, a voiceover, and a video clip in a single workflow must juggle three sets of credentials and billing systems. ## How MPP enables paid media generation With MPP, your agent pays per generation using stablecoins on Tempo. The price is declared upfront in the `402` Challenge — the agent knows exactly what it will pay before generating anything. One wallet, one balance, any provider. **Tempo Charge** is ideal for media generation: each request is a discrete unit with a known cost. The agent signs a TIP-20 transfer, the provider verifies it in ~500ms, and returns the generated media. ## Media generation services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | fal.ai | 600+ models: Flux, SD, Recraft, Grok | `fal.mpp.tempo.xyz` | | OpenAI | DALL·E image generation, Whisper audio | `openai.mpp.tempo.xyz` | | Google Gemini | Veo video, Nano Banana image gen | `gemini.mpp.tempo.xyz` | | [Deepgram](https://deepgram.mpp.paywithlocus.com) | Nova-3 transcription, Aura-2 TTS | `deepgram.mpp.paywithlocus.com` | | [Mathpix](https://mathpix.mpp.paywithlocus.com) | OCR for math, science docs, LaTeX | `mathpix.mpp.paywithlocus.com` | ## Try fal.ai with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Generate an image via fal.ai tempo request fal.mpp.tempo.xyz/fal-ai/flux/dev \ -d '{"prompt": "a futuristic city skyline at sunset"}' ``` ## Prompt your agent to buy media generation ``` Use fal.mpp.tempo.xyz to generate images via Tempo. Pay per image with stablecoins — no API key needed. ``` ## Next steps for media generation payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated media generation API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for browser automation and web scraping Let your agents run headless browser sessions, solve CAPTCHAs, and scrape web pages with geo-targeting — pay per task with stablecoins on Tempo. No API keys, no browser infrastructure to manage. ## The browser automation payment problem Browser automation at scale requires managing headless browser infrastructure, proxy networks, and CAPTCHA-solving services — each with separate accounts, API keys, and billing. Agents that need to interact with the web face a fragmented stack of tools, each requiring its own integration. ## How MPP enables paid browser automation With MPP, your agent pays per browser session, per page scrape, or per CAPTCHA solve using stablecoins on Tempo. The agent sends a request, the service returns its price via a `402` Challenge, the agent pays, and the work is done. No infrastructure setup, no API key management. ## Browser automation services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | [Browserbase](https://mpp.browserbase.com) | Headless browser sessions, web search, page fetching | `mpp.browserbase.com` | | 2Captcha | reCAPTCHA, Turnstile, hCaptcha, image captchas | `twocaptcha.mpp.tempo.xyz` | | Oxylabs | Web scraping with geo-targeting and JS rendering | `oxylabs.mpp.tempo.xyz` | | Firecrawl | Web crawling and structured data extraction | `firecrawl.mpp.tempo.xyz` | | [Diffbot](https://diffbot.mpp.paywithlocus.com) | Article, product, and discussion extraction | `diffbot.mpp.paywithlocus.com` | ## Try Oxylabs with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Scrape a page via Oxylabs tempo request oxylabs.mpp.tempo.xyz/v1/queries \ -d '{"source": "universal", "url": "https://example.com"}' ``` ## Prompt your agent to buy browser automation ``` Use mpp.browserbase.com to run headless browser sessions via Tempo. Pay per session with stablecoins — no account needed. ``` ## Next steps for browser automation payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated scraping API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for compute and code execution Let your agents run code in sandboxed environments, deploy containers, and access GPU compute — pay per use with stablecoins on Tempo. No cloud accounts, no billing dashboards, no credit cards. ## The compute payment problem Cloud compute requires account creation, billing setup, and credential management before running a single line of code. For agents that need to execute code snippets, deploy temporary services, or run GPU workloads, the overhead of provisioning cloud accounts is disproportionate to the task. Sandbox environments like CodeSandbox or Replit target human developers, not programmatic access. ## How MPP enables paid code execution With MPP, your agent pays per execution or per compute-minute with stablecoins on Tempo. Submit code, pay, get results — all in one HTTP request. No accounts, no API keys, no cloud provider onboarding. **Tempo Charge** works well for discrete tasks like running a code snippet. For longer-running workloads or metered compute, **Tempo Sessions** let the agent pay incrementally as resources are consumed. ## Compute services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | Modal | Serverless GPU compute, AI/ML workloads | `modal.mpp.tempo.xyz` | | [Judge0](https://judge0.mpp.paywithlocus.com) | Code execution in 60+ languages, sandboxed | `judge0.mpp.paywithlocus.com` | | [Build With Locus](https://mpp.buildwithlocus.com) | Containers, Postgres, Redis, custom domains | `mpp.buildwithlocus.com` | ## Try Judge0 with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Execute Python code via Judge0 tempo request judge0.mpp.paywithlocus.com/submissions \ -d '{"source_code": "print(42)", "language_id": 71}' ``` ## Prompt your agent to buy code execution ``` Use judge0.mpp.paywithlocus.com to run Python code via Tempo. Pay per execution with stablecoins — no account needed. ``` ## Next steps for compute payments * [Accept pay-as-you-go payments](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) — Build session-based billing for metered compute * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Add payment gating to your own compute API * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for object storage and Git repos Let your agents store files, upload objects, and create Git repositories — pay per operation with stablecoins on Tempo. No AWS accounts, no access keys, no billing dashboards. ## The storage payment problem Cloud storage requires account creation, IAM configuration, and billing setup before an agent can store a single byte. Even "simple" object storage like S3 requires access keys, bucket policies, and region selection. For agents that need temporary or task-specific storage, the overhead of cloud account provisioning is disproportionate to the need. ## How MPP enables paid object storage With MPP, your agent pays per upload or per repo creation using stablecoins. The storage service publishes its pricing via the `402` Challenge, the agent pays, and the operation completes. No cloud credentials, no IAM roles. ## Storage services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | Object Storage | S3/R2-compatible storage, dynamic per-size pricing | `storage.mpp.tempo.xyz` | | Code Storage | Paid Git repo creation, authenticated clone URLs | `codestorage.mpp.tempo.xyz` | ## Try object storage with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Upload a file to object storage tempo request storage.mpp.tempo.xyz/upload \ -F "file=@document.pdf" ``` ## Prompt your agent to buy storage ``` Use storage.mpp.tempo.xyz to upload files via Tempo. Pay per upload with stablecoins — no cloud account needed. ``` ## Next steps for storage payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated storage service * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Add payment gating to your own storage API * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for blockchain data and analytics Access on-chain data — token prices, wallet analytics, DEX trades, stablecoin flows, and smart contract events — from any MPP-enabled blockchain data provider. Pay per query with stablecoins on Tempo. ## The blockchain data payment problem Blockchain data APIs gate access behind API keys, developer accounts, and monthly subscription tiers. Agents that need to query multiple providers — one for wallet balances, another for DEX analytics, a third for historical prices — must manage separate credentials and billing for each. Free tiers run out fast, and upgrading means committing to monthly plans before knowing actual usage. ## How MPP enables paid blockchain data With MPP, your agent pays per query. No accounts, no API keys, no tier commitments. The agent calls any blockchain data provider, pays with a stablecoin transfer on Tempo, and gets the data back in the same response. **Tempo Charge** is a natural fit for data queries: each request has a known cost, settlement takes ~500ms, and the agent only pays for queries it actually makes. ## Blockchain data services you can pay for | Provider | Data | Service URL | |---|---|---| | [Alchemy](https://mpp.alchemy.com) | Core RPC, prices, portfolios, NFTs across 100+ chains | `mpp.alchemy.com` | | [Allium](https://agents.allium.so) | Token prices, wallet balances, transactions, PnL, SQL | `agents.allium.so` | | [Nansen](https://api.nansen.ai) | Smart money, wallet profiling, DEX trades, flow analysis | `api.nansen.ai` | | [Dune](https://api.dune.com) | Raw transactions, decoded events, stablecoin flows, DeFi | `api.dune.com` | | [Codex](https://graph.codex.io) | Token data, prediction markets, charts, wallet analytics | `graph.codex.io` | ## Try Alchemy with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Query blockchain data via Alchemy tempo request mpp.alchemy.com/v2 \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` ## Prompt your agent to buy blockchain data ``` Use agents.allium.so to query token prices and wallet balances via Tempo. Pay per query with stablecoins — no API key needed. ``` ## Next steps for blockchain data payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated data API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for financial and market data Access real-time stock prices, forex rates, crypto market data, SEC filings, and economic indicators — pay per request with stablecoins on Tempo. No API keys, no subscriptions, no billing accounts. ## The financial data payment problem Financial data is fragmented across dozens of providers, each with its own API key, pricing tier, and billing cycle. Agents that need multi-source data — stock prices from one provider, SEC filings from another, crypto data from a third — face credential sprawl and unpredictable monthly bills. Free tiers are limited, and most providers require credit cards before returning a single data point. ## How MPP enables paid financial data With MPP, your agent pays per data request using stablecoins on Tempo. No signup, no API keys. Point your agent at any MPP-enabled financial data provider, and it handles the rest: price discovery via the `402` Challenge, payment via a TIP-20 transfer, and data delivery in the same response. ## Financial data services you can pay for | Provider | Data | Service URL | |---|---|---| | [Alpha Vantage](https://alphavantage.mpp.paywithlocus.com) | Stocks, forex, crypto, commodities, technicals | `alphavantage.mpp.paywithlocus.com` | | [CoinGecko](https://coingecko.mpp.paywithlocus.com) | Crypto prices, market cap, exchanges, trending | `coingecko.mpp.paywithlocus.com` | | [EDGAR (SEC)](https://edgar.mpp.paywithlocus.com) | Company filings, XBRL financials, full-text search | `edgar.mpp.paywithlocus.com` | | [EDGAR Full-Text](https://edgar-search.mpp.paywithlocus.com) | 10-K, 10-Q, 8-K, proxy statement search | `edgar-search.mpp.paywithlocus.com` | | [Exchange Rates](https://abstract-exchange-rates.mpp.paywithlocus.com) | Live and historical FX rates for 150+ currencies | `abstract-exchange-rates.mpp.paywithlocus.com` | ## Try Alpha Vantage with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Get stock data via Alpha Vantage tempo request alphavantage.mpp.paywithlocus.com/query \ -d '{"function": "TIME_SERIES_DAILY", "symbol": "AAPL"}' ``` ## Prompt your agent to buy financial data ``` Use edgar.mpp.paywithlocus.com to search SEC filings via Tempo. Pay per query with stablecoins — no API key needed. ``` ## Next steps for financial data payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated financial data API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for data enrichment and lead generation Let your agents enrich contacts, find emails, profile companies, and search LinkedIn — pay per lookup with stablecoins on Tempo. No API keys, no monthly minimums, no seat-based pricing. ## The data enrichment payment problem Sales and marketing data APIs charge monthly subscriptions with per-seat pricing, even when usage is sporadic. An agent that enriches 50 leads one week and zero the next still pays the same monthly fee. Each provider requires its own account, API key, and billing setup. ## How MPP enables paid data enrichment With MPP, your agent pays per enrichment request. Look up a company, enrich a contact, verify an email — each operation is a single paid HTTP request. No contracts, no minimums, no wasted spend during quiet periods. ## Data enrichment services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | [Apollo](https://apollo.mpp.paywithlocus.com) | 275M+ contacts, company enrichment, lead search | `apollo.mpp.paywithlocus.com` | | [Hunter](https://hunter.mpp.paywithlocus.com) | Email finding, verification, company enrichment | `hunter.mpp.paywithlocus.com` | | [Clado](https://clado.mpp.paywithlocus.com) | People search, LinkedIn enrichment, deep research | `clado.mpp.paywithlocus.com` | | [Company Enrichment](https://abstract-company-enrichment.mpp.paywithlocus.com) | Company data from domain name | `abstract-company-enrichment.mpp.paywithlocus.com` | | [Email Reputation](https://abstract-email-reputation.mpp.paywithlocus.com) | Email reputation and risk scoring | `abstract-email-reputation.mpp.paywithlocus.com` | | [BuiltWith](https://builtwith.mpp.paywithlocus.com) | Technology profiling for 100M+ websites | `builtwith.mpp.paywithlocus.com` | ## Try Apollo with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Enrich a company via Apollo tempo request apollo.mpp.paywithlocus.com/v1/organizations/enrich \ -d '{"domain": "tempo.xyz"}' ``` ## Prompt your agent to buy lead enrichment ``` Use apollo.mpp.paywithlocus.com to enrich company data via Tempo. Pay per lookup with stablecoins — no API key needed. ``` ## Next steps for data enrichment payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated enrichment API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for translation and language services Let your agents translate text across 30+ languages, transcribe audio, generate speech, and analyze sentiment — pay per request with stablecoins on Tempo. ## The translation payment problem Language APIs require separate developer accounts for each provider, with usage tied to monthly subscription tiers. An agent that needs to translate a document, transcribe a call, and generate a voiceover must manage three sets of credentials and billing cycles. Most providers don't offer true pay-per-use pricing. ## How MPP enables paid translation services With MPP, your agent pays per translation, per transcription, or per TTS request using stablecoins. One Tempo wallet works across all language service providers. The agent pays only for what it uses — no monthly commitments, no unused credits. ## Translation services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | [DeepL](https://deepl.mpp.paywithlocus.com) | 30+ languages, professional quality translation | `deepl.mpp.paywithlocus.com` | | [Deepgram](https://deepgram.mpp.paywithlocus.com) | Nova-3 transcription, Aura-2 TTS, sentiment | `deepgram.mpp.paywithlocus.com` | | OpenAI | Whisper transcription, TTS | `openai.mpp.tempo.xyz` | ## Try DeepL with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Translate text via DeepL tempo request deepl.mpp.paywithlocus.com/v2/translate \ -d '{"text": ["Hello, world!"], "target_lang": "DE"}' ``` ## Prompt your agent to buy translation ``` Use deepl.mpp.paywithlocus.com to translate text via Tempo. Pay per request with stablecoins — no API key needed. ``` ## Next steps for translation payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated translation API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for maps, geocoding, and location data Let your agents geocode addresses, get directions, check weather, and track flights — pay per request with stablecoins on Tempo. No Google Cloud accounts, no Mapbox tokens, no API keys. ## The location data payment problem Location APIs require cloud platform accounts with billing verification before returning data. Google Maps requires a Google Cloud project with a credit card. Mapbox requires a developer account with access tokens. Weather and flight APIs have their own signup flows. For agents that need location data as part of a larger workflow, this credential overhead is a barrier. ## How MPP enables paid location data With MPP, your agent pays per geocode, per route, or per weather query using stablecoins. No cloud accounts, no access tokens. The agent makes a request, the service returns its price, the agent pays, and the data arrives. ## Location data services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | Google Maps | Geocoding, directions, places, routes, weather | `googlemaps.mpp.tempo.xyz` | | [Mapbox](https://mapbox.mpp.paywithlocus.com) | Geocoding, directions, isochrones, static maps | `mapbox.mpp.paywithlocus.com` | | [OpenWeather](https://openweather.mpp.paywithlocus.com) | Current weather, forecasts, air quality, alerts | `openweather.mpp.paywithlocus.com` | | [StableTravel](https://stabletravel.dev) | Flights, hotels, activities, transfers, tracking | `stabletravel.dev` | | FlightAPI | Flight prices, tracking, airport schedules | `flightapi.mpp.tempo.xyz` | | [IPinfo](https://ipinfo.mpp.paywithlocus.com) | IP geolocation, ASN, privacy detection | `ipinfo.mpp.paywithlocus.com` | ## Try Google Maps with Tempo payments ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Geocode an address via Google Maps tempo request googlemaps.mpp.tempo.xyz/maps/api/geocode/json \ -d '{"address": "1600 Amphitheatre Parkway, Mountain View, CA"}' ``` ## Prompt your agent to buy location data ``` Use googlemaps.mpp.tempo.xyz to geocode addresses via Tempo. Pay per request with stablecoins — no API key needed. ``` ## Next steps for location data payments * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Build a payment-gated location API * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Set up your agent to discover and pay for services * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # Pay for agent-to-agent services Let your agents hire other agents — for coding, design, writing, research, and email — pay per task with stablecoins on Tempo. No platform accounts, no human intermediaries. ## The agent-to-agent payment problem Agent-to-agent commerce barely exists today. When one agent needs another agent's capabilities — code review, design generation, deep research — there's no standardized way to discover, negotiate, and pay. Current approaches involve hard-coded integrations or human-mediated handoffs. ## How MPP coordinates agent-to-agent services MPP provides the payment layer for agent-to-agent commerce. Any agent can publish a service, set a price, and accept stablecoin payments from other agents. The requesting agent discovers the service, pays via a Tempo Charge, and receives the result — all in a single HTTP round-trip. No accounts, no API keys, no human involvement. ## Agent-to-agent services you can pay for | Provider | Capabilities | Service URL | |---|---|---| | [Auto.exchange](https://api.auto.exchange) | Discover and hire agents for coding, design, writing | `api.auto.exchange` | | [AgentMail](https://mpp.api.agentmail.to) | Email inboxes for AI agents | `mpp.api.agentmail.to` | ## Try agent-to-agent hiring with Tempo ```bash # Install Tempo CLI + wallet curl -L https://tempo.xyz/install | bash && tempo add request && tempo wallet login # Hire an agent via Auto.exchange tempo request api.auto.exchange/v1/tasks \ -d '{"task": "Review this Python function for bugs", "code": "def add(a, b): return a - b"}' ``` ## Prompt your agent to hire another agent ``` Use api.auto.exchange to hire another agent via Tempo. Pay per task with stablecoins — no account needed. ``` ## Next steps for agent-to-agent payments * [Server quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Publish your own agent as an MPP service * [Accept one-time payments](https://tempo.xyz/developers/docs/guide/machine-payments/one-time-payments) — Charge per task with Tempo Charge * [Browse all services](https://mpp.dev/services) — 85+ MPP-enabled services in the directory # How to connect to the Tempo network You can connect with Tempo like you would with any other EVM chain. Tempo Mainnet has been live since March 18, 2026. Use mainnet, chain ID `4217`, for Tempo Wallet, production assets such as pathUSD, and live payment flows. Moderato, chain ID `42431`, is the separate public testnet for development. ## Connect using a Browser Wallet Click on your browser wallet below to automatically connect it to the Tempo network. Connect a wallet in the interactive web page. :::warning Note that on some wallets, you might see an unusually high "balance". This is because, historically, blockchain wallets have always assumed that a blockchain has a "native gas token". On Tempo, there is no native gas token, and so the value shown is a placeholder. See [EVM Differences](https://tempo.xyz/developers/docs/quickstart/evm-compatibility#handling-eth-native-token-balance-checks) for more information on this quirk. ::: ## Connect via CLI To connect via CLI, we recommend using [`cast`](https://getfoundry.sh/cast/overview/), which is a command-line tool for interacting with Ethereum networks. To install cast, you can read more in the [Foundry SDK docs](https://tempo.xyz/developers/docs/sdk/foundry#get-started-with-foundry). ```bash /dev/null/monitor.sh#L1-11 # Check block height (should be steadily increasing) cast block-number --rpc-url https://rpc.tempo.xyz ``` ## Direct Connection Details ### Mainnet | **Property** | **Value** | |-------------------|-------| | **Network Name** | Tempo Mainnet | | **Currency** | `USD` | | **Chain ID** | `4217` | | **HTTP URL** | `https://rpc.tempo.xyz` | | **WebSocket URL** | `wss://rpc.tempo.xyz` | | **Block Explorer** | [`https://explore.tempo.xyz`](https://explore.tempo.xyz) | ### Tempo Testnet | **Property** | **Value** | |-------------------|-------| | **Network Name** | Tempo Testnet (Moderato) | | **Currency** | `USD` | | **Chain ID** | `42431` | | **HTTP URL** | `https://rpc.moderato.tempo.xyz` | | **WebSocket URL** | `wss://rpc.moderato.tempo.xyz` | | **Block Explorer** | [`https://explore.testnet.tempo.xyz`](https://explore.testnet.tempo.xyz) | # Using Tempo Transactions: integration guides Tempo Transactions are a new [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transaction type, exclusively available on Tempo. :::note[SDKs Support] Transaction [SDKs](#integration-guides) are available for TypeScript, Rust, Go, Python, and Foundry. ::: If you're integrating with Tempo, we **strongly recommend** using Tempo Transactions, and not regular Ethereum transactions. Learn more about the benefits below, or follow the guide on issuance [here](https://tempo.xyz/developers/docs/guide/issuance). * [Configurable Fee Tokens](#configurable-fee-tokens) — Pay transaction fees with any USD-denominated TIP-20 token via automatic Fee AMM conversion. * [Fee Sponsorship](#fee-sponsorship) — Sponsor gas fees for users, enabling feeless transaction experiences in your application. * [Batch Calls](#batch-calls) — Batch multiple transactions together for higher throughput and simpler wallet management. * [Access Keys](#access-keys) — Delegate transaction signing capabilities to specific keys with customizable permissions. * [Concurrent Transactions](#concurrent-transactions) — Execute transactions in parallel using independent nonces for improved throughput. * [Expiring Nonces](#expiring-nonces) — Create nonces that automatically expire after a set time window. * [2D Nonces](#2d-nonces) — Use two-dimensional nonces for flexible transaction ordering. * [Scheduled Transactions](#scheduled-transactions) — Schedule transactions to execute within a specific time window for automated payments. ## Integration Guides Integrating Tempo Transactions is easy and can be done quickly by a developer in multiple languages. See below for quick links to some of our guides. |Language|Source|Integration Time| |--------|--------|--------| | **TypeScript** | [tempoxyz/tempo-ts](https://tempo.xyz/developers/docs/sdk/typescript) | \< 1 hour | | **Rust** | [tempo-alloy](https://tempo.xyz/developers/docs/sdk/rust) | \< 1 hour | | **Golang** | [tempo-go](https://github.com/tempoxyz/tempo-go) | \< 1 hour | | **Python** | [pytempo](https://github.com/tempoxyz/pytempo) | \< 1 hour | | **Other Languages** | Reach out to us! The specification is [here](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction) and easy to build against. | 1-3 days | If you are an EVM smart contract developer, see the [Foundry guide for Tempo](https://tempo.xyz/developers/docs/sdk/foundry). ## Properties ### Configurable Fee Tokens A fee token is a permissionless [TIP-20 token](https://tempo.xyz/developers/docs/protocol/tip20/overview) that can be used to pay fees on Tempo. When a TIP-20 token is passed as the `fee_token` parameter in a transaction, Tempo's [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) automatically facilitates conversion between the user's preferred fee token and the validator's preferred token. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const alphaUsd = '0x20c0000000000000000000000000000000000001' const receipt = await client.sendTransactionSync({ data: '0xdeadbeef', feeToken: alphaUsd, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransactionSync } from 'wagmi' const { sendTransactionSync } = useSendTransactionSync() const alphaUsd = '0x20c0000000000000000000000000000000000001' sendTransactionSync({ data: '0xdeadbeef', feeToken: alphaUsd, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); let pending = provider .send_transaction( TempoTransactionRequest::default() .with_fee_token(alpha_usd) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef")), ) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account alpha_usd = "0x20c0000000000000000000000000000000000001" tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), fee_token=alpha_usd, # [!code hl] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef", ), ), ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetFeeToken(transaction.AlphaUSDAddress). // [!code hl] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef"), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.fee-token 0x20c0000000000000000000000000000000000001 # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, // [!code focus] fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` :::info See a full guide on [paying fees in any stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin). ::: ### Fee Sponsorship Fee sponsorship enables a third party (the fee payer) to pay transaction fees on behalf of the transaction sender. The process uses dual signature domains: the sender signs their transaction, and then the fee payer signs over the transaction with a special "fee payer envelope" to commit to paying fees for that specific sender. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' import { privateKeyToAccount } from 'viem/accounts' const feePayer = privateKeyToAccount('0x...') const receipt = await client.sendTransactionSync({ data: '0xdeadbeef', feePayer, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransactionSync } from 'wagmi' import { privateKeyToAccount } from 'viem/accounts' export const feePayer = privateKeyToAccount('0x...') const { sendTransactionSync } = useSendTransactionSync() sendTransactionSync({ data: '0xdeadbeef', feePayer, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use alloy::signers::{SignerSync, local::PrivateKeySigner}; use tempo_alloy::primitives::transaction::tempo_transaction::Call; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let tx = TempoTransactionRequest { calls: vec![Call { to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), value: U256::ZERO, input: bytes!("deadbeef"), }], ..Default::default() }; // Step 1: Build the transaction let mut tempo_tx = provider.fill(tx).await?.build_aa()?; let sender_addr = provider.default_signer_address(); let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); // Step 2: Fee payer counter-signs the transaction // [!code hl] let fee_payer: PrivateKeySigner = "0x...".parse()?; // [!code hl] tempo_tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&fee_payer_hash)?); // [!code hl] // Step 3: Broadcast let pending = provider.send_transaction(tempo_tx).await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account fee_payer_key = "0x..." # Sender signs with awaiting_fee_payer flag tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), awaiting_fee_payer=True, # [!code hl] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef", ), ), ) sender_signed = tx.sign(account.key.hex()) # Fee payer co-signs the transaction // [!code hl] fully_signed = sender_signed.sign(fee_payer_key, for_fee_payer=True) # [!code hl] tx_hash = w3.eth.send_raw_transaction(fully_signed.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { senderSgn, _ := signer.NewSigner("0x...") sponsorSgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) // Sender builds and signs a sponsored transaction tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetSponsored(true). // [!code hl] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef"), ). Build() _ = transaction.SignTransaction(tx, senderSgn) // Fee payer co-signs the transaction // [!code hl] tx.FeeToken = transaction.AlphaUSDAddress // [!code hl] tx.AwaitingFeePayer = false // [!code hl] _ = transaction.AddFeePayerSignature(tx, sponsorSgn) // [!code hl] serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash # 1. Get the fee payer signature hash $ FEE_PAYER_HASH=$(cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef \ --rpc-url $TEMPO_RPC_URL \ --private-key $SENDER_KEY \ --tempo.print-sponsor-hash) # [!code hl] # 2. Sponsor signs the hash $ SPONSOR_SIG=$(cast wallet sign \ --private-key $SPONSOR_KEY \ "$FEE_PAYER_HASH" \ --no-hash) # [!code hl] # 3. Send with sponsor signature $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef \ --rpc-url $TEMPO_RPC_URL \ --private-key $SENDER_KEY \ --tempo.sponsor-signature "$SPONSOR_SIG" # [!code hl] ``` #### RLP ```tsx // 1. User signs over `user_envelope` // [!code focus] user_envelope = 0x77 ∥ rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, 0x00, // indicate intention for a fee payer // [!code focus] aa_authorization_list, key_authorization ]) // 2. Fee payer signs over `fee_payer_envelope` // [!code focus] fee_payer_envelope = 0x76 ∥ rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, sender_address, // scope to sender // [!code focus] aa_authorization_list, key_authorization ]) // 3. Construct + send off `final_envelope` to the network // [!code focus] final_envelope = 0x77 ∥ rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, fee_payer_signature, // signature over `fee_payer_envelope` // [!code focus] aa_authorization_list, key_authorization, signature, // signature over `user_envelope` // [!code focus] ]) ``` :::tip You can also use a remote [fee payer relay](https://tempo.xyz/developers/docs/api/fee-payer) instead of a local account. ::: :::tip For demos and testnet development, use the public fee payer endpoint at `https://sponsor.moderato.tempo.xyz` without an API key. For authenticated sandbox and production sponsorship, use the [Fee Payer API](https://tempo.xyz/developers/docs/api/fee-payer). ::: :::info See a full guide on [sponsoring fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees). ::: ### Batch Calls Batch calls enable multiple operations to be executed atomically within a single transaction. Instead of sending separate transactions for each operation, you can bundle multiple calls together using the `calls` parameter. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const receipt = await client.sendTransactionSync({ calls: [ // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] ] // [!code hl] }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransactionSync } from 'wagmi' const { sendTransactionSync } = useSendTransactionSync() sendTransactionSync({ calls: [ // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] ] // [!code hl] }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::primitives::transaction::Call; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let pending = provider .send_transaction(TempoTransactionRequest { calls: vec![ // [!code hl] Call { // [!code hl] to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] value: U256::ZERO, // [!code hl] input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] }, // [!code hl] Call { // [!code hl] to: address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").into(), // [!code hl] value: U256::ZERO, // [!code hl] input: bytes!("cafebabe0000000000000000000000000000000001"), // [!code hl] }, // [!code hl] Call { // [!code hl] to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] value: U256::ZERO, // [!code hl] input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] }, // [!code hl] ], // [!code hl] ..Default::default() }) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=600_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=( # [!code hl] Call.create( # [!code hl] to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] ), # [!code hl] Call.create( # [!code hl] to="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", # [!code hl] data="0xcafebabe0000000000000000000000000000000001", # [!code hl] ), # [!code hl] Call.create( # [!code hl] to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] ), # [!code hl] ), # [!code hl] ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(600_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( // [!code hl] common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] ). // [!code hl] AddCall( // [!code hl] common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("cafebabe0000000000000000000000000000000001"), // [!code hl] ). // [!code hl] AddCall( // [!code hl] common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] ). // [!code hl] Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast batch-send \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" \ --call "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef::setNumber(uint256):500" \ --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, // [!code focus] access_list, nonce_key, nonce, valid_before, valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` ### Access Keys Access keys enable you to delegate signing authority from a primary account to a secondary key, such as device-bound non-extractable [WebCrypto key](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair). The primary account signs a key authorization that grants the access key permission to sign transactions on its behalf. This authorization is then attached to the next transaction (that can be signed by either the primary or the access key), then all transactions thereafter can be signed by the access key. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { parseUnits } from 'viem' import { Account, P256 } from 'viem/tempo' import { client } from './viem.config' const account = Account.fromSecp256k1('0x...') const alphaUsd = '0x20c0000000000000000000000000000000000001' const treasury = '0xcafebabecafebabecafebabecafebabecafebabe' const accessKey = Account.fromP256(P256.randomPrivateKey(), { access: account, }) const keyAuthorization = await account.signKeyAuthorization(accessKey, { chainId: BigInt(client.chain.id), expiry: Math.floor(Date.now() / 1000) + 3600, limits: [ { token: alphaUsd, limit: parseUnits('1000', 6), period: 60 * 60 * 24 * 30, }, ], scopes: [ { address: alphaUsd, selector: 'transfer(address,uint256)', recipients: [treasury], }, ], }) // `keyAuthorization` provisions the access key and uses it in this same transaction. const receipt = await client.sendTransactionSync({ account: accessKey, // [!code hl] data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240', keyAuthorization, // [!code hl] to: alphaUsd, }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi ```tsx twoslash [example.tsx] // @noErrors import { parseUnits } from 'viem' import { Account, Expiry, P256, Period, tempoActions } from 'viem/tempo' import { useConnectorClient } from 'wagmi' export function useAuthorizeAccessKey() { const { data: connectorClient } = useConnectorClient() async function authorize() { if (!connectorClient) return const client = connectorClient.extend(tempoActions()) const alphaUsd = '0x20c0000000000000000000000000000000000001' const accessKey = Account.fromP256(P256.randomPrivateKey(), { access: connectorClient.account, }) const { receipt } = await client.accessKey.authorizeSync({ accessKey, // [!code hl] expiry: Expiry.hours(1), limits: [ { token: alphaUsd, limit: parseUnits('1000', 6), period: Period.months(1), }, ], scopes: [ { address: alphaUsd, selector: 'transfer(address,uint256)', }, ], }) return receipt.transactionHash } return { authorize } } ``` #### Rust :::code-group ```rust [example.rs] use std::str::FromStr; use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use alloy::signers::{SignerSync, local::PrivateKeySigner}; use tempo_alloy::primitives::transaction::key_authorization::{ CallScope, KeyAuthorization, SelectorRule, TokenLimit, }; use tempo_alloy::primitives::transaction::tt_signature::{ KeychainSignature, PrimitiveSignature, SignatureType, TempoSignature, }; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let root: PrivateKeySigner = std::env::var("PRIVATE_KEY")?.parse()?; let access_key = PrivateKeySigner::random(); let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); let treasury = address!("0xcafebabecafebabecafebabecafebabecafebabe"); let authorization = KeyAuthorization::unrestricted( // [!code hl] 4217, // [!code hl] SignatureType::Secp256k1, // [!code hl] access_key.address(), // [!code hl] ) // [!code hl] .with_expiry(1_893_456_000) // [!code hl] .with_limits(vec![TokenLimit { // [!code hl] token: alpha_usd, // [!code hl] limit: U256::from(1_000_000u64), // [!code hl] period: 86_400, // [!code hl] }]) // [!code hl] .with_allowed_calls(vec![CallScope { // [!code hl] target: alpha_usd, // [!code hl] selector_rules: vec![SelectorRule { // [!code hl] selector: [0xa9, 0x05, 0x9c, 0xbb], // transfer(address,uint256) // [!code hl] recipients: vec![treasury], // [!code hl] }], // [!code hl] }]); // [!code hl] let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] let key_authorization = // [!code hl] authorization.into_signed(PrimitiveSignature::Secp256k1(sig)); // [!code hl] provider .send_transaction( TempoTransactionRequest { key_authorization: Some(key_authorization), // [!code hl] ..Default::default() } .with_to(alpha_usd) .with_input(bytes!("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240")), ) .await? .get_receipt() .await?; let tx = TempoTransactionRequest::default() .with_to(alpha_usd) .with_input(bytes!("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240")); let filled = provider.fill(tx).await?; let tempo_tx = filled.build_aa()?; // Keychain signatures are domain-separated by the root account address. let inner_hash = // [!code hl] KeychainSignature::signing_hash(tempo_tx.signature_hash(), root.address()); // [!code hl] let inner_sig = access_key.sign_hash_sync(&inner_hash)?; // [!code hl] let signature = TempoSignature::Keychain(KeychainSignature::new( // [!code hl] root.address(), // [!code hl] PrimitiveSignature::Secp256k1(inner_sig), // [!code hl] )); // [!code hl] let envelope = tempo_tx.into_signed(signature); // [!code hl] let pending = provider // [!code hl] .send_raw_transaction(envelope.encoded_2718().as_ref()) // [!code hl] .await?; // [!code hl] Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] import time from eth_account import Account as EthAccount from pytempo import ( Call, CallScope, KeyRestrictions, SignatureType, TempoTransaction, TokenLimit, ) from pytempo.contracts import AccountKeychain from provider import w3, account access_key = EthAccount.create() alpha_usd = "0x20c0000000000000000000000000000000000001" treasury = "0xcafebabecafebabecafebabecafebabecafebabe" auth_nonce = w3.eth.get_transaction_count(account.address) authorize_call = AccountKeychain.authorize_key( # [!code hl] key_id=access_key.address, # [!code hl] signature_type=SignatureType.SECP256K1, # [!code hl] restrictions=KeyRestrictions( # [!code hl] expiry=int(time.time()) + 3600, # [!code hl] limits=[TokenLimit(token=alpha_usd, limit=1_000_000, period=86_400)], # [!code hl] allowed_calls=[CallScope.transfer(target=alpha_usd, recipients=[treasury])], # [!code hl] ), # [!code hl] ) # [!code hl] # Root key authorizes first, then the access key signs later transactions. auth_tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=auth_nonce, calls=(authorize_call,), ) signed_auth_tx = auth_tx.sign(account.key.hex()) w3.eth.send_raw_transaction(signed_auth_tx.encode()) tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=auth_nonce + 1, calls=( Call.create( to=alpha_usd, data="0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240", ), ), ) signed_tx = tx.sign_access_key( # [!code hl] access_key_private_key=access_key.key.hex(), # [!code hl] root_account=account.address, # [!code hl] ) # [!code hl] tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/keychain" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { rootSgn, _ := signer.NewSigner("0x...") accessKey, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() chainID := big.NewInt(transaction.ChainIdMainnet) gasPrice := big.NewInt(25_000_000_000) alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") treasury := common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe") // Authorize the access key with T3 restrictions. // [!code hl] restrictions := keychain.NewKeyRestrictions(uint64(time.Now().Add(1 * time.Hour).Unix())). // [!code hl] WithLimits([]keychain.TokenLimit{{ // [!code hl] Token: alphaUSD, // [!code hl] Amount: big.NewInt(1_000_000), // [!code hl] Period: 86_400, // [!code hl] }}). // [!code hl] WithAllowedCalls([]keychain.CallScope{ // [!code hl] keychain.NewCallScopeBuilder(alphaUSD).Transfer([]common.Address{treasury}).Build(), // [!code hl] }) // [!code hl] authorizeCall, _ := keychain.AuthorizeKey( // [!code hl] accessKey.Address(), // [!code hl] keychain.SignatureTypeSecp256k1, // [!code hl] restrictions, // [!code hl] ) // [!code hl] // Go shows the explicit two-step flow: root key authorizes first, then the access key signs later transactions. nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) authTx := transaction.NewBuilder(chainID). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(gasPrice). SetMaxPriorityFeePerGas(gasPrice). AddCall(authorizeCall.To, big.NewInt(0), authorizeCall.Data). Build() _ = transaction.SignTransaction(authTx, rootSgn) serializedAuth, _ := transaction.Serialize(authTx, nil) authHash, _ := c.SendRawTransaction(ctx, serializedAuth) log.Printf("Authorized access key: %s", authHash) // Sign a transaction with the access key. // [!code hl] tx := transaction.NewBuilder(chainID). // [!code hl] SetNonce(nonce + 1). // [!code hl] SetGas(300_000). // [!code hl] SetMaxFeePerGas(gasPrice). // [!code hl] SetMaxPriorityFeePerGas(gasPrice). // [!code hl] AddCall( // [!code hl] alphaUSD, // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240"), // [!code hl] ). // [!code hl] Build() // [!code hl] _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash # 1. Authorize the access key with a recurring limit and transfer scope $ cast keychain authorize $ACCESS_KEY_ADDR secp256k1 $(($(date +%s) + 3600)) \ --limit 0x20c0000000000000000000000000000000000001:1000000:86400 \ --scope 0x20c0000000000000000000000000000000000001:transfer@0xcafebabecafebabecafebabecafebabecafebabe \ --rpc-url $TEMPO_RPC_URL \ --private-key $ROOT_PRIVATE_KEY # [!code hl] # 2. Send using the access key $ cast send 0x20c0000000000000000000000000000000000001 \ --data 0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240 \ --rpc-url $TEMPO_RPC_URL \ --tempo.root-account $ROOT_ADDRESS \ --tempo.access-key $ACCESS_KEY_PRIVATE_KEY # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, // rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, signature]) // [!code focus] signature, ]) ``` :::info Learn more about [Access Keys](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#access-keys). ::: ### Concurrent Transactions Concurrent transactions enable higher throughput by allowing multiple transactions from the same account to be sent in parallel without waiting for sequential nonce confirmation. By utilizing nonce keys, you can submit multiple transactions simultaneously that don't conflict with each other, enabling parallel execution and significantly improved transaction throughput for high-activity accounts. Concurrent transactions can be achieved with nonce keys via: * [Expiring Nonces](#expiring-nonces) * [2D Nonces](#2d-nonces) In **Viem** and **Wagmi**, expiring nonces are handled automatically. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const [receipt1, receipt2, receipt3] = await Promise.all([ client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }), client.sendTransactionSync({ data: '0xcafebabe0000000000000000000000000000000001', to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', }), client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }), ]) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransaction } from 'wagmi' const { sendTransaction } = useSendTransaction() sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; // Send three transactions concurrently using different nonce keys let (r1, r2, r3) = tokio::try_join!( provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(1)) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(2)) // [!code hl] .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) .with_input(bytes!("cafebabe0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(3)) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), )?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account # Send three transactions concurrently using different nonce keys for nonce_key, to, data in [ (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), ]: tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=0, nonce_key=nonce_key, # [!code hl] calls=(Call.create(to=to, data=data),), ) signed_tx = tx.sign(account.key.hex()) w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "sync" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() // Send three transactions concurrently using different nonce keys type txParams struct { nonceKey int64 to string data string } params := []txParams{ {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, } var wg sync.WaitGroup for _, p := range params { wg.Add(1) go func(p txParams) { defer wg.Done() tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(0). SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( common.HexToAddress(p.to), big.NewInt(0), common.Hex2Bytes(p.data), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) }(p) } wg.Wait() } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash # Send three transactions concurrently using different nonce keys $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --async --nonce 0 --tempo.nonce-key 1 # [!code hl] $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ --data 0xcafebabe0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --async --nonce 0 --tempo.nonce-key 2 # [!code hl] $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --async --nonce 0 --tempo.nonce-key 3 # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, // [!code focus] nonce, valid_before, // [!code focus] valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` ### Expiring Nonces The [expiring nonces specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1009.md) defines transactions that automatically expire if they are not executed within a specified time window. **Benefits:** * No nonce tracking required * Automatic replay protection via circular buffer * No permanent state bloat from unused nonce keys Set `nonceKey` to `maxUint256`, `nonce` to `0`, and `validBefore` to a Unix timestamp in seconds satisfying `now < validBefore <= now + 300`, where `now` is the current block timestamp. The maximum window is five minutes; SDK defaults may use shorter windows. See [TIP-1093](https://tips.sh/1093). #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { maxUint256 } from 'viem' import { client } from './viem.config' const receipt = await client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: maxUint256, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] }) ``` ```tsx twoslash [viem.config.ts] filename="viem.config.ts" import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { maxUint256 } from 'viem' import { useSendTransaction } from 'wagmi' const { sendTransaction } = useSendTransaction() sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: maxUint256, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use std::time::{SystemTime, UNIX_EPOCH}; use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let valid_before = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + 30; let pending = provider .send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::MAX) // [!code focus] .with_valid_before(valid_before) // [!code focus] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] import time from pytempo import Call, TempoTransaction from provider import w3, account # maxUint256: signals an expiring nonce MAX_UINT256 = 2**256 - 1 valid_before = int(time.time()) + 20 tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce_key=MAX_UINT256, # [!code focus] valid_before=valid_before, # [!code focus] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef0000000000000000000000000000000001", ), ), ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() // maxUint256: signals an expiring nonce maxUint256, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) validBefore := uint64(time.Now().Unix()) + 20 tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetNonceKey(maxUint256). // [!code focus] SetValidBefore(validBefore). // [!code focus] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ VALID_BEFORE=$(($(date +%s) + 20)) $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, // set to `maxUint256` // [!code focus] nonce, valid_before, // e.g. `now + 20`; maximum `now + 300` // [!code focus] valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` ### 2D Nonces For cases requiring ordered sequences within a key, Tempo's **2D nonce system** enables parallel transaction execution: * **Protocol nonce (key 0)**: The default sequential nonce. Transactions must be processed in order. * **User nonces (keys 1+)**: Independent nonce sequences that allow concurrent transaction submission. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const [receipt1, receipt2, receipt3] = await Promise.all([ client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 1n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }), client.sendTransactionSync({ data: '0xcafebabe0000000000000000000000000000000001', nonceKey: 2n, // [!code focus] to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', }), client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 3n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }), ]) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransaction } from 'wagmi' const { sendTransaction } = useSendTransaction() sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 1n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 2n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 3n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let (r1, r2, r3) = tokio::try_join!( provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(1)) // [!code focus] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(2)) // [!code focus] .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) .with_input(bytes!("cafebabe0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(3)) // [!code focus] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), )?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account for nonce_key, to, data in [ (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), ]: tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=0, nonce_key=nonce_key, # [!code focus] calls=(Call.create(to=to, data=data),), ) signed_tx = tx.sign(account.key.hex()) w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "sync" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() type txParams struct { nonceKey int64 to string data string } params := []txParams{ {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, } var wg sync.WaitGroup for _, p := range params { wg.Add(1) go func(p txParams) { defer wg.Done() tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(0). SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( common.HexToAddress(p.to), big.NewInt(0), common.Hex2Bytes(p.data), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) }(p) } wg.Wait() } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 1 # [!code hl] $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ --data 0xcafebabe0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 2 # [!code hl] $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 3 # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, // [!code focus] nonce, valid_before, valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` :::warning **Reuse nonce keys instead of generating random ones.** Creating a new nonce key incurs a state creation cost that increases with the number of active keys (see [State creation costs](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1000.md)). For most applications, using a small set of sequential nonce keys (e.g., `1n`, `2n`, `3n`) is sufficient and much more cost-effective than generating random nonce keys for each transaction. ::: ### Scheduled Transactions Scheduled transactions allow you to sign a transaction in advance and specify a time window for when it can be executed onchain. By setting `validAfter` and `validBefore` timestamps, you define the earliest and latest times the transaction can be included in a block. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const signature = await client.signTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { signTransaction } from 'wagmi/actions' import { config } from './wagmi.config' const signature = await signTransaction(config, { data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; // 2026-01-01 00:00:00 UTC let valid_after = 1_767_225_600; // 2026-01-02 00:00:00 UTC let valid_before = 1_767_312_000; let pending = provider .send_transaction( TempoTransactionRequest::default() .with_valid_after(valid_after) // [!code hl] .with_valid_before(valid_before) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from datetime import datetime, timezone from pytempo import Call, TempoTransaction from provider import w3, account # 2026-01-01 00:00:00 UTC valid_after = int(datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp()) # 2026-01-02 00:00:00 UTC valid_before = int(datetime(2026, 1, 2, tzinfo=timezone.utc).timestamp()) tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), valid_after=valid_after, # [!code hl] valid_before=valid_before, # [!code hl] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef0000000000000000000000000000000001", ), ), ) # Sign now, submit to the network for later execution signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) // 2026-01-01 00:00:00 UTC validAfter := uint64(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()) // 2026-01-02 00:00:00 UTC validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetValidAfter(validAfter). // [!code hl] SetValidBefore(validBefore). // [!code hl] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), ). Build() // Sign now, submit to the network for later execution _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ VALID_AFTER=$(date -d '2026-01-01' +%s) $ VALID_BEFORE=$(date -d '2026-01-02' +%s) $ cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.valid-after $VALID_AFTER \ --tempo.valid-before $VALID_BEFORE # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, // [!code focus] valid_after, // [!code focus] fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` # Tempo faucet: get testnet funds Get test stablecoins on Tempo testnet. ## Fund an address Send test stablecoins to any address. **Interactive demo: Fund an address** 1. Add funds to others ## Fund your wallet Connect your wallet to receive test stablecoins directly. **Interactive demo: Connect and fund your wallet** 1. Connect wallet 2. Add funds to wallet 3. Add tokens to wallet 4. Set fee token ## cURL request Request tokens programmatically via the faucet API. ```bash curl -X POST https://tempo.xyz/developers/api/faucet \ -H "Content-Type: application/json" \ -d '{"address": ""}' ``` Replace `` with a lowercase wallet address. ## Cast RPC Request tokens using the `tempo_fundAddress` RPC method. ```bash cast rpc tempo_fundAddress \ --rpc-url https://rpc.moderato.tempo.xyz ``` Replace `` with your wallet address. The faucet funds the following assets. | Asset | Address |Amount| |-------|---------|----:| | [pathUSD](https://explore.testnet.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | `0x20c0000000000000000000000000000000000000` | `1M` | | [AlphaUSD](https://explore.testnet.tempo.xyz/address/0x20c0000000000000000000000000000000000001) | `0x20c0000000000000000000000000000000000001` | `1M` | | [BetaUSD](https://explore.testnet.tempo.xyz/address/0x20c0000000000000000000000000000000000002) | `0x20c0000000000000000000000000000000000002` | `1M` | | [ThetaUSD](https://explore.testnet.tempo.xyz/address/0x20c0000000000000000000000000000000000003) | `0x20c0000000000000000000000000000000000003` | `1M` | # Tempo EVM differences Tempo is fully compatible with the Ethereum Virtual Machine (EVM), targeting the **Osaka** EVM hard fork. Developers can deploy and interact with smart contracts using the same tools, languages, and frameworks they use on Ethereum, such as Solidity, Foundry, and Hardhat. All Ethereum JSON-RPC methods work out of the box. While the execution environment mirrors Ethereum's, Tempo introduces some differences optimized for payments, described below. * [Wallet Differences](#wallet-differences) — How Tempo handles wallet compatibility and native token representation. * [Transaction Differences](#transaction-differences) — Key differences when sending transactions, including fee tokens and default preferences. * [VM Layer Differences](#vm-layer-differences) — VM-level differences including balance opcodes and Solidity compatibility. * [Consensus & Finality](#consensus--finality) — How Tempo's consensus and finality differ from Ethereum's approach. ## Wallet Differences By default, all existing functionality will work for EVM-compatible wallets, with only a few quirks. For developers of wallets, we strongly encourage you to implement support for Tempo Transactions over regular EVM transactions. See the [transaction differences](#transaction-differences) for more. :::tip If you are building a wallet, read our [guide for wallet developers](https://tempo.xyz/developers/docs/quickstart/wallet-developers). ::: ### Handling ETH (native token) Balance Checks Remember that on Tempo, there is no native gas token. Many wallets and applications check a user's "native account balance" before letting them complete some action. In this scenario, you might see an error message like "Insufficient balance". This stems from the return value of the `eth_getBalance` RPC method. When a wallet calls this method, it expects a hex string representing the "native token balance", hard-coded to be represented as an 18-decimal place number. On Tempo, the `eth_getBalance` method returns a hex string representing an extremely large number. Specifically it returns: `0x9612084f0316e0ebd5182f398e5195a51b5ca47667d4c9b26c9b26c9b26c9b2` which is represented in decimals as 4.242424242424242e+75. Our recommendation to wallets and to applications using this method is to remove this balance check, and to not represent any "native balance" in your user's UI. This will allow users to complete actions without being blocked by balance checks. We endorse [this proposed ERC](https://github.com/ethereum/ERCs/pull/1220) to standardize this behavior. ### Specifying a Native Token Currency Symbol Sometimes wallets will need to specify the currency symbol for the native token. On Tempo, there is no native token, but fees are denominated in USD. So, we recommend using the currency symbol "USD". ## Transaction Differences ### Dealing with the fee token selection Tempo does not have a native gas token. Instead, fees are denominated in USD and fees can be paid in an stablecoin. For Tempo Transactions, the `fee_token` field can be set to any TIP-20 token, and fees are paid in that token. If your transactions are not using Tempo Transactions, there is a cascading fee token selection algorithm that determines the default fee token based on the user's preferences and the contract being called. This preference system is specified [here](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#fee-token-preferences) in detail. #### Consideration 1: Setting a user default fee token As specified in the preference system above, the simplest way to specify the fee token for a user is to set the user default fee token. Read about how to do that [here](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#account-level) on behalf of an account. #### Consideration 2: Paying fees in the TIP-20 contract being interacted with If the user is calling a method on a TIP-20 token (e.g., `transfer`), the default fee token is that token itself. For example, if the user is calling the `transfer` method on a TIP-20 token with a symbol of "USDG", the default fee token would be "USDG". Importantly, note that the `amount` field in this case is sent in full. So, if the user is calling the `transfer` method on a TIP-20 token with a symbol of "USDG" with the `amount` field set to 1000, the full amount of the token will be transferred **and** the sender's balance will be reduced by the amount spent in fees. So, the recipient will receive 1000 USDG. #### Consideration 3: The fallback in the case of a non-TIP-20 contract If the user is calling a contract that is not a TIP-20 token, the EVM transaction will default to the pathUSD token. Thus, in order to send transactions to non-TIP-20 contracts, the wallet must hold some balance of pathUSD. On the Tempo Testnet, pathUSD is available from the [faucet](https://tempo.xyz/developers/docs/quickstart/faucet). If a wallet wants to submit a non-TIP20 transaction without having to submit the above transaction, we recommend investing in using [Tempo Transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction) instead. ## VM Layer Differences At the VM layer, all opcodes are supported out of the box. Due to the lack of a native token, native token balance is always returning zero balances. ### State Creation Costs Tempo's [state creation costs](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1000.md) are higher to prevent state growth attacks: | Operation | Tempo | Ethereum | |-----------|-------|----------| | New storage slot (SSTORE 0→non-zero) | 250,000 gas | 20,000 gas | | Account creation | 250,000 gas | 0 gas | | Contract creation per byte | 1,000 gas | 200 gas | | Transaction gas cap | 30M gas | 30M gas | This means transfers to new addresses cost ~300k gas, and contract deployments cost 5-10x more than on Ethereum. Update your `gas_limit` estimates accordingly. ### Balance Opcodes and RPC Methods | Feature | Behavior on Tempo | Alternatives | |---------|-------------------|--------------| | **`BALANCE` and `SELFBALANCE`** | Will always return 0 | Use TIP-20 `balanceOf` instead | | **`CALLVALUE`** | Will always return 0 | There is no alternative | :::info We are exploring transaction level introspection for Tempo Transactions, with an ability to declare things like `tx.fee_token` and `tx.fee_payer` in Solidity. ::: ## Consensus & Finality Tempo uses **Simplex BFT consensus** with a permissioned validator set at launch, providing deterministic finality, unlike Ethereum's finality gadget which takes approximately 12 minutes. Block times are targeted at ~0.5 seconds compared to Ethereum's ~12 second slots. # Predeployed contracts on Tempo ## System Contracts Core protocol contracts that power Tempo's features. | Contract | Address | Description | |----------|---------|-------------| | [**TIP-20 Factory**](https://tempo.xyz/developers/docs/protocol/tip20/overview) | [`0x20fc000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20fc000000000000000000000000000000000000) | Create new TIP-20 tokens | | [**Fee Manager**](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm#2-feemanager-contract) | [`0xfeec000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xfeec000000000000000000000000000000000000) | Handle fee payments and conversions | | [**Stablecoin DEX**](https://tempo.xyz/developers/docs/protocol/exchange) | [`0xdec0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xdec0000000000000000000000000000000000000) | Enshrined DEX for stablecoin swaps | | [**TIP-403 Registry**](https://tempo.xyz/developers/docs/protocol/tip403/spec) | [`0x403c000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x403c000000000000000000000000000000000000) | Transfer policy registry | | [**ReceivePolicyGuard**](https://tempo.xyz/developers/docs/protocol/upgrades/t6#account-level-receive-policies) | [`0xB10C000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xB10C000000000000000000000000000000000000) | Holds TIP-20 transfers and mints blocked by account-level receive policies | | [**Signature Verifier**](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md) | [`0x5165300000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x5165300000000000000000000000000000000000) | Verify secp256k1, P256, and WebAuthn signatures onchain | | [**Address Registry**](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) | [`0xFDC0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0xFDC0000000000000000000000000000000000000) | Resolve virtual TIP-20 deposit addresses to registered master wallets | | [**pathUSD**](https://tempo.xyz/developers/docs/protocol/exchange/quote-tokens#pathusd) | [`0x20c0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000) | First stablecoin deployed | ## Zone contracts The protocol provides the factory and shared Zone runtimes below. Each zone has its own portal proxy pointing to the shared implementation. These protocol contracts do not imply production readiness: [Tempo Zones](https://tempo.xyz/developers/docs/protocol/zones) remains available for testing on Tempo Testnet only. | Contract | Address | Description | |----------|---------|-------------| | [ZoneFactory](https://tempo.xyz/developers/docs/protocol/zones/architecture#creating-a-zone) | [`0x5AF2000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x5AF2000000000000000000000000000000000000) | Create zones and register their portals | | [ZonePortal implementation](https://tempo.xyz/developers/docs/protocol/zones/architecture#contract-architecture) | [`0x5AD1000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x5AD1000000000000000000000000000000000000) | Shared bridge logic used by each zone's portal proxy | | [Zone verifier](https://tempo.xyz/developers/docs/protocol/zones/architecture#contract-architecture) | [`0x5a56000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x5a56000000000000000000000000000000000000) | Protocol-managed verifier used by zone portals | | [ZoneMessenger](https://tempo.xyz/developers/docs/protocol/zones/architecture#contract-architecture) | [`0x5A4d000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x5A4d000000000000000000000000000000000000) | Shared withdrawal callback handler | ## Standard Utilities Popular Ethereum contracts deployed for convenience. | Contract | Address | Description | |----------|---------|-------------| | [**Multicall3**](https://www.multicall3.com/) | [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://explore.tempo.xyz/address/0xcA11bde05977b3631167028862bE2a173976CA11) | Batch multiple calls in one transaction | | [**CreateX**](https://github.com/pcaversaccio/createx) | [`0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed`](https://explore.tempo.xyz/address/0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed) | Deterministic contract deployment | | [**Permit2**](https://docs.uniswap.org/contracts/permit2/overview) | [`0x000000000022d473030f116ddee9f6b43ac78ba3`](https://explore.tempo.xyz/address/0x000000000022d473030f116ddee9f6b43ac78ba3) | Token approvals and transfers | | [**Arachnid Create2 Factory**](https://github.com/Arachnid/deterministic-deployment-proxy) | [`0x4e59b44847b379578588920cA78FbF26c0B4956C`](https://explore.tempo.xyz/address/0x4e59b44847b379578588920cA78FbF26c0B4956C) | CREATE2 deployment proxy | | [**Safe Deployer**](https://github.com/safe-fndn/safe-singleton-factory) | [`0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7`](https://explore.tempo.xyz/address/0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7) | Safe deployer contract | | **8004 Identity Registry** | [`0x8004A169FB4a3325136EB29fA0ceB6D2e539a432`](https://explore.tempo.xyz/address/0x8004A169FB4a3325136EB29fA0ceB6D2e539a432) | Identity registry | | **8004 Reputation Registry** | [`0x8004BAa17C55a88189AE136b182e5fdA19dE9b63`](https://explore.tempo.xyz/address/0x8004BAa17C55a88189AE136b182e5fdA19dE9b63) | Reputation registry | ## Contract ABIs ABIs for these contracts are available in the SDK: ```typescript import { Abis } from 'viem/tempo' const tip20Abi = Abis.tip20 const tip20FactoryAbi = Abis.tip20Factory const stablecoinDexAbi = Abis.stablecoinDex const feeManagerAbi = Abis.feeManager const feeAmmAbi = Abis.feeAmm // ... ``` # Tempo Token List Registry A [Uniswap Token Lists](https://tokenlists.org)-compatible API for token metadata and icons on Tempo. As an example, here's Tempo's tokenlist, fetched from [tokenlist.tempo.xyz/list/4217](https://tokenlist.tempo.xyz/list/4217): The interactive web page displays the current Tempo token list. ## Token list API endpoints | Endpoint | Description | |----------|-------------| [`/list/{chain_id}`](https://tokenlist.tempo.xyz/list/4217) | Token list for a chain | [`/asset/{chain_id}/{id}`](https://tokenlist.tempo.xyz/asset/4217/pathUSD) | Get a single token by symbol or address​ [`/icon/{chain_id}`](https://tokenlist.tempo.xyz/icon/4217) | Chain icon (SVG) | [`/icon/{chain_id}/{address}`](https://tokenlist.tempo.xyz/icon/4217/0x20c0000000000000000000000000000000000000) | Token icon (SVG) | | Chain | `chain_id` | |-------|------------| | Mainnet | `4217` | | Testnet (Moderato) | `42431` | ## Adding a New Token 1. **Fork** [tempoxyz/tempo-apps](https://github.com/tempoxyz/tempo-apps) 2. **Add token** to `data//tokenlist.json` in `apps/tokenlist`: ```json { "name": "piUSD", "symbol": "PiUSD", "decimals": 6, "chainId": 4217, "address": "0x...", "extensions": { "chain": "tempo", "coingeckoId": "pi-usd" } } ``` 3. **Add icon** to `data//icons/
.svg` (lowercase address) in `apps/tokenlist` Separately, TIP-20 tokens can also carry an optional on-chain [`logoURI`](https://tempo.xyz/developers/docs/protocol/tip20/spec#logo-uri) that wallets and explorers read directly from the token contract. The Logo URI specification recommends a square, rasterized PNG or WebP (max 256 bytes; `https`, `http`, `ipfs`, or `data` scheme) because clients fetch the icon from an untrusted source. Setting it is optional and independent of this PR — registering here still adds richer metadata (`coingeckoId`, `bridgeInfo`, display `label`) and a fallback icon for clients that don't read on-chain `logoURI`. 4. **Submit PR** with as much information as you think is helpful for review. ### Token Extensions Tokens support optional `extensions` for richer metadata: | Field | Description | |-------|-------------| | `chain` | Always `"tempo"` by convention | | `coingeckoId` | [CoinGecko](https://www.coingecko.com) identifier for price mapping | | `label` | Display label override (used by the Explorer) | | `bridgeInfo` | Origin chain and contract info for bridged tokens | For **bridged tokens**, include `bridgeInfo` so aggregators like DeFi Llama can automatically map to the canonical asset: ```json { "extensions": { "chain": "tempo", "coingeckoId": "usd-coin", "bridgeInfo": { "sourceChainId": 1, "sourceAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" } } } ``` For **native tokens** (e.g., PathUSD), omit `bridgeInfo`: ```json { "extensions": { "chain": "tempo", "coingeckoId": "pathusd" } } ``` ### Icon Requirements * Format: SVG * Address filename must be lowercase (e.g., `0xabcd...1234.svg`) * Recommended: square aspect ratio, minimal whitespace > **Note** > > A token that gets added to the tokenlist will automatically reflect in the Explorer in the next deployment. [Full OpenAPI Spec →](https://tokenlist.tempo.xyz/docs) # Integrating wallet support for Tempo Tempo is EVM-compatible, so standard transactions work out of the box. However, Tempo has [no native gas token](https://tempo.xyz/developers/docs/quickstart/evm-compatibility#handling-eth-native-token-balance-checks), which means wallet behaviors like balance display and gas quoting need adjustment. To deliver the best experience for your users, integrate [Tempo Transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction) — a protocol-native [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transaction type (type byte `0x76`) that provides fee token selection, fee sponsorship, call batching, concurrent nonces, passkey signing, and scheduled execution — without requiring a bundler, paymaster, or third-party vendor. [SDKs](https://tempo.xyz/developers/docs/guide/tempo-transaction#integration-guides) are available for TypeScript, Rust, Go, Python, and Foundry. Integration typically takes less than an hour. ## Wallet integration steps ::::steps ### Integrate Tempo Transactions Replace your wallet's transaction construction with Tempo Transactions. The minimum change is switching from a type-2 (EIP-1559) envelope to a type-`0x76` Tempo Transaction envelope using one of the [Tempo SDKs](https://tempo.xyz/developers/docs/guide/tempo-transaction#integration-guides). :::code-group ```ts twoslash [example.ts] // @noErrors import { client } from './viem.config' import { parseUnits } from 'viem' // Sends a Tempo Transaction (type 0x76) const { receipt } = await client.token.transferSync({ amount: parseUnits('100', 6), to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', token: '0x20c0000000000000000000000000000000000001', }) ``` ```ts twoslash [viem.config.ts] filename="viem.config.ts" import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: :::tip With Tempo Transactions, you can also: * Set the fee token for your users' transactions ([guide](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin)) * Sponsor transaction fees for your users ([guide](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees)) * Send concurrent transactions with independent nonces ([guide](https://tempo.xyz/developers/docs/guide/payments/send-parallel-transactions)) * Use expiring nonces for cheaper transactions that don't require nonce tracking ([guide](https://tempo.xyz/developers/docs/guide/tempo-transaction#expiring-nonces)) ::: ### Handle the absence of a native token If you use `eth_getBalance` to validate a user's balance, you should instead check the user's account fee token balance on Tempo. Additionally, you should not display any "native balance" in your UI for Tempo users. :::info In testnet, `eth_getBalance` [returns a large placeholder value](https://tempo.xyz/developers/docs/quickstart/evm-compatibility#handling-eth-native-token-balance-checks) for the native token balance to unblock existing assumptions wallets have about the native token balance. ::: :::code-group ```ts twoslash [example.ts] // @noErrors import { client } from './viem.config' const userFeeToken = await client.fee.getUserToken({ account: '0x...' }) const balance = await client.token.getBalance({ account: '0x...', token: userFeeToken.address }) ``` ```ts twoslash [viem.config.ts] filename="viem.config.ts" import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: ### Configure native currency symbol If you need to display a native token symbol, such as showing how much gas a transaction requires, you can set the currency symbol to `USD` for Tempo as fees are denominated in USD. ### Use fee token preferences to quote gas prices On Tempo, users can pay fees in any supported stablecoin. You should quote gas/fee prices in your UI based on a transaction's fee token. :::info As a wallet developer, you can set the fee token for your user at the account level. If you don't, Tempo uses a cascading fee token selection algorithm to determine the fee token for a transaction – learn more about [Fee Token Preferences](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#fee-token-preferences). ::: ### Add fee token selection to your UI Your wallet should provide a way for users to choose which stablecoin they pay fees in. This can be a dropdown in the transaction confirmation screen or a setting in account preferences. To set the fee token on a per-transaction basis, pass the `feeToken` parameter when submitting a Tempo Transaction: :::code-group ```ts twoslash [example.ts] // @noErrors import { client } from './viem.config' import { parseUnits } from 'viem' const { receipt } = await client.token.transferSync({ amount: parseUnits('100', 6), feeToken: '0x20c0000000000000000000000000000000000002', // [!code hl] to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', token: '0x20c0000000000000000000000000000000000001', }) ``` ```ts twoslash [viem.config.ts] filename="viem.config.ts" import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: To set a persistent default so users don't need to select on every transaction, use `setUserToken`: ```ts await client.fee.setUserTokenSync({ token: '0x20c0000000000000000000000000000000000001', }) ``` See [Fee Token Preferences](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#fee-token-preferences) for the full cascading resolution order. ### Display token and network assets Tempo provides a public tokenlist service that hosts token and network assets. You can pull these assets from our public tokenlist service to display in your UI. * **GitHub**: [tempoxyz/tempo-apps/apps/tokenlist](https://github.com/tempoxyz/tempo-apps/tree/main/apps/tokenlist) * **Tokenlist JSON**: [tokenlist.tempo.xyz/list/42431](https://tokenlist.tempo.xyz/list/42431) :::: ## Already using EIP-7702 or EIP-4337? If you've integrated a third-party account abstraction provider for batching, sponsorship, or smart accounts, Tempo Transactions provide these features natively at the protocol level. See the [feature comparison](https://tempo.xyz/developers/docs/protocol/transactions/eip-7702#feature-comparison) for details. ## Wallet integration recipes ### Get user's fee token Retrieve the user's configured fee token preference: ```ts import { getUserToken } from 'viem/tempo' const feeToken = await client.fee.getUserToken({ account: userAddress }) ``` See [`getUserToken`](https://viem.sh/tempo/actions/fee.getUserToken) for full documentation. ### Get token balance Check a user's balance for a specific token: ```ts import { getBalance } from 'viem/tempo' const balance = await client.token.getBalance({ account: userAddress, token: tokenAddress }) // ^? { amount: bigint; decimals: number; formatted: string } ``` See [`getBalance`](https://viem.sh/tempo/actions/token.getBalance) for full documentation. ### Set user fee token Set the user's default fee token preference. This will be used for all transactions unless a different fee token is specified at the transaction level. ```ts import { setUserToken } from 'viem/tempo' await client.fee.setUserTokenSync({ token: '0x20c0000000000000000000000000000000000001', }) ``` See [`setUserToken`](https://viem.sh/tempo/actions/fee.setUserToken) for full documentation. ## Checklist Before launching Tempo support, ensure your wallet: * \[ ] Integrates Tempo Transactions for transaction submission * \[ ] Checks fee token balance instead of native balance * \[ ] Hides or removes native balance display for Tempo * \[ ] Displays `USD` as the currency symbol for gas * \[ ] Quotes gas prices in the user's fee token * \[ ] Provides fee token selection in the UI (dropdown or account setting) * \[ ] Pulls token/network assets from Tempo's tokenlist * \[ ] (Recommended) Sponsors fees for your users via [fee sponsorship](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) * \[ ] (Recommended) Uses [expiring nonces](https://tempo.xyz/developers/docs/guide/tempo-transaction#expiring-nonces) for lower-cost transactions that don't require nonce management ## Learning Resources * [Tempo Transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction) — Integrate Tempo Transactions for full control over transaction parameters * [Fee Token Preferences](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#fee-token-preferences) — Learn how fee token preferences work in the protocol * [Sponsor User Fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) — Sponsor user fees to enable feeless transaction experiences in your application * [EIP-7702 Comparison](https://tempo.xyz/developers/docs/protocol/transactions/eip-7702) — How Tempo Transactions compare to EIP-7702 delegation * [Pay Fees in Any Stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) — Pay fees in any supported stablecoin # Contract verification using Foundry Verify your smart contracts on Tempo using [contracts.tempo.xyz](https://contracts.tempo.xyz), a Sourcify-compatible contract verification service. Verified contracts display source code and ABI in the [Tempo Explorer](https://explore.tempo.xyz), making it easier for users to interact with your contracts. ## Verify with Foundry The easiest way to verify contracts is to include the `--verify` flag when deploying. You can specify Tempo's verifier by either setting the `VERIFIER_URL` environment variable: ```bash export VERIFIER_URL=https://contracts.tempo.xyz ``` Or by passing `--verifier-url https://contracts.tempo.xyz` directly to the command. The chain ID is auto-detected from the RPC URL, but you can specify it explicitly with `--chain ` if needed. ### Verify during deployment Deploy and verify in a single command: ```bash # Deploy and verify with forge create forge create src/Token.sol:Token \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --broadcast \ --verify # Deploy and verify with forge script forge script script/Deploy.s.sol \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --sender \ --broadcast \ --verify ``` ### Verify an existing contract To verify a contract that's already deployed, use `forge verify-contract`: ```bash forge verify-contract \ --rpc-url $TEMPO_RPC_URL \ --verifier-url https://contracts.tempo.xyz \ \ src/MyContract.sol:MyContract ``` Replace `` with your deployed contract address and `src/MyContract.sol:MyContract` with the path and name of your contract. :::tip Make sure you're using the same compiler settings (optimizer, EVM version) that you used when deploying the contract. ::: ### Retry options If verification fails intermittently, use `--retries` and `--delay` to automatically retry: ```bash forge create src/Token.sol:Token \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --broadcast \ --verify \ --retries 10 \ --delay 10 ``` This retries verification up to 10 times with a 10-second delay between attempts. For more details on deployment and verification options, see the [Foundry documentation](https://getfoundry.sh/forge/deploying). ## Verify with Hardhat If you deployed with Hardhat, add Sourcify verification to your `hardhat.config.ts` using [`@nomicfoundation/hardhat-verify`](https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify): ```ts import "@nomicfoundation/hardhat-verify"; const config: HardhatUserConfig = { // ... your existing config sourcify: { enabled: true, apiUrl: "https://contracts.tempo.xyz", browserUrl: "https://explore.tempo.xyz", }, }; ``` Then verify: ```bash npx hardhat verify --network tempo [constructor args...] ``` ### Verify from deployment artifacts If you have Hardhat deployment JSON files (from `hardhat-deploy` or `hardhat-ignition`) but no longer have the project set up, you can still verify. These files contain embedded compiler metadata with full source code (when compiled with `useLiteralContent: true`, the default for `hardhat-deploy`). Extract the `metadata` field, parse it as JSON, then construct a [verification API request](#verify-with-the-api) using: * `metadata.sources` → `stdJsonInput.sources` * `metadata.settings` (optimizer, evmVersion, remappings) → `stdJsonInput.settings` * `metadata.compiler.version` → `compilerVersion` * `metadata.settings.compilationTarget` → `contractIdentifier` (format: `path/to/File.sol:ContractName`) ## Proxy Contracts OpenZeppelin proxy deployments (e.g., `TransparentUpgradeableProxy`) create multiple contracts — typically an implementation, a proxy, and a `ProxyAdmin`. Each must be verified **separately** and may use **different compiler versions** (e.g., your contract uses `solc 0.8.22` while the OZ proxy uses `solc 0.8.10`). If you have the deployment artifact for each contract, the correct compiler version and settings are already embedded in each file's metadata. ## Verify with the API You can also verify contracts directly using the REST API. Verification is asynchronous—you submit a request, then poll for the result. ### Submit for Verification ```bash curl -X POST https://contracts.tempo.xyz/v2/verify/42431/ \ -H 'Content-Type: application/json' \ -d '{ "stdJsonInput": { "language": "Solidity", "sources": { "src/MyContract.sol": { "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.20;\n\ncontract MyContract { }" } }, "settings": { "optimizer": { "enabled": false, "runs": 200 }, "evmVersion": "cancun" } }, "compilerVersion": "0.8.20+commit.a1b79de6", "contractIdentifier": "src/MyContract.sol:MyContract" }' ``` The API returns `202 Accepted` with a verification ID: ```json { "verificationId": "550e8400-e29b-41d4-a716-446655440000" } ``` :::tip If verification has trouble determining the creation bytecode, include `creationTransactionHash` in the request body. ::: ### Check Verification Status Poll the status endpoint until verification completes: ```bash curl https://contracts.tempo.xyz/v2/verify/ ``` The endpoint returns `200` for completed jobs, but you must check the response body to determine success or failure: ```json { "isJobCompleted": true, "contract": { "match": "exact_match", "chainId": "42431", "address": "0x1234567890abcdef1234567890abcdef12345678", "name": "MyContract" } } ``` The `match` field can be `exact_match` (bytecode and metadata match), `match` (bytecode matches but metadata differs), or `null` (verification failed). ### Retrieve Verified Contract Once verified, retrieve the contract details: ```bash curl https://contracts.tempo.xyz/v2/contract/42431/ ``` Add `?fields=all` to get full compilation artifacts including ABI, source files, and bytecode. ## Vyper Support The verification service supports Vyper contracts. Use `"language": "Vyper"` in the `stdJsonInput`: ```bash curl -X POST https://contracts.tempo.xyz/v2/verify/42431/ \ -H 'Content-Type: application/json' \ -d '{ "stdJsonInput": { "language": "Vyper", "sources": { "contracts/Token.vy": { "content": "# @version ^0.3.10\n..." } }, "settings": {} }, "compilerVersion": "0.3.10+commit.91361694", "contractIdentifier": "contracts/Token.vy:Token" }' ``` ## Contract verification API reference | Endpoint | Description | |----------|-------------| | `POST /v2/verify/{chainId}/{address}` | Submit contract for verification | | `GET /v2/verify/{verificationId}` | Check verification status | | `GET /v2/contract/{chainId}/{address}` | Get verified contract details | | `GET /v2/contracts/{chainId}` | List all verified contracts | | `GET /chains` | Get supported chains | View the full API documentation at [contracts.tempo.xyz/docs](https://contracts.tempo.xyz/docs). ## Supported Chains | Network | Chain ID | |---------|----------| | Tempo Mainnet | `4217` | | Tempo Testnet (Moderato) | `42431` | | Tempo Devnet | `31318` | ## Troubleshooting :::tip If you encounter unexpected failures, you might be running an older version of Foundry/Forge. See the [Foundry setup guide](https://tempo.xyz/developers/docs/sdk/foundry) for installation instructions. ::: ### Verification Failed If verification fails, check the following: * **Compiler version**: Use the full version string with commit hash (e.g., `0.8.20+commit.a1b79de6`) * **Optimizer settings**: Optimizer enabled/disabled and runs must match deployment settings * **EVM version**: Must match the EVM version used during deployment * **Source files**: All imported files must be included in the `sources` object * **Contract identifier**: Must match the format `path/to/Contract.sol:ContractName` ### Common Errors | Error | Cause | |-------|-------| | `contract_not_found` | No bytecode exists at the address | | `compilation_error` | Source code has syntax errors | | `compilation_failed` | Compilation service returned an error | | `contract_not_found_in_output` | Contract identifier not found in compiled output | | `no_match` | Compiled bytecode doesn't match on-chain code | # Bridging stablecoins and USDT0 via LayerZero [LayerZero](https://layerzero.network) is the omnichain messaging protocol that powers token bridging on Tempo. Tokens are bridged using the [OFT (Omnichain Fungible Token)](https://docs.layerzero.network/v2/developers/evm/oft/quickstart) standard - the source chain locks or burns tokens and the destination chain mints the bridged equivalent. There are two flavors of OFT on Tempo: * **Stargate** - an application built on LayerZero that manages liquidity pools. Tokens like USDC.e and EURC.e use Stargate's `sendToken()` interface. * **Standard OFT** - token issuers (e.g. Tether for USDT0) deploy their own OFT adapters using LayerZero's `send()` interface directly. Both use the same underlying LayerZero endpoint on Tempo. If you are bringing an existing ERC-20 to Tempo, first decide whether to preserve it as the canonical asset behind a bridge adapter or migrate issuance to native TIP-20. See [Migrate an ERC-20 to TIP-20](https://tempo.xyz/developers/docs/guide/issuance/migrate-erc20-to-tip20). ## USDC.e and native USDC USDC.e is the bridged representation of USDC on Tempo. It is backed 1:1 by native USDC in Stargate liquidity infrastructure. When USDC is bridged to Tempo through Stargate, native USDC is deposited into a Stargate pool and the equivalent amount of USDC.e is minted on Tempo. When USDC.e is bridged out, USDC.e is burned on Tempo and USDC is released through Stargate. The zero-transfer-fee path is between Tempo and Ethereum: | Route | Asset received | Stargate transfer fee | |-------|----------------|----------------------:| | Ethereum to Tempo | USDC.e on Tempo | 0 bps | | Tempo to Ethereum | Native USDC on Ethereum | 0 bps | | Tempo to or from other chains | Route-dependent | Quote before execution | Routes between Tempo and other chains can carry standard Stargate route fees. If an integrator needs native USDC on another chain, the preferred settlement path is to bridge USDC.e from Tempo to native USDC on Ethereum, then move USDC onward using CCTP or another supported route. The [LayerZero Value Transfer API](https://docs.layerzero.network/v2/developers/value-transfer-api/overview) can help discover and execute available routes. ## Bridged tokens on Tempo | Token | Address | Bridge | |-------|---------|--------| | **USDC.e** (Bridged USDC) | [`0x20C000000000000000000000b9537d11c60E8b50`](https://explore.tempo.xyz/address/0x20C000000000000000000000b9537d11c60E8b50) | Stargate | | **EURC.e** (Bridged EURC) | [`0x20c0000000000000000000001621e21F71CF12fb`](https://explore.tempo.xyz/address/0x20c0000000000000000000001621e21F71CF12fb) | Stargate | | **USDT0** | [`0x20c00000000000000000000014f22ca97301eb73`](https://explore.tempo.xyz/address/0x20c00000000000000000000014f22ca97301eb73) | OFT | | **frxUSD** | [`0x20c0000000000000000000003554d28269e0f3c2`](https://explore.tempo.xyz/address/0x20c0000000000000000000003554d28269e0f3c2) | OFT | | **cUSD** | [`0x20c0000000000000000000000520792dcccccccc`](https://explore.tempo.xyz/address/0x20c0000000000000000000000520792dcccccccc) | OFT | | **stcUSD** | [`0x20c0000000000000000000008ee4fcff88888888`](https://explore.tempo.xyz/address/0x20c0000000000000000000008ee4fcff88888888) | OFT | | **GUSD** | [`0x20c0000000000000000000005c0bac7cef389a11`](https://explore.tempo.xyz/address/0x20c0000000000000000000005c0bac7cef389a11) | OFT | | **rUSD** | [`0x20c0000000000000000000007f7ba549dd0251b9`](https://explore.tempo.xyz/address/0x20c0000000000000000000007f7ba549dd0251b9) | OFT | | **wsrUSD** | [`0x20c000000000000000000000aeed2ec36a54d0e5`](https://explore.tempo.xyz/address/0x20c000000000000000000000aeed2ec36a54d0e5) | OFT | See the full token list at [tokenlist.tempo.xyz](https://tokenlist.tempo.xyz/list/4217). ## LayerZero contracts on Tempo | Contract | Address | |----------|---------| | **EndpointV2** | [`0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C`](https://explore.tempo.xyz/address/0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C) | | **LZEndpointDollar** | [`0x0cEb237E109eE22374a567c6b09F373C73FA4cBb`](https://explore.tempo.xyz/address/0x0cEb237E109eE22374a567c6b09F373C73FA4cBb) | Tempo's LayerZero Endpoint ID is **`30410`**. ## Stargate tokens [Stargate](https://stargate.finance/) manages liquidity pools for USDC.e and EURC.e. Use the Stargate `sendToken()` interface for these tokens. ### Stargate contracts on Tempo | Token | Stargate OFT Contract | |-------|----------------------| | **USDC.e** | [`0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392`](https://explore.tempo.xyz/address/0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392) | | **EURC.e** | [`0x7753Dc8d4bd48Db599Da21E08b1Ab1D6FDFfdC71`](https://explore.tempo.xyz/address/0x7753Dc8d4bd48Db599Da21E08b1Ab1D6FDFfdC71) | These are the contracts users call to bridge tokens. They are not the authoritative contracts for Stargate v2 message security configuration. To inspect the live DVN setup for Stargate routes, resolve the chain's `TokenMessaging` OApp from Stargate metadata and read the LayerZero EndpointV2 config for that OApp. On Tempo, the Stargate v2 `TokenMessaging` OApp is [`0x19Ff94Fe4C93D546e4DB3E1FB124D45366B0b9F5`](https://explore.tempo.xyz/address/0x19Ff94Fe4C93D546e4DB3E1FB124D45366B0b9F5). ### Source chain Stargate pools | Chain | LZ Endpoint ID | Stargate USDC Pool | |-------|---------------:|--------------------| | Ethereum | `30101` | [`0xc026395860Db2d07ee33e05fE50ed7bD583189C7`](https://etherscan.io/address/0xc026395860Db2d07ee33e05fE50ed7bD583189C7) | | Arbitrum | `30110` | [`0xe8CDF27AcD73a434D661C84887215F7598e7d0d3`](https://arbiscan.io/address/0xe8CDF27AcD73a434D661C84887215F7598e7d0d3) | | Base | `30184` | [`0x27a16dc786820B16E5c9028b75B99F6f604b5d26`](https://basescan.org/address/0x27a16dc786820B16E5c9028b75B99F6f604b5d26) | | Optimism | `30111` | [`0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0`](https://optimistic.etherscan.io/address/0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0) | | Polygon | `30109` | [`0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4`](https://polygonscan.com/address/0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4) | | Avalanche | `30106` | [`0x5634c4a5FEd09819E3c46D86A965Dd9447d86e47`](https://snowtrace.io/address/0x5634c4a5FEd09819E3c46D86A965Dd9447d86e47) | ## Bridge to Tempo #### Using the Stargate app 1. Go to [stargate.finance](https://stargate.finance/) 2. Select your source chain and token (USDC or EURC) 3. Set **Tempo** as the destination chain 4. Enter the amount, approve, and send #### Using cast (Foundry) This example bridges USDC from Base to Tempo. Replace addresses for other tokens or source chains. :::steps ### Get a quote ```bash cast call 0x27a16dc786820B16E5c9028b75B99F6f604b5d26 \ 'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \ "(30410,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ false \ --rpc-url https://mainnet.base.org ``` Take the first returned number as ``. ### Approve token on source chain ```bash cast send 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \ 'approve(address,uint256)' \ 0x27a16dc786820B16E5c9028b75B99F6f604b5d26 \ \ --rpc-url https://mainnet.base.org \ --private-key $PRIVATE_KEY ``` ### Send bridge transaction ```bash cast send 0x27a16dc786820B16E5c9028b75B99F6f604b5d26 \ 'sendToken((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)' \ "(30410,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ "(,0)" \ \ --value \ --rpc-url https://mainnet.base.org \ --private-key $PRIVATE_KEY ``` ### Verify transaction status ```text https://scan.layerzero-api.com/v1/messages/tx/ ``` ::: #### Using TypeScript (viem) ```typescript import { createWalletClient, createPublicClient, http, parseUnits, pad } from 'viem' import { base } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const walletClient = createWalletClient({ account, chain: base, transport: http(), }) // Stargate pool on Base const stargatePool = '0x27a16dc786820B16E5c9028b75B99F6f604b5d26' as const // USDC on Base const usdc = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const const amount = parseUnits('1', 6) // 1 USDC const minAmount = parseUnits('0.99', 6) // 1% slippage tolerance const sendParam = { dstEid: 30410, // Tempo to: pad(account.address), amountLD: amount, minAmountLD: minAmount, extraOptions: '0x' as const, composeMsg: '0x' as const, oftCmd: '0x' as const, // taxi mode (immediate) } const stargateAbi = [ { name: 'quoteSend', type: 'function', stateMutability: 'view', inputs: [ { name: '_sendParam', type: 'tuple', components: [ { name: 'dstEid', type: 'uint32' }, { name: 'to', type: 'bytes32' }, { name: 'amountLD', type: 'uint256' }, { name: 'minAmountLD', type: 'uint256' }, { name: 'extraOptions', type: 'bytes' }, { name: 'composeMsg', type: 'bytes' }, { name: 'oftCmd', type: 'bytes' }, ], }, { name: '_payInLzToken', type: 'bool' }, ], outputs: [ { name: 'msgFee', type: 'tuple', components: [ { name: 'nativeFee', type: 'uint256' }, { name: 'lzTokenFee', type: 'uint256' }, ], }, ], }, { name: 'sendToken', type: 'function', stateMutability: 'payable', inputs: [ { name: '_sendParam', type: 'tuple', components: [ { name: 'dstEid', type: 'uint32' }, { name: 'to', type: 'bytes32' }, { name: 'amountLD', type: 'uint256' }, { name: 'minAmountLD', type: 'uint256' }, { name: 'extraOptions', type: 'bytes' }, { name: 'composeMsg', type: 'bytes' }, { name: 'oftCmd', type: 'bytes' }, ], }, { name: '_fee', type: 'tuple', components: [ { name: 'nativeFee', type: 'uint256' }, { name: 'lzTokenFee', type: 'uint256' }, ], }, { name: '_refundAddress', type: 'address' }, ], outputs: [], }, ] as const const erc20Abi = [ { name: 'approve', type: 'function', stateMutability: 'nonpayable', inputs: [ { name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ type: 'bool' }], }, ] as const // 1. Quote the fee const publicClient = createPublicClient({ chain: base, transport: http() }) const msgFee = await publicClient.readContract({ address: stargatePool, abi: stargateAbi, functionName: 'quoteSend', args: [sendParam, false], }) // 2. Approve token await walletClient.writeContract({ address: usdc, abi: erc20Abi, functionName: 'approve', args: [stargatePool, amount], }) // 3. Send the bridge transaction await walletClient.writeContract({ address: stargatePool, abi: stargateAbi, functionName: 'sendToken', args: [sendParam, msgFee, account.address], value: msgFee.nativeFee, }) ``` ## Bridge from Tempo To bridge from Tempo back to another chain, call `sendToken` on the Stargate OFT contract on Tempo. The process is similar to bridging in - quote, approve, send - but includes additional steps to prepare the messaging fee. Because Tempo has no native gas token, LayerZero messaging fees are paid in a TIP-20 stablecoin via [LZEndpointDollar](#endpointdollar). Before sending a bridge transaction, you must wrap your USDC.e into an LZD (LayerZero Dollar) token that the endpoint can consume as a fee. This involves approving USDC.e to the LZD wrapper contract, wrapping it, and then approving the resulting LZD to the Stargate pool. #### Using cast (Foundry) This example bridges USDC.e from Tempo to Base. :::steps ### Quote the fee ```bash cast call 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ 'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \ "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ false \ --rpc-url https://rpc.tempo.xyz ``` Take the first returned number as `` (in stablecoin units, not ETH). ### Approve USDC.e to the LZD wrapper Approve the `LZEndpointDollar` wrapper contract to spend `` of your USDC.e. This is the amount needed to cover the LayerZero messaging fee. ```bash cast send 0x20C000000000000000000000b9537d11c60E8b50 \ "approve(address,uint256)" \ 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` ### Wrap USDC.e into LZD Wrap your USDC.e into the LZD token so it can be used as a messaging fee by the LayerZero endpoint. ```bash cast send 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \ "wrap(address,address,uint256)" \ 0x20C000000000000000000000b9537d11c60E8b50 \ \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` ### Approve LZD to Stargate Approve the Stargate OFT contract to spend your LZD so it can pay the messaging fee when sending. ```bash cast send 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \ "approve(address,uint256)" \ 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` ### Approve token on Tempo ```bash cast send 0x20C000000000000000000000b9537d11c60E8b50 \ 'approve(address,uint256)' \ 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` ### Send bridge transaction No `--value` is needed on Tempo - the messaging fee is paid in a TIP-20 stablecoin via [EndpointDollar](#endpointdollar). ```bash cast send 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ 'sendToken((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)' \ "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ "(,0)" \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` ### Verify transaction status ```text https://scan.layerzero-api.com/v1/messages/tx/ ``` ::: #### Using TypeScript (viem) ```typescript import { parseUnits, pad } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' const account = privateKeyToAccount('0x...') const client = createClient({ account, }) // Stargate OFT for USDC.e on Tempo const stargateOFT = '0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392' as const // USDC.e on Tempo const usdce = '0x20C000000000000000000000b9537d11c60E8b50' as const // LZEndpointDollar wrapper const lzd = '0x0cEb237E109eE22374a567c6b09F373C73FA4cBb' as const const amount = parseUnits('1', 6) // 1 USDC.e const minAmount = parseUnits('0.99', 6) // 1% slippage tolerance const sendParam = { dstEid: 30184, // Base to: pad(account.address), amountLD: amount, minAmountLD: minAmount, extraOptions: '0x' as const, composeMsg: '0x' as const, oftCmd: '0x' as const, // taxi mode (immediate) } const wrapAbi = [ { name: 'wrap', type: 'function', stateMutability: 'nonpayable', inputs: [ { name: 'token', type: 'address' }, { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [], }, ] as const // 1. Quote the fee const msgFee = await client.readContract({ address: stargateOFT, abi: stargateAbi, // same ABI as above functionName: 'quoteSend', args: [sendParam, false], }) // 2. Approve USDC.e to LZD wrapper (for the messaging fee) await client.writeContract({ address: usdce, abi: erc20Abi, functionName: 'approve', args: [lzd, msgFee.nativeFee], }) // 3. Wrap USDC.e into LZD await client.writeContract({ address: lzd, abi: wrapAbi, functionName: 'wrap', args: [usdce, account.address, msgFee.nativeFee], }) // 4. Approve LZD to Stargate (for the messaging fee) await client.writeContract({ address: lzd, abi: erc20Abi, functionName: 'approve', args: [stargateOFT, msgFee.nativeFee], }) // 5. Approve USDC.e to Stargate (for the bridge amount) await client.writeContract({ address: usdce, abi: erc20Abi, functionName: 'approve', args: [stargateOFT, amount], }) // 6. Send the bridge transaction (no value - fee handled via EndpointDollar) await client.writeContract({ address: stargateOFT, abi: stargateAbi, functionName: 'sendToken', args: [sendParam, msgFee, account.address], }) ``` ### Bus vs. Taxi mode Stargate offers two delivery modes: | Mode | `oftCmd` | Delivery | Cost | |------|----------|----------|------| | **Taxi** | `0x` (empty) | Immediate - message sent right away | Higher gas cost | | **Bus** | `0x00` (1 byte) | Batched - waits for other passengers | Lower gas cost | All examples above use taxi mode. To use bus mode, set `oftCmd` to `0x00`: ```bash # cast - bus mode oftCmd=0x00 ``` ```typescript // viem - bus mode const sendParam = { // ... oftCmd: '0x00' as const, // bus mode } ``` :::warning Bus mode is not available on all routes. If a bus route is not configured for your source/destination pair, the transaction will revert. Use taxi mode (`0x`) for guaranteed delivery. ::: ## Standard OFT tokens Tokens like USDT0, frxUSD, cUSD, and others are bridged using the standard LayerZero OFT `send()` interface. Each token issuer deploys their own OFT adapter contract. The `send()` interface uses the same `SendParam` struct as Stargate but calls `send()` instead of `sendToken()`. To bridge a standard OFT token, you need the OFT adapter contract address on the source chain. Refer to the token issuer's documentation for their deployment addresses: * **USDT0** - [Tether](https://tether.io) * **frxUSD** - [Frax](https://docs.frax.com) * **cUSD** - [Cap](https://docs.cap.app/) The flow is the same as Stargate - quote, approve, send - but you call `send()` on the OFT adapter instead of `sendToken()` on a Stargate pool: ```bash # Quote cast call \ 'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \ "(30410,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ false \ --rpc-url # Approve cast send \ 'approve(address,uint256)' \ \ \ --rpc-url \ --private-key $PRIVATE_KEY # Send cast send \ 'send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)' \ "(30410,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ "(,0)" \ \ --value \ --rpc-url \ --private-key $PRIVATE_KEY ``` ## EndpointDollar Tempo has no native gas token, so there is no `msg.value`. Standard LayerZero endpoints require `msg.value` to pay messaging fees, which doesn't work on Tempo. **LZEndpointDollar** ([`0x0cEb237E109eE22374a567c6b09F373C73FA4cBb`](https://explore.tempo.xyz/address/0x0cEb237E109eE22374a567c6b09F373C73FA4cBb)) is an adapter contract that routes LayerZero messaging fees through a TIP-20 stablecoin instead of `msg.value`. It wraps the standard `EndpointV2` so that OFT contracts can function on Tempo without modification. How fees flow: * **Bridging to Tempo** - fees are paid in native gas on the source chain (ETH, MATIC, AVAX, etc.) as normal. No interaction with `LZEndpointDollar` is required. * **Bridging from Tempo** - `LZEndpointDollar` deducts the messaging fee from an LZD token (a wrapped TIP-20 stablecoin) instead of `msg.value`. Before calling `sendToken` / `send`, you must wrap USDC.e into LZD and approve LZD to the OFT contract. See [Bridge from Tempo](#bridge-from-tempo) for the exact steps. ## Further reading * [LayerZero V2 documentation](https://docs.layerzero.network/v2) * [Stargate documentation](https://stargateprotocol.gitbook.io/stargate/v2-developer-docs) * [Bridges & Exchanges on Tempo](https://tempo.xyz/developers/docs/ecosystem/bridges) * [Getting Funds on Tempo](https://tempo.xyz/developers/docs/guide/getting-funds) # Bridging stablecoins with Bungee [Bungee](https://www.bungee.exchange/) is a cross-chain routing protocol built by the [SOCKET](https://docs.socket.tech/) team. For Tempo, the recommended integration path is Bungee Deposit: request a quote, execute the returned source-chain transaction, and track the request until Bungee delivers funds on the destination chain. If you are bringing an existing ERC-20 to Tempo, first decide whether to preserve it as the canonical asset behind a bridge adapter or migrate issuance to native TIP-20. See [Migrate an ERC-20 to TIP-20](https://tempo.xyz/developers/docs/guide/issuance/migrate-erc20-to-tip20). Tempo's Bungee chain ID is **`4217`**. ## How Bungee Deposit works Bungee has Auto and Manual routing modes for general cross-chain swaps, but Tempo routes are exposed through the deposit flow. Each quote returns a concrete `deposit.txData` transaction and `deposit.requestHash` status identifier. The flow is: 1. Request a quote with `enableDepositAddress=true` and a `refundAddress`. 2. Read `result.deposit` from the quote response. 3. Submit `deposit.txData`, or present `deposit.depositData` for a user-driven transfer. 4. Poll `/api/v1/bungee/status` with `deposit.requestHash`. Query the Bungee supported chains API to confirm current Tempo route support: ```bash curl "https://public-backend.bungee.exchange/api/v1/supported-chains" ``` Query TIP-20 tokens from Bungee's token list when building token selectors: ```bash curl "https://public-backend.bungee.exchange/api/v1/tokens/list?chainIds=4217&list=full" ``` The list of supported chains, tokens, and routes changes over time as Bungee adds routes. Always check the API for the latest availability. ## Common tokens | Token | Address | Decimals | |-------|---------|---------:| | **pathUSD** | [`0x20C0000000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x20C0000000000000000000000000000000000000) | 6 | | **USDT0** | [`0x20C00000000000000000000014f22CA97301EB73`](https://explore.tempo.xyz/address/0x20C00000000000000000000014f22CA97301EB73) | 6 | | **USDC.E** (Bridged USDC) | [`0x20C000000000000000000000b9537d11c60E8b50`](https://explore.tempo.xyz/address/0x20C000000000000000000000b9537d11c60E8b50) | 6 | | **EURAU** (AllUnity EUR) | [`0x20c0000000000000000000009A4a4b17E0Dc6651`](https://explore.tempo.xyz/address/0x20c0000000000000000000009A4a4b17E0Dc6651) | 6 | For wallet funding and most Bungee-to-Tempo flows, route to **USDC.E** on Tempo. Use the token list endpoint for the latest output token availability, and request a quote to confirm route availability for your origin token and amount. :::note Bungee may use the native token sentinel `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` for chain-currency entries in API responses. For API requests, prefer the exact token address returned by Bungee's token list or quote response for the route you are building. ::: ## API endpoints Use the public endpoint for testing and request production access from Bungee for higher limits. | Purpose | Endpoint | |---------|----------| | Quote | `GET https://public-backend.bungee.exchange/api/v1/bungee/quote` | | Status | `GET https://public-backend.bungee.exchange/api/v1/bungee/status` | | Supported chains | `GET https://public-backend.bungee.exchange/api/v1/supported-chains` | | Token list | `GET https://public-backend.bungee.exchange/api/v1/tokens/list` | :::info Bungee returns a `server-req-id` response header. Log it alongside quote and status errors so Bungee support can trace requests. ::: ## Bridge to Tempo ### Using the Bungee app Open the [Bungee app](https://www.bungee.exchange/) and select Tempo as the destination chain. You can also preselect a route with Bungee Link. This example starts from USDC on Base and sets USDC.e on Tempo as the output token: ```text https://bungee.exchange/?originChainId=8453&inputToken=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913&destinationChainId=4217&outputToken=0x20c000000000000000000000b9537d11c60e8b50 ``` ### Using curl + cast (Foundry) This example bridges USDC from Base to USDC.e on Tempo. Replace addresses, chain IDs, and amounts for other routes. :::steps ### Get a deposit quote Replace `` with the wallet sending funds on the origin chain, `` with the recipient on Tempo, and `` with the source token amount in base units. ```bash curl -G "https://public-backend.bungee.exchange/api/v1/bungee/quote" \ --data-urlencode "originChainId=8453" \ --data-urlencode "destinationChainId=4217" \ --data-urlencode "inputToken=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" \ --data-urlencode "outputToken=0x20c000000000000000000000b9537d11c60e8b50" \ --data-urlencode "inputAmount=" \ --data-urlencode "receiverAddress=" \ --data-urlencode "refundAddress=" \ --data-urlencode "enableDepositAddress=true" ``` Save: * `result.deposit.requestHash` for status tracking * `result.deposit.txData.to` * `result.deposit.txData.data` * `result.deposit.txData.value` ### Submit the source-chain transaction Use the `txData` fields from the quote response. For USDC on Base, `value` is usually `0`. ```bash cast send \ \ --value \ --rpc-url https://mainnet.base.org \ --private-key $PRIVATE_KEY ``` If you are building a UI instead of submitting the transaction programmatically, show the user `result.deposit.depositData.address`, `token`, `amount`, and `chainId` exactly as returned. ### Track status ```bash curl "https://public-backend.bungee.exchange/api/v1/bungee/status?requestHash=" ``` Status codes `3` (`FULFILLED`) and `4` (`SETTLED`) are successful terminal states. Status codes `5` (`EXPIRED`), `6` (`CANCELLED`), and `7` (`REFUNDED`) are failure terminal states. ::: ## Bridge from Tempo To bridge from Tempo to another chain, swap the origin and destination in the quote request. ::::steps ### Get a deposit quote This example bridges USDC.e from Tempo to USDC on Base. ```bash curl -G "https://public-backend.bungee.exchange/api/v1/bungee/quote" \ --data-urlencode "originChainId=4217" \ --data-urlencode "destinationChainId=8453" \ --data-urlencode "inputToken=0x20c000000000000000000000b9537d11c60e8b50" \ --data-urlencode "outputToken=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" \ --data-urlencode "inputAmount=" \ --data-urlencode "receiverAddress=" \ --data-urlencode "refundAddress=" \ --data-urlencode "enableDepositAddress=true" ``` When Tempo is the origin chain, the response can include `result.deposit.depositData.memo`. If you use `deposit.depositData` for a manual transfer UI, show the memo and require the user to include it. If you submit `deposit.txData`, the calldata already includes the routing data Bungee needs. ### Submit the Tempo transaction Use the `to`, `data`, and `value` fields from `result.deposit.txData`: ```bash cast send \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` :::info Tempo has no native gas token, so Bungee transactions from Tempo normally use `value=0`. Transaction fees are paid in the account's configured fee token. If the bridge spends the same token selected for fees, leave enough balance to pay gas. ::: ### Track status ```bash curl "https://public-backend.bungee.exchange/api/v1/bungee/status?requestHash=" ``` :::: ## Using TypeScript (viem) ```typescript import { createPublicClient, createWalletClient, http } from 'viem' import { base } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' const BUNGEE_API_BASE_URL = 'https://public-backend.bungee.exchange' const account = privateKeyToAccount('0x...') const walletClient = createWalletClient({ account, chain: base, transport: http(), }) const publicClient = createPublicClient({ chain: base, transport: http(), }) const quoteParams = new URLSearchParams({ originChainId: '8453', destinationChainId: '4217', inputToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', outputToken: '0x20c000000000000000000000b9537d11c60e8b50', inputAmount: '1000000', receiverAddress: account.address, refundAddress: account.address, enableDepositAddress: 'true', }) const quoteRes = await fetch(`${BUNGEE_API_BASE_URL}/api/v1/bungee/quote?${quoteParams}`) const quote = await quoteRes.json() if (!quote.success) { throw new Error(`Bungee quote failed: ${quote.message}`) } const deposit = quote.result.deposit if (!deposit?.txData || !deposit?.requestHash) { throw new Error('Bungee did not return deposit transaction data') } const hash = await walletClient.sendTransaction({ to: deposit.txData.to, data: deposit.txData.data, value: BigInt(deposit.txData.value ?? '0'), }) await publicClient.waitForTransactionReceipt({ hash }) while (true) { const statusRes = await fetch( `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${deposit.requestHash}` ) const statusJson = await statusRes.json() const status = statusJson.result?.[0] const code = status?.bungeeStatusCode if (code === 3 || code === 4) { console.log('Bridge complete:', status.destinationData?.txHash) break } if (code === 5 || code === 6 || code === 7) { throw new Error(`Bungee request failed with status code ${code}`) } await new Promise((resolve) => setTimeout(resolve, 10_000)) } ``` ## Production checklist * Request production API access from Bungee and keep API keys server-side. * Persist `requestHash` immediately after quote generation. * Preserve `server-req-id` from quote and status responses for support. * Validate `depositData.address`, `token`, `amount`, `chainId`, and `memo` before displaying transfer instructions. * Use `requestHash` for Bungee status checks, not the source transaction hash. * Show users terminal failures (`EXPIRED`, `CANCELLED`, `REFUNDED`) and retry guidance. ## Further reading * [Bungee Deposit flow](https://docs.bungee.exchange/integrate/integration-guides/deposit) * [Bungee API reference](https://docs.bungee.exchange/api-reference) * [Bungee Link](https://docs.bungee.exchange/integrate/bungee-link) * [SOCKET Protocol documentation](https://docs.socket.tech/) * [Getting Funds on Tempo](https://tempo.xyz/developers/docs/guide/getting-funds) # Bridging stablecoins with Relay [Relay](https://relay.link/) is a cross-chain payments network powered by a solver that fills bridge requests instantly. Users deposit on the source chain and receive funds on the destination chain within seconds - no lock-and-mint or messaging protocol required. If you are bringing an existing ERC-20 to Tempo, first decide whether to preserve it as the canonical asset behind a bridge adapter or migrate issuance to native TIP-20. See [Migrate an ERC-20 to TIP-20](https://tempo.xyz/developers/docs/guide/issuance/migrate-erc20-to-tip20). Tempo's Relay chain ID is **`4217`**. ## Contracts on Tempo | Contract | Address | |----------|---------| | **ERC20Router** | [`0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f`](https://explore.tempo.xyz/address/0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f) | | **ApprovalProxy** | [`0xccc88a9d1b4ed6b0eaba998850414b24f1c315be`](https://explore.tempo.xyz/address/0xccc88a9d1b4ed6b0eaba998850414b24f1c315be) | ## Supported tokens Query the Relay chains API to see which tokens currently support bridging on Tempo: ```bash curl -s "https://api.relay.link/chains" | jq '.chains[] | select(.id == 4217) | .erc20Currencies[] | select(.supportsBridging == true) | {symbol, name, address, decimals}' ``` The list of supported tokens changes over time as Relay adds new routes. Always check the API for the latest availability. ## How Relay works Relay uses an intent-based model with three steps: 1. **Quote** - Request a quote from the Relay API specifying origin chain, destination chain, currencies, and amount. The API returns ready-to-sign transaction data. 2. **Execute** - Submit the transaction to the source chain. The user deposits funds into Relay's depository contract. 3. **Fill** - A Relay solver detects the deposit and fills the request on the destination chain, typically within seconds. For ERC-20 tokens, the quote response includes any required approval steps automatically. ## Bridge to Tempo ### Using the Relay app 1. Go to [relay.link/bridge](https://relay.link/bridge) 2. Select your source chain and token 3. Set **Tempo** as the destination chain and choose the destination token 4. Enter the amount and confirm the transaction ### Using curl + cast (Foundry) This example bridges USDC.e (Bridged USDC) from Base to Tempo. Replace the currency addresses and chain IDs for other tokens or routes. :::steps ### Get a quote Replace `` with your wallet address and `` with the amount in base units (e.g. `1000000` for 1 USDC.e with 6 decimals). ```bash curl -X POST "https://api.relay.link/quote/v2" \ -H "Content-Type: application/json" \ -d '{ "user": "", "originChainId": 8453, "destinationChainId": 4217, "originCurrency": "", "destinationCurrency": "", "amount": "", "tradeType": "EXACT_INPUT" }' ``` The response contains a `steps` array with transaction data. Save the `requestId` from the step for tracking. ### Approve token (if required) If the quote response includes an approval step, approve the Relay contract to spend your tokens. The approval target address is provided in the quote response's step data. ```bash cast send \ 'approve(address,uint256)' \ \ \ --rpc-url https://mainnet.base.org \ --private-key $PRIVATE_KEY ``` ### Submit the deposit transaction Use the `to`, `data`, and `value` fields from the quote response's transaction step: ```bash cast send \ \ --value \ --rpc-url https://mainnet.base.org \ --private-key $PRIVATE_KEY ``` ### Track status Poll the status endpoint with the `requestId` from the quote response: ```bash curl "https://api.relay.link/intents/status/v3?requestId=" ``` Status values: `waiting` -> `depositing` -> `pending` -> `success`. Once complete, view the destination transaction on Tempo: ```text https://explore.tempo.xyz/tx/ ``` ::: ### Using TypeScript (viem) ```typescript import { createWalletClient, createPublicClient, http } from 'viem' import { base } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') const walletClient = createWalletClient({ account, chain: base, transport: http(), }) const publicClient = createPublicClient({ chain: base, transport: http(), }) // 1. Get a quote from Relay const quoteRes = await fetch('https://api.relay.link/quote/v2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user: account.address, originChainId: 8453, // Base destinationChainId: 4217, // Tempo originCurrency: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base destinationCurrency: '0x20C000000000000000000000b9537d11c60E8b50', // USDC.e on Tempo amount: '1000000', // 1 USDC (6 decimals) tradeType: 'EXACT_INPUT', }), }) const quote = await quoteRes.json() // 2. Execute each step (approval + deposit) for (const step of quote.steps) { for (const item of step.items) { if (step.kind === 'transaction') { const hash = await walletClient.sendTransaction({ to: item.data.to, data: item.data.data, value: BigInt(item.data.value || '0'), }) // Wait for confirmation await publicClient.waitForTransactionReceipt({ hash }) } } } // 3. Poll for completion const requestId = quote.steps[0].requestId const pollStatus = async () => { while (true) { const statusRes = await fetch( `https://api.relay.link/intents/status/v3?requestId=${requestId}` ) const status = await statusRes.json() if (status.status === 'success') { console.log('Bridge complete:', status.txHashes) return status } if (status.status === 'failure') { throw new Error('Bridge failed') } await new Promise((r) => setTimeout(r, 1000)) } } await pollStatus() ``` ## Bridge from Tempo To bridge tokens from Tempo to another chain, swap the origin and destination in the quote request. ### Using curl + cast (Foundry) ::::steps ### Get a quote ```bash curl -X POST "https://api.relay.link/quote/v2" \ -H "Content-Type: application/json" \ -d '{ "user": "", "originChainId": 4217, "destinationChainId": 8453, "originCurrency": "", "destinationCurrency": "", "amount": "", "tradeType": "EXACT_INPUT" }' ``` ### Approve token (if required) If the quote includes an approval step, approve the Relay contract to spend your tokens on Tempo. ```bash cast send \ 'approve(address,uint256)' \ \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` ### Submit the deposit transaction Use the `to`, `data`, and `value` fields from the quote response: ```bash cast send \ \ --rpc-url https://rpc.tempo.xyz \ --private-key $PRIVATE_KEY ``` :::info Tempo has no native gas token, so no `--value` flag is needed. Transaction fees on Tempo are paid in a TIP-20 stablecoin automatically. ::: ### Track status ```bash curl "https://api.relay.link/intents/status/v3?requestId=" ``` :::: ### Using TypeScript (viem) ```typescript import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' const account = privateKeyToAccount('0x...') const client = createClient({ account, }) // 1. Get a quote from Relay (Tempo -> Base) const quoteRes = await fetch('https://api.relay.link/quote/v2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user: account.address, originChainId: 4217, // Tempo destinationChainId: 8453, // Base originCurrency: '0x20C000000000000000000000b9537d11c60E8b50', // USDC.e on Tempo destinationCurrency: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base amount: '1000000', // 1 USDC.e (6 decimals) tradeType: 'EXACT_INPUT', }), }) const quote = await quoteRes.json() // 2. Execute each step for (const step of quote.steps) { for (const item of step.items) { if (step.kind === 'transaction') { const hash = await client.sendTransaction({ to: item.data.to, data: item.data.data, value: BigInt(item.data.value || '0'), }) await client.waitForTransactionReceipt({ hash }) } } } // 3. Poll for completion const requestId = quote.steps[0].requestId const pollStatus = async () => { while (true) { const statusRes = await fetch( `https://api.relay.link/intents/status/v3?requestId=${requestId}` ) const status = await statusRes.json() if (status.status === 'success') { console.log('Bridge complete:', status.txHashes) return status } if (status.status === 'failure') { throw new Error('Bridge failed') } await new Promise((r) => setTimeout(r, 1000)) } } await pollStatus() ``` ## Supported chains Relay supports bridging to and from Tempo on many chains. Query the Relay API for the full list: ```bash curl "https://api.relay.link/chains" ``` Common routes include Ethereum, Base, Arbitrum, Optimism, Polygon, and more. See [Relay's supported chains](https://docs.relay.link/resources/supported-chains) for the complete list. ## Further reading * [Relay documentation](https://docs.relay.link) * [Relay API reference](https://docs.relay.link/references/api/overview) * [Bridges & Exchanges on Tempo](https://tempo.xyz/developers/docs/ecosystem/bridges) * [Getting Funds on Tempo](https://tempo.xyz/developers/docs/guide/getting-funds) # Tempo Ecosystem Infrastructure Integrating with Tempo is easy by leveraging services provided by our infrastructure partners. These partners take advantage of Tempo Transactions, TIP-20 tokens, and more. Visit their documentation for more information on how to get started. * [Bridges & Exchanges](https://tempo.xyz/developers/docs/ecosystem/bridges) — Move assets to and from Tempo with cross-chain bridges and exchange infrastructure * [Security & Compliance](https://tempo.xyz/developers/docs/ecosystem/security-compliance) — Transaction scanning, threat detection, and compliance infrastructure for Tempo applications * [Issuance & Orchestration](https://tempo.xyz/developers/docs/ecosystem/orchestration) — Move money globally between local currencies and stablecoins. Issue, transfer, and manage stablecoins * [Data & Analytics](https://tempo.xyz/developers/docs/ecosystem/data-analytics) — Query blockchain data with indexers, analytics platforms, and monitoring tools * [Block Explorers](https://tempo.xyz/developers/docs/ecosystem/block-explorers) — View transactions, blocks, accounts, and token activity on Tempo * [Wallets](https://tempo.xyz/developers/docs/ecosystem/wallets) — Embedded, custodial, self-custodial, and agentic wallet infrastructure for Tempo * [Smart Contract Libraries](https://tempo.xyz/developers/docs/ecosystem/smart-contract-libraries) — Build with account abstraction and programmable smart contract wallets * [Node Infrastructure](https://tempo.xyz/developers/docs/ecosystem/node-infrastructure) — Connect to Tempo with reliable RPC endpoints and managed node services * [Tempo SDKs](https://tempo.xyz/developers/docs/sdk) — Build on Tempo with official SDKs for TypeScript, Go, Foundry, and Rust # Bridges & exchanges for stablecoin liquidity Move assets to and from Tempo with cross-chain bridges and exchange infrastructure. ## Across [Across](https://across.to) provides fast, capital-efficient bridging for moving assets to and from Tempo. Across uses an intent-based architecture with optimistic verification, enabling near-instant cross-chain transfers with competitive fees. Bridge assets to Tempo through the [Across app](https://app.across.to/) and explore the integration docs at [docs.across.to](https://docs.across.to/). ## Bungee [Bungee](https://bungee.exchange) enables seamless swaps within and between blockchains. Bungee aggregates bridge and DEX liquidity to deliver fast, cost-efficient cross-chain transfers and swaps to and from Tempo, with a simple integration path via link, widget, or API. Get started with the [Tempo Bungee guide](https://tempo.xyz/developers/docs/guide/bridge-bungee), read the [Bungee docs](https://docs.bungee.exchange/), or try the [Bungee app](https://bungee.exchange). ## Chainlink CCIP [Chainlink Cross-Chain Interoperability Protocol (CCIP)](https://chain.link/cross-chain) enables applications to transfer tokens and messages across blockchains. CCIP connects Tempo to supported networks through active cross-chain lanes. View supported tokens, lanes, fees, and contract configuration in the [Tempo Mainnet CCIP Directory](https://docs.chain.link/ccip/directory/mainnet/chain/tempo-mainnet). ## Coinbase [Coinbase](https://www.coinbase.com/) supports Tempo across custody, trading, and transfers, including in its iOS and Android apps. Retail and exchange customers can use Coinbase's existing stablecoin markets, then select Tempo as the network when sending or receiving. Eligible Coinbase Prime clients can also access custody support, with availability varying by asset and contracting entity. For businesses and developers using Tempo, this adds a familiar route to source supported stablecoins and move them between Coinbase and the network. That reduces the operational steps between exchange or custody infrastructure and stablecoin payments or treasury activity on Tempo. ## LayerZero [LayerZero](https://layerzero.network) is an omnichain interoperability protocol that enables secure, low-level message passing between blockchains. Stargate is the global interface for value movement onchain. Within Tempo, Stargate enables native, 1:1 asset transfers into and out of the ecosystem. Transfer assets to Tempo with [Stargate](https://stargate.finance/). ## Relay [Relay](https://relay.link) provides instant cross-chain bridging and transaction execution. Relay enables users and applications to move assets to Tempo from other chains with fast finality and low fees, powered by a network of relayers that fill orders on the destination chain. Bridge to Tempo through the [Relay app](https://relay.link) and explore the [Relay docs](https://docs.relay.link/). ## Rhino.fi [Rhino.fi](https://rhino.fi) provides cross-chain liquidity infrastructure for stablecoin onboarding and settlement, enabling seamless deposits and withdrawals across 20+ chains in under 10 seconds. Learn more at [rhino.fi](https://rhino.fi) or explore the [Rhino.fi docs](https://docs.rhino.fi/get-started/introduction). ## Squid [Squid](https://www.squidrouter.com) provides intent-based cross-chain routing and execution from any token on any supported chain to any token on Tempo. Squid aggregates DEXs, bridges, and market makers to deliver optimal routes with sub-5s execution. Developers can integrate via API, SDK, or widget. Start swapping in the [Squid app](https://app.squidrouter.com/) or explore the [Squid docs](https://docs.squidrouter.com/). ## Uniswap [Uniswap](https://hub.uniswap.org/) is the leading decentralized exchange with over $4T in all-time volume. Any institution, builder or agent can connect to and start building on the most battle tested infrastructure in the space with 99+% uptime, 200ms routing latency, and over 10m assets, all for free. To get started, create an account and start building on the [Uniswap API platform](https://developers.uniswap.org/dashboard/welcome). ## 0x [0x](https://0x.org) provides institutional DEX aggregation and smart order routing powered by production-grade execution infrastructure. Access deep liquidity across major DEXs on Tempo with low revert rates, sub-250ms response times, and built-in monetization tools. Create an account on the [0x Dashboard](https://dashboard.0x.org/create-account) and explore the [0x docs](https://0x.org/docs/0x-swap-api/introduction). # Data and analytics providers Query blockchain data with indexers, analytics platforms, and monitoring tools. ## Allium [Allium](https://www.allium.so) is an enterprise blockchain data platform that delivers real-time, analytics-ready datasets through a unified schema across chains. Developers can fetch wallet, token, and price data in milliseconds without managing infrastructure, decoding raw data, or inferring transactions—making it easy to focus on building Tempo applications. Get access to Tempo data through the [Allium App](https://app.allium.so/join), explore the full API in the [Allium docs](https://docs.allium.so/), and browse real examples of production apps built on Allium [here](https://docs.allium.so/api/developer/overview). :::tip Allium has a [ready-to-use recipe](https://github.com/Allium-Science/allium-recipes/tree/main/tempo) for querying Tempo data with SQL. ::: ## Artemis [Artemis](https://about.artemis.ai/) provides a unified analytics terminal for monitoring onchain activity across stablecoins, assets, and networks. Developers use Artemis to analyze flows, liquidity, token performance, and ecosystem-level trends through a clean, queryable interface. Tempo is already supported within Artemis, with a dedicated analytics page for [Tempo Testnet](https://app.artemisanalytics.com/asset/tempo_moderato). Artemis also maintains a cross-chain stablecoin dashboard covering major USD-pegged assets across numerous networks. Stablecoins launched on Tempo will appear in the [Stablecoins dashboard](https://app.artemisanalytics.com/stablecoins). ## Chainlink [Chainlink](https://chain.link) is the industry-standard oracle platform powering the majority of DeFi and bringing capital markets onchain. The Chainlink stack provides the data, interoperability, and security needed for tokenized assets, stablecoins, payments, lending, and other advanced onchain use cases. Chainlink supports Tempo through: * **Price Feeds:** Chainlink Price Feeds provide decentralized market data onchain for lending markets, FX, tokenized assets, stablecoins, and risk-management applications. View the live feeds and contract addresses in the [Chainlink Price Feed directory](https://docs.chain.link/data-feeds/price-feeds/addresses?network=tempo). * **Cross-Chain Interoperability Protocol (CCIP):** A secure interoperability layer for sending messages and value across chains, enabling cross-chain user flows and multi-chain architectures.\ Explore CCIP in the [Chainlink CCIP docs](https://docs.chain.link/ccip). * **Data Streams:** Chainlink Data Streams delivers low-latency market data offchain, which can be verified onchain. This pull-based design gives dApps on-demand access to high-frequency market data backed by decentralized, fault-tolerant, and transparent infrastructure—an improvement over traditional push-based oracles that update only at fixed intervals or price thresholds.\ View the Chainlink Data Stream deployed on Tempo [here](https://explore.tempo.xyz/address/0xcE73c8ad08CBDEaCa6078BF0627C8fe0a9a536E7?tab=contract). Developers can explore Price Feeds, CCIP, Data Streams, and the full Chainlink platform through the [Chainlink Developer Docs](https://docs.chain.link/). ## Chronicle [Chronicle](https://chroniclelabs.org/) is an onchain data infrastructure provider founded in 2017 within MakerDAO. Chronicle built the first oracle on Ethereum and has continuously operated onchain data infrastructure through multiple market cycles, securing billions in assets across institutional and onchain financial platforms. Chronicle supports Tempo through: * DeFi Price Feeds: High-integrity price oracles purpose-built for DeFi protocols, delivering reliable onchain data for lending, stablecoins, and other onchain financial applications.  * Proof of Asset: Institutional-grade verification infrastructure for tokenized assets, enabling issuers to provide cryptographic attestations of reserves, collateral, and asset backing. This supports tokenized treasuries, stablecoins, RWAs, and other use cases where verifiable proof of underlying assets is essential. Developers can learn more about Chronicle's data feeds on [Chronicle Docs](https://docs.chroniclelabs.org/). ## CoinGecko [CoinGecko](https://www.coingecko.com) provides comprehensive cryptocurrency market data, including prices, trading volume, market capitalization, and token metadata. Developers can use the CoinGecko API to access Tempo token data for building dashboards, portfolio trackers, and analytics tools. Get started with the [CoinGecko API](https://docs.coingecko.com/reference/introduction). ## Dune [Dune](https://dune.com) delivers real-time onchain data through the Dune Sim API, a low-latency interface for querying wallet balances, token prices, and account activity across multiple chains. Tempo is fully supported, giving developers instant access to account balances valued in USD, current token prices, and transaction history and activity — all reflecting the state of the chain at the exact moment of the query. This point-in-time accuracy makes the Dune Sim API well-suited for applications where data freshness matters, such as payment flows, portfolio dashboards, and real-time trading interfaces, without the need to manage any indexing infrastructure. [Sign up](https://sim.dune.com/) to get started with Tempo data, and explore the full API in the [Sim docs](https://docs.sim.dune.com/). ## Goldsky [Goldsky](https://goldsky.com) makes it easy to access real-time Tempo data with minimal maintenance. Goldsky offers two core products for indexing and streaming onchain data: * **[Subgraphs](https://docs.goldsky.com/subgraphs/):** A fully backwards-compatible subgraph indexing solution that handles reorgs, RPC failures, and scaling automatically, with improved reliability and performance over traditional subgraph hosts. * **[Mirror](https://docs.goldsky.com/mirror/):** A simple way to replicate subgraph or chain-level streams directly into your own databases or message queues, powering flexible front-end and back-end data pipelines. Start indexing Tempo [here](https://goldsky.com/chains/tempo). ## Range [Range](https://www.range.org) powers the Stablecoin Explorer, which provides a unified view of major stablecoins across 100+ chains. Tempo is fully supported, allowing developers and users to trace stablecoin flows in a way traditional explorers cannot. Range stands out through: * **Complete cross-chain visibility**, showing the entire lifecycle of a transfer in one place * **Enriched context**, including bridge routes, verified entities, and risk signals * **Built-in compliance checks** via global sanctions lists Explore Tempo activity in the [Stablecoin Explorer](https://explorer.money/transactions?dn=tempo-testnet\&sc=INTRACHAIN\&sn=tempo-testnet). ## RedStone [RedStone](https://redstone.finance) delivers modular oracle infrastructure with a Push and Pull model for onchain price feeds. RedStone's architecture minimizes gas costs by delivering data on-demand, making it well-suited for DeFi applications, lending protocols, FX and stablecoin systems on Tempo. Explore the available Push data feeds [here](https://app.redstone.finance/push-feeds?networks=tempo\&testnets=true) and integration guides in the [RedStone docs](https://docs.redstone.finance/). ## SonarX [SonarX](https://www.sonarx.com) is an institutional-grade blockchain data infrastructure platform trusted by leading financial institutions, custodians, and Web3 teams. SonarX delivers structured, historical, and real-time on-chain data backed by a proprietary Data Quality Framework and SOC 2-certified controls. SonarX provides full Tempo coverage from genesis, including a Full Historical Stream Dataset for deep archival analysis and a Real-Time Dataset for live monitoring, payment validation, and trading applications. Data is available via Snowflake, Databricks, BigQuery, Kafka streams, real-time APIs, or custom file drops. Request a data trial on the [SonarX platform](https://www.sonarx.com/trial) and explore the [SonarX docs](https://docs.sonarx.com/). ## SQD [SQD](https://sqd.ai) is a decentralized query engine and high-performance indexing toolkit for extracting and transforming on-chain data. With the Squid SDK, developers can build custom indexers for Tempo that are up to 100x faster than direct RPC indexing, with data served through the SQD Network's decentralized data layer. Get started with the [SQD docs](https://docs.sqd.ai/) and deploy indexers via [SQD Cloud](https://app.subsquid.io/). ## Zerion [Zerion](https://zerion.io/api) provides an enterprise-grade wallet data API that delivers portfolio balances, transaction history, DeFi positions, PnL tracking, and real-time webhooks — including Tempo — through a single unified interface. Developers can add comprehensive blockchain data to their applications without running any indexing infrastructure. Get a free API key from the [Zerion dashboard](https://dashboard.zerion.io/) and explore the [API documentation](https://developers.zerion.io/reference/authentication). # Block explorers: using the Tempo Explorer View transactions, blocks, accounts, and token activity on Tempo. ## Tempo Explorer Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](https://tempo.xyz/developers/docs/quickstart/connection-details). ## Tenderly [Tenderly](https://tenderly.co) delivers full-stack observability, debugging, and simulation tools for Tempo smart contract development and monitoring. With Tenderly you get real-time error tracking, EVM-level tracing, and off-chain transaction simulation — enabling you to catch bugs, analyze reverts, and inspect gas usage before transactions go live. You can enable Tempo in the [Tenderly Dashboard](https://dashboard.tenderly.co/) to use its tracing, alerts, and debugging tools with no infrastructure to manage. # Wallets: embedded and external options Integrate user-friendly wallet experiences directly into your application. ## Embedded ### Blockradar [Blockradar](https://blockradar.co) provides non-custodial wallet infrastructure purpose-built for fintechs running stablecoin payments. The platform focuses on real financial use cases, from merchant settlement to cross-border payouts, with tools designed for payments, compliance, treasury operations, and multi-chain liquidity. Explore the full platform in the [Blockradar Docs](https://docs.blockradar.co/). **Wallet and Payment Operations:** Through one unified API, teams can issue wallets for users, merchants, or treasury; accept fiat inflows through virtual accounts; enable gasless stablecoin transactions; apply AML checks automatically; consolidate balances through configurable sweeps; and handle cross-chain movement using swap and bridge. Fintechs can start building immediately from our API or [Blockradar Dashboard](https://dashboard.blockradar.co/). For advanced flows or high-volume programs, fintechs can [book a demo](https://www.blockradar.co/contact) to walk through production architectures. ### Crossmint [Crossmint](https://www.crossmint.com) is an all-in-one platform, with unified APIs for [wallets](https://docs.crossmint.com/wallets/), [stablecoin orchestration](https://docs.crossmint.com/stablecoin-orchestration/), [checkout flows](https://docs.crossmint.com/payments), and [tokenization](https://docs.crossmint.com/minting), giving developers a single interface for everything from payments to asset management on Tempo. Crossmint delivers a gasless, seed-phrase-free UX backed by bank-grade security and compliance, along with no-code dashboards for managing programs across your team. Set up a project in the [Crossmint console](https://crossmint.com/console) and explore the [Solution Guide](https://docs.crossmint.com/solutions/overview#fintech) tailored for payment use-cases. ### Dynamic [Dynamic](https://dynamic.xyz) combines authentication, smart wallets, and key management into a flexible SDK for Tempo developers. Teams can onboard users with familiar login methods and provision Tempo-compatible wallets through Dynamic's secure infrastructure. Enable Tempo testnet in the [Dynamic dashboard](https://app.dynamic.xyz/dashboard/chains-and-networks), and create an account [here](https://www.dynamic.xyz/get-started) to start integrating Dynamic into your app. ### Para [Para](https://getpara.com) is a comprehensive wallet and authentication suite for fintech and crypto applications. It provides flexible login methods, secure MPC-backed wallets, fast authentication, and infrastructure for automating onchain activity. Para is adding Tempo chain support so developers can easily build Tempo-enabled wallets and payment flows. Get started by signing up through the [Para Dev Portal](https://developer.getpara.com/) and following the quickstart in the [Para docs](https://docs.getpara.com/v2/introduction/welcome). ### Privy [Privy](https://www.privy.io/) builds secure key management and embedded wallets so any developer can easily build secure, scalable wallets into their app. Easily spin up self-custodial wallets for users, manage your treasury wallets and more. Privy takes advantage of Tempo-native experiences to enable better stablecoin and payments experiences. Easily enable gas sponsorship, leverage webhooks for onchain events, delegated signatures, simple wallet funding, etc. You can get started now. Simply [create](https://docs.privy.io/wallets/wallets/create/create-a-wallet#param-chain-type-1) an ethereum wallet with Privy and pass in `"caip2": "eip155:4217"` when [making transactions](https://docs.privy.io/wallets/using-wallets/ethereum/send-a-transaction#usage-9). :::tip Check out the [Tempo + Privy guide](https://docs.privy.io/wallets/using-wallets/tempo/send-a-transaction) and Privy's [example](https://github.com/privy-io/examples/tree/main/examples/privy-next-tempo) peer-to-peer payments app that uses Tempo transaction memos. ::: ### Turnkey [Turnkey](https://www.turnkey.com) provides programmable key management and non-custodial wallet infrastructure for applications that need granular signing policies and automated transaction flows. With Turnkey, developers can securely sign Tempo Transactions, automate wallet operations, and build custom logic around how keys are used. Turnkey also supports sponsor-style workflows, enabling gasless or subsidized transaction flows through configurable signing policies. [Create your Turnkey account](https://app.turnkey.com/dashboard) and follow the [Turnkey Embedded Wallet Kit guide](https://docs.turnkey.com/sdks/react/getting-started) to integrate embedded wallets into your Tempo app. :::tip Turnkey has a [`with-tempo`](https://github.com/tkhq/sdk/tree/main/examples/chain-integrations/with-tempo) example in their SDK to get you started quickly. ::: ## Custodial & Institutional ### BitGo [BitGo](https://www.bitgo.com) provides institutional-grade custody, trading, and wallet infrastructure. BitGo supports Tempo with both custodial and self-custody wallet solutions, enabling enterprises to securely store, manage, and transact with Tempo-based assets under robust security and compliance controls. BitGo is a qualified custodian in the United States and globally [licensed and regulated](https://www.bitgo.com/company/licenses/). Get started through the [BitGo platform](https://www.bitgo.com) or explore their [developer docs](https://developers.bitgo.com/). ### Coinbase Eligible [Coinbase Prime](https://www.coinbase.com/prime) clients can access custody support for Tempo, with availability varying by asset and contracting entity. Coinbase also supports Tempo for retail and exchange trading and transfers, including in its iOS and Android apps. Customers can use existing stablecoin markets, then select Tempo as the network when sending or receiving. See [Coinbase trading and transfers](https://tempo.xyz/developers/docs/ecosystem/bridges#coinbase) for more context. ### Cubist [Cubist](https://cubist.dev) provides high-performance, institutional-grade infrastructure spanning wallets, tokenization, payments, and private smart contracts. Institutions use Cubist to manage their treasuries, automate internal operations, and programmatically control how digital assets move while delivering cryptographic audit trails for regulators. Developers use Cubist to provision wallets with familiar authentication methods and gas sponsorship for end users and AI agents. Learn about [Cubist on Tempo](https://cubist.dev/use-cases/payments-tokenization) and [request a demo](https://cubist.dev/contact) for more information. ### DFNS [DFNS](https://www.dfns.co) provides programmable key management and wallet-as-a-service infrastructure for Tempo. With DFNS, developers can create and manage wallets, automate signing workflows, and build custom orchestration logic, all backed by distributed MPC key generation and policy-based access controls. Create an account on the [DFNS dashboard](https://app.dfns.io) and explore the [DFNS docs](https://docs.dfns.co/) to integrate wallets into your Tempo application. ### Fireblocks [Fireblocks](https://www.fireblocks.com) provides enterprise-grade digital asset infrastructure for custody, transfers, and tokenization. Tempo is supported through Fireblocks' MPC-based signing, policy engine, and transaction API, enabling institutions to securely manage Tempo assets with configurable approval workflows and direct network connectivity. Access Tempo through the [Fireblocks console](https://console.fireblocks.io/) and explore the [Fireblocks Developer docs](https://developers.fireblocks.com/). ### Utila [Utila](https://utila.io) provides secure MPC wallet infrastructure and asset-management tooling for teams building with stablecoins and digital assets. Developers can use Utila to manage Tempo-based payments and treasury operations across multiple wallets and blockchains, all within a single policy-driven platform. Utila's MPC technology reduces counterparty risk, while its configurable approval engine gives teams granular control over how funds are moved. [Learn more](https://utila.io/product/payments/) about how Utila supports stablecoin operations on Tempo, and [request a demo](https://utila.io/request-a-demo/) if you're interested in secure MPC infrastructure. ## Self-Custodial ### Bitget Wallet [Bitget Wallet](https://web3.bitget.com) makes crypto simple, secure, and seamless for everyone. With support for 130+ blockchains and 80 million users, Bitget Wallet offers one-click trading at low fees without cross-chain and gas complexities, real-time alpha insights, and a $300M Protection Fund. Security is backed by MPC and smart wallet technology with no single point of failure. Get started at [Bitget Wallet](https://web3.bitget.com). ### Bridge Wallet [Bridge Wallet](https://www.mtpelerin.com/bridge-wallet) is Mt Pelerin's self-custodial mobile wallet with Tempo support. Users hold their own secret phrase and can send, receive, manage, buy, swap, or cash out supported assets through the app. Download Bridge Wallet from the [Mt Pelerin website](https://www.mtpelerin.com/bridge-wallet). ### Gem Wallet [Gem Wallet](https://gemwallet.com) is an open-source, self-custodial mobile wallet for iOS and Android with native Tempo support. Users can store, send, receive, and swap Tempo assets. Download Gem Wallet from the [Gem Wallet website](https://gemwallet.com) or explore its [open-source repository](https://github.com/gemwalletcom/wallet). ### OKX Wallet [OKX Wallet](https://www.okx.com/web3) is a self-custodial multi-chain wallet with native Tempo support. Users can store, swap, and manage Tempo-based assets, with built-in DEX aggregation and DApp connectivity. Get started at [OKX Wallet](https://www.okx.com/web3). ### Rabby [Rabby](https://rabby.io) is an open-source, multi-chain wallet for Ethereum and all EVM chains, built by DeBank. Rabby automatically detects and switches to the correct chain when connecting to DApps, and features pre-sign transaction simulation, balance change previews, and built-in risk alerts to keep your assets safe. Get started at [Rabby](https://rabby.io). ### Safe [Safe](https://safe.global) provides a modular smart account framework used across leading Web3 applications and institutions. With Safe, developers can build Tempo applications that take advantage of multi-sig controls, programmable permissions, session keys, and automated transaction policies. Get started at [Safe](https://safe.global). ## Agentic ### Enact [Enact](https://www.enact.finance/) provides programmable wallet infrastructure for teams and AI agents that need granular onchain controls. Set your policy once (spending limits, multisig thresholds, approval windows, automation rules) and let agents execute against it autonomously, with every action verifiable onchain. Enact never holds your funds; it enforces the logic you define. Get started with the [Enact CLI](https://docs.enact.finance/) or the [Enact app](https://app.enact.finance/) to create a self-custodial passkey wallet, add signers, and deploy your first onchain policy. ### Sponge [Sponge](https://paysponge.com) provides financial infrastructure for AI agents, enabling them to hold, send, and swap crypto autonomously on Tempo. With Sponge, developers can give their agents wallets with configurable spending controls, allowlists, and audit logging, along with first-class support for Claude and other AI frameworks via MCP and SDK. Get started on the [Sponge platform](https://paysponge.com) and explore the [Sponge docs](https://paysponge.com/docs). # Smart Contract Libraries Build with account abstraction and programmable smart contract wallets. ## Pimlico [Pimlico](https://www.pimlico.io) provides smart account infrastructure for Tempo, including ERC-4337 bundlers and paymasters. With Pimlico, developers can sponsor gas fees, accept ERC-20 tokens for gas, and relay smart account transactions — enabling seamless, gasless onchain experiences for end users. Get started on the [Pimlico dashboard](https://dashboard.pimlico.io/) and explore the [Pimlico docs](https://docs.pimlico.io/). ## Safe *(coming soon)* [Safe](https://safe.global) provides a modular smart account framework used across leading Web3 applications and institutions. With Safe, developers can build Tempo applications that take advantage of multi-sig controls, programmable permissions, session keys, and automated transaction policies. Safe integration for Tempo is coming soon. Stay tuned for updates as support becomes available. ## ZeroDev [ZeroDev](https://zerodev.app) provides a powerful smart account platform for Tempo, supporting both ERC-4337 and EIP-7702. Developers can onboard users with social logins, enable gas sponsorship, and automate transactions while taking advantage of ZeroDev's chain-abstracted workflows. Its modular wallet stack also allows teams to build customized features such as custom transaction policies and tailored approval logic. Create a project in the [ZeroDev dashboard](https://dashboard.zerodev.app) and follow the [SDK quickstart](https://docs.zerodev.app/sdk/getting-started/quickstart) to integrate smart accounts into your Tempo application. # Node infrastructure providers Connect to Tempo with reliable RPC endpoints and managed node services. ## Alchemy With [Alchemy](https://alchemy.com), build the fastest and most reliable Tempo applications, powered by industry-leading latency, uptime, and elastic throughput. Alchemy's global RPC infrastructure supports everything from stablecoins to tokenization and large-scale consumer apps. Sign up through the [Alchemy dashboard](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=tempo) and visit the [Alchemy docs](https://www.alchemy.com/docs/node#tldr) to start building. ## Blockdaemon [Blockdaemon](https://app.blockdaemon.com/) provides institutional-grade node and API infrastructure, along with staking and MPC wallet services. Their globally distributed platform supports enterprise-scale, production workloads with strong reliability and compliance guarantees. Sign up through the [Blockdaemon Developer Dashboard](https://app.blockdaemon.com/) and deploy a Tempo node by navigating to **Nodes & RPC → Deploy a Node**. ## Chainstack [Chainstack](https://chainstack.com) provides managed blockchain infrastructure with high-performance, secure RPC nodes. The platform offers reliable Tempo endpoints with built-in monitoring and analytics. Create an account through the [Chainstack console](https://console.chainstack.com) to deploy Tempo nodes and access RPC endpoints. ## Conduit [Conduit](https://conduit.xyz) provides production-grade blockchain infrastructure and developer tooling for teams building on Tempo. Their RPC stack delivers the performance and reliability needed to build financial applications at scale. Access [Conduit](https://hub.conduit.xyz/tempo-testnet) or read the [Tempo RPC Quickstart](https://docs.conduit.xyz/rpc-nodes/getting-started/tempo-rpc-quickstart) to get started. ## dRPC [dRPC](https://drpc.org) provides managed Tempo RPC endpoints through NodeCloud, with smart routing, analytics, key control, and front-end protection across 180+ networks. The platform runs on 40 providers in 8 geoclusters, with a free tier and flat-rate plans starting at $10. Get started by visiting the [dRPC chain list](https://drpc.org/chainlist), and learn more about NodeCloud on the [dRPC NodeCloud page](https://drpc.org/nodecloud-multichain-rpc-management). ## Luganodes [Luganodes](https://www.luganodes.com/) provides institutional-grade blockchain infrastructure across 40+ networks, delivering non-custodial node operations backed by SOC2 Type II certification. Luganodes brings Swiss-grade precision across bare metal and cloud deployments, with enterprise-grade business continuity and risk management frameworks built for the demands of protocols, global VCs, custodians, and exchanges. ## Quicknode [Quicknode](https://quicknode.com) is the enterprise-grade development platform for building, scaling, and launching onchain applications with speed and reliability. Their globally optimized RPC network makes it easy to run high-performance Tempo workloads from day one. Get started on the [Tempo Chain Page](https://www.quicknode.com/chains/tempo) and follow the [QuickStart guide](https://www.quicknode.com/docs/tempo) to create your Tempo RPC endpoint. ## SenseiNode [SenseiNode](https://www.senseinode.com/) operates institutional-grade RPC and validator infrastructure across 35+ protocols, backed by SOC 2 Type II and ISO 27001:2022 certifications. SenseiNode does not ship commodity endpoints. Their engineering team sizes, tunes, and operates every deployment around your specific workload, available out of the box. Reach out to the concierge infrastructure team at [info@senseinode.com](mailto\:info@senseinode.com) or visit [senseinode.com](https://www.senseinode.com/) to scope your Tempo RPC requirements and receive a tailored proposal within 24 hours. ## Validation Cloud [Validation Cloud](https://www.validationcloud.io/tempo) provides validators and institutional-grade, full-archive RPC nodes for Tempo. Built for high performance, low latency, and SOC 2 Type II compliance, Validation Cloud is purpose-built for powering real-world payments and stablecoin use cases at scale. Get started on the [Validation Cloud platform](https://www.validationcloud.io/tempo). # Security and compliance tools Transaction scanning, threat detection, and compliance infrastructure for Tempo applications. ## Blockaid [Blockaid](https://blockaid.io) provides real-time security infrastructure for Web3 applications. Its transaction scanning and threat detection systems identify malicious activity before users sign transactions, improving safety across wallets and interfaces. Learn how Blockaid's transaction scanning improves security by visiting their [overview page](https://www.blockaid.io/transaction-security), and reach out to their team [here](https://www.blockaid.io/contact) to get started. ## Chainalysis [Chainalysis](https://www.chainalysis.com) delivers industry-leading onchain intelligence, compliance, and security infrastructure. Through Hexagate, Chainalysis supports Tempo with real-time monitoring, anomaly detection, and threat insights to help developers and platforms better understand and manage onchain risk as the ecosystem grows. Discover how Hexagate supports Tempo [here](https://www.hexagate.com), or request a dedicated walkthrough from the Chainalysis team through their [demo form](https://www.hexagate.com/request-demo). ## Elliptic [Elliptic](https://www.elliptic.co) provides blockchain analytics and compliance solutions for detecting and preventing financial crime. Elliptic supports Tempo with transaction screening, wallet risk scoring, and regulatory compliance tools — helping platforms meet AML obligations while operating on the Tempo network. Learn more about Elliptic's compliance solutions at [elliptic.co](https://www.elliptic.co) or explore their [developer docs](https://docs.elliptic.co/). ## TRES [TRES](https://www.tres.finance) is the accounting and reconciliation layer for digital asset payments. TRES reconciles onchain settlement against internal and custodial records daily, then delivers the output in bank-grade formats such as MT940 and custom formats so finance and treasury teams can keep using their existing TMS and ERP without a rewrite. ## TRM Labs [TRM Labs](https://www.trmlabs.com) delivers blockchain intelligence and compliance infrastructure for detecting fraud, money laundering, and financial crime. TRM supports Tempo with transaction monitoring, wallet screening, and risk assessment tools that help platforms operate safely and meet regulatory requirements. Get started at [trmlabs.com](https://www.trmlabs.com) or explore their [documentation](https://docs.trmlabs.com/). # Issuance & orchestration for stablecoins Move money globally between local currencies and stablecoins. Issue, transfer, and manage stablecoins. ## AllUnity [AllUnity](https://allunity.com) is a BaFin-regulated e-money institute providing institutional-grade infrastructure for seamless, secure, real-time digital currency transactions, with full reserve backing and regulatory transparency. AllUnity assets live on Tempo include EURAU, SEKAU, and CHFAU. ## Brale [Brale](https://brale.xyz) provides infrastructure for issuing, transferring, and managing stablecoins across chains. Developers can create new stablecoins or work with existing issued assets using Brale's APIs to support on- and off-ramps, payouts, and cross-ecosystem stablecoin movement. Brale exposes two complementary APIs: * **[Stablecoin Movement & Account Management](https://docs.brale.xyz/#stablecoin-movement--account-management-apibralexyz):**\ An authenticated API for orchestrating stablecoin workflows, including issuance, transfers across accounts or chains, custody management, and integration with financial institutions. * **[Stablecoin Market Data](https://docs.brale.xyz/#stablecoin-market-data-databralexyz):**\ A public, read-only API that provides token metadata, stablecoin definitions, and price feeds. These APIs support common stablecoin workflows such as minting, redemption, swaps, payouts, and treasury operations, making Brale suitable for fintechs, exchanges, and payment platforms building on Tempo. Get started by creating an account [here](https://app.brale.xyz/buy/signup/). ## Bridge [Bridge](https://www.bridge.xyz) (a Stripe Company) provides stablecoin orchestration infrastructure for moving money between fiat and crypto rails. Bridge supports Tempo with APIs for issuance, wallets, and cross-border stablecoin transfers, enabling fintechs and platforms to build payment flows that span traditional and onchain systems. Get started with [Bridge's Tempo Integration Guide](https://apidocs.bridge.xyz/get-started/guides/move-money/tempo-integration-guide#tempo-integration-guide). ## Coins.ph [Coins.ph](https://www.coins.ph/en-ph/business) provides regulated payment and stablecoin infrastructure for businesses operating in the Philippines. Its APIs support PHP collections and disbursements, cross-border payments, virtual accounts, institutional trading, and fiat-to-stablecoin on-ramps and off-ramps. Explore the [Coins.ph API documentation](https://docs.coins.ph/rest-api/) or [contact the Coins.ph business team](https://www.coins.ph/en-ph/business) to get started. ## Hercle [Hercle](https://www.hercle.com/) provides institutional cross-border payment infrastructure connecting fiat, stablecoins, and digital assets. Teams on Tempo can access competitive FX rates in EUR, GBP, and 30+ local currencies. For EUR and GBP, Hercle also supports pay-ins, payouts, and stablecoin on-ramps and off-ramps, giving payment companies one route from local currency through the onchain leg and back to local currency. [Check supported corridors](https://www.hercle.com/corridor-checker/) or [contact Hercle](https://www.hercle.com/contact/) to get started. ## MoonPay [MoonPay](https://www.moonpay.com/business/ramps) provides global on-ramp and off-ramp infrastructure for applications that need to move users between fiat currencies and stablecoins. Teams can integrate MoonPay Ramps through a widget, SDK, or API, with payment methods, identity verification, fraud protection, and compliance built into the flow. Explore the [MoonPay developer docs](https://dev.moonpay.com/) to get started. ## Mt Pelerin [Mt Pelerin](https://www.mtpelerin.com/) provides non-custodial on-ramp and off-ramp services for supported assets on Tempo. Users can buy by bank transfer, card, Apple Pay, or Google Pay and cash out to bank accounts in 18 fiat currencies. Explore [Mt Pelerin's buy and cash-out services](https://www.mtpelerin.com/) or use its [Bridge Wallet mobile app](https://www.mtpelerin.com/bridge-wallet). ## Rio [Rio](https://www.rio.trade/stablecoin-fx) provides stablecoin FX infrastructure for converting between fiat currencies and stablecoins. Its real-time execution engine and integration APIs help platforms embed FX conversion and settle transactions onchain. [Contact Rio](https://www.rio.trade/stablecoin-fx) to discuss an integration. ## UR [UR](https://ur.app/partners/tempo) provides infrastructure that connects stablecoin settlement with fiat rails for platforms building global financial services. UR supports Swiss IBAN accounts, seven fiat currencies, SEPA and SWIFT transfers, Mastercard card programs, and compliance workflows for wallet developers and financial platforms. Get started with [UR's partner portal](https://partner.ur.app/) or [book a demo](https://ur.app/partners/tempo). ## Wirex [Wirex](https://www.wirexapp.com/) provides card issuance, wallets, and stablecoin settlement infrastructure. Its integration lets partners use Tempo as the onchain settlement layer for enterprise stablecoin card programs. Explore the [Wirex platform documentation](https://docs.wirexapp.com/) and [supported environments and chains](https://docs.wirexapp.com/docs/retail-environments) to plan an integration. ## XFX [XFX](https://xfx.io) provides institutional FX execution and settlement infrastructure, unifying fiat and stablecoin liquidity into one execution venue (API, web, or OTC) — delivering best price, instant settlement, and zero pre-funding for capital-efficient FX. # TIP-20 tokens: A stablecoin-native standard TIP-20 tokens are Tempo's native token standard for stablecoins and payment tokens. They are designed for stablecoin payments, and are the foundation for many token-related functions on Tempo including transaction fees, payment lanes, DEX quote tokens, optimized routing for DEX liquidity, optional on-chain token `logoURI` metadata, implicit approvals for listed precompiles, and enshrined payment-channel reserve flows. :::info[Live with T6] The [T6 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t6) added [account-level receive policies](https://tempo.xyz/developers/docs/protocol/tip403/receive-policies) for TIP-20 transfers and mints. A receiver can choose which tokens and senders they accept, helping wallets and deposit addresses avoid unsupported assets, unwanted counterparties, and wrong-token deposits. If a receive policy blocks delivery, the transfer or mint still succeeds, but funds are redirected to `ReceivePolicyGuard` so they can be claimed later. See the [T5 → T6 migration appendix on the TIP-20 spec](https://tempo.xyz/developers/docs/protocol/tip20/spec#t5--t6-migration) for the TIP-20 surface area. ::: All TIP-20 tokens are created by interacting with the [TIP-20 Factory contract](https://tempo.xyz/developers/docs/protocol/tip20/spec#tip20factory), calling the `createToken` function. If you're issuing a stablecoin on Tempo, we **strongly recommend** using the TIP-20 standard. Learn more about the benefits, or follow the guide on issuance [here](https://tempo.xyz/developers/docs/guide/issuance). ## Benefits & Features of TIP-20 Tokens Below are some of the key benefits and features of TIP-20 tokens: ### Payments * [Pay for Blockchain Transaction Fees](#pay-fees-in-any-stablecoin) — Only TIP-20 tokens can be used to pay for transaction fees on Tempo. * [Get Predictable Payment Fees](#get-predictable-payment-fees) — TIP-20 tokens have dedicated blockspace from all other transactions, ensuring predictable payment fees. * [Transfer Memos](#transfer-memos) — Attach 32-byte memos to transfers for payment references, invoice IDs, or transaction notes. ### Exchange * [Currency Declaration](#currency-declaration) — Declare currency identifiers (e.g., USD, EUR) for proper routing and pricing in the Stablecoin DEX. * [DEX Quote Tokens](#dex-quote-tokens) — TIP-20 tokens can serve as quote tokens in Tempo's DEX for trading pairs and liquidity pools. ### Compliance & Controls * [Built-in Role-Based Access Control](#role-based-access-control-rbac) — Set access control roles for minting, burning, pausing, and administrative operations. * [Enforce Transfer Policies](#tempo-policy-registry-tip-403) — Enforce compliance with whitelist and blacklist policies via the Tempo Policy Registry (TIP-403). * [Operational Controls](#operational-controls) — Supply caps, pause/unpause controls, and 32-byte transfer memos for payment references. ### Pay Fees in Any Stablecoin Any USD-denominated TIP-20 token can be used to pay transaction fees on Tempo. The [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) automatically converts your token to the validator's preferred fee token, eliminating the need for users to hold a separate gas token. This feature works natively: no additional infrastructure or integration required. Full specification of this feature can be found in the [Payment Lanes Specification](https://tempo.xyz/developers/docs/protocol/blockspace/payment-lane-specification). ### Get Predictable Payment Fees Tempo has dedicated payment lanes: reserved blockspace for payment TIP-20 transactions that other applications cannot consume. Even if there are extremely popular applications on the chain competing for blockspace, payroll runs or customer disbursements execute predictably. Learn more about the [payments lane](https://tempo.xyz/developers/docs/protocol/blockspace/payment-lane-specification). ### Role-Based Access Control (RBAC) TIP-20 includes a built-in [RBAC system](https://tempo.xyz/developers/docs/protocol/tip20/spec#tip-20-roles) that separates administrative responsibilities: * **ISSUER\_ROLE**: Grants permission to mint and burn tokens, enabling controlled token issuance * **PAUSE\_ROLE** / **UNPAUSE\_ROLE**: Allows pausing and unpausing token transfers for emergency controls * **BURN\_BLOCKED\_ROLE**: Permits burning tokens from blocked addresses (e.g., for compliance actions) Roles can be granted, revoked, and delegated without custom contract changes. This enables issuers to separate operational roles (e.g., who can mint) from administrative roles (e.g., who can pause). Learn more in the [TIP-20 specification](https://tempo.xyz/developers/docs/protocol/tip20/spec#tip-20-roles). ### Tempo Policy Registry (TIP-403) TIP-20 tokens integrate with the [Tempo Policy Registry (TIP-403)](https://tempo.xyz/developers/docs/protocol/tip403/overview) to enforce compliance policies. Each token can reference a policy that controls who can send and receive tokens: * **Whitelist policies**: Only addresses in the whitelist can transfer tokens * **Blacklist policies**: Addresses in the blacklist are blocked from transferring tokens Policies can be shared across multiple tokens, enabling consistent compliance enforcement across your token ecosystem. See the [TIP-403 specification](https://tempo.xyz/developers/docs/protocol/tip403/spec) for details. ### Operational Controls TIP-20 tokens can set **supply caps**, which allow you to set a maximum token supply to control issuance. TIP-20 tokens also have **pause/unpause** commands, which provide emergency controls to halt transfers when needed. ### Transfer Memos **Transfer memos** enable you to attach 32-byte memos to transfers for payment references, invoice IDs, or transaction notes. ### Currency Declaration A TIP-20 token can declare a currency identifier that identifies the reference asset whose price the token is designed to track. This enables proper routing and pricing in Tempo's [Stablecoin DEX](https://tempo.xyz/developers/docs/protocol/exchange). Currently, **only `USD`-denominated stablecoins** can be used to pay transaction fees on Tempo or traded on the StablecoinDEX. #### General principle **The `currency` field identifies the reference asset that 1 unit of the token is designed to be worth** — not what the token is called or what it is denominated in. Two tokens that track the same asset should have the same `currency`. When in doubt, consider that the purpose of this field is to determine what assets a token would trade against on a DEX designed for assets that trade within 2% of a 1:1 price. #### Guidelines 1. **Tokens that track an asset with an [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) code** — use that code. This includes fiat currencies (`"USD"`, `"EUR"`) as well as commodities (`"XAU"` for gold, `"XAG"` for silver). 2. **Tokens tracking a non-ISO asset at 1:1** — use the symbol of the reference asset. For example, a bridged WBTC that tracks BTC should use `"BTC"`; a bridged WETH that tracks ETH should use `"ETH"`. Prefer `"BTC"` over `"XBT"`. Use the symbol from the asset's origin chain when there is ambiguity across platforms. 3. **Tokens denominated in but not pegged to a currency** — do **not** use that currency's code. A tokenized gold product priced in USD should not use `"USD"`, because 1 unit is not designed to be worth 1 USD. 4. **Tokens with no reference asset** — if a token does not track any external asset (e.g. a governance or utility token), use its own symbol as the currency. #### Currency declaration examples | Token | Tracks | `currency` | |-------|--------|------------| | USDC | US Dollar | `"USD"` | | USDT | US Dollar | `"USD"` | | EURC | Euro | `"EUR"` | | Bridged WBTC | Bitcoin | `"BTC"` | | Bridged stETH | Ether | `"ETH"` | | Bridged wstETH | Wrapped staked Ether | `"wstETH"` | | Tokenized gold (priced in USD) | Gold | `"XAU"` | | Governance token (e.g. UNI) | Itself | `"UNI"` | :::warning The currency code is **immutable** — it cannot be changed after token creation. An incorrect currency code will affect fee payment eligibility, DEX routing, and quote token pairing. ::: ### DEX Quote Tokens TIP-20 tokens can serve as quote tokens in Tempo's decentralized exchange (DEX). When creating trading pairs on the [Stablecoin DEX](https://tempo.xyz/developers/docs/protocol/exchange), TIP-20 tokens function as the quote currency against which other tokens are priced and traded. This enables efficient stablecoin-to-stablecoin trading and provides optimized routing for liquidity. For example, a USDG TIP-20 token can be paired with other stablecoins, allowing traders to swap between different USD-denominated tokens with minimal slippage through concentrated liquidity pools. By using TIP-20 tokens as quote tokens, the DEX benefits from the same payment-optimized features like deterministic addresses, currency identifiers, and compliance policies, ensuring secure and efficient exchange operations. ## Additional Links * [TIP-20 Specification](https://tempo.xyz/developers/docs/protocol/tip20/spec) — Learn how TIP-20 works and its features * [Guide: Make Payments](https://tempo.xyz/developers/docs/guide/payments) — Send and receive payments using stablecoins on Tempo * [Virtual Addresses](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses) — Generate one deposit address per customer without sweep transactions * [Guide: Issue Stablecoins](https://tempo.xyz/developers/docs/guide/issuance) — Create and manage your own stablecoin on Tempo # TIP-20 Tokens Specification ## Abstract TIP-20 tokens are a suite of precompiles that provide a built-in optimized token implementation in the core protocol. They extend the ERC-20 token standard with built-in functionality like memo fields and transfer policies. ## Motivation All major stablecoins today use the ERC-20 token standard. While ERC-20 provides a solid foundation for fungible tokens, it lacks features critical for stablecoin issuers today such as memos and transfer policies. Additionally, since each ERC-20 token has its own implementation, integrators can't depend on consistent behavior across tokens. TIP-20 extends ERC-20, building these features into precompiled contracts that anyone can permissionlessly deploy on Tempo. This makes token operations much more efficient, allows issuers to quickly set up on Tempo, and simplifies integrations since it ensures standardized behavior across tokens. It also enables deeper integration with token-specific Tempo features like paying gas in stablecoins and payment lanes. ## Specification TIP-20 tokens support standard fungible token operations such as transfers, mints, and burns. They also support transfers, mints, and burns with an attached 32-byte memo and a role-based access control system for token administrative operations. ## TIP20 The core TIP-20 contract exposes standard ERC-20 functions for balances, allowances, transfers, and delegated transfers, and also adds: * 32-byte memo support on transfers, mints, and burns. * A `TIP20Roles` module for permissioned actions like issuing, pausing, unpausing, and burning blocked balances. * Configuration options for currencies, quote tokens, and transfer policies. The complete TIP20 interface is defined below: ```solidity interface ITIP20 { // ========================================================================= // ERC-20 standard functions // ========================================================================= /// @notice Returns the name of the token /// @return The token name function name() external view returns (string memory); /// @notice Returns the symbol of the token /// @return The token symbol function symbol() external view returns (string memory); /// @notice Returns the number of decimals for the token /// @return Always returns 6 for TIP-20 tokens function decimals() external pure returns (uint8); /// @notice Returns the total amount of tokens in circulation /// @return The total supply of tokens function totalSupply() external view returns (uint256); /// @notice Returns the token balance of an account /// @param account The address to check the balance for /// @return The token balance of the account function balanceOf(address account) external view returns (uint256); /// @notice Transfers tokens from caller to recipient /// @param to The recipient address /// @param amount The amount of tokens to transfer /// @return True if successful function transfer(address to, uint256 amount) external returns (bool); /// @notice Returns the remaining allowance for a spender /// @param owner The token owner address /// @param spender The spender address /// @return The remaining allowance amount function allowance(address owner, address spender) external view returns (uint256); /// @notice Approves a spender to spend tokens on behalf of caller /// @param spender The address to approve /// @param amount The amount to approve /// @return True if successful function approve(address spender, uint256 amount) external returns (bool); /// @notice Transfers tokens from one address to another using allowance /// @param from The sender address /// @param to The recipient address /// @param amount The amount to transfer /// @return True if successful function transferFrom(address from, address to, uint256 amount) external returns (bool); /// @notice Mints new tokens to an address (requires ISSUER_ROLE) /// @param to The recipient address /// @param amount The amount of tokens to mint function mint(address to, uint256 amount) external; /// @notice Burns tokens from caller's balance (requires ISSUER_ROLE) /// @param amount The amount of tokens to burn function burn(uint256 amount) external; // ========================================================================= // TIP-20 extended functions // ========================================================================= /// @notice Transfers tokens from caller to recipient with a memo /// @param to The recipient address /// @param amount The amount of tokens to transfer /// @param memo A 32-byte memo attached to the transfer function transferWithMemo(address to, uint256 amount, bytes32 memo) external; /// @notice Transfers tokens from one address to another with a memo using allowance /// @param from The sender address /// @param to The recipient address /// @param amount The amount to transfer /// @param memo A 32-byte memo attached to the transfer /// @return True if successful function transferFromWithMemo(address from, address to, uint256 amount, bytes32 memo) external returns (bool); /// @notice Mints new tokens to an address with a memo (requires ISSUER_ROLE) /// @param to The recipient address /// @param amount The amount of tokens to mint /// @param memo A 32-byte memo attached to the mint function mintWithMemo(address to, uint256 amount, bytes32 memo) external; /// @notice Burns tokens from caller's balance with a memo (requires ISSUER_ROLE) /// @param amount The amount of tokens to burn /// @param memo A 32-byte memo attached to the burn function burnWithMemo(uint256 amount, bytes32 memo) external; /// @notice Burns tokens from a blocked address (requires BURN_BLOCKED_ROLE) /// @param from The address to burn tokens from (must be unauthorized by transfer policy) /// @param amount The amount of tokens to burn function burnBlocked(address from, uint256 amount) external; /// @notice Returns the quote token used for DEX pairing /// @return The quote token address function quoteToken() external view returns (ITIP20); /// @notice Returns the next quote token staged for update /// @return The next quote token address (zero if none staged) function nextQuoteToken() external view returns (ITIP20); /// @notice Returns the currency identifier for this token /// @return The currency string function currency() external view returns (string memory); /// @notice Returns whether the token is currently paused /// @return True if paused, false otherwise function paused() external view returns (bool); /// @notice Returns the maximum supply cap for the token /// @return The supply cap (checked on mint operations) function supplyCap() external view returns (uint256); /// @notice Returns the current transfer policy ID from TIP-403 registry /// @return The transfer policy ID function transferPolicyId() external view returns (uint64); /// @notice Returns the on-chain logo URI for this token (empty if unset) function logoURI() external view returns (string memory); // ========================================================================= // Admin Functions // ========================================================================= /// @notice Pauses the contract, blocking transfers (requires PAUSE_ROLE) function pause() external; /// @notice Unpauses the contract, allowing transfers (requires UNPAUSE_ROLE) function unpause() external; /// @notice Changes the transfer policy ID (requires DEFAULT_ADMIN_ROLE) /// @param newPolicyId The new policy ID from TIP-403 registry /// @dev Validates that the policy exists using TIP403Registry.policyExists(). /// Built-in policies (ID 0 = always-reject, ID 1 = always-allow) are always valid. /// For custom policies (ID >= 2), the policy must exist in the TIP-403 registry. /// Reverts with InvalidTransferPolicyId if the policy does not exist. function changeTransferPolicyId(uint64 newPolicyId) external; /// @notice Stages a new quote token for update (requires DEFAULT_ADMIN_ROLE) /// @param newQuoteToken The new quote token address function setNextQuoteToken(ITIP20 newQuoteToken) external; /// @notice Completes the quote token update process (requires DEFAULT_ADMIN_ROLE) function completeQuoteTokenUpdate() external; /// @notice Sets the maximum supply cap (requires DEFAULT_ADMIN_ROLE) /// @param newSupplyCap The new supply cap (cannot be less than current supply) function setSupplyCap(uint256 newSupplyCap) external; /// @notice Updates the logo URI (requires DEFAULT_ADMIN_ROLE) /// @param newLogoURI The new logo URI (max 256 bytes; empty string clears the field) /// @dev If non-empty, MUST be a syntactically valid URI with a scheme in the /// allowlist {https, http, ipfs, data} (case-insensitive). /// Reverts LogoURITooLong if length > 256 bytes. /// Reverts InvalidLogoURI if the scheme is not in the allowlist or the URI is malformed. function setLogoURI(string calldata newLogoURI) external; // ========================================================================= // Role Management // ========================================================================= /// @notice Returns the BURN_BLOCKED_ROLE constant /// @return keccak256("BURN_BLOCKED_ROLE") function BURN_BLOCKED_ROLE() external view returns (bytes32); /// @notice Returns the ISSUER_ROLE constant /// @return keccak256("ISSUER_ROLE") function ISSUER_ROLE() external view returns (bytes32); /// @notice Returns the PAUSE_ROLE constant /// @return keccak256("PAUSE_ROLE") function PAUSE_ROLE() external view returns (bytes32); /// @notice Returns the UNPAUSE_ROLE constant /// @return keccak256("UNPAUSE_ROLE") function UNPAUSE_ROLE() external view returns (bytes32); /// @notice Grants a role to an account (requires role admin) /// @param role The role to grant (keccak256 hash) /// @param account The account to grant the role to function grantRole(bytes32 role, address account) external; /// @notice Revokes a role from an account (requires role admin) /// @param role The role to revoke (keccak256 hash) /// @param account The account to revoke the role from function revokeRole(bytes32 role, address account) external; /// @notice Allows an account to remove a role from itself /// @param role The role to renounce (keccak256 hash) function renounceRole(bytes32 role) external; /// @notice Changes the admin role for a specific role (requires current role admin) /// @param role The role whose admin is being changed /// @param adminRole The new admin role function setRoleAdmin(bytes32 role, bytes32 adminRole) external; // ========================================================================= // EIP-2612 Permit // ========================================================================= /// @notice Approves a spender via an off-chain signature (EIP-2612) /// @param owner The token owner who signed the permit /// @param spender The address being approved /// @param value The allowance amount /// @param deadline The timestamp after which the signature expires /// @param v ECDSA recovery byte (must be 27 or 28; 0/1 is not normalized) /// @param r ECDSA signature component /// @param s ECDSA signature component function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; /// @notice Returns the current nonce for an owner (incremented on each permit) /// @param owner The address to query /// @return The current nonce function nonces(address owner) external view returns (uint256); /// @notice Returns the EIP-712 domain separator for this token /// @return The domain separator hash (computed dynamically using block.chainid) function DOMAIN_SEPARATOR() external view returns (bytes32); // ========================================================================= // System Functions // ========================================================================= /// @notice System-level transfer function (restricted to precompiles) /// @param from The sender address /// @param to The recipient address /// @param amount The amount to transfer /// @return True if successful function systemTransferFrom(address from, address to, uint256 amount) external returns (bool); /// @notice Pre-transaction fee transfer (restricted to precompiles) /// @param from The account to charge fees from /// @param amount The fee amount function transferFeePreTx(address from, uint256 amount) external; /// @notice Post-transaction fee handling (restricted to precompiles) /// @param to The account to refund /// @param refund The refund amount /// @param actualUsed The actual fee used function transferFeePostTx(address to, uint256 refund, uint256 actualUsed) external; // ========================================================================= // Events // ========================================================================= /// @notice Emitted when a new allowance is set by `owner` for `spender` /// @param owner The account granting the allowance /// @param spender The account being approved to spend tokens /// @param amount The new allowance amount event Approval(address indexed owner, address indexed spender, uint256 amount); /// @notice Emitted when tokens are burned from an address /// @param from The address whose tokens were burned /// @param amount The amount of tokens that were burned event Burn(address indexed from, uint256 amount); /// @notice Emitted when tokens are burned from a blocked address /// @param from The blocked address whose tokens were burned /// @param amount The amount of tokens that were burned event BurnBlocked(address indexed from, uint256 amount); /// @notice Emitted when new tokens are minted to an address /// @param to The address receiving the minted tokens /// @param amount The amount of tokens that were minted event Mint(address indexed to, uint256 amount); /// @notice Emitted when a new quote token is staged for this token /// @param updater The account that staged the new quote token /// @param nextQuoteToken The quote token that has been staged event NextQuoteTokenSet(address indexed updater, ITIP20 indexed nextQuoteToken); /// @notice Emitted when the pause state of the token changes /// @param updater The account that changed the pause state /// @param isPaused The new pause state; true if paused, false if unpaused event PauseStateUpdate(address indexed updater, bool isPaused); /// @notice Emitted when the quote token update process is completed /// @param updater The account that completed the quote token update /// @param newQuoteToken The new quote token that has been set event QuoteTokenUpdate(address indexed updater, ITIP20 indexed newQuoteToken); /// @notice Emitted when the token's supply cap is updated /// @param updater The account that updated the supply cap /// @param newSupplyCap The new maximum total supply event SupplyCapUpdate(address indexed updater, uint256 indexed newSupplyCap); /// @notice Emitted for all token movements, including mints and burns /// @param from The address sending tokens (address(0) for mints) /// @param to The address receiving tokens (address(0) for burns) /// @param amount The amount of tokens transferred event Transfer(address indexed from, address indexed to, uint256 amount); /// @notice Emitted when the transfer policy ID is updated /// @param updater The account that updated the transfer policy /// @param newPolicyId The new transfer policy ID from the TIP-403 registry event TransferPolicyUpdate(address indexed updater, uint64 indexed newPolicyId); /// @notice Emitted when a transfer, mint, or burn is performed with an attached memo /// @param from The address sending tokens (address(0) for mints) /// @param to The address receiving tokens (address(0) for burns) /// @param amount The amount of tokens transferred /// @param memo The 32-byte memo associated with this movement event TransferWithMemo( address indexed from, address indexed to, uint256 amount, bytes32 indexed memo ); /// @notice Emitted when the membership of a role changes for an account /// @param role The role being granted or revoked /// @param account The account whose membership was changed /// @param sender The account that performed the change /// @param hasRole True if the role was granted, false if it was revoked event RoleMembershipUpdated( bytes32 indexed role, address indexed account, address indexed sender, bool hasRole ); /// @notice Emitted when the admin role for a role is updated /// @param role The role whose admin role was changed /// @param newAdminRole The new admin role for the given role /// @param sender The account that performed the update event RoleAdminUpdated( bytes32 indexed role, bytes32 indexed newAdminRole, address indexed sender ); /// @notice Emitted when the logo URI changes /// @param updater The address that updated the logo URI (msg.sender) /// @param newLogoURI The new logo URI value event LogoURIUpdated(address indexed updater, string newLogoURI); // ========================================================================= // Errors // ========================================================================= /// @notice The token operation is blocked because the contract is currently paused error ContractPaused(); /// @notice The permit signature has expired (block.timestamp > deadline) error PermitExpired(); /// @notice The recovered signer does not match the permit owner error InvalidSignature(); /// @notice The spender does not have enough allowance for the attempted transfer error InsufficientAllowance(); /// @notice The account does not have the required token balance for the operation /// @param currentBalance The current balance of the account /// @param expectedBalance The required balance for the operation to succeed /// @param token The address of the token contract error InsufficientBalance(uint256 currentBalance, uint256 expectedBalance, address token); /// @notice The provided amount is zero or otherwise invalid for the attempted operation error InvalidAmount(); /// @notice The provided currency identifier is invalid or unsupported error InvalidCurrency(); /// @notice The specified quote token is invalid, incompatible, or would create a circular reference error InvalidQuoteToken(); /// @notice The recipient address is not a valid destination for this operation /// (for example, another TIP-20 token contract) error InvalidRecipient(); /// @notice The specified transfer policy ID does not exist in the TIP-403 registry error InvalidTransferPolicyId(); /// @notice The new supply cap is invalid, for example lower than the current total supply error InvalidSupplyCap(); /// @notice The configured transfer policy denies authorization for the sender or recipient error PolicyForbids(); /// @notice The attempted operation would cause total supply to exceed the configured supply cap error SupplyCapExceeded(); /// @notice The caller does not have the required role or permission for this operation error Unauthorized(); /// @notice The provided logoURI exceeds the 256-byte cap error LogoURITooLong(); /// @notice The provided logoURI is malformed or uses a scheme outside the allowlist error InvalidLogoURI(); } ``` :::warning When interacting with precompiles, **always use the provided ABI** rather than reading directly from storage slots. Direct storage access may lead to undefined behavior. ::: ## Memos Memo functions `transferWithMemo`, `transferFromWithMemo`, `mintWithMemo`, and `burnWithMemo` behave like their ERC-20 equivalents but additionally emit memo data in dedicated events. The memo is always a fixed 32-byte field. Callers should pack shorter strings or identifiers directly into this field, and use hashes or external references when the underlying payload exceeds 32 bytes. ## TIP-403 Transfer Policies All operations that move tokens: `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, `burn`, `mintWithMemo`, and `burnWithMemo` — enforce the token’s configured TIP-403 transfer policy. Internally, this is implemented via a `transferAuthorized` modifier that: * Calls `TIP403_REGISTRY.isAuthorized(transferPolicyId, from)` for the sender. * Calls `TIP403_REGISTRY.isAuthorized(transferPolicyId, to)` for the recipient. Both checks must return `true`, otherwise the call reverts with `PolicyForbids`. ## Invalid Recipient Protection TIP-20 tokens cannot be sent to other TIP-20 token contract addresses. The implementation uses a `validRecipient` guard that rejects recipients whose address is zero, or has the TIP-20 prefix (`0x20c000000000000000000000`). Any attempt to transfer to a TIP-20 token address must revert with `InvalidRecipient`. This prevents accidental token loss by sending funds to token contracts instead of user accounts. ## Virtual Address Recipients Recipient-bearing TIP-20 paths — `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, and `mintWithMemo` — resolve [virtual addresses](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses) before running recipient authorization and mint-recipient checks. When a recipient `to` is a registered virtual address, the effective recipient becomes the registered master wallet, and authorization, balance updates, and event emission target that master wallet. Virtual addresses are valid TIP-20 recipients on those paths but remain forwarding aliases rather than canonical TIP-20 holders. Non-TIP-20 tokens sent to a virtual address do not forward. Forwarded deposits appear as two-hop standard `Transfer` events in the same transaction; indexers and explorers should collapse that pair into one logical deposit to the resolved master wallet. ## Currencies and Quote Tokens Each TIP-20 token declares a [currency identifier](https://tempo.xyz/developers/docs/protocol/tip20/overview#currency-declaration) and a corresponding `quoteToken` used for pricing and routing in the Stablecoin DEX. The currency is set at token creation and **cannot be changed afterward**. **Only tokens with `currency == "USD"` are eligible for paying transaction fees.** Tokens with `currency == "USD"` must pair with a USD-denominated TIP-20 token. Updating the quote token occurs in two phases: 1. `setNextQuoteToken` stages a new quote token. 2. `completeQuoteTokenUpdate` finalizes the change. The implementation must validate that the new quote token is a TIP-20 token, matches currency rules, and does not create circular quote-token chains. :::note While quote tokens can be changed, choose carefully as the update process requires careful coordination with the DEX. ::: ## Permit TIP-20 tokens support [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permit`, added in the [T2 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t2). A token owner signs an EIP-712 typed message off-chain authorizing a spender, and any third party can submit that signature on-chain — combining approve and action into a single transaction without the owner paying gas. The `DOMAIN_SEPARATOR` is computed dynamically on every call using `block.chainid`, so it remains correct after a chain fork. Each owner has a monotonically increasing `nonce` to prevent replay. Only `v = 27` or `v = 28` is accepted; `v = 0` or `v = 1` is intentionally **not** normalized (see the [specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1004.md) for rationale). ## Logo URI Every TIP-20 exposes an optional on-chain `logoURI`, added in the [T5 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t5). Wallets and explorers read the icon directly from the token contract via `logoURI()`, without an off-chain registry round-trip. Tokens without a `logoURI` continue to work; the field is empty by default. The admin (`DEFAULT_ADMIN_ROLE`) sets or clears it via `setLogoURI(string newLogoURI)`, which emits `LogoURIUpdated(msg.sender, newLogoURI)`. An empty string is valid and clears the field. A non-empty value must be at most 256 bytes (`LogoURITooLong` otherwise) and a syntactically valid URI whose scheme is in the allowlist `{https, http, ipfs, data}`, matched case-insensitively (`InvalidLogoURI` otherwise). Per the [specification's recommended formats](https://tips.sh/1026#recommended-formats), use a square, single-frame rasterized image (PNG or WebP). SVG is allowed by the scheme allowlist but not recommended — integrators that accept SVG must follow its SVG-handling guidance. ## Pause Controls Pause controls `pause` and `unpause` govern token movement. When paused, transfers and memo transfers halt, but administrative and configuration functions remain allowed. The `paused()` getter reflects the current state and must be checked by all affected entrypoints. ## TIP-20 Roles TIP-20 uses a role-based authorization system. The main roles are: * `ISSUER_ROLE`: controls minting and burning. * `PAUSE_ROLE` / `UNPAUSE_ROLE`: controls the token’s paused state. * `BURN_BLOCKED_ROLE`: allows burning balances belonging to addresses that fail TIP-403 authorization. Roles are assigned and managed through `grantRole`, `revokeRole`, `renounceRole`, and `setRoleAdmin`, via the contract admin. ## System Functions System level functions `systemTransferFrom`, `transferFeePreTx`, and `transferFeePostTx` are only callable by other Tempo protocol precompiles. These entrypoints power transaction fee collection, refunds, and internal accounting within the Fee AMM and Stablecoin DEX. They must not be callable by general contracts or users. `transferFeePreTx` respects the token's pause state and will revert if the token is paused. However, `transferFeePostTx` is intentionally allowed to execute even when the token is paused. This ensures that a transaction which pauses the token can still complete successfully and receive its fee refund. Apart from this specific refund transfer, no other token transfers can occur after a pause event. ### Implicit approvals for listed precompiles An Implicit Approval List names the precompiles that may pull TIP-20 tokens without a prior `approve`. Listed precompiles call the internal `system_transfer_from(from, to, amount)` entrypoint, which: * Is **not** part of the public TIP-20 ABI and **not** callable by external contracts or EOAs. * Skips allowance checks and the allowance storage write. * Still enforces balance checks, TIP-403 transfer policies, and AccountKeychain spending limits. * Emits the standard TIP-20 `Transfer` event. This generalizes the system-only path described above (`systemTransferFrom`, `transferFeePreTx`, `transferFeePostTx`) to an allow-list of precompiles — the StablecoinDEX, FeeAMM, and the `TIP20ChannelReserve` precompile. Normal `approve`, `permit`, `allowance`, and `transferFrom` behavior is unchanged. ### Payment-channel reserve The enshrined `TIP20ChannelReserve` precompile at [`0x4D50500000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x4D50500000000000000000000000000000000000) (ASCII `MPP`) is a TIP-20 consumer rather than a change to the TIP-20 contract itself — it pulls funds via the implicit-approval path above and emits standard `Transfer` events from the host TIP-20. See the [enshrined TIP-20 reserve channel section of the T5 page](https://tempo.xyz/developers/docs/protocol/upgrades/t5#enshrined-tip-20-reserve-channel) for the channel lifecycle, channel ID derivation, and event surface. ## TIP20Factory The `TIP20Factory` contract is the canonical entrypoint for creating new TIP-20 tokens on Tempo. The factory derives deterministic deployment addresses using a caller-provided salt, combined with the caller's address, under a fixed 12-byte TIP-20 prefix. This ensures that every TIP-20 token exists at a predictable, collision-free address. The `TIP20Factory` precompile is deployed at `0x20Fc000000000000000000000000000000000000`. Newly created TIP-20 addresses are deployed to a deterministic address derived from `TIP20_PREFIX || lowerBytes`, where: * `TIP20_PREFIX` is the 12-byte prefix `20C000000000000000000000` * `lowerBytes` is the highest 64 bits of `keccak256(msg.sender, salt)` The first 1000 addresses (where `lowerBytes < 1000`) are reserved for protocol use and cannot be deployed to via the factory. When creating a token, the factory performs several checks to guarantee consistency across the TIP-20 ecosystem: * The specified Quote token must be a currently deployed TIP20. * Tokens that specify their currency as USD must also specify a quote token that is denoted in USD. * At deployment, the factory initializes defaults on the TIP-20:\ `transferPolicyId = 1`, `supplyCap = type(uint128).max`, `paused = false`, and `totalSupply = 0`. * The provided `admin` address receives `DEFAULT_ADMIN_ROLE`, enabling it to manage roles and token configurations. The factory provides two `createToken` overloads: the original 6-argument form, and a 7-argument form that additionally sets an initial [`logoURI`](#logo-uri) at creation (validated with the same rules as `setLogoURI`). The 6-argument overload is unchanged. The complete `TIP20Factory` interface is defined below: ```solidity /// @title TIP-20 Factory Interface /// @notice Deploys and initializes new TIP-20 tokens at deterministic addresses interface ITIP20Factory { /// @notice Creates and deploys a new TIP-20 token /// @param name The token's ERC-20 name /// @param symbol The token's ERC-20 symbol /// @param currency The token's currency identifier (ISO 4217 code, when available). Immutable after creation. See Currency Declaration (https://tempo.xyz/developers/docs/protocol/tip20/overview#currency-declaration). /// @param quoteToken The TIP-20 quote token used for exchange pricing /// @param admin The address to receive DEFAULT_ADMIN_ROLE on the new token /// @param salt A unique salt for deterministic address derivation /// /// @return token The deployed TIP-20 token address /// @dev /// - Computes the TIP-20 deployment address as TIP20_PREFIX || lowerBytes, /// where lowerBytes is the highest 64 bits of keccak256(msg.sender, salt) /// - Reverts with AddressReserved if lowerBytes < 1000 /// - Ensures the provided quote token is itself a valid TIP-20 /// - Enforces USD-denomination rules (USD tokens must use USD quote tokens) /// - Initializes the token with default settings: /// transferPolicyId = 1 (always-allow) /// supplyCap = type(uint128).max /// paused = false /// totalSupply = 0 /// - Grants DEFAULT_ADMIN_ROLE on the new token to `admin` /// - Emits a {TokenCreated} event function createToken( string memory name, string memory symbol, string memory currency, ITIP20 quoteToken, address admin, bytes32 salt ) external returns (address token); /// @notice Creates and deploys a new TIP-20 token with an initial logoURI /// @dev Identical to the 6-arg overload, but additionally sets logoURI at creation. /// Validation rules for logoURI are the same as setLogoURI (256-byte cap, /// allowlisted schemes, syntactically valid URI). Empty string is valid and /// leaves logoURI unset (no LogoURIUpdated event emitted). When non-empty, the /// new token emits LogoURIUpdated(msg.sender, logoURI) from its own address. function createToken( string memory name, string memory symbol, string memory currency, ITIP20 quoteToken, address admin, bytes32 salt, string memory logoURI ) external returns (address token); // ========================================================================= // Helpers // ========================================================================= /// @notice Returns true if `token` is a valid TIP-20 address /// @param token The address to check /// @return True if the address is a well-formed TIP-20 /// @dev Checks the TIP-20 prefix and verifies the token has code deployed function isTIP20(address token) external view returns (bool); /// @notice Computes the deterministic TIP-20 address for a given sender and salt /// @param sender The address that will call {createToken} /// @param salt The salt that will be passed to {createToken} /// @return token The TIP-20 address that would be deployed /// @dev Computes the address as TIP20_PREFIX || lowerBytes, where lowerBytes is /// the highest 64 bits of keccak256(sender, salt), matching the factory deployment scheme. function getTokenAddress(address sender, bytes32 salt) external pure returns (address token); // ========================================================================= // Events // ========================================================================= /// @notice Emitted when a new TIP-20 token is created /// @param token The newly deployed TIP-20 address /// @param name The token name /// @param symbol The token symbol /// @param currency The token currency /// @param quoteToken The token's assigned quote token /// @param admin The address receiving DEFAULT_ADMIN_ROLE /// @param salt The salt used for deterministic address derivation event TokenCreated( address indexed token, string name, string symbol, string currency, ITIP20 quoteToken, address admin, bytes32 salt ); // ========================================================================= // Errors // ========================================================================= /// @notice The computed address falls within the reserved range (lowerBytes < 1000) error AddressReserved(); /// @notice The provided quote token address is invalid or not a TIP-20 error InvalidQuoteToken(); } ``` ## Invariants * `totalSupply()` must always equal to the sum of all `balanceOf(account)` over all accounts. * `totalSupply()` must always be `<= supplyCap` * When `paused` is `true`, no functions that move tokens (`transfer`, `transferFrom`, memo variants, `systemTransferFrom`, `transferFeePreTx`) can succeed. * TIP20 tokens cannot be transferred to another TIP20 token contract address. * `systemTransferFrom`, `transferFeePreTx`, and `transferFeePostTx` never change `totalSupply()`. * `bytes(logoURI()).length` must always be `<= 256`. ## T5 → T6 migration :::info[Migration appendix] This section captures TIP-20 changes introduced by the [T6 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t6). These changes are active on both testnet and mainnet. ::: T6 introduces one change that affects the TIP-20 transfer and mint surface: ### Account-level receive policies The TIP-20 `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `systemTransferFrom`, `mint`, and `mintWithMemo` flows gain a receive-policy check after the existing TIP-403 transfer-policy check. The receive policy is owned by the receiver and configured on the TIP-403 precompile via a new `setReceivePolicy(...)` function; it is enforced via `validateReceivePolicy(token, sender, receiver)`. For `transfer`/`transferFrom`/memo variants/`systemTransferFrom`, the sender for policy purposes is the `from` argument; for `mint`/`mintWithMemo` it is `msg.sender`. If the receive policy blocks the operation, **the call still succeeds**. Delivery is redirected to a new `ReceivePolicyGuard` precompile at `0xB10C000000000000000000000000000000000000`, which records a receipt for the transfer or mint. The originator or a recovery address designated by the receiver can later claim the recorded amount from the guard. * The token's existing TIP-403 policy is checked first and continues to revert on failure. Only receive-policy failures redirect. * The host TIP-20 emits its standard `Transfer` event to `0xB10C000000000000000000000000000000000000` on a redirected transfer; the guard separately emits a `TransferBlocked` event with the information needed to claim the receipt. Blocked receipts are not enumerable on chain — indexers must subscribe to `TransferBlocked` to surface claimable funds. * Virtual addresses are resolved to the master address before any receive-policy check. Receipts are recorded against the master while preserving the original `to` for attribution. * `approve`, `permit`, and `burn` are not affected. Fee deposits and refunds via `transfer_fee_pre_tx` and `transfer_fee_post_tx` are not affected. TIP-20 internal balances and reward flows are not affected. Read the [full specification](https://tips.sh/1028), including the `ReceivePolicyGuard` claim interface and recovery-authority rules. # Virtual addresses for TIP-20 deposits Virtual addresses let you give each customer their own TIP-20 deposit address without giving each customer their own onchain wallet balance. A deposit sent to that address is forwarded by the protocol to a registered master wallet. For exchanges, ramps, custodians, and payment processors, this changes the operational model. You still get one address per customer for attribution and reconciliation, but you no longer need sweep jobs to consolidate funds. ## Why this feature exists Without virtual addresses, per-customer deposit addresses are operationally expensive. Each deposit address becomes a real onchain balance holder. Funds land there first, and then the operator has to sweep those funds into a central wallet. With virtual addresses, the customer-facing address is still unique, but it behaves like a routing alias. The protocol resolves it to the registered master wallet during the TIP-20 transfer itself. Without virtual addresses, each customer deposit address holds a separate onchain balance that must be swept to the master wallet. With virtual addresses, TIP-20 forwarding routes deposits directly to the master wallet. Without virtual addresses, each customer deposit address holds a separate onchain balance that must be swept to the master wallet. With virtual addresses, TIP-20 forwarding routes deposits directly to the master wallet. This means: * you keep one deposit address per customer * the master wallet receives the balance directly * no sweep transaction is needed * no separate TIP-20 balance is created for each deposit address Forwarding happens inside the same TIP-20 precompile call that processes the transfer — there is no second transaction or additional token movement. The only extra cost is a single storage read (SLOAD) to look up the registered master wallet in the virtual-address registry. ## The mental model A virtual address is not a second wallet. It is a deposit alias for one canonical wallet. The important idea is simple: the virtual address is for routing and attribution, while the master wallet is where the TIP-20 balance actually lives. ## Address format A virtual address is still a normal 20-byte EVM address. The [specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) gives those 20 bytes a specific layout: ```text 0x | masterId (4 bytes) | VIRTUAL_MAGIC (10 bytes) | userTag (6 bytes) ``` Example: ```text 0x2612766c fdfdfdfdfdfdfdfdfdfd 000000000001 ``` Where: | Part | Size | Purpose | | --- | --- | --- | | `masterId` | 4 bytes | identifies which registered master wallet should receive the funds | | `VIRTUAL_MAGIC` | 10 bytes | marks the address as virtual so TIP-20 can recognize it | | `userTag` | 6 bytes | operator-chosen routing or attribution value | TIP-20 recognizes a virtual address by the fixed 10-byte middle marker. It then uses the leading `masterId` to resolve the registered wallet and leaves the trailing `userTag` available for operator-side attribution. ## What happens when someone sends funds When a sender transfers a covered TIP-20 token to a virtual address, the TIP-20 precompile detects the virtual format, looks up the registered master, and credits that master wallet. ```mermaid sequenceDiagram participant Sender participant TIP20 as TIP-20 participant Registry as Virtual registry participant Master as Registered wallet Sender->>TIP20: transfer(virtualAddress, amount) TIP20->>Registry: resolve(masterId) Registry-->>TIP20: master wallet TIP20->>Master: credit balance Note over TIP20: emits Transfer(sender → virtual, amount) Note over TIP20: emits Transfer(virtual → master, amount) ``` Two things matter here: 1. The balance is credited only to the master wallet. 2. The transaction still exposes the virtual address in events, so backends and indexers can attribute the deposit correctly. That is why `balanceOf(virtualAddress)` remains `0`. The virtual address is visible in the transfer path, but it does not end up holding the token balance. ## What this changes for operators Virtual addresses are mainly an operations feature. For an exchange or payment processor, the normal flow becomes: 1. register one master wallet 2. derive deposit addresses offchain for each customer 3. watch TIP-20 events and map the `userTag` back to the customer record on the backend 4. credit the customer internally once the deposit is observed This gives you the accounting benefits of per-customer addresses without managing thousands or millions of real onchain balances. ## What this changes for wallets, explorers, and indexers A virtual address is a forwarding alias, not a balance-holding account. Treat it as such in any UI or tooling: do not show it as holding a balance. Wallets, block explorers, and operational tooling that truncate addresses should display enough of the address to distinguish both the `masterId` and the `userTag`; ideally show the full address. For indexers, the event sequences vary by operation. The basic transfer pattern is shown above. Memo and mint paths produce additional events in the same forwarding pattern: **transferWithMemo / transferFromWithMemo:** ``` Transfer(sender, virtualAddress, amount) TransferWithMemo(sender, virtualAddress, amount, memo) Transfer(virtualAddress, masterWallet, amount) ``` **mint:** ``` Transfer(0x0, virtualAddress, amount) Mint(virtualAddress, amount) Transfer(virtualAddress, masterWallet, amount) ``` **mintWithMemo:** ``` Transfer(0x0, virtualAddress, amount) TransferWithMemo(0x0, virtualAddress, amount, memo) Mint(virtualAddress, amount) Transfer(virtualAddress, masterWallet, amount) ``` In all cases, treat the full sequence as one logical deposit to the master wallet. If you surface each `Transfer` log independently, forwarded deposits will appear twice and the effective recipient will be wrong. For deposit attribution, extract the `userTag` (trailing 6 bytes) directly from the virtual address to map the deposit to the right customer record without additional onchain queries. If the sender and registered master wallet are the same address, two `Transfer` events still emit but the net balance change is zero. Account for this when counting deposits or computing balances. ## What this does not do Virtual address forwarding is deliberately narrow in scope. ### It only changes TIP-20 deposit paths Virtual forwarding applies only to the TIP-20 transfer and mint paths defined by the [specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md). It is not a general EVM alias system. ### It does not change ERC-20 contracts deployed on Tempo If a non-TIP-20 token contract receives a transfer to a virtual address, that contract treats it as a normal literal address. Virtual address forwarding does not apply there. ### It does not make every protocol virtual-address aware Some protocols record ownership against the literal address they are given. If they mint LP shares, receipts, or similar positions to a virtual address, those positions can become stranded unless that protocol explicitly supports resolution. ### It does not bypass TIP-403 policy checks Policy checks run against the resolved master wallet. If the master is not allowed to receive a token, deposits to that master's virtual addresses fail too. ## Adoption at a glance Adopting virtual addresses is straightforward conceptually: * one-time setup: register a master wallet and mine the required salt * ongoing operations: derive deposit addresses offchain * reconciliation: decode the `userTag` from events and credit the right customer internally If you want the exact transfer semantics, event shape, and validation rules, read the [virtual address specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) alongside the [TIP-20 specification](https://tempo.xyz/developers/docs/protocol/tip20/spec). ## Learn more about TIP-20 virtual addresses * [TIP-20 specification](https://tempo.xyz/developers/docs/protocol/tip20/spec) — See how T3 updates recipient resolution and event semantics for TIP-20 transfers and mints. * [Virtual address specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) — Read the full TIP with address derivation, forwarding semantics, and invariants. * [T3 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t3) — See when virtual addresses activate and what else ships in T3. # Tempo Policy Registry (TIP-403) ## What is the Tempo Policy Registry? The Tempo Policy Registry (TIP-403) enables TIP-20 tokens to enforce access control. Instead of each token implementing its own logic, the registry lets policies be created once and shared across multiple tokens. ## Links * [TIP-403 Specification](https://tempo.xyz/developers/docs/protocol/tip403/spec) — Learn how TIP-403 works and its features * [Guide: Manage Your Stablecoin](https://tempo.xyz/developers/docs/guide/issuance/manage-stablecoin) — Manage your stablecoin's permissions, supply, and compliance settings * [Rust Implementation](https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/tip403_registry) — Rust implementation in the Tempo client # Tempo Policy Registry (TIP-403) Specification ## Abstract The Tempo Policy Registry (TIP-403) allows TIP-20 tokens to inherit access control and compliance policies. The registry supports two types of policies (whitelist and blacklist) and includes special built-in policies for common use cases. Policies can be shared across multiple tokens, enabling consistent compliance enforcement. ## Motivation Token issuers often need to implement compliance policies such as KYC/AML requirements, access control, and risk management. Without a standardized system, each token would need to implement its own policy logic, making policy management more difficult and inconsistent across the ecosystem. TIP-403 addresses this by providing a centralized registry that tokens can reference for authorization decisions. This enables consistent policy enforcement across multiple tokens and reduces implementation complexity for token issuers. *** ## Specification The TIP-403 registry stores policies that TIP-20 tokens check against on any token transfer. Policies are associated with a unique `policyId`, can either be a blacklist or a whitelist policy, and contain a list of addresses. This list of addresses can be updated by the policy `admin`. The TIP403Registry is deployed at address `0x403c000000000000000000000000000000000000`. ### Built-in Policies Custom policies start with `policyId = 2`. The registry reserves the first two ids for built-in policies: * `policyId = 0` is the `always-reject` policy and rejects all token transfers * `policyId = 1` is the `always-allow` policy and allows all token transfers The `policyIdCounter` starts at `2` and increments with each new policy creation. ### Policy Types TIP-403 supports two policy types: * **Whitelist Policies:** Only addresses in the whitelist can transfer tokens. All other addresses are blocked * **Blacklist Policies:** Addresses in the blacklist are blocked from transferring tokens. All other addresses can transfer ### Storage and State The registry maintains the following state: * `policyIdCounter`: Starts at `2`, increments with each new policy creation. Returns the next policy ID that will be assigned. * `policyData`: Mapping from `policyId` to `PolicyData` struct containing policy type and admin address. * `policySet`: Internal mapping from `policyId` to address to boolean, tracking which addresses are in each policy's set. ### Interface Definition The complete TIP403Registry interface is defined below: ```solidity interface ITIP403Registry { // ========================================================================= // Types and Enums // ========================================================================= enum PolicyType { WHITELIST, BLACKLIST } struct PolicyData { PolicyType policyType; address admin; } // ========================================================================= // Policy Creation // ========================================================================= /// @notice Creates a new policy with the specified admin and type /// @param admin Address that can modify this policy /// @param policyType Type of policy (whitelist or blacklist) /// @return newPolicyId ID of the newly created policy /// @dev Anyone can create a policy. The creator specifies an admin address that can modify the policy. /// Assigns the next available policyId starting from 2, sets the policy admin, and initializes an empty policy set. /// Emits PolicyCreated and PolicyAdminUpdated events. function createPolicy( address admin, PolicyType policyType ) external returns (uint64 newPolicyId); /// @notice Creates a policy and immediately adds the provided accounts to the policy set /// @param admin Address that can modify this policy /// @param policyType Type of policy (whitelist or blacklist) /// @param accounts Initial addresses to add to the policy /// @return newPolicyId ID of the newly created policy /// @dev For whitelist policies: adds accounts as authorized. For blacklist policies: adds accounts as restricted. /// Emits PolicyCreated, PolicyAdminUpdated, and either WhitelistUpdated or BlacklistUpdated events for each account added. function createPolicyWithAccounts( address admin, PolicyType policyType, address[] calldata accounts ) external returns (uint64 newPolicyId); // ========================================================================= // Policy Administration // ========================================================================= /// @notice Transfers admin rights to another address /// @param policyId ID of the policy to update /// @param admin New admin address for the policy /// @dev Only the current policy admin can call this function. The new admin immediately gains full control over the policy. /// Emits PolicyAdminUpdated event. function setPolicyAdmin(uint64 policyId, address admin) external; /// @notice Adds or removes addresses from a whitelist policy /// @param policyId ID of the whitelist policy /// @param account Address to add or remove /// @param allowed true to allow, false to block /// @dev Only the policy admin can call this function. allowed = true adds the address to the whitelist (authorized to transfer). /// allowed = false removes the address from the whitelist (not authorized). Reverts if policy is not a whitelist. /// Emits WhitelistUpdated event. function modifyPolicyWhitelist( uint64 policyId, address account, bool allowed ) external; /// @notice Adds or removes addresses from a blacklist policy /// @param policyId ID of the blacklist policy /// @param account Address to add or remove /// @param restricted true to block, false to allow /// @dev Only the policy admin can call this function. restricted = true adds the address to the blacklist (not authorized to transfer). /// restricted = false removes the address from the blacklist (authorized). Reverts if policy is not a blacklist. /// Emits BlacklistUpdated event. function modifyPolicyBlacklist( uint64 policyId, address account, bool restricted ) external; // ========================================================================= // Policy Queries // ========================================================================= /// @notice Returns whether the provided user is allowed to transfer tokens under the provided policy ID /// @param policyId Policy ID to check against /// @param user Address to check /// @return True if authorized, false if blocked /// @dev For policyId = 0 (always-reject): Always returns false /// For policyId = 1 (always-allow): Always returns true /// For whitelist policies: Returns true if address is in the whitelist, false otherwise /// For blacklist policies: Returns true if address is NOT in the blacklist, false if it is function isAuthorized(uint64 policyId, address user) external view returns (bool); /// @notice Returns the next policy ID that will be assigned to a newly created policy /// @return The current policyIdCounter value /// @dev Starts at 2 and increments with each policy creation function policyIdCounter() external view returns (uint64); /// @notice Returns whether a policy exists /// @param policyId ID of the policy to check /// @return True if the policy exists, false otherwise /// @dev Policy IDs 0 and 1 (built-in policies) always exist. For custom policies (ID >= 2), /// checks if the policy ID is within the range of created policies based on policyIdCounter. function policyExists(uint64 policyId) external view returns (bool); /// @notice Returns the policy type and admin address of the policy associated with the provided policy ID /// @param policyId ID of the policy to query /// @return policyType Type of the policy (whitelist or blacklist) /// @return admin Admin address of the policy function policyData(uint64 policyId) external view returns (PolicyType policyType, address admin); // ========================================================================= // Events // ========================================================================= /// @notice Emitted when a new policy is created /// @param policyId ID of the newly created policy /// @param updater Address that created the policy /// @param policyType Type of policy created event PolicyCreated( uint64 indexed policyId, address indexed updater, PolicyType policyType ); /// @notice Emitted when a policy's admin is changed /// @param policyId ID of the policy /// @param updater Address that made the change /// @param admin New admin address event PolicyAdminUpdated( uint64 indexed policyId, address indexed updater, address indexed admin ); /// @notice Emitted when an address is added to or removed from a whitelist policy /// @param policyId ID of the whitelist policy /// @param updater Address that made the change /// @param account Account that was added or removed /// @param allowed true if added, false if removed event WhitelistUpdated( uint64 indexed policyId, address indexed updater, address indexed account, bool allowed ); /// @notice Emitted when an address is added to or removed from a blacklist policy /// @param policyId ID of the blacklist policy /// @param updater Address that made the change /// @param account Account that was added or removed /// @param restricted true if blocked, false if unblocked event BlacklistUpdated( uint64 indexed policyId, address indexed updater, address indexed account, bool restricted ); // ========================================================================= // Errors // ========================================================================= /// @notice Caller is not the policy admin error Unauthorized(); /// @notice Wrong policy type for the operation error IncompatiblePolicyType(); } ``` ### Usage with TIP-20 Tokens TIP-20 tokens store the current TIP403 registry policy ID they adhere to in their storage. On any token transfer, they perform a TIP-403 policy check by calling `isAuthorized()` for both sender and recipient addresses. The policy to use for the token can only be set by the admin of the token. **Default Policy:** New tokens start with `transferPolicyId = 1` (always-allow policy). **Policy Changes:** When a token's transfer policy is changed via `changeTransferPolicyId()`, all future transfers are immediately subject to the new policy. **Virtual addresses:** Policy-configuration functions that accept literal member addresses (`createPolicyWithAccounts`, `modifyPolicyWhitelist`, `modifyPolicyBlacklist`) reject [virtual addresses](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses). TIP-20 policy checks for transfers and mints to a virtual address run against the resolved master wallet rather than the forwarding alias, so policy membership must be configured on the master address. #### Example Usage Creating and setting a policy: ```solidity address admin = address(this); // Create policy with registry uint64 policyId = tip403Registry.createPolicy(admin, PolicyType.WHITELIST); // Add authorized addresses to whitelist tip403Registry.modifyPolicyWhitelist(policyId, authorizedUser, true); // Set policy on the token token.changeTransferPolicyId(policyId); ``` ### Authorization Logic The `isAuthorized()` function implements the following logic: ```solidity if (policyId < 2) { return policyId == 1; // 0 = reject, 1 = allow } PolicyData memory data = policyData[policyId]; return data.policyType == PolicyType.WHITELIST ? policySet[policyId][user] : !policySet[policyId][user]; ``` ## Invariants * When policyId = 0, all authorization checks must return false for every address. * When policyId = 1, all authorization checks must return true for every address. * Only the policy’s current admin may update the admin address for that policy. # TIP-403 receive policies Receive policies let a receiver control which [TIP-20](https://tempo.xyz/developers/docs/protocol/tip20/overview) tokens it accepts and which senders may send those tokens to it. This page summarizes the protocol behavior from the [specification](https://tips.sh/1028). When a receive policy blocks an inbound TIP-20 transfer or mint, the call still succeeds. Delivery is redirected to `ReceivePolicyGuard`, and the guard records a receipt for that blocked transfer or mint. The token's [TIP-403](https://tempo.xyz/developers/docs/protocol/tip403/spec) policy checks still run first and still revert on failure. ## Protocol changes Receive policies add three protocol surfaces: * TIP-403 stores per-address receive policies and exposes `setReceivePolicy(...)` and `validateReceivePolicy(...)`. * `ReceivePolicyGuard` records receipts for blocked transfers and mints at `0xB10C000000000000000000000000000000000000`. * TIP-20 transfer and mint flows check the receiver's policy before crediting the receiver. ## Transfer flow ```mermaid sequenceDiagram participant Sender participant TIP20 as TIP-20 participant TIP403 as TIP-403 participant Guard as ReceivePolicyGuard participant Recovery as Recovery authority Sender->>TIP20: transfer(receiver, amount) TIP20->>TIP403: check token TIP-403 policy TIP403-->>TIP20: allowed TIP20->>TIP403: validateReceivePolicy(token, sender, receiver) TIP403-->>TIP20: blocked TIP20->>Guard: storeBlocked(...) TIP20-->>Sender: success Recovery->>Guard: claim(to, receipt) Guard->>TIP20: release amount from guard ``` The receive-policy check applies to: * `transfer` * `transferFrom` * `transferWithMemo` * `transferFromWithMemo` * `systemTransferFrom` * `mint` * `mintWithMemo` The check does not apply to `approve`, `permit`, or `burn`. It also does not affect fee deposits or refunds through `transfer_fee_pre_tx` or `transfer_fee_post_tx`, TIP-20 rewards, or internal balances. For transfer-like operations, the sender checked by the policy is the `from` address. For `mint` and `mintWithMemo`, the sender checked by the policy is `msg.sender`. ## Receive-policy configuration Receive policies are stored per address in the TIP-403 registry. Conceptually, each configured address has: ```text ReceivePolicy(account) = ( senderPolicyId, tokenFilterId, recoveryAuthority ) ``` `senderPolicyId` and `tokenFilterId` must reference built-in policy `0`, built-in policy `1`, or a simple TIP-403 `WHITELIST` or `BLACKLIST` policy. `COMPOUND` policies are not valid for receive policies. The recovery authority controls who can claim future blocked receipts: | Recovery authority | Claimer | |---|---| | `address(0)` | the transfer or mint originator | | receiver address | the receiver | | another nonzero address | that address | Nonzero recovery authorities must not be `ReceivePolicyGuard`, [virtual addresses](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses), or system precompile addresses that cannot initiate calls. A virtual address must not call `setReceivePolicy(...)`; configure the resolved master address instead. If no receive policy is set, all transfers and mints are allowed by the receive-policy layer. ## Evaluation order `validateReceivePolicy(token, sender, receiver)` checks configured policies in a fixed order: 1. Check `token` against the receiver's token filter. If rejected, return `TOKEN_FILTER`. 2. Check `sender` against the receiver's sender policy. If rejected, return `RECEIVE_POLICY`. 3. If both checks pass, return `NONE`. If both checks would reject, `TOKEN_FILTER` is returned because the token filter is the first canonical check. Blocked events must not use `NONE`. ## ReceivePolicyGuard `ReceivePolicyGuard` tracks blocked inbound TIP-20 transfers and mints. The aggregate blocked balance for each TIP-20 token is held at `0xB10C000000000000000000000000000000000000`. Each blocked transfer or mint creates one receipt. The v1 receipt includes: * version * token * recovery authority * originator * recipient * blocked timestamp * blocked nonce * blocked reason * inbound kind * memo The receipt key is `keccak256(abi.encode(receipt))`, and the guard stores the full amount for that receipt key. Receipts are not enumerable onchain, so claimers must supply the receipt they want to consume, typically by indexing `TransferBlocked` events. ## Claims A claim consumes one full receipt and releases the stored amount to one destination. Partial claims are not supported. Only the authorized claimer may call `claim(...)`: * if `recoveryAuthority == address(0)`, only the receipt originator may claim * otherwise, only the nonzero recovery authority may claim Changing a receiver's recovery authority affects future receipts only. Existing receipts remain governed by the recovery authority captured in their receipt. ### Resume claims A claim resumes the original inbound when `recoveryAuthority != address(0)` and `to == receiver`. A resume claim does not recheck the receiver's receive policy. It still requires the receiver to be currently authorized under the token's TIP-403 policy as the destination of the release. For blocked transfers to a virtual address, the receiver is the resolved master, so a resume releases directly to the master. ### Reroute claims All other claims are reroutes, including every originator-authorized claim. A reroute must: * reject `to == ReceivePolicyGuard` * resolve `to` if it is a virtual address, or revert if resolution fails * require the policy subject to be authorized as a sender under the token's TIP-403 policy * require the resolved destination to be authorized as a recipient under the token's TIP-403 policy * require the resolved destination's receive policy to accept the policy subject as sender For originator recovery, the policy subject is the originator. For nonzero recovery authority, the policy subject is the receiver. ## Burns `burnBlockedReceipt(...)` consumes one full receipt and burns the stored amount from `ReceivePolicyGuard`. The caller must hold `BURN_BLOCKED_ROLE` for the token. A receipt is burnable only when its policy subject is currently unauthorized as a sender under the token's TIP-403 policy. ## Events Receive policy updates emit: ```solidity event ReceivePolicyUpdated( address indexed account, uint64 senderPolicyId, uint64 tokenFilterId, address recoveryAuthority ); ``` Blocked, claimed, and burned receipts emit: ```solidity event TransferBlocked( address indexed token, address indexed receiver, uint64 indexed blockedNonce, uint256 amount, uint8 receiptVersion, bytes receipt ); event ReceiptClaimed( address indexed token, address indexed receiver, uint64 indexed blockedNonce, uint64 blockedAt, uint8 receiptVersion, address originator, address recipient, address recoveryAuthority, address caller, address to, uint256 amount ); event ReceiptBurned( address indexed token, address indexed receiver, uint64 indexed blockedNonce, uint64 blockedAt, uint8 receiptVersion, address originator, address recipient, address recoveryAuthority, address caller, uint256 amount ); ``` `TransferBlocked.receipt` is the ABI-encoded receipt witness and must be directly usable as the `receipt` argument to `balanceOf`, `claim`, and `burnBlockedReceipt`. ## Related receive policy docs * [Receive policies specification](https://tips.sh/1028) — Approved specification for address-level receive policies. * [Configure Receive Policies](https://tempo.xyz/developers/docs/guide/payments/configure-receive-policies) — Builder guide for configuring receive policies and indexing blocked receipts. * [TIP-20 Tokens](https://tempo.xyz/developers/docs/protocol/tip20/overview) — Token standard whose transfers and mints run receive-policy checks. # Understanding transaction fees on Tempo Tempo has no native token. Instead, transaction fees—including both gas fees and priority fees—can be paid directly in stablecoins. When you send a transaction, you can choose which supported stablecoin to use for fees. ## Choose a fee setup | Goal | Use | | --- | --- | | Choose the fee token for one transaction | Pass [`feeToken`](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin#quick-fee-token-snippet). | | Set an account's persistent default fee token | Call [`client.fee.setUserToken`](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin#set-a-default-user-fee-token). | | Have another local account pay | Pass the account as [`feePayer`](https://tempo.xyz/developers/docs/guide/tempo-transaction#fee-sponsorship). | | Request sponsorship from a relay or API | Configure a relay and pass [`feePayer: true`](https://tempo.xyz/developers/docs/api/fee-payer#sponsor-transaction-fees). | For a stablecoin to be accepted, it must be USD-denominated, issued as a native TIP-20 contract, and have sufficient liquidity on the native Fee AMM. Tempo uses a bounded dynamic base fee. It can fall when block gas usage is below target and rise back toward the cap when the network is busy. For a 50,000 gas transfer, the base-fee cap is about $0.0006, with a quiet-period floor around $0.00003. All fees accrue to the validator who proposes the block. :::info[Dynamic base fee] T7 activated the dynamic base fee on testnet and mainnet. See the [T7 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t7) and [dynamic base fee specification](https://tips.sh/1067). ::: For pool mechanics, swaps, liquidity accounting, and fee-token conversion rules, see the [Fee AMM specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm). ## Learn more about Tempo fees * [Managing Fee Liquidity](https://tempo.xyz/developers/docs/guide/stablecoin-dex/managing-fee-liquidity) — Provide liquidity to enable fee token conversions * [Fee Specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee) — Complete fee system specification * [Fee AMM Specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) — Fee AMM protocol specification * [Guide: Sponsor Transaction Fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) — Sponsor transaction fees * [Guide: Pay Fees in Any Supported Stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) — Pay fees in any supported stablecoin # Fee specification: the canonical reference ## Abstract This spec lays out how fees work on Tempo, including how fees are calculated, who pays them, and how the default fee token for a transaction is determined. ## Motivation Tempo has no native token. Transaction fees are paid directly in USD-denominated stablecoins. This design removes the need for users or applications to hold volatile assets for gas, keeping the entire payment experience USD-native. Users can pay gas fees in any [TIP-20](https://tempo.xyz/developers/docs/protocol/tip20/spec) token whose currency is USD, as long as that stablecoin has sufficient liquidity on the enshrined [fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) against the token that the current validator wants to receive. In determining *which* token a user pays fees in, we want to maximize customizability (so that wallets or users can implement more sophisticated UX than is possible at the protocol layer), minimize surprise (particularly surprises in which a user pays fees in a stablecoin they did not expect to), and have sane default behavior so that users can begin using basic functions like payments even using wallets that are not customized for Tempo support. ## Fee units Fees in the `max_base_fee_per_gas` and `max_fee_per_gas` fields of transactions, as well as in the block's `base_fee_per_gas` field, are specified in **attodollars** (10^-18 USD) per gas. Since TIP-20 tokens have 6 decimal places — where 1 token unit = 1 **microdollar** (10^-6 USD) — the fee for a transaction can be calculated as `ceil(base_fee * gas_used / 10^12)`. Attodollars provide sufficient precision for low-fee transactions. Since TIP-20 tokens have only 6 decimal places (microdollars), expressing fees directly in token units per gas would not provide enough precision for transactions with very low gas costs. By using attodollars (10^-18 USD) and dividing by 10^12 to convert to microdollars, the protocol ensures that even small fee amounts can be accurately represented and calculated. ### Base Fee Model Tempo uses a bounded dynamic base fee. The base fee can fall when block gas usage is below target and rise back toward the cap when usage increases. The cap is set such that a TIP-20 transfer costs about $0.0006, with a quiet-period floor around $0.00003. The bounded base fee combined with USD-denominated payment provides predictable unit economics. Applications can budget for transaction costs without exposure to native token price fluctuations. Congestion is managed through: * **Payment lanes**: Reserved blockspace for TIP-20 transfers as specified in the [Payment Lane Specification](https://tempo.xyz/developers/docs/protocol/blockspace/payment-lane-specification). Approximately 94% of blockspace is reserved for payment transactions, with the remaining 6% available for general computation. This allocation is conservative and the blockspace available for general computation may increase over time as total throughput scales. * **Priority fees**: The `max_priority_fee_per_gas` field allows transactions to bid for faster inclusion during periods of high demand. * **Block gas limits**: Standard per-block gas limits constrain total computation per block. The payment lane mechanism ensures that payment transactions are not crowded out by other network activity. General network congestion from non-payment use cases cannot affect payment throughput or fees, providing the consistency that payment applications require. ## Fee payment Before the execution of each transaction, the protocol takes the following steps: * Determine the [`fee_payer`](#fee-payer) of the transaction. * Determine the `fee_token` of the transaction, according to the [rules for fee token preferences](#fee-token-preferences). If the fee token cannot be determined, the transaction is invalid. * Compute the `max_fee` of the transaction as `gas_limit * gas_price`. * Deduct `max_fee` from the `fee_payer`'s balance of `fee_token`. If `fee_payer` does not have sufficient balance in `fee_token`, the transaction is invalid. * Reserve `max_fee` of liquidity on the [fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) between the `fee_token` and the validator's preferred fee token. If there is insufficient liquidity, the transaction is invalid. After the execution of each transaction: * Compute the `refund_amount` as `(gas_limit - gas_used) * gas_price`. * Credit the `fee_payer`'s address with `refund_amount` of `fee_token`. * Log a `Transfer` event from the user to the [fee manager contract](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) for the net amount of the fee payment. :::info[Atomic fee handling] The protocol executes the max fee deduction and refund atomically. If insufficient liquidity is encountered at any point during fee calculation (for example, if a previous transaction by the same sender in the same block exhausted the fee token reserves), the entire transaction reverts. ::: ## Fee payer Tempo supports *sponsored transactions* in which the `fee_payer` is a different address from the `tx.origin` of the transaction. This is supported by Tempo's [new transaction type](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction), which has a `fee_payer_signature` field. If no `fee_payer_signature` is provided, then the `fee_payer` of the transaction is its sender (`tx.origin`). If the `fee_payer_signature` field is set, then it is used to derive the `fee_payer` for the transaction, as described in the [transaction spec](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction). For purposes of [fee token preferences](#fee-token-preferences), the `fee_payer` is the account that chooses the fee token. ### Fee sponsorship flow Presence of the `fee_payer_signature` field authorizes a third party to pay the transaction's gas costs while the original sender executes the transaction logic. :::steps #### Sender signs the transaction The sender signs the transaction with their private key, signing over a blank fee token field. This means the sender delegates the choice of which fee token to use to the fee payer. #### Fee payer selects and signs The fee payer selects which fee token to use, then signs over the transaction. #### Transaction submission The fee token and fee payer signature is added to the transaction using the `fee_payer_signature` field and is then submitted. #### Network validation The network validates both signatures and executes the transaction. ::: #### Validation When `feePayerSignature` is present: * Both sender and fee payer signatures must be valid * Fee payer must have sufficient balance in the fee token * Transaction is rejected if either signature fails or fee payer's balance is insufficient ## Fee token preferences The protocol checks for token preferences in five ways, with this order of precedence: 1. Transaction (set by the `fee_token` field of the transaction) 2. Account (set on the FeeManager contract by the `fee_payer` of the transaction) 3. TIP-20 contract (if the transaction is calling `transfer`, `transferWithMemo`, or `startReward` on a TIP-20 token contract, the transaction uses that token as its fee token) 4. Stablecoin DEX (for certain swap calls, the transaction uses the `tokenIn` argument as its fee token) 5. PathUSD (as a fallback) The protocol checks preferences at each of these levels, stopping at the first one at which a preference is specified. At that level, the protocol performs the following checks. If any of the checks fail, the transaction is invalid (without looking at any further levels): * The token must be a TIP-20 token whose currency is USD. * The user must have sufficient balance in that token to pay the `gasLimit` on the transaction at the transaction's `gasPrice`. * There must be sufficient liquidity on the [fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm), as discussed in that specification. If no preference is specified at the transaction, account, or contract level, the protocol falls back to [pathUSD](#pathusd). ### Transaction level Tempo's [new transaction type](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction), allows transactions to specify a `fee_token` on the transaction. This overrides any preferences set at the account, contract, or validator level. For [sponsored transactions](#fee-payer), the `tx.origin` address does not sign over the `fee_token` field (allowing the `fee_payer` to choose the fee token). ### Account level An account can specify a fee token preference for all transactions for which it is the `fee_payer` (including both transactions it sponsors as well as non-sponsored transactions for which it is the `tx.origin`). This overrides any preference set at the contract or validator level. To set its preference, the account can call the `setUserToken` function on the FeeManager precompile. At this step, the protocol does one more check: * If the transaction is not a [Tempo transaction](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction) *and* the transaction is a top-level call to the `setUserToken` function on the FeeManager, then the protocol checks the `token` argument to the function: * If that token is a TIP-20 whose currency is USD, that token is used as the fee token (unless the transaction specifies a `fee_token` at the [transaction level](#transaction-level)). * If that token is not a TIP-20 or its currency is not USD, the transaction is invalid. ### TIP-20 contracts If the top-level call of a transaction is to one of the following functions on a TIP-20 token whose currency is USD: * `transfer(address to, uint256 amount)` * `transferWithMemo(address to, uint256 amount, bytes32 memo)` * `startReward(uint256 amount, uint32 seconds_)` then that TIP-20 token is used as the user's fee token for that transaction (unless there is a preference specified at the [transaction](#transaction-level) or [account](#account-level) level). For [Tempo Transactions](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction), this rule applies only if *all* top-level calls are to the same TIP-20 contract, and each such call is to one of the functions listed above, with `fee_payer == tx.origin`. ### Stablecoin DEX contract If the top-level call of a transaction is to the [Stablecoin DEX](https://tempo.xyz/developers/docs/protocol/exchange/spec) contract, the function being called is either `swapExactAmountIn` or `swapExactAmountOut`, and the `tokenIn` argument to that function is the address of a TIP-20 token for which the currency is USD, then the `tokenIn` argument is used as the user's fee token for the transaction (unless there is a preference specified at the [transaction](#transaction-level) or [account](#account-level) level). For [Tempo Transactions](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction), this rule applies only if there is only one top-level call in the transaction. ### pathUSD If no fee preference is set at the transaction, account, or contract level, the protocol falls back to [pathUSD](https://tempo.xyz/developers/docs/protocol/exchange/quote-tokens#pathusd) as the user's fee token preference. ## Validator preferences Validators can set a default fee token preference that determines which stablecoin they receive for transaction fees. When users pay in different tokens, the Fee AMM automatically converts to the validator's preferred token. ### Setting validator preference To set their preference, validators call the `setValidatorToken` function on the FeeManager precompile: ```solidity // Set your preferred fee token feeManager.setValidatorToken(preferredTokenAddress); ``` After setting a validator token preference, all fees collected in blocks the validator proposes will be automatically converted to the chosen token (if needed) and transferred to the validator's account. On the Moderato testnet, validators currently expect alphaUSD (one of the tokens distributed by the faucet) as their fee token. If validators have not specified a fee token preference, the protocol falls back to expecting pathUSD as their fee token. ## Gas Parameters Tempo uses the following mainnet gas parameters: | Parameter | Value | |-----------|-------| | Base fee cap | 12 billion attodollars per gas (`1.2 × 10^10`) | | Base fee floor | 600 million attodollars per gas (`6 × 10^8`) | | Total block gas limit | 500M gas | | General gas limit | 30M gas/block | | Precompile input | `30 × ceil(calldata_length / 32)` gas ([TIP-1100](https://tips.sh/1100)) | | Duplicate validation | 20 gas per value processed ([TIP-1105](https://tips.sh/1105)) | A standard TIP-20 transfer (~50,000 gas) costs approximately 600 microdollars ($0.0006) at the cap and approximately 30 microdollars ($0.00003) at the floor. Precompile input charges include the selector and are deducted before dispatch and ABI decoding. Duplicate-check charges apply to AccountKeychain call scopes and ZoneFactory role lists before sorting. Calls require canonical ABI encoding without gaps, overlaps, trailing bytes, or nonzero padding. ### Removing validator preference To remove a validator token preference, set it to the zero address: ```solidity // Remove validator token preference feeManager.setValidatorToken(address(0)); ``` ## Fee lifecycle This section describes the complete flow of how fees are collected, converted, and distributed from user to validator. ### Fee flow steps When a user submits a transaction on Tempo, fees are paid in their chosen stablecoin (determined by the [fee token preferences](#fee-token-preferences) hierarchy). If the validator prefers a different stablecoin, the Fee AMM automatically converts the user's payment to the validator's preferred token. #### 1. User submits transaction The transaction is submitted with the fee token determined by the preference hierarchy. #### 2. Pre-transaction collection Before the transaction executes, the `FeeManager` contract collects the maximum possible fee amount from the user: * Verifies the user has sufficient balance in their chosen fee token * Checks if the Fee AMM has enough liquidity (if conversion is needed) * Collects the maximum fee amount based on the transaction's gas limit If either check fails, the transaction is rejected before execution. #### 3. Transaction execution The transaction executes normally. The actual gas consumed may be less than the maximum that was collected. #### 4. Post-transaction refund After execution, the `FeeManager`: * Calculates the actual fee owed based on gas used * Refunds any unused tokens to the user * Queues the actual fee amount for conversion (if needed) #### 5. Fee swap execution If the user's fee token differs from the validator's preferred token, the fee swap executes immediately during the post-transaction step at a fixed rate of **0.9970** (validator receives 0.9970 of their token per 1.0 user token paid). If the user's fee token matches the validator's preference, no conversion is needed. Fees accumulate in the FeeManager contract. Validators can claim their accumulated fees at any time by calling `distributeFees()`. ### Fee swap mechanics Fee swaps always execute at a fixed rate of **0.9970**: ``` validatorTokenOut = userTokenIn × 0.9970 ``` This means: * User pays 1.0 USDG for fees * Validator receives 0.9970 USDT (if that's their preferred token) * The 0.003 (0.3%) difference goes to liquidity providers as a fee ### Example flow Here's a complete example of the fee lifecycle: 1. **Alice** wants to send a transaction and pays fees in **USDG** (her preferred token) 2. **Validator** prefers to receive fees in **USDT** 3. Alice's transaction has a max fee of 1.0 USDG 4. The FeeManager collects 1.0 USDG from Alice before execution 5. Transaction executes and uses 0.8 USDG worth of gas 6. The FeeManager refunds 0.2 USDG to Alice 7. The Fee AMM immediately swaps 0.8 USDG → 0.7976 USDT (0.8 × 0.9970) 8. The 0.7976 USDT is added to the validator's accumulated fees 9. Validator calls `distributeFees()` to claim their accumulated fees 10. Liquidity providers earn 0.0024 USDT from the 0.3% fee ### Gas costs The fee conversion process adds minimal overhead to transactions: * **Pre-transaction**: ~5,000 gas for balance and liquidity checks * **Post-transaction**: ~3,000 gas for refund and queue operations * **Block settlement**: Amortized across all transactions in the block For complete technical specifications on the Fee AMM mechanism, see the [Fee AMM Protocol Specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm). # Fee AMM overview: converting stablecoin fees The Fee AMM (Automated Market Maker) is a dedicated system for converting transaction fees between different stablecoins. It enables users to pay fees in any supported stablecoin while allowing validators to receive fees in their preferred token. :::info **Fee AMM vs. Exchange** — these are two different systems. The Fee AMM is protocol-driven: it converts transaction-fee payments into validators' preferred tokens at a fixed price, and only the protocol (and arbitrageurs rebalancing it) swaps against it — it is not a trading venue. For user-facing stablecoin trading (swaps, orders, prices, orderbook), use the [Exchange](https://tempo.xyz/developers/docs/protocol/exchange). See [Fee AMM vs. Exchange](#fee-amm-vs-exchange) below. ::: ## How the Fee AMM converts stablecoin fees When a user pays fees in a stablecoin that differs from the validator's preference, the Fee AMM automatically converts the payment: * **User pays**: 1.0 of their chosen stablecoin * **Validator receives**: 0.9970 of their preferred stablecoin * **Liquidity providers earn**: 0.003 (0.3%) as fees This conversion happens automatically at the end of each block through batched swaps, preventing MEV attacks like sandwiching. ## Fee AMM vs. Exchange Tempo has two stablecoin-swapping systems that are easy to confuse. They serve different purposes: | | [Exchange (DEX)](https://tempo.xyz/developers/docs/protocol/exchange) | Fee AMM | |---|---|---| | **Purpose** | User-facing stablecoin trading | Converting transaction fees into validators' preferred tokens | | **Who initiates swaps** | Users and apps | The protocol, automatically (plus arbitrageurs rebalancing) | | **Pricing** | Market-driven orderbook (price-time priority) | Fixed conversion price | | **Liquidity** | Limit and flip orders resting in the orderbook | LP deposits into per-pair fee pools | | **Contract** | Stablecoin DEX precompile (`0xdec0…0000`) | `FeeManager` precompile (`0xfeec…0000`) | | **Use it for** | Swaps, prices, orderbook depth, fills | Letting users pay fees in any stablecoin | If you want to **trade** stablecoins or read market data, use the [Exchange](https://tempo.xyz/developers/docs/protocol/exchange). If you want to **enable fee payments** in your stablecoin or provide fee-conversion liquidity, you're in the right place — continue below. ## Learn more about fee conversion * [Use Your Stablecoin for Fees](https://tempo.xyz/developers/docs/guide/issuance/use-for-fees) — Enable users to pay fees using your stablecoin * [Fee AMM Specification](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) — Complete Fee AMM protocol specification * [Managing Fee Liquidity](https://tempo.xyz/developers/docs/guide/stablecoin-dex/managing-fee-liquidity) — Provide liquidity to enable fee token conversions # Fee AMM specification: how fee conversion works ## Abstract This specification defines a system of one-way Automated Market Makers (AMMs) designed to facilitate gas fee payments from a user using one stablecoin (the `userToken`) to a validator who prefers a different stablecoin (the `validatorToken`). Each AMM handles fee swaps from a `userToken` to a `validatorToken` at one price (0.9970 `validatorToken` per `userToken`), and allows rebalancing in the other direction at another fixed price (1.0015 `userToken` per `validatorToken`). ## Motivation Current blockchain fee systems typically require users to hold native tokens for gas payments. This creates friction for users who prefer to transact in stablecoins. The Fee AMM is a dedicated AMM for trading between stablecoins, which can only be used by the protocol (and by arbitrageurs rebalancing it to keep it balanced). The protocol automatically collects fees in many different coins and immediately swaps them (paying a constant price) into the token preferred by the validator. Fees accumulate in the FeeManager, and validators can claim them on-demand. The system is designed to minimize several forms of MEV: * **No Probabilistic MEV**: The fixed fee swap rate prevents profitable backrunning of fee swaps. There is no way to profitably spam the chain with transactions hoping an opportunity might arise. * **No Sandwich Attacks**: Fee swaps execute at a fixed rate, eliminating sandwich attack vectors. * **Top-of-Block Auction**: The main MEV in the AMM (from rebalancing) occurs as a single race at the top of the next block rather than creating probabilistic spam throughout. ## Specification ### Overview The Fee AMM implements two distinct swap mechanisms: 1. **Fee Swaps**: Fixed-rate swaps at a price of `0.9970` (validator token per user token) from `userToken` to `validatorToken` 2. **Rebalancing Swaps**: Fixed-rate swaps at a price of `1.0015` (user token per validator token) from `validatorToken` to `userToken` ### Core Components #### 1. FeeAMM Contract The primary AMM contract managing liquidity pools and swap operations. ##### Pool Structure ```solidity struct Pool { uint128 reserveUserToken; // Reserve of userToken uint128 reserveValidatorToken; // Reserve of validatorToken } ``` Each pool is directional: `userToken` → `validatorToken`. For a pair of tokens A and B, there are two separate pools: * Pool(A, B): for swapping A to B at fixed rate of 0.997 (fee swaps) and B to A at fixed rate of 0.9985 (rebalancing) * Pool(B, A): for swapping B to A at fixed rate of 0.997 (fee swaps) and A to B at fixed rate of 0.9985 (rebalancing) ##### Constants * `M = 9970` (scaled by 10000, representing 0.9970) * `N = 9985` (scaled by 10000, representing 0.9985) * `SCALE = 10000` * `MIN_LIQUIDITY = 1000` ##### Key Functions ```solidity function getPool( address userToken, address validatorToken ) external view returns (Pool memory) ``` Returns the pool structure for a given token pair. ```solidity function getPoolId( address userToken, address validatorToken ) external pure returns (bytes32) ``` Returns the pool ID for a given token pair (used internally for pool lookup). ```solidity function rebalanceSwap( address userToken, address validatorToken, uint256 amountOut, address to ) external returns (uint256 amountIn) ``` Executes rebalancing swaps from `validatorToken` to `userToken` at fixed rate of 1.0015 (user token per validator token). Can be executed by anyone. Calculates `amountIn = (amountOut * N) / SCALE + 1` (rounds up). Updates reserves immediately. Emits `RebalanceSwap` event. ```solidity function mint( address userToken, address validatorToken, uint256 amountUserToken, uint256 amountValidatorToken, address to ) external returns (uint256 liquidity) ``` Adds liquidity to a pool with both tokens. First provider sets initial reserves and must burn `MIN_LIQUIDITY` tokens. Subsequent providers must provide proportional amounts. Receives fungible LP tokens representing pro-rata share of pool reserves. ```solidity function mint( address userToken, address validatorToken, uint256 amountValidatorToken, address to ) external returns (uint256 liquidity) ``` Single-sided liquidity provision with validator token only. Treats the deposit as equivalent to performing a hypothetical `rebalanceSwap` first at rate `n = 0.9985` until the ratio of reserves match, then minting liquidity by depositing both. Formula: `liquidity = amountValidatorToken * _totalSupply / (V + n * U)`, where `n = N / SCALE`. Rounds down to avoid over-issuing LP tokens. Updates reserves by increasing only `validatorToken` by `amountValidatorToken`. Emits `Mint` event with `amountUserToken = 0`. ```solidity function burn( address userToken, address validatorToken, uint256 liquidity, address to ) external returns (uint256 amountUserToken, uint256 amountValidatorToken) ``` Burns LP tokens and receives pro-rata share of reserves. Emits `Burn` event. ```solidity function executeFeeSwap( address userToken, address validatorToken, uint256 amountIn ) internal returns (uint256 amountOut) ``` Executes a fee swap immediately. Calculates `amountOut = (amountIn * M) / SCALE`. Only executed by the protocol during transaction execution. Emits `FeeSwap` event. Note: `FeeSwap` events are not emitted for immediate swaps. ```solidity function checkSufficientLiquidity( address userToken, address validatorToken, uint256 maxAmount ) internal view ``` Verifies sufficient validator token reserves for the fee swap. Calculates `maxAmountOut = (maxAmount * M) / SCALE`. Reverts if insufficient liquidity. #### 2. FeeManager Contract Tempo introduces a precompiled contract, the `FeeManager`, at the address `0xfeec000000000000000000000000000000000000`. The `FeeManager` is a singleton contract that implements all the functions of the Fee AMM for every pool. It handles the collection and refunding of fees during each transaction, executes fee swaps immediately, stores fee token preferences for users and validators, and accumulates fees for validators to claim via `distributeFees()`. ##### Key Functions ```solidity function setUserToken(address token) external ``` Sets the default fee token preference for the caller (user). Requires token to be a USD TIP-20 token. Emits `UserTokenSet` event. Access: Direct calls only (not via delegatecall). ```solidity function setValidatorToken(address token) external ``` Sets the fee token preference for the caller (validator). Requires token to be a USD TIP-20 token. Cannot be called during a block built by that validator. Emits `ValidatorTokenSet` event. Access: Direct calls only (not via delegatecall). ```solidity function collectFeePreTx( address user, address userToken, uint256 maxAmount ) external ``` Called by the protocol before transaction execution. The fee token (`userToken`) is determined by the protocol before calling using logic that considers: explicit tx fee token, setUserToken calls, stored user preference, tx.to if TIP-20. Reserves AMM liquidity if user token differs from validator token. Collects maximum possible fee from user. Access: Protocol only (`msg.sender == address(0)`). ```solidity function collectFeePostTx( address user, uint256 maxAmount, uint256 actualUsed, address userToken ) external ``` Called by the protocol after transaction execution. The validator token and fee recipient are inferred from `block.coinbase`. Calculates refund amount: `refundAmount = maxAmount - actualUsed`. Refunds unused tokens to user. If user token differs from validator token, executes the fee swap immediately and accumulates the output for the validator. Access: Protocol only (`msg.sender == address(0)`). ```solidity function distributeFees(address validator, address token) external ``` Allows anyone to trigger distribution of accumulated fees for a specific token to a validator. Transfers all accumulated fees in the specified token to the validator address. If no fees have accumulated for that token, this is a no-op. ```solidity function collectedFees(address validator, address token) external view returns (uint256) ``` Returns the amount of accumulated fees for a validator and specific token that can be claimed via `distributeFees()`. ### Swap Mechanisms #### Fee Swaps * **Rate**: Fixed at m=0.9970 (validator receives 0.9970 of their preferred token per 1 user token that user pays) * **Direction**: User token to validator token * **Purpose**: Convert tokens paid by users as fees to tokens preferred by validators * **Settlement**: Immediate during transaction execution * **Access**: Protocol only * **Routing**: If the direct `userToken → validatorToken` pool has insufficient liquidity, the Fee AMM tries one two-hop route through `userToken.quoteToken()`. Both hops must have sufficient liquidity. The validator receives `floor(floor(actualSpending × 9970 / 10000) × 9970 / 10000)` on the two-hop path. #### Rebalancing Swaps * **Rate**: Fixed at n=0.9985 (swapper receives 1 of the user token for every 0.9985 that they put in of the validator's preferred token) * **Direction**: Validator token to user token * **Purpose**: Refill reserves of validator token in the pool * **Settlement**: Immediate * **Access**: Anyone ### Fee Collection Flow 1. **Pre-Transaction**: * Protocol determines user's fee token using logic that considers: explicit tx fee token, setUserToken calls, stored user preference, tx.to if TIP-20 * Protocol calculates maximum gas needed (`maxAmount = gasLimit * maxFeePerGas`) * `FeeManager.collectFeePreTx(user, userToken, maxAmount)` is called: * If user token differs from validator token, checks AMM has sufficient liquidity via `checkSufficientLiquidity()` * Collects maximum fee from user using `transferFeePreTx()` * If any check fails (insufficient balance, insufficient liquidity), transaction is invalid 2. **Post-Transaction**: * Calculate actual gas used (`actualUsed = gasUsed * gasPrice`) * `FeeManager.collectFeePostTx(user, maxAmount, actualUsed, userToken)` is called: * Validator token and fee recipient are inferred from `block.coinbase` * Calculates refund: `refundAmount = maxAmount - actualUsed` * Refunds unused tokens to user via `transferFeePostTx()` * If user token differs from validator token and `actualUsed > 0`, executes fee swap immediately via `executeFeeSwap()` * Accumulates swapped fees for the validator 3. **Fee Distribution**: * Validators (or anyone) can call `distributeFees(validator)` at any time to transfer accumulated fees to the validator ### Events ```solidity event RebalanceSwap( address indexed userToken, address indexed validatorToken, address indexed swapper, uint256 amountIn, uint256 amountOut ) event FeeSwap( address indexed userToken, address indexed validatorToken, uint256 amountIn, uint256 amountOut ) event Mint( address indexed sender, address indexed userToken, address indexed validatorToken, uint256 amountUserToken, uint256 amountValidatorToken, uint256 liquidity ) event Burn( address indexed sender, address indexed userToken, address indexed validatorToken, uint256 amountUserToken, uint256 amountValidatorToken, uint256 liquidity, address to ) event UserTokenSet(address indexed user, address indexed token) event ValidatorTokenSet(address indexed validator, address indexed token) ``` `Transfer` events are emitted as usual for transactions, with the exception of paying gas fees via TIP20 tokens. For fee payments, a single `Transfer` event is emitted post execution to represent the actual fee amount consumed (i.e. `gasUsed * gasPrice`). ### Gas Fee swaps are designed to be gas-free from the user perspective. The pre-tx and post-tx steps in each transaction do not cost any gas. # Tempo Transactions: protocol overview Tempo Transactions are a new [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transaction type, exclusively available on Tempo. If you're integrating with Tempo, we **strongly recommend** using Tempo Transactions, and not regular Ethereum transactions. Learn more about the benefits below, or follow the guide on issuance [here](https://tempo.xyz/developers/docs/guide/issuance). * [Configurable Fee Tokens](#configurable-fee-tokens) — Pay transaction fees with any USD-denominated TIP-20 token via automatic Fee AMM conversion. * [Fee Sponsorship](#fee-sponsorship) — Sponsor gas fees for users, enabling feeless transaction experiences in your application. * [Batch Calls](#batch-calls) — Batch multiple transactions together for higher throughput and simpler wallet management. * [Access Keys](#access-keys) — Delegate transaction signing capabilities to specific keys with customizable permissions. * [Concurrent Transactions](#concurrent-transactions) — Execute transactions in parallel using independent nonces for improved throughput. * [Scheduled Transactions](#scheduled-transactions) — Schedule transactions to execute within a specific time window for automated payments. ## Integration Guides Integrating Tempo Transactions is easy and can be done quickly by a developer in multiple languages. See below for quick links to some of our guides. |Language|Source|Integration Time| |--------|--------|--------| | **TypeScript** | [tempoxyz/tempo-ts](https://tempo.xyz/developers/docs/sdk/typescript) | \< 1 hour | | **Rust** | [tempo-alloy](https://tempo.xyz/developers/docs/sdk/rust) | \< 1 hour | | **Golang** | [tempo-go](https://github.com/tempoxyz/tempo-go) | \< 1 hour | | **Python** | [pytempo](https://github.com/tempoxyz/pytempo) | \< 1 hour | | **Other Languages** | Reach out to us! The specification is [here](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction) and easy to build against. | 1-3 days | If you are an EVM smart contract developer, see the [Foundry guide for Tempo](https://tempo.xyz/developers/docs/sdk/foundry). ## Properties ### Configurable Fee Tokens A fee token is a permissionless [TIP-20 token](https://tempo.xyz/developers/docs/protocol/tip20/overview) that can be used to pay fees on Tempo. When a TIP-20 token is passed as the `fee_token` parameter in a transaction, Tempo's [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/spec-fee-amm) automatically facilitates conversion between the user's preferred fee token and the validator's preferred token. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const alphaUsd = '0x20c0000000000000000000000000000000000001' const receipt = await client.sendTransactionSync({ data: '0xdeadbeef', feeToken: alphaUsd, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransactionSync } from 'wagmi' const { sendTransactionSync } = useSendTransactionSync() const alphaUsd = '0x20c0000000000000000000000000000000000001' sendTransactionSync({ data: '0xdeadbeef', feeToken: alphaUsd, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); let pending = provider .send_transaction( TempoTransactionRequest::default() .with_fee_token(alpha_usd) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef")), ) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account alpha_usd = "0x20c0000000000000000000000000000000000001" tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), fee_token=alpha_usd, # [!code hl] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef", ), ), ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetFeeToken(transaction.AlphaUSDAddress). // [!code hl] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef"), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.fee-token 0x20c0000000000000000000000000000000000001 # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, // [!code focus] fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` :::info See a full guide on [paying fees in any stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin). ::: ### Fee Sponsorship Fee sponsorship enables a third party (the fee payer) to pay transaction fees on behalf of the transaction sender. The process uses dual signature domains: the sender signs their transaction, and then the fee payer signs over the transaction with a special "fee payer envelope" to commit to paying fees for that specific sender. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' import { privateKeyToAccount } from 'viem/accounts' const feePayer = privateKeyToAccount('0x...') const receipt = await client.sendTransactionSync({ data: '0xdeadbeef', feePayer, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransactionSync } from 'wagmi' import { privateKeyToAccount } from 'viem/accounts' export const feePayer = privateKeyToAccount('0x...') const { sendTransactionSync } = useSendTransactionSync() sendTransactionSync({ data: '0xdeadbeef', feePayer, // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use alloy::signers::{SignerSync, local::PrivateKeySigner}; use tempo_alloy::primitives::transaction::tempo_transaction::Call; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let tx = TempoTransactionRequest { calls: vec![Call { to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), value: U256::ZERO, input: bytes!("deadbeef"), }], ..Default::default() }; // Step 1: Build the transaction let mut tempo_tx = provider.fill(tx).await?.build_aa()?; let sender_addr = provider.default_signer_address(); let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); // Step 2: Fee payer counter-signs the transaction // [!code hl] let fee_payer: PrivateKeySigner = "0x...".parse()?; // [!code hl] tempo_tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&fee_payer_hash)?); // [!code hl] // Step 3: Broadcast let pending = provider.send_transaction(tempo_tx).await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account fee_payer_key = "0x..." # Sender signs with awaiting_fee_payer flag tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), awaiting_fee_payer=True, # [!code hl] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef", ), ), ) sender_signed = tx.sign(account.key.hex()) # Fee payer co-signs the transaction // [!code hl] fully_signed = sender_signed.sign(fee_payer_key, for_fee_payer=True) # [!code hl] tx_hash = w3.eth.send_raw_transaction(fully_signed.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { senderSgn, _ := signer.NewSigner("0x...") sponsorSgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) // Sender builds and signs a sponsored transaction tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetSponsored(true). // [!code hl] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef"), ). Build() _ = transaction.SignTransaction(tx, senderSgn) // Fee payer co-signs the transaction // [!code hl] tx.FeeToken = transaction.AlphaUSDAddress // [!code hl] tx.AwaitingFeePayer = false // [!code hl] _ = transaction.AddFeePayerSignature(tx, sponsorSgn) // [!code hl] serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash # 1. Get the fee payer signature hash $ FEE_PAYER_HASH=$(cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef \ --rpc-url $TEMPO_RPC_URL \ --private-key $SENDER_KEY \ --tempo.print-sponsor-hash) # [!code hl] # 2. Sponsor signs the hash $ SPONSOR_SIG=$(cast wallet sign \ --private-key $SPONSOR_KEY \ "$FEE_PAYER_HASH" \ --no-hash) # [!code hl] # 3. Send with sponsor signature $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef \ --rpc-url $TEMPO_RPC_URL \ --private-key $SENDER_KEY \ --tempo.sponsor-signature "$SPONSOR_SIG" # [!code hl] ``` #### RLP ```tsx // 1. User signs over `user_envelope` // [!code focus] user_envelope = 0x77 ∥ rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, 0x00, // indicate intention for a fee payer // [!code focus] aa_authorization_list, key_authorization ]) // 2. Fee payer signs over `fee_payer_envelope` // [!code focus] fee_payer_envelope = 0x76 ∥ rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, sender_address, // scope to sender // [!code focus] aa_authorization_list, key_authorization ]) // 3. Construct + send off `final_envelope` to the network // [!code focus] final_envelope = 0x77 ∥ rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, fee_payer_signature, // signature over `fee_payer_envelope` // [!code focus] aa_authorization_list, key_authorization, signature, // signature over `user_envelope` // [!code focus] ]) ``` :::tip You can also use a remote [fee payer relay](https://tempo.xyz/developers/docs/api/fee-payer) instead of a local account. ::: :::tip For demos and testnet development, use the public fee payer endpoint at `https://sponsor.moderato.tempo.xyz` without an API key. For authenticated sandbox and production sponsorship, use the [Fee Payer API](https://tempo.xyz/developers/docs/api/fee-payer). ::: :::info See a full guide on [sponsoring fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees). ::: ### Batch Calls Batch calls enable multiple operations to be executed atomically within a single transaction. Instead of sending separate transactions for each operation, you can bundle multiple calls together using the `calls` parameter. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const receipt = await client.sendTransactionSync({ calls: [ // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] ] // [!code hl] }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransactionSync } from 'wagmi' const { sendTransactionSync } = useSendTransactionSync() sendTransactionSync({ calls: [ // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', // [!code hl] data: '0xcafebabe0000000000000000000000000000000001', // [!code hl] }, // [!code hl] { // [!code hl] to: '0xcafebabecafebabecafebabecafebabecafebabe', // [!code hl] data: '0xdeadbeef0000000000000000000000000000000001', // [!code hl] }, // [!code hl] ] // [!code hl] }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::primitives::transaction::Call; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let pending = provider .send_transaction(TempoTransactionRequest { calls: vec![ // [!code hl] Call { // [!code hl] to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] value: U256::ZERO, // [!code hl] input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] }, // [!code hl] Call { // [!code hl] to: address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").into(), // [!code hl] value: U256::ZERO, // [!code hl] input: bytes!("cafebabe0000000000000000000000000000000001"), // [!code hl] }, // [!code hl] Call { // [!code hl] to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] value: U256::ZERO, // [!code hl] input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] }, // [!code hl] ], // [!code hl] ..Default::default() }) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=600_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=( # [!code hl] Call.create( # [!code hl] to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] ), # [!code hl] Call.create( # [!code hl] to="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", # [!code hl] data="0xcafebabe0000000000000000000000000000000001", # [!code hl] ), # [!code hl] Call.create( # [!code hl] to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] ), # [!code hl] ), # [!code hl] ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(600_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( // [!code hl] common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] ). // [!code hl] AddCall( // [!code hl] common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("cafebabe0000000000000000000000000000000001"), // [!code hl] ). // [!code hl] AddCall( // [!code hl] common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] ). // [!code hl] Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast batch-send \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" \ --call "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef::setNumber(uint256):500" \ --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, // [!code focus] access_list, nonce_key, nonce, valid_before, valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` ### Access Keys Access keys enable you to delegate signing authority from a primary account to a secondary key, such as device-bound non-extractable [WebCrypto key](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair). The primary account signs a key authorization that grants the access key permission to sign transactions on its behalf. This authorization is then attached to the next transaction (that can be signed by either the primary or the access key), then all transactions thereafter can be signed by the access key. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { parseUnits } from 'viem' import { Account, P256 } from 'viem/tempo' import { client } from './viem.config' const account = Account.fromSecp256k1('0x...') const alphaUsd = '0x20c0000000000000000000000000000000000001' const treasury = '0xcafebabecafebabecafebabecafebabecafebabe' const accessKey = Account.fromP256(P256.randomPrivateKey(), { access: account, }) const keyAuthorization = await account.signKeyAuthorization(accessKey, { chainId: BigInt(client.chain.id), expiry: Math.floor(Date.now() / 1000) + 3600, limits: [ { token: alphaUsd, limit: parseUnits('1000', 6), period: 60 * 60 * 24 * 30, }, ], scopes: [ { address: alphaUsd, selector: 'transfer(address,uint256)', recipients: [treasury], }, ], }) // `keyAuthorization` provisions the access key and uses it in this same transaction. const receipt = await client.sendTransactionSync({ account: accessKey, // [!code hl] data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240', keyAuthorization, // [!code hl] to: alphaUsd, }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi ```tsx twoslash [example.tsx] // @noErrors import { parseUnits } from 'viem' import { Account, Expiry, P256, Period, tempoActions } from 'viem/tempo' import { useConnectorClient } from 'wagmi' export function useAuthorizeAccessKey() { const { data: connectorClient } = useConnectorClient() async function authorize() { if (!connectorClient) return const client = connectorClient.extend(tempoActions()) const alphaUsd = '0x20c0000000000000000000000000000000000001' const accessKey = Account.fromP256(P256.randomPrivateKey(), { access: connectorClient.account, }) const { receipt } = await client.accessKey.authorizeSync({ accessKey, // [!code hl] expiry: Expiry.hours(1), limits: [ { token: alphaUsd, limit: parseUnits('1000', 6), period: Period.months(1), }, ], scopes: [ { address: alphaUsd, selector: 'transfer(address,uint256)', }, ], }) return receipt.transactionHash } return { authorize } } ``` #### Rust :::code-group ```rust [example.rs] use std::str::FromStr; use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use alloy::signers::{SignerSync, local::PrivateKeySigner}; use tempo_alloy::primitives::transaction::key_authorization::{ CallScope, KeyAuthorization, SelectorRule, TokenLimit, }; use tempo_alloy::primitives::transaction::tt_signature::{ KeychainSignature, PrimitiveSignature, SignatureType, TempoSignature, }; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let root: PrivateKeySigner = std::env::var("PRIVATE_KEY")?.parse()?; let access_key = PrivateKeySigner::random(); let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); let treasury = address!("0xcafebabecafebabecafebabecafebabecafebabe"); let authorization = KeyAuthorization::unrestricted( // [!code hl] 4217, // [!code hl] SignatureType::Secp256k1, // [!code hl] access_key.address(), // [!code hl] ) // [!code hl] .with_expiry(1_893_456_000) // [!code hl] .with_limits(vec![TokenLimit { // [!code hl] token: alpha_usd, // [!code hl] limit: U256::from(1_000_000u64), // [!code hl] period: 86_400, // [!code hl] }]) // [!code hl] .with_allowed_calls(vec![CallScope { // [!code hl] target: alpha_usd, // [!code hl] selector_rules: vec![SelectorRule { // [!code hl] selector: [0xa9, 0x05, 0x9c, 0xbb], // transfer(address,uint256) // [!code hl] recipients: vec![treasury], // [!code hl] }], // [!code hl] }]); // [!code hl] let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] let key_authorization = // [!code hl] authorization.into_signed(PrimitiveSignature::Secp256k1(sig)); // [!code hl] provider .send_transaction( TempoTransactionRequest { key_authorization: Some(key_authorization), // [!code hl] ..Default::default() } .with_to(alpha_usd) .with_input(bytes!("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240")), ) .await? .get_receipt() .await?; let tx = TempoTransactionRequest::default() .with_to(alpha_usd) .with_input(bytes!("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240")); let filled = provider.fill(tx).await?; let tempo_tx = filled.build_aa()?; // Keychain signatures are domain-separated by the root account address. let inner_hash = // [!code hl] KeychainSignature::signing_hash(tempo_tx.signature_hash(), root.address()); // [!code hl] let inner_sig = access_key.sign_hash_sync(&inner_hash)?; // [!code hl] let signature = TempoSignature::Keychain(KeychainSignature::new( // [!code hl] root.address(), // [!code hl] PrimitiveSignature::Secp256k1(inner_sig), // [!code hl] )); // [!code hl] let envelope = tempo_tx.into_signed(signature); // [!code hl] let pending = provider // [!code hl] .send_raw_transaction(envelope.encoded_2718().as_ref()) // [!code hl] .await?; // [!code hl] Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] import time from eth_account import Account as EthAccount from pytempo import ( Call, CallScope, KeyRestrictions, SignatureType, TempoTransaction, TokenLimit, ) from pytempo.contracts import AccountKeychain from provider import w3, account access_key = EthAccount.create() alpha_usd = "0x20c0000000000000000000000000000000000001" treasury = "0xcafebabecafebabecafebabecafebabecafebabe" auth_nonce = w3.eth.get_transaction_count(account.address) authorize_call = AccountKeychain.authorize_key( # [!code hl] key_id=access_key.address, # [!code hl] signature_type=SignatureType.SECP256K1, # [!code hl] restrictions=KeyRestrictions( # [!code hl] expiry=int(time.time()) + 3600, # [!code hl] limits=[TokenLimit(token=alpha_usd, limit=1_000_000, period=86_400)], # [!code hl] allowed_calls=[CallScope.transfer(target=alpha_usd, recipients=[treasury])], # [!code hl] ), # [!code hl] ) # [!code hl] # Root key authorizes first, then the access key signs later transactions. auth_tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=auth_nonce, calls=(authorize_call,), ) signed_auth_tx = auth_tx.sign(account.key.hex()) w3.eth.send_raw_transaction(signed_auth_tx.encode()) tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=auth_nonce + 1, calls=( Call.create( to=alpha_usd, data="0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240", ), ), ) signed_tx = tx.sign_access_key( # [!code hl] access_key_private_key=access_key.key.hex(), # [!code hl] root_account=account.address, # [!code hl] ) # [!code hl] tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/keychain" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { rootSgn, _ := signer.NewSigner("0x...") accessKey, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() chainID := big.NewInt(transaction.ChainIdMainnet) gasPrice := big.NewInt(25_000_000_000) alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") treasury := common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe") // Authorize the access key with T3 restrictions. // [!code hl] restrictions := keychain.NewKeyRestrictions(uint64(time.Now().Add(1 * time.Hour).Unix())). // [!code hl] WithLimits([]keychain.TokenLimit{{ // [!code hl] Token: alphaUSD, // [!code hl] Amount: big.NewInt(1_000_000), // [!code hl] Period: 86_400, // [!code hl] }}). // [!code hl] WithAllowedCalls([]keychain.CallScope{ // [!code hl] keychain.NewCallScopeBuilder(alphaUSD).Transfer([]common.Address{treasury}).Build(), // [!code hl] }) // [!code hl] authorizeCall, _ := keychain.AuthorizeKey( // [!code hl] accessKey.Address(), // [!code hl] keychain.SignatureTypeSecp256k1, // [!code hl] restrictions, // [!code hl] ) // [!code hl] // Go shows the explicit two-step flow: root key authorizes first, then the access key signs later transactions. nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) authTx := transaction.NewBuilder(chainID). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(gasPrice). SetMaxPriorityFeePerGas(gasPrice). AddCall(authorizeCall.To, big.NewInt(0), authorizeCall.Data). Build() _ = transaction.SignTransaction(authTx, rootSgn) serializedAuth, _ := transaction.Serialize(authTx, nil) authHash, _ := c.SendRawTransaction(ctx, serializedAuth) log.Printf("Authorized access key: %s", authHash) // Sign a transaction with the access key. // [!code hl] tx := transaction.NewBuilder(chainID). // [!code hl] SetNonce(nonce + 1). // [!code hl] SetGas(300_000). // [!code hl] SetMaxFeePerGas(gasPrice). // [!code hl] SetMaxPriorityFeePerGas(gasPrice). // [!code hl] AddCall( // [!code hl] alphaUSD, // [!code hl] big.NewInt(0), // [!code hl] common.Hex2Bytes("a9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240"), // [!code hl] ). // [!code hl] Build() // [!code hl] _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash # 1. Authorize the access key with a recurring limit and transfer scope $ cast keychain authorize $ACCESS_KEY_ADDR secp256k1 $(($(date +%s) + 3600)) \ --limit 0x20c0000000000000000000000000000000000001:1000000:86400 \ --scope 0x20c0000000000000000000000000000000000001:transfer@0xcafebabecafebabecafebabecafebabecafebabe \ --rpc-url $TEMPO_RPC_URL \ --private-key $ROOT_PRIVATE_KEY # [!code hl] # 2. Send using the access key $ cast send 0x20c0000000000000000000000000000000000001 \ --data 0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe00000000000000000000000000000000000000000000000000000000000f4240 \ --rpc-url $TEMPO_RPC_URL \ --tempo.root-account $ROOT_ADDRESS \ --tempo.access-key $ACCESS_KEY_PRIVATE_KEY # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, // rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, signature]) // [!code focus] signature, ]) ``` :::info Learn more about [Access Keys](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#access-keys). ::: ### Concurrent Transactions Concurrent transactions enable higher throughput by allowing multiple transactions from the same account to be sent in parallel without waiting for sequential nonce confirmation. By utilizing nonce keys, you can submit multiple transactions simultaneously that don't conflict with each other, enabling parallel execution and significantly improved transaction throughput for high-activity accounts. Concurrent transactions can be achieved with nonce keys via: * [Expiring Nonces](#expiring-nonces) * [2D Nonces](#2d-nonces) In **Viem** and **Wagmi**, expiring nonces are handled automatically. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const [receipt1, receipt2, receipt3] = await Promise.all([ client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }), client.sendTransactionSync({ data: '0xcafebabe0000000000000000000000000000000001', to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', }), client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }), ]) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransaction } from 'wagmi' const { sendTransaction } = useSendTransaction() sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; // Send three transactions concurrently using different nonce keys let (r1, r2, r3) = tokio::try_join!( provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(1)) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(2)) // [!code hl] .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) .with_input(bytes!("cafebabe0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(3)) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), )?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account # Send three transactions concurrently using different nonce keys for nonce_key, to, data in [ (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), ]: tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=0, nonce_key=nonce_key, # [!code hl] calls=(Call.create(to=to, data=data),), ) signed_tx = tx.sign(account.key.hex()) w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "sync" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() // Send three transactions concurrently using different nonce keys type txParams struct { nonceKey int64 to string data string } params := []txParams{ {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, } var wg sync.WaitGroup for _, p := range params { wg.Add(1) go func(p txParams) { defer wg.Done() tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(0). SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( common.HexToAddress(p.to), big.NewInt(0), common.Hex2Bytes(p.data), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) }(p) } wg.Wait() } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash # Send three transactions concurrently using different nonce keys $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --async --nonce 0 --tempo.nonce-key 1 # [!code hl] $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ --data 0xcafebabe0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --async --nonce 0 --tempo.nonce-key 2 # [!code hl] $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --async --nonce 0 --tempo.nonce-key 3 # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, // [!code focus] nonce, valid_before, // [!code focus] valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` ### Expiring Nonces The [expiring nonces specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1009.md) defines transactions that automatically expire if they are not executed within a specified time window. **Benefits:** * No nonce tracking required * Automatic replay protection via circular buffer * No permanent state bloat from unused nonce keys Set `nonceKey` to `maxUint256`, `nonce` to `0`, and `validBefore` to a Unix timestamp in seconds satisfying `now < validBefore <= now + 300`, where `now` is the current block timestamp. The maximum window is five minutes; SDK defaults may use shorter windows. See [TIP-1093](https://tips.sh/1093). #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { maxUint256 } from 'viem' import { client } from './viem.config' const receipt = await client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: maxUint256, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] }) ``` ```tsx twoslash [viem.config.ts] filename="viem.config.ts" import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { maxUint256 } from 'viem' import { useSendTransaction } from 'wagmi' const { sendTransaction } = useSendTransaction() sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: maxUint256, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', validBefore: Math.floor(Date.now() / 1000) + 20, // [!code focus] }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use std::time::{SystemTime, UNIX_EPOCH}; use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let valid_before = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + 30; let pending = provider .send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::MAX) // [!code focus] .with_valid_before(valid_before) // [!code focus] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] import time from pytempo import Call, TempoTransaction from provider import w3, account # maxUint256: signals an expiring nonce MAX_UINT256 = 2**256 - 1 valid_before = int(time.time()) + 20 tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce_key=MAX_UINT256, # [!code focus] valid_before=valid_before, # [!code focus] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef0000000000000000000000000000000001", ), ), ) signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() // maxUint256: signals an expiring nonce maxUint256, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) validBefore := uint64(time.Now().Unix()) + 20 tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetNonceKey(maxUint256). // [!code focus] SetValidBefore(validBefore). // [!code focus] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ VALID_BEFORE=$(($(date +%s) + 20)) $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, // set to `maxUint256` // [!code focus] nonce, valid_before, // e.g. `now + 20`; maximum `now + 300` // [!code focus] valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` ### 2D Nonces For cases requiring ordered sequences within a key, Tempo's **2D nonce system** enables parallel transaction execution: * **Protocol nonce (key 0)**: The default sequential nonce. Transactions must be processed in order. * **User nonces (keys 1+)**: Independent nonce sequences that allow concurrent transaction submission. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const [receipt1, receipt2, receipt3] = await Promise.all([ client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 1n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }), client.sendTransactionSync({ data: '0xcafebabe0000000000000000000000000000000001', nonceKey: 2n, // [!code focus] to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', }), client.sendTransactionSync({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 3n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }), ]) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { useSendTransaction } from 'wagmi' const { sendTransaction } = useSendTransaction() sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 1n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 2n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) sendTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', nonceKey: 3n, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; let (r1, r2, r3) = tokio::try_join!( provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(1)) // [!code focus] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(2)) // [!code focus] .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) .with_input(bytes!("cafebabe0000000000000000000000000000000001")), ), provider.send_transaction( TempoTransactionRequest::default() .with_nonce_key(U256::from(3)) // [!code focus] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ), )?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from pytempo import Call, TempoTransaction from provider import w3, account for nonce_key, to, data in [ (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), ]: tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=0, nonce_key=nonce_key, # [!code focus] calls=(Call.create(to=to, data=data),), ) signed_tx = tx.sign(account.key.hex()) w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "sync" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() type txParams struct { nonceKey int64 to string data string } params := []txParams{ {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, } var wg sync.WaitGroup for _, p := range params { wg.Add(1) go func(p txParams) { defer wg.Done() tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(0). SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). AddCall( common.HexToAddress(p.to), big.NewInt(0), common.Hex2Bytes(p.data), ). Build() _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) }(p) } wg.Wait() } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 1 # [!code hl] $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ --data 0xcafebabe0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 2 # [!code hl] $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 3 # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, // [!code focus] nonce, valid_before, valid_after, fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` :::warning **Reuse nonce keys instead of generating random ones.** Creating a new nonce key incurs a state creation cost that increases with the number of active keys (see [State creation costs](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1000.md)). For most applications, using a small set of sequential nonce keys (e.g., `1n`, `2n`, `3n`) is sufficient and much more cost-effective than generating random nonce keys for each transaction. ::: ### Scheduled Transactions Scheduled transactions allow you to sign a transaction in advance and specify a time window for when it can be executed onchain. By setting `validAfter` and `validBefore` timestamps, you define the earliest and latest times the transaction can be included in a block. #### Viem :::code-group ```tsx twoslash [example.ts] // @noErrors import { client } from './viem.config' const signature = await client.signTransaction({ data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] }) ``` ```tsx twoslash [viem.config.ts] import { privateKeyToAccount } from 'viem/accounts' import { createClient } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), }) ``` ::: #### Wagmi :::code-group ```tsx twoslash [example.ts] // @noErrors import { signTransaction } from 'wagmi/actions' import { config } from './wagmi.config' const signature = await signTransaction(config, { data: '0xdeadbeef0000000000000000000000000000000001', to: '0xcafebabecafebabecafebabecafebabecafebabe', validAfter: Math.floor(Number(new Date('2026-01-01')) / 1000), // [!code hl] validBefore: Math.floor(Number(new Date('2026-01-02')) / 1000), // [!code hl] }) ``` ```tsx twoslash [wagmi.config.ts] // @noErrors import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { tempoWallet } from 'wagmi/connectors' export const config = createConfig({ connectors: [tempoWallet()], chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), }, }) ``` ::: #### Rust :::code-group ```rust [example.rs] use alloy::primitives::{address, bytes}; use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; #[tokio::main] async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; // 2026-01-01 00:00:00 UTC let valid_after = 1_767_225_600; // 2026-01-02 00:00:00 UTC let valid_before = 1_767_312_000; let pending = provider .send_transaction( TempoTransactionRequest::default() .with_valid_after(valid_after) // [!code hl] .with_valid_before(valid_before) // [!code hl] .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) .with_input(bytes!("deadbeef0000000000000000000000000000000001")), ) .await?; Ok(()) } ``` ```rust [provider.rs] use alloy::providers::ProviderBuilder; use alloy::signers::local::PrivateKeySigner; use tempo_alloy::TempoNetwork; pub async fn get_provider() -> Result< impl alloy::providers::Provider, Box, > { let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") .expect("PRIVATE_KEY not set") .parse()?; let provider = ProviderBuilder::new_with_network::() .wallet(signer) .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) .await?; Ok(provider) } ``` ::: #### Python :::code-group ```python [example.py] from datetime import datetime, timezone from pytempo import Call, TempoTransaction from provider import w3, account # 2026-01-01 00:00:00 UTC valid_after = int(datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp()) # 2026-01-02 00:00:00 UTC valid_before = int(datetime(2026, 1, 2, tzinfo=timezone.utc).timestamp()) tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=300_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), valid_after=valid_after, # [!code hl] valid_before=valid_before, # [!code hl] calls=( Call.create( to="0xcafebabecafebabecafebabecafebabecafebabe", data="0xdeadbeef0000000000000000000000000000000001", ), ), ) # Sign now, submit to the network for later execution signed_tx = tx.sign(account.key.hex()) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ```python [provider.py] from web3 import Web3 from eth_account import Account w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` ::: #### Go :::code-group ```go [main.go] package main import ( "context" "log" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { sgn, _ := signer.NewSigner("0x...") c := newClient() ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) // 2026-01-01 00:00:00 UTC validAfter := uint64(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()) // 2026-01-02 00:00:00 UTC validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). SetValidAfter(validAfter). // [!code hl] SetValidBefore(validBefore). // [!code hl] AddCall( common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), big.NewInt(0), common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), ). Build() // Sign now, submit to the network for later execution _ = transaction.SignTransaction(tx, sgn) serialized, _ := transaction.Serialize(tx, nil) txHash, _ := c.SendRawTransaction(ctx, serialized) log.Printf("Transaction hash: %s", txHash) } ``` ```go [provider.go] package main import ( "os" "github.com/tempoxyz/tempo-go/pkg/client" ) func newClient() *client.Client { return client.New(os.Getenv("TEMPO_RPC_URL")) } ``` ::: #### Cast ```bash $ VALID_AFTER=$(date -d '2026-01-01' +%s) $ VALID_BEFORE=$(date -d '2026-01-02' +%s) $ cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ --data 0xdeadbeef0000000000000000000000000000000001 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.valid-after $VALID_AFTER \ --tempo.valid-before $VALID_BEFORE # [!code hl] ``` #### RLP ```tsx rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas, calls, access_list, nonce_key, nonce, valid_before, // [!code focus] valid_after, // [!code focus] fee_token, fee_payer_signature, aa_authorization_list, key_authorization, signature, ]) ``` ## Learn more about Tempo Transactions * [Specification](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction) — Native protocol support for new transaction features including WebAuthn/P256 signatures, parallelizable nonces, gas sponsorship, call batching, and scheduled transactions * [Guide: Pay Fees in Any Stablecoin](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) — Configure users to pay transaction fees in any supported stablecoin * [Guide: Sponsor Transaction Fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) — Enable gasless transactions with fee payers # Tempo Transactions Specification ## Abstract This spec introduces native protocol support for the following features, using Tempo Transactions: * WebAuthn/P256 signature validation - enables passkey signing * Parallelizable nonces - allows higher tx throughput for each account * Gas sponsorship - allows apps to pay for their users' transactions * Call Batching - allows users to multicall efficiently and atomically * Scheduled Txs - allow users to specify a time window in which their tx can be executed * Access Keys - allow a sender's key to provision scoped access keys with spending limits or admin access keys for key management ## Motivation Current accounts are limited to secp256k1 signatures and sequential nonces, creating UX and scalability challenges.\ Users cannot leverage modern authentication methods like passkeys, applications face throughput limitations due to sequential nonces. ## Specification ### Transaction Type A new EIP-2718 transaction type is introduced with type byte `0x76`: ```rust pub struct TempoTransaction { // Standard EIP-1559 fields chain_id: ChainId, // EIP-155 replay protection max_priority_fee_per_gas: u128, max_fee_per_gas: u128, gas_limit: u64, calls: Vec, // Batch of calls to execute atomically access_list: AccessList, // EIP-2930 access list // nonce-related fields nonce_key: U256, // 2D nonce key (0 = protocol nonce, >0 = user nonces) nonce: u64, // Current nonce value for the nonce key // Optional features fee_token: Option
, // Optional fee token preference fee_payer_signature: Option, // Sponsored transactions (secp256k1 only) valid_before: Option, // Transaction expiration timestamp (seconds) valid_after: Option, // Transaction can only be included after this timestamp (seconds) key_authorization: Option, // Access key authorization (optional) aa_authorization_list: Vec, // EIP-7702 style authorizations with AA signatures } // Call structure for batching pub struct Call { to: TxKind, // Can be Address or Create value: U256, input: Bytes // Calldata for the call } // Key authorization for provisioning access keys // RLP encoding: [chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?] pub struct KeyAuthorization { chain_id: u64, // Chain ID for replay protection (0 = valid on any chain) key_type: SignatureType, // Type of key: Secp256k1 (0), P256 (1), or WebAuthn (2) key_id: Address, // Key identifier (address derived from public key) expiry: Option, // Unix timestamp when key expires (omit / None for never expires) limits: Option>, // TIP20 spending limits (None = unlimited spending) allowed_calls: Option>, // Call-scope allowlist (None = unrestricted; Some(empty) = scoped deny-all) witness: Option, // App-defined witness digest for replay protection is_admin: Option, // true to provision an admin access key account: Option
, // Target account for admin-signed authorizations } // Signed key authorization (authorization + root/admin key signature) pub struct SignedKeyAuthorization { authorization: KeyAuthorization, signature: PrimitiveSignature, // Root/admin key's signature over keccak256(rlp(authorization)) } // TIP20 spending limit for access keys pub struct TokenLimit { token: Address, // TIP20 token address limit: U256, // Maximum spending amount for this token period: u64, // Recurring period in seconds (0 = one-time, non-zero = recurring) } // Call-scope allowlist entry: a target contract and its allowed selector rules pub struct CallScope { target: Address, // Target contract address selector_rules: Vec, // Allowed selectors on that target (empty = any selector allowed) } // Selector rule: a function selector and optional recipient allowlist (for recipient-bound TIP-20 selectors) pub struct SelectorRule { selector: [u8; 4], // 4-byte function selector recipients: Vec
, // Allowed recipients (empty = any recipient) } ``` ### Signature Types Four signature schemes are supported. The signature type is determined by length and type identifier: #### secp256k1 (65 bytes) ```rust pub struct Signature { r: B256, // 32 bytes s: B256, // 32 bytes v: u8 // 1 byte (recovery id) } ``` **Format**: No type identifier prefix (backward compatible). Total length: 65 bytes. **Detection**: Exactly 65 bytes with no type identifier. #### P256 (130 bytes) ```rust pub struct P256SignatureWithPreHash { typeId: u8, // 0x01 r: B256, // 32 bytes s: B256, // 32 bytes pub_key_x: B256, // 32 bytes pub_key_y: B256, // 32 bytes pre_hash: bool // 1 byte } ``` **Format**: Type identifier `0x01` + 129 bytes of signature data. Total length: 130 bytes. The `typeId` is a wire format prefix (not a struct field) prepended during encoding. Note: Some P256 implementers (like Web Crypto) require the digests to be pre-hashed before verification. If `pre_hash` is set to `true`, then before verification: `digest = sha256(digest)`. #### WebAuthn (Variable length, max 2KB) ```rust pub struct WebAuthnSignature { typeId: u8, // 0x02 webauthn_data: Bytes, // Variable length (authenticatorData || clientDataJSON) r: B256, // 32 bytes s: B256, // 32 bytes pub_key_x: B256, // 32 bytes pub_key_y: B256 // 32 bytes } ``` **Format**: Type identifier `0x02` + variable webauthn\_data + 128 bytes (r, s, pub\_key\_x, pub\_key\_y). Total length: variable (minimum 129 bytes, maximum 2049 bytes). The `typeId` is a wire format prefix prepended during encoding. Parse by working backwards: last 128 bytes are r, s, pub\_key\_x, pub\_key\_y. #### Keychain (Variable length) ```rust pub struct KeychainSignature { typeId: u8, // 0x03 user_address: Address, // 20 bytes - root account address signature: PrimitiveSignature // Inner signature (Secp256k1, P256, or WebAuthn) } ``` **Format**: Type identifier `0x03` + user\_address (20 bytes) + inner signature. The `typeId` is a wire format prefix prepended during encoding. **Purpose**: Allows an access key to sign on behalf of a root account. The handler validates that `user_address` has authorized the access key in the AccountKeychain precompile. ### Address Derivation #### secp256k1 ```solidity address(uint160(uint256(keccak256(abi.encode(x, y))))) ``` #### P256 and WebAuthn ```solidity function deriveAddressFromP256(bytes32 pubKeyX, bytes32 pubKeyY) public pure returns (address) { // Hash bytes32 hash = keccak256(abi.encodePacked( pubKeyX, pubKeyY )); // Take last 20 bytes as address return address(uint160(uint256(hash))); } ``` ### Tempo Authorization List The `aa_authorization_list` field enables EIP-7702 style delegation with support for all three AA signature types (secp256k1, P256, and WebAuthn), not just secp256k1. #### Structure ```rust pub struct TempoSignedAuthorization { inner: Authorization, // Standard EIP-7702 authorization signature: TempoSignature, // Can be Secp256k1, P256, or WebAuthn } ``` Each authorization in the list: * Delegates an account to a specified implementation contract * Is signed by the account's authority using any supported signature type * Follows EIP-7702 semantics for delegation and execution #### Validation * Cannot have `Create` calls when `aa_authorization_list` is non-empty (follows EIP-7702 semantics) * Authority address is recovered from the signature and matched against the authorization ### Parallelizable Nonces * **Protocol nonce (key 0)**: Existing account nonce, incremented for regular txs, 7702 authorization, or `CREATE` * **User nonces (keys 1-N)**: Enable parallel execution with special gas schedule * **Reserved sequence keys**: Nonce sequence keys with the most significant byte `0x5b` are reserved for protocol-managed validator sequencing. #### Account State Changes * `nonces: mapping(uint256 => uint64)` - 2D nonce tracking **Implementation Note:** Nonces are stored in the storage of a designated precompile at address `0x4E4F4E4345000000000000000000000000000000` (ASCII hex for "NONCE"), as there is currently no clean way to extend account state in Reth. **Storage Layout at 0x4E4F4E4345:** * Storage key: `keccak256(abi.encode(account_address, nonce_key))` * Storage value: `nonce` (uint64) Note: Protocol Nonce key (0), is directly stored in the account state, just like normal transaction types. #### Nonce Precompile The nonce precompile implements the following interface for managing 2D nonces: ```solidity /// @title INonce - Nonce Precompile Interface /// @notice Interface for managing 2D nonces as per the Tempo Transaction spec /// @dev This precompile manages user nonce keys (1-N) while protocol nonces (key 0) /// are handled directly by account state. Each account can have multiple /// independent nonce sequences identified by a nonce key. interface INonce { /// @notice Emitted when a nonce is incremented for an account and nonce key /// @param account The account whose nonce was incremented /// @param nonceKey The nonce key that was incremented /// @param newNonce The new nonce value after incrementing event NonceIncremented(address indexed account, uint256 indexed nonceKey, uint64 newNonce); /// @notice Thrown when trying to access protocol nonce (key 0) through the precompile /// @dev Protocol nonce should be accessed through account state, not this precompile error ProtocolNonceNotSupported(); /// @notice Thrown when an invalid nonce key is provided error InvalidNonceKey(); /// @notice Thrown when a nonce value would overflow error NonceOverflow(); /// @notice Get the current nonce for a specific account and nonce key /// @param account The account address /// @param nonceKey The nonce key (must be > 0, protocol nonce key 0 not supported) /// @return nonce The current nonce value function getNonce(address account, uint256 nonceKey) external view returns (uint64 nonce); } ``` #### Precompile Implementation The precompile contract maintains a single storage mapping: ```solidity contract Nonce is INonce { /// @dev Mapping from account -> nonce key -> nonce value mapping(address => mapping(uint256 => uint64)) private nonces; } ``` #### Gas Schedule For transactions using nonce keys: 1. **Protocol nonce (key 0)**: No additional gas cost * Uses the standard account nonce stored in account state 2. **Existing user key (nonce > 0)**: Add 5,000 gas to base cost * Rationale: Cold SLOAD (2,100) + warm SSTORE reset (2,900) 3. **New user key (nonce == 0)**: Add 22,100 gas to base cost * Rationale: Cold SLOAD (2,100) + SSTORE set for 0 → non-zero (20,000) We specify the complete gas schedule in more detail in the [gas costs section](#gas-costs) ### Transaction Validation #### Signature Validation 1. Determine type from signature format: * 65 bytes (no type identifier) = secp256k1 * First byte `0x01` + 129 bytes = P256 (total 130 bytes) * First byte `0x02` + variable data = WebAuthn (total 129-2049 bytes) * First byte `0x03` + 20 bytes + inner signature = Keychain * Otherwise invalid 2. Apply appropriate verification: * secp256k1: Standard `ecrecover` * P256: P256 curve verification with provided public key (sha256 pre-hash if flag set) * WebAuthn: Parse clientDataJSON, verify challenge and type, then P256 verify * Keychain: Verify inner signature, then validate access key authorization via AccountKeychain precompile #### Nonce Validation 1. Fetch sequence for given nonce key 2. Verify sequence matches transaction 3. Increment sequence #### Fee Payer Validation (if present) 1. Verify fee payer signature (K1 only initially) 2. Recover payer address via `ecrecover` 3. Deduct fees from payer instead of sender ### Fee Payer Signature Details The Tempo Transaction Type (0x76) supports **gas sponsorship** where a third party (fee payer) can pay transaction fees on behalf of the sender. This is achieved through dual signature domains—the sender signs with transaction type byte `0x76`, while the fee payer signs with magic byte `0x78` to ensure domain separation and prevent signature reuse attacks. #### Signing Domains ##### Sender Signature For computing the transaction hash that the sender signs: * Fields are preceded by transaction type byte `0x76` * Field 11 (`fee_token`) is encoded as empty string (`0x80`) **if and only if** `fee_payer_signature` is present. This allows the fee payer to specify the fee token. * Field 12 (`fee_payer_signature`) is encoded as: * Single byte `0x00` if fee payer signature will be present (placeholder) * Empty string `0x80` if no fee payer **Sender Signature Hash:** ```rust // When fee_payer_signature is present: sender_hash = keccak256(0x76 || rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, calls, access_list, nonce_key, nonce, valid_before, valid_after, 0x80, // fee_token encoded as EMPTY (skipped) 0x00 // placeholder byte for fee_payer_signature ])) // When no fee_payer_signature: sender_hash = keccak256(0x76 || rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, // fee_token is INCLUDED 0x80 // empty for no fee_payer_signature ])) ``` ##### Fee Payer Signature Only included for sponsored transactions. For computing the fee payer's signature hash: * Fields are preceded by **magic byte `0x78`** (different from transaction type `0x76`) * Field 11 (`fee_token`) is **always included** (20-byte address or `0x80` for None) * Field 12 is serialized as the **sender address** (20 bytes). This commits the fee payer to sponsoring a specific sender. **Fee Payer Signature Hash:** ```rust fee_payer_hash = keccak256(0x78 || rlp([ // Note: 0x78 magic byte chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, calls, access_list, nonce_key, nonce, valid_before, valid_after, fee_token, // fee_token ALWAYS included sender_address // 20-byte sender address key_authorization, ])) ``` #### Key Properties 1. **Sender Flexibility**: By omitting `fee_token` from sender signature when fee payer is present, the fee payer can specify which token to use for payment without invalidating the sender's signature 2. **Fee Payer Commitment**: Fee payer's signature includes `fee_token` and `sender_address`, ensuring they agree to: * Pay for the specific sender * Use the specific fee token 3. **Domain Separation**: Different magic bytes (`0x76` vs `0x78`) prevent signature reuse attacks between sender and fee payer roles 4. **Deterministic Fee Payer**: The fee payer address is statically recoverable from the transaction via secp256k1 signature recovery #### Validation Rules **Signature Requirements:** * Sender signature MUST be valid (secp256k1, P256, or WebAuthn depending on signature length) * If `fee_payer_signature` present: * MUST be recoverable via secp256k1 (only secp256k1 supported for fee payers) * Recovery MUST succeed, otherwise transaction is invalid * If `fee_payer_signature` absent: * Fee payer defaults to sender address (self-paid transaction) **Token Preference:** * When `fee_token` is `Some(address)`, this overrides any account/validator-level preferences * Validation ensures the token is a valid TIP-20 token with sufficient balance/liquidity * Failures reject the transaction before execution (see Token Preferences spec) **Fee Payer Resolution:** * Fee payer signature present → recovered address via `ecrecover` * Fee payer signature absent → sender address * This address is used for all fee accounting (pre-charge, refund) via TIP Fee Manager precompile #### Transaction Flow 1. **User prepares transaction**: Sets `fee_payer_signature` to placeholder (`Some(Signature::default())`) 2. **User signs**: Computes sender hash (with fee\_token skipped) and signs 3. **Fee payer receives** user-signed transaction 4. **Fee payer verifies** user signature is valid 5. **Fee payer signs**: Computes fee payer hash (with fee\_token and sender\_address) and signs 6. **Complete transaction**: Replace placeholder with actual fee payer signature 7. **Broadcast**: Transaction is sent to network with both signatures #### Error Cases * `fee_payer_signature` present but unrecoverable → invalid transaction * Fee payer balance insufficient for `gas_limit * max_fee_per_gas` in fee token → invalid * Any sender signature failure → invalid * Malformed RLP → invalid ### RLP Encoding The transaction is RLP encoded as follows: **Signed Transaction Envelope:** ``` 0x76 || rlp([ chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, calls, // RLP list of Call structs access_list, nonce_key, nonce, valid_before, // 0x80 if None valid_after, // 0x80 if None fee_token, // 0x80 if None fee_payer_signature, // 0x80 if None, RLP list [v, r, s] if Some aa_authorization_list, // EIP-7702 style authorization list with AA signatures key_authorization?, // Only encoded if present (backwards compatible) sender_signature // TempoSignature bytes (secp256k1, P256, WebAuthn, or Keychain) ]) ``` **Call Encoding:** ``` rlp([to, value, input]) ``` **Key Authorization Encoding:** ``` rlp([ chain_id, key_type, key_id, expiry?, // Optional trailing field (omitted or 0x80 if None) limits?, // Optional trailing field (omitted or 0x80 if None) signature // PrimitiveSignature bytes ]) ``` **Notes:** * Optional fields encode as `0x80` (EMPTY\_STRING\_CODE) when `None` * The `key_authorization` field is truly optional - when `None`, no bytes are encoded (backwards compatible) * The `calls` field is a list that must contain at least one Call (empty calls list is invalid) * The `sender_signature` field is the final field and contains the TempoSignature bytes (secp256k1, P256, WebAuthn, or Keychain) * KeyAuthorization uses RLP trailing field semantics for optional `expiry`, `limits`, `allowed_calls`, `witness`, `is_admin`, and `account` ### WebAuthn Signature Verification WebAuthn verification follows the [Daimo P256 verifier approach](https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol). #### Signature Format ``` signature = authenticatorData || clientDataJSON || r (32) || s (32) || pubKeyX (32) || pubKeyY (32) ``` Parse by working backwards: * Last 32 bytes: `pubKeyY` * Previous 32 bytes: `pubKeyX` * Previous 32 bytes: `s` * Previous 32 bytes: `r` * Remaining bytes: `authenticatorData || clientDataJSON` (requires parsing to split) #### Authenticator Data Structure (minimum 37 bytes) ``` Bytes 0-31: rpIdHash (32 bytes) Byte 32: flags (1 byte) - Bit 0 (0x01): User Presence (UP) - must be set Bytes 33-36: signCount (4 bytes) ``` #### Verification Steps ```python def verify_webauthn(tx_hash: bytes32, signature: bytes, require_uv: bool) -> bool: # 1. Parse signature pubKeyY = signature[-32:] pubKeyX = signature[-64:-32] s = signature[-96:-64] r = signature[-128:-96] webauthn_data = signature[:-128] # Parse authenticatorData and clientDataJSON # Minimum authenticatorData is 37 bytes # Simple approach: try to decode clientDataJSON from different split points authenticatorData, clientDataJSON = split_webauthn_data(webauthn_data) # 2. Validate authenticator data if len(authenticatorData) < 37: return False flags = authenticatorData[32] if not (flags & 0x01): # UP bit must be set return False # 3. Validate client data JSON if not contains(clientDataJSON, '"type":"webauthn.get"'): return False challenge_b64url = base64url_encode(tx_hash) challenge_property = '"challenge":"' + challenge_b64url + '"' if not contains(clientDataJSON, challenge_property): return False # 4. Compute message hash clientDataHash = sha256(clientDataJSON) messageHash = sha256(authenticatorData || clientDataHash) # 5. Verify P256 signature return p256_verify(messageHash, r, s, pubKeyX, pubKeyY) ``` #### What We Verify * Authenticator data minimum length (37 bytes) * User Presence (UP) flag is set * `"type":"webauthn.get"` in clientDataJSON * Challenge matches tx\_hash (Base64URL encoded) * P256 signature validity #### What We Skip * Origin verification (not applicable to blockchain) * RP ID hash validation (no central RP in decentralized context) * Signature counter (anti-cloning left to application layer) * Backup flags (account policy decision) #### Parsing authenticatorData and clientDataJSON Since authenticatorData has variable length, finding the split point requires: 1. Check if AT flag (bit 6) is set at byte 32 2. If not set, authenticatorData is exactly 37 bytes 3. If set, need to parse CBOR credential data (complex, see implementation) 4. Everything after authenticatorData is clientDataJSON (valid UTF-8 JSON) **Simplified approach:** For TempoTransactions, wallets should send minimal authenticatorData (37 bytes, no AT/ED flags) to minimize gas costs and simplify parsing. ### Access Keys A sender can choose to authorize an Access Key to sign transactions on the sender's behalf. This is useful to enable flows where a root key (e.g. a passkey) would provision a short-lived (scoped) Access Key to be able to sign transactions on the sender's behalf without inducing another passkey prompt. More information about Access Keys can be found in the [Account Keychain Specification](./AccountKeychain). A sender can authorize a key by signing over a "key authorization" item that contains the following information: * **Chain ID** for replay protection (0 = valid on any chain) * **Key type** (Secp256k1, P256, or WebAuthn) * **Key ID** (address derived from the public key) * **Expiration** timestamp of when the key should expire (optional - None means never expires) * TIP20 token **spending limits** for the key (optional - None means unlimited spending): * Each limit carries a `period` (0 = one-time, non-zero = recurring in seconds). Recurring limits roll over to `max` when `current_timestamp >= periodEnd`. * Root keys and active admin access keys can update limits via `updateSpendingLimit()` without revoking the key. Updates reset `remaining` and `max` to `newLimit` while preserving `period` and `periodEnd`. * Note: Spending limits only apply to TIP20 token transfers, not ETH or other asset transfers * **Call scopes** for the key (optional - None means unrestricted): * Each entry pins a `target` contract and a list of allowed selector rules. An empty selector list on a target means any selector is allowed on that target. * Selector rules can additionally constrain TIP-20 recipient-bearing selectors to a recipient allowlist. * `Some([])` (an empty top-level allowlist) means scoped deny-all. Access-key-signed transactions cannot perform contract creation. Calls within the batch that would `CREATE` or `CREATE2` (including via factory contracts) are rejected. Use the Root Key for deployment flows. #### RLP Encoding **Unsigned Format:** The root key or an active admin access key signs over the keccak256 hash of the RLP encoded `KeyAuthorization`: ``` key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?])) chain_id = u64 (0 = valid on any chain) key_type = 0 (Secp256k1) | 1 (P256) | 2 (WebAuthn) key_id = Address (derived from the public key) expiry = Option (unix timestamp, None = never expires; omitted expiry is translated to u64::MAX when the protocol calls the precompile) limits = Option> (None = unlimited spending; period = 0 means one-time) allowed_calls = Option> (None = unrestricted; Some([]) = scoped deny-all) witness = Option (app-defined witness digest) is_admin = Option (true provisions an admin access key) account = Option
(required when an admin access key signs the authorization) ``` **Signed Format:** The signed format (`SignedKeyAuthorization`) includes all fields with the `signature` appended: ``` signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?, signature]) ``` The `signature` is a `PrimitiveSignature` (secp256k1, P256, or WebAuthn) signed by the root key or an active admin access key. Note: `expiry`, `limits`, `allowed_calls`, `witness`, `is_admin`, and `account` use RLP trailing field semantics — they can be omitted entirely when None. When `is_admin` is `true`, `expiry`, `limits`, and `allowed_calls` must be omitted or empty because admin keys are only for key management. When an admin access key signs the authorization, `account` must be present and must equal the account being modified. :::warning[Expiry encoding] For `key_authorization`, the canonical non-expiring encoding omits `expiry` (`None`). There is one decoder nuance: because `KeyAuthorization` uses canonical trailing optional fields, an explicit empty `expiry` placeholder (`0x80`) is also interpreted as `None` when another trailing optional field follows it. But a final `expiry` encoded as zero/empty is rejected, and a literal `0x00` is invalid RLP for this field. Do not hand-encode `expiry = 0` or rely on `Some(0)` as a sentinel. The supported encoding to target is omission, and the protocol translates omitted expiry to `u64::MAX` when materializing the `AccountKeychain.authorizeKey(...)` call. ::: Intrinsic gas for `key_authorization` accounts for the storage written for periodic-limit state and call-scope entries. See the [specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1011.md#intrinsic-gas-for-key-authorization) for slot-counting rules. #### Keychain Precompile The Account Keychain precompile (deployed at address `0xAAAAAAAA00000000000000000000000000000000`) manages authorized access keys for accounts. It enables root keys and admin access keys to provision scoped access keys with expiry timestamps and per-TIP20 token spending limits, or admin access keys for key management. **See the [Account Keychain Specification](./AccountKeychain) for complete interface details, storage layout, and implementation.** #### Protocol Behavior The protocol enforces Access Key authorization and spending limits natively. ##### Transaction Validation When a TempoTransaction is received, the protocol: 1. **Identifies the signing key** from the transaction signature * If signature is a `Keychain` variant: extracts the `keyId` (address) of the Access Key * Otherwise: treats it as the Root Key (keyId = address(0)) 2. **Validates KeyAuthorization** (if present in transaction) * The `key_authorization` field in `TempoTransaction` provisions a NEW Access Key * The Root Key or an active admin access key MUST sign: * The `key_authorization` digest: `keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]))` * If an admin access key signs the authorization, the `account` field must be present and must equal the account being modified * Access Key (being authorized) CAN sign the same tx which it is authorized in. * This enables "authorize and use" in a single transaction 3. **Sets transaction context** * Stores `transactionKey[account] = keyId` in protocol state * Used to enforce authorization hierarchy during execution, can also be used by DApps to see which key authorized the current tx. 4. **Validates Key Authorization** (for Access Keys) * Queries precompile: `getKey(account, keyId)` returns `KeyInfo` * Checks key is active (not revoked) * Checks expiry: `current_timestamp < expiry`; non-expiring keys are stored with `expiry = u64::MAX` * Rejects transaction if validation fails ##### Authorization Hierarchy Enforcement The protocol enforces a strict three-role hierarchy: **Root Key** (keyId = address(0)): * The account's primary key (address matches account address) * Can call ALL precompile functions * No spending limits * Can authorize, revoke, and update access keys **Admin Access Keys** (keyId != address(0), `is_admin = true`): * Secondary keys authorized by the Root Key or another active admin access key * Can call key-management mutators (`authorizeKey`, `authorizeAdminKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) * Cannot carry spending limits, call scopes, or expiry * Cannot create contracts (`CREATE` and `CREATE2` are rejected anywhere in the call batch) **Limited Access Keys** (keyId != address(0), `is_admin = false`): * Secondary keys authorized by the Root Key or an active admin access key * CANNOT call mutable precompile functions (`authorizeKey`, `authorizeAdminKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) * Subject to per-TIP20 token spending limits and call-scope checks during execution * Cannot create contracts (`CREATE` and `CREATE2` are rejected anywhere in the call batch) * Can have expiry timestamps When a limited Access Key attempts to call any mutable keychain function: 1. Transaction executes normally until the precompile call 2. Precompile checks `getTransactionKey()` and confirms the key is not the Root Key or an active admin access key 3. Call reverts with `UnauthorizedCaller` error 4. Entire transaction is reverted ##### Spending Limit Enforcement The protocol tracks and enforces spending limits for TIP20 token transfers: **Scope:** Only TIP20 `transfer()`, `transferWithMemo()`, `approve()`, and `startReward()` calls are tracked * Spending limits only apply when `msg.sender == tx.origin` (direct EOA calls) * When a contract makes transfers on behalf of the user, spending limits do NOT apply (e.g., `transferFrom()`) * Native value transfers are NOT limited * NFT transfers are NOT limited * Other asset types are NOT limited **Tracking:** During transaction execution, when an Access Key's transaction directly calls TIP20 methods: 1. Protocol intercepts `transfer(to, amount)`, `transferWithMemo()`, `approve(spender, amount)`, and `startReward()` calls 2. For `transfer`/`transferWithMemo`, the full `amount` is checked against the remaining limit 3. For `approve`, only **increases** in approval (new approval minus previous allowance) are checked and counted against the limit 4. Queries: `getRemainingLimitWithPeriod(account, keyId, token)`, which returns `(remaining, periodEnd)` and reflects any periodic rollover 5. Checks: relevant amount (transfer amount or approval increase) `<= remaining` 6. If check fails: reverts with `SpendingLimitExceeded` 7. If check passes: decrements the limit by the relevant amount 8. Updates are stored in precompile state **Root Key Behavior:** Spending limit checks are skipped entirely (no limits apply) **Recurring Limits:** When a `TokenLimit.period` is non-zero, the limit recurs. `remaining` rolls over to `max` once `current_timestamp >= periodEnd`, and `periodEnd` advances by `period`. Callers observe rollover state via `getRemainingLimitWithPeriod`. **Limit Updates:** * Limits deplete as tokens are spent * Root Key or an active admin access key can call `updateSpendingLimit(keyId, token, newLimit)` to set new limits * Setting a new limit REPLACES both `remaining` and `max` with `newLimit`. The configured `period` and current `periodEnd` are preserved. ##### Call Scope Enforcement When an Access Key has stored call scopes (`allowed_calls` was set at authorization, or `setAllowedCalls(...)` was called later), the protocol enforces them on top-level calls signed by that Access Key: 1. For each call in the batch, look up the matching `(target, selector)` allowlist entry 2. If the target is not in the allowlist, or the selector is not allowed on that target, revert with `CallNotAllowed` 3. For recipient-bound TIP-20 selectors (e.g., `transfer`, `transferFrom`, `transferWithMemo`), additionally enforce that the call's recipient is in the rule's recipient allowlist (when non-empty) 4. Access keys with `allowed_calls = None` are unrestricted; `Some([])` is scoped deny-all ##### Contract Creation Restriction Access-key-signed transactions cannot perform contract creation. Any `CREATE` or `CREATE2` (including via factory contracts or internal calls) reverts the transaction. Use the Root Key for deployment flows. ##### Creating and Using KeyAuthorization **First-Time Authorization Flow:** 1. **Generate Access Key** ```typescript // Generate a new P256 or secp256k1 key pair const accessKey = generateKeyPair("p256"); // or "secp256k1" const keyId = deriveAddress(accessKey.publicKey); ``` 2. **Create Authorization Message** ```typescript // Define key parameters const keyAuth = { chain_id: 1, key_type: SignatureType.P256, // 1 key_id: keyId, // address derived from public key expiry: timestamp + 86400, // 24 hours from now; omit this field for a non-expiring key authorization limits: [ // One-time limit (period = 0) { token: USDG_ADDRESS, limit: 1000000000, period: 0 }, // 1000 USDG (6 decimals), one-time // Recurring weekly limit (period = 604800 seconds) { token: DAI_ADDRESS, limit: 500000000000000000000n, period: 604800 } // 500 DAI / week ], // Optional call scopes — omit for an unrestricted key allowed_calls: [ { target: USDG_ADDRESS, selector_rules: [ // transfer(address,uint256) restricted to a single recipient { selector: "0xa9059cbb", recipients: [TRUSTED_RECIPIENT] } ] } ] }; // Compute digest: keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?])) const authDigest = computeAuthorizationDigest(keyAuth); ``` 3. **Root Key Signs Authorization** ```typescript // Sign with Root Key (e.g., passkey prompt) const rootSignature = await signWithRootKey(authDigest); ``` 4. **Build TempoTransaction** ```typescript const tx = { chain_id: 1, nonce: await getNonce(account), nonce_key: 0, calls: [{ to: recipient, value: 0, input: "0x" }], gas_limit: 200000, max_fee_per_gas: 1000000000, max_priority_fee_per_gas: 1000000000, key_authorization: { authorization: keyAuth, signature: rootSignature // Root Key's signature on authDigest }, // ... other fields }; ``` 5. **Access Key Signs Transaction** ```typescript // Sign transaction with the NEW Access Key being authorized const txHash = computeTxSignatureHash(tx); const accessSignature = await signWithAccessKey(txHash, accessKey); // Wrap in Keychain signature const finalSignature = { Keychain: { user_address: account, signature: { P256: accessSignature } // or Secp256k1 } }; ``` 6. **Submit Transaction** * Protocol validates Root Key signed the `key_authorization` * Protocol calls `authorizeKey()` on the precompile to store the key * Protocol validates Access Key signature on transaction * Transaction executes with spending limits enforced **Subsequent Usage (Key Already Authorized):** ```typescript // Access Key is already authorized, just sign transactions directly const tx = { chain_id: 1, nonce: await getNonce(account), calls: [{ to: recipient, value: 0, input: calldata }], key_authorization: null, // No authorization needed // ... other fields }; const txHash = computeTxSignatureHash(tx); const accessSignature = await signWithAccessKey(txHash, accessKey); const finalSignature = { Keychain: { user_address: account, signature: { P256: accessSignature } } }; // Submit - protocol validates key is authorized and not expired ``` ##### Key Management Operations **Revoking an Access Key:** ```typescript // Must be signed by the Root Key or an active admin key const tx = { chain_id: 1, nonce: await getNonce(account), calls: [{ to: ACCOUNT_KEYCHAIN_ADDRESS, value: 0, input: encodeCall("revokeKey", [keyId]) }], // ... sign with the Root Key or an active admin key }; ``` **Updating Spending Limits:** ```typescript // Must be signed by the Root Key or an active admin key const tx = { chain_id: 1, nonce: await getNonce(account), calls: [{ to: ACCOUNT_KEYCHAIN_ADDRESS, value: 0, input: encodeCall("updateSpendingLimit", [ keyId, USDG_ADDRESS, 2000000000 // New limit: 2000 USDG ]) }], // ... sign with the Root Key or an active admin key }; ``` **Note:** After updating, both `remaining` and `max` are set to `newLimit`. The configured `period` and current `periodEnd` are preserved. ##### Querying Key State Applications can query key information and spending limits: ```typescript // Check if key is authorized and get info const keyInfo = await precompile.getKey(account, keyId); // Returns: { signatureType, keyId, expiry, enforceLimits, isRevoked } // Check remaining spending limit and current period end for a token const { remaining, periodEnd } = await precompile.getRemainingLimitWithPeriod( account, keyId, USDG_ADDRESS ); // Returns: (uint256 remaining, uint64 periodEnd). Reflects periodic rollover. // Inspect call scopes const { isScoped, scopes } = await precompile.getAllowedCalls(account, keyId); // isScoped = false → key is unrestricted // isScoped = true, scopes = [...] → key is scoped to those (target, selector, recipient) entries // isScoped = true, scopes = [] → scoped deny-all (also returned for missing/revoked/expired keys) // Get which key signed current transaction (callable from contracts) const currentKey = await precompile.getTransactionKey(); // Returns: address (0x0 for Root Key, keyId for Access Key) ``` ## Rationale ### Signature Type Detection by Length Using signature length for type detection avoids adding explicit type fields while maintaining deterministic parsing. The chosen lengths (65, 129, variable) are naturally distinct. ### Linear Gas Scaling for Nonce Keys The progressive pricing model prevents state bloat while keeping initial keys affordable. The 20,000 gas increment approximates the long-term state cost of maintaining each additional nonce mapping. ### No Nonce Expiry Avoiding expiry simplifies the protocol and prevents edge cases where in-flight transactions become invalid. Wallets handle nonce key allocation to prevent conflicts. ### Backwards Compatibility This spec introduces a new transaction type and does not modify existing transaction processing. Legacy transactions continue to work unchanged. We special case `nonce key = 0` (also referred to as the protocol nonce key) to maintain compatibility with existing nonce behavior. ## Gas Costs ### Signature Verification Gas Schedule Different signature types incur different base transaction costs to reflect their computational complexity: | Signature Type | Base Gas Cost | Calculation | Rationale | |----------------|---------------|-------------|-----------| | **secp256k1** | 21,000 | Standard | Includes 3,000 gas for ecrecover precompile | | **P256** | 26,000 | 21,000 + 5,000 | Base 21k + additional 5k for P256 verification | | **WebAuthn** | 26,000 + variable data cost | 26,000 + (calldata gas for clientDataJSON) | Base P256 cost plus variable cost for clientDataJSON based on size | | **Keychain** | Inner signature + 3,000 | primitive\_sig\_cost + 3,000 | Inner signature cost + key validation overhead (2,100 SLOAD + 900 buffer) | **Rationale:** * The base 21,000 gas for standard transactions already includes the cost of secp256k1 signature verification via ecrecover (3,000 gas) * [EIP 7951](https://eips.ethereum.org/EIPS/eip-7951) sets P256 verification cost at 6,900 gas. We add 1,100 gas to account for the additional 65 bytes of signature size (129 bytes total vs 64 bytes for secp256k1), giving 8,000 gas total. Since the base 21k already includes 3,000 gas for ecrecover (which P256 doesn't use), the net additional cost is 8,000 - 3,000 = **5,000 gas**. * WebAuthn signatures require additional computation to parse and validate the clientDataJSON structure. We cap the total signature size at 2kb. The signature is also charged using the same gas schedule as calldata (16 gas per non-zero byte, 4 gas per zero byte) to prevent the use of this signature space from spam. * Keychain signatures wrap a primitive signature and are used by access keys. They add 3,000 gas to cover key validation during transaction validation (cold SLOAD to verify key exists + processing overhead). * Individual per-signature-type gas costs allow us to add more advanced verification methods in the future like multisigs, which could have dynamic gas pricing. ### Nonce Key Gas Schedule Transactions using parallelizable nonces incur additional costs based on the nonce key usage pattern: #### Case 1: Protocol Nonce (Key 0) * **Additional Cost:** 0 gas * **Total:** 21,000 gas (base transaction cost) * **Rationale:** Maintains backward compatibility with existing transaction flow #### Case 2: Existing User Nonce Key (nonce > 0) * **Additional Cost:** 5,000 gas * **Total:** 26,000 gas * **Rationale:** Cold SLOAD (2,100) + warm SSTORE reset (2,900) for incrementing an existing nonce #### Case 3: New User Nonce Key (nonce == 0) * **Additional Cost:** 22,100 gas * **Total:** 43,100 gas * **Rationale:** Cold SLOAD (2,100) + SSTORE set (20,000) for writing to a new storage slot **Rationale for Fixed Pricing:** 1. **Simplicity:** Fixed costs based on actual EVM storage operations are straightforward to reason about 2. **Storage Pattern Alignment:** Costs directly mirror EVM cold SSTORE costs for new vs existing slots 3. **State Growth:** Creating new nonce keys incurs the higher cost naturally through SSTORE set pricing ### Key Authorization Gas Schedule When a transaction includes a `key_authorization` field to provision a new access key, additional intrinsic gas is charged to cover signature verification and storage operations. This gas is charged **before execution** as part of the transaction's intrinsic gas cost. #### Gas Components | Component | Gas Cost | Notes | |-----------|----------|-------| | **Signature verification** | 3,000 (secp256k1) / 8,000 (P256) / 8,000 + calldata (WebAuthn) | Verifying the root/admin key's signature on the authorization | | **Key storage** | 22,000 | Cold SSTORE to store new key (0→non-zero) | | **Overhead buffer** | 5,000 | Buffer for event emission, storage reads, and other overhead | | **Per spending limit** | 22,000 each | Cold SSTORE per token limit (0→non-zero) | **Signature verification rationale:** KeyAuthorization requires an *additional* signature verification beyond the transaction signature. Unlike the transaction signature (where ecrecover cost is included in the base 21k), KeyAuthorization must pay the full verification cost: * **secp256k1**: 3,000 gas (ecrecover precompile cost) * **P256**: 8,000 gas (6,900 from EIP-7951 + 1,100 for signature size). Note: the transaction signature schedule charges only 5,000 additional gas for P256 because it subtracts the 3,000 ecrecover "savings" already in base 21k. KeyAuthorization pays the full 8,000. * **WebAuthn**: 8,000 + calldata gas for webauthn\_data #### Gas Formula ``` KEY_AUTH_BASE_GAS = 30,000 # For secp256k1 signature (3,000 + 22,000 + 5,000) KEY_AUTH_BASE_GAS = 35,000 # For P256 signature (5,000 + 3,000 + 22,000 + 5,000) KEY_AUTH_BASE_GAS = 35,000 + webauthn_calldata_gas # For WebAuthn signature PER_LIMIT_GAS = 22,000 # Per spending limit entry total_key_auth_gas = KEY_AUTH_BASE_GAS + (num_limits * PER_LIMIT_GAS) ``` #### Examples | Configuration | Gas Cost | Calculation | |--------------|----------|-------------| | secp256k1, no limits | 30,000 | Base only | | secp256k1, 1 limit | 52,000 | 30,000 + 22,000 | | secp256k1, 3 limits | 96,000 | 30,000 + (3 × 22,000) | | P256, no limits | 35,000 | Base with P256 verification | | P256, 2 limits | 79,000 | 35,000 + (2 × 22,000) | #### Rationale 1. **Pre-execution charging**: KeyAuthorization is validated and executed during transaction validation (before the EVM runs), so its gas must be included in intrinsic gas 2. **Storage cost alignment**: The 22,000 gas per storage slot approximates EVM cold SSTORE costs for new slots 3. **DoS prevention**: Progressive cost based on number of limits prevents abuse through excessive limit creation ### Reference Pseudocode ```python def calculate_calldata_gas(data: bytes) -> uint256: """ Calculate gas cost for calldata based on zero and non-zero bytes Args: data: bytes to calculate cost for Returns: gas_cost: uint256 """ CALLDATA_ZERO_BYTE_GAS = 4 CALLDATA_NONZERO_BYTE_GAS = 16 gas = 0 for byte in data: if byte == 0: gas += CALLDATA_ZERO_BYTE_GAS else: gas += CALLDATA_NONZERO_BYTE_GAS return gas def calculate_signature_verification_gas(signature: PrimitiveSignature) -> uint256: """ Calculate gas cost for verifying a primitive signature. Returns the ADDITIONAL gas beyond the base 21k transaction cost. - secp256k1: 0 (already included in base 21k via ecrecover) - P256: 5,000 (8,000 full cost - 3,000 ecrecover already in base 21k) - WebAuthn: 5,000 + calldata gas for webauthn_data """ # P256 full verification cost is 8,000 (6,900 from EIP-7951 + 1,100 for signature size) # But base 21k already includes 3,000 for ecrecover, so additional cost is 5,000 P256_ADDITIONAL_GAS = 5_000 if signature.type == Secp256k1: return 0 # Already included in base 21k elif signature.type == P256: return P256_ADDITIONAL_GAS elif signature.type == WebAuthn: webauthn_data_gas = calculate_calldata_gas(signature.webauthn_data) return P256_ADDITIONAL_GAS + webauthn_data_gas else: revert("Invalid signature type") def calculate_key_authorization_gas(key_auth: SignedKeyAuthorization) -> uint256: """ Calculate the intrinsic gas cost for a KeyAuthorization. This is charged BEFORE execution as part of transaction validation. Args: key_auth: SignedKeyAuthorization with fields: - signature: PrimitiveSignature (root/admin key's signature) - limits: Optional[List[TokenLimit]] # each carries a `period` - allowed_calls: Optional[List[CallScope]] # call-scope allowlist Returns: gas_cost: uint256 Note: This is a simplified illustration. See the enhanced access key permissions specification for the canonical slot-counting rules covering periodic-limit state and call-scope storage. """ # Constants - KeyAuthorization pays FULL signature verification costs # (not the "additional" costs used for transaction signatures) ECRECOVER_GAS = 3_000 # Full ecrecover cost P256_FULL_GAS = 8_000 # Full P256 cost (6,900 + 1,100) COLD_SSTORE_SET_GAS = 22_000 # Storage cost for new slot OVERHEAD_BUFFER = 5_000 # Buffer for event emission, storage reads, etc. gas = 0 # Step 1: Signature verification cost (full cost, not additional) if key_auth.signature.type == Secp256k1: gas += ECRECOVER_GAS # 3,000 elif key_auth.signature.type == P256: gas += P256_FULL_GAS # 8,000 elif key_auth.signature.type == WebAuthn: webauthn_data_gas = calculate_calldata_gas(key_auth.signature.webauthn_data) gas += P256_FULL_GAS + webauthn_data_gas # 8,000 + calldata # Step 2: Key storage gas += COLD_SSTORE_SET_GAS # 22,000 - store new key (0 → non-zero) # Step 3: Overhead buffer gas += OVERHEAD_BUFFER # 5,000 # Step 4: Per-limit storage cost (each TokenLimit carries period state) num_limits = len(key_auth.limits) if key_auth.limits else 0 gas += num_limits * COLD_SSTORE_SET_GAS # 22,000 per limit # Step 5: Per-call-scope storage cost (target + selector + recipients). # See the enhanced access key permissions specification for exact slot accounting; this counts one slot per # (target, selector, recipient) triple as a conservative approximation. num_scope_slots = 0 if key_auth.allowed_calls: for scope in key_auth.allowed_calls: for rule in scope.selector_rules: # one slot for the (target, selector) entry, plus one per recipient num_scope_slots += 1 + max(len(rule.recipients), 0) gas += num_scope_slots * COLD_SSTORE_SET_GAS return gas def calculate_tempo_tx_base_gas(tx): """ Calculate the base gas cost for a TempoTransaction Args: tx: TempoTransaction object with fields: - signature: TempoSignature (variable length) - nonce_key: uint192 - nonce: uint64 - sender_address: address - key_authorization: Optional[SignedKeyAuthorization] Returns: total_gas: uint256 """ # Constants BASE_TX_GAS = 21_000 EXISTING_NONCE_KEY_GAS = 5_000 # Cold SLOAD (2,100) + warm SSTORE reset (2,900) NEW_NONCE_KEY_GAS = 22_100 # Cold SLOAD (2,100) + SSTORE set (20,000) KEYCHAIN_VALIDATION_GAS = 3_000 # 2,100 SLOAD + 900 processing buffer # Step 1: Determine signature verification cost # For Keychain signatures, use the inner primitive signature if tx.signature.type == Keychain: inner_sig = tx.signature.inner_signature else: inner_sig = tx.signature signature_gas = BASE_TX_GAS + calculate_signature_verification_gas(inner_sig) # Add keychain validation overhead if using access key if tx.signature.type == Keychain: signature_gas += KEYCHAIN_VALIDATION_GAS # Step 2: Calculate nonce key cost if tx.nonce_key == 0: # Protocol nonce (backward compatible) nonce_gas = 0 else: # User nonce key current_nonce = get_nonce(tx.sender_address, tx.nonce_key) if current_nonce > 0: # Existing nonce key - cold SLOAD + warm SSTORE reset nonce_gas = EXISTING_NONCE_KEY_GAS else: # New nonce key - cold SLOAD + SSTORE set nonce_gas = NEW_NONCE_KEY_GAS # Step 3: Calculate key authorization cost (if present) if tx.key_authorization is not None: key_auth_gas = calculate_key_authorization_gas(tx.key_authorization) else: key_auth_gas = 0 # Step 4: Calculate total base gas total_gas = signature_gas + nonce_gas + key_auth_gas return total_gas ``` ## Security Considerations ### Mempool DOS Protection Transaction pools perform pre-execution validation checks before accepting transactions. These checks are performed for free by the nodes, making them potential DOS vectors. The three primary validation checks are: 1. **Signature verification** - Must be valid 2. **Nonce verification** - Must match current account nonce 3. **Balance check** - Account must have sufficient balance to pay for transaction This transaction type impacts all three areas: #### Signature Verification Impact * **P256 signatures**: Fixed computational cost similar to ecrecover. * **WebAuthn signatures**: Variable cost due to clientDataJSON parsing, but **capped at 2KB total signature size** to prevent abuse * **Mitigation**: All signature types have bounded computational costs that are in the same ballpark as standard ecrecover. #### Nonce Verification Impact * **2D nonce lookup**: Requires additional storage read from nonce precompile * **Cost**: Equivalent to a cold SLOAD (~2,100 gas worth of free computation) * **Mitigation**: Cost is bounded to a manageable value. #### Fee Payer Impact * **Additional account read**: When fee payer is specified, must fetch fee payer's account to verify balance * **Cost**: Effectively doubles the free account access work for sponsored transactions * **Mitigation**: Cost is still bounded to a single additional account read. #### Comparison to Ethereum The introduction of 7702 delegated accounts already created complex cross-transaction dependencies in the mempool, which prevents any static pool checks from being useful. Because a single transaction can invalidate multiple others by spending balances of multiple accounts **Assessment:** While this transaction type introduces additional pre-execution validation costs, all costs are bounded to reasonable limits. The mempool complexity issues around cross-transaction dependencies already exist in Ethereum due to 7702 and accounts with code, making static validation inherently difficult. So the incremental cost from this transaction type is acceptable given these existing constraints. # Comparing Tempo Transactions and EIP-4337 EIP-4337 introduced account abstraction to Ethereum through a system of bundlers, paymasters, and an EntryPoint contract. Tempo Transactions achieve the same goals through protocol-native features that require no additional infrastructure. ## EIP-4337 Design Goals EIP-4337 enables several key capabilities for Ethereum accounts: * **Fee sponsorship**: Third parties can pay gas fees on behalf of users * **Batched operations**: Multiple calls can execute atomically in one transaction * **Alternative signatures**: Accounts can use signature schemes beyond secp256k1 * **Custom validation**: Accounts can define arbitrary validation logic ## How Tempo Achieves These Goals Tempo Transactions provide these capabilities at the protocol level. ### Fee Sponsorship EIP-4337 requires deploying a Paymaster contract, funding it with ETH, and running or paying for bundler infrastructure. The Paymaster validates sponsorship requests and the bundler aggregates UserOperations. Tempo Transactions include a `fee_payer_signature` field directly in the transaction. A sponsor signs the transaction to agree to pay fees. The protocol validates both signatures and deducts fees from the sponsor. No contracts or infrastructure are required. See the [fee sponsorship guide](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) for implementation details. ### Batched Operations EIP-4337 bundles multiple UserOperations through an external bundler service. Each UserOperation can contain one call. Tempo Transactions include a native `calls` array. Multiple contract calls execute atomically in a single transaction. The protocol handles execution directly without external services. ### Alternative Signatures EIP-4337 requires deploying a custom validation contract for each signature scheme. The contract must implement signature verification logic. Tempo Transactions natively support secp256k1, P256, and WebAuthn signatures. The protocol verifies these signatures directly, so passkey authentication works without custom contracts. ### Gas Token Flexibility EIP-4337 Paymasters can accept alternative tokens but must convert them to ETH internally. Tempo Transactions pay fees directly in any USD stablecoin that has liquidity on the Fee AMM. No conversion infrastructure is needed. See the [stablecoin fees guide](https://tempo.xyz/developers/docs/guide/payments/pay-fees-in-any-stablecoin) for implementation details. ## Integration Comparison | Aspect | EIP-4337 | Tempo Transactions | |--------|----------|-------------------| | Contracts to deploy | EntryPoint, Paymaster, Account | None required | | Infrastructure to run | Bundler service | None required | | Fee payment | ETH via Paymaster | Any USD stablecoin | | Signature verification | Custom validation contract | Protocol-native | ## When to Use EIP-4337 on Tempo Tempo has the ERC-4337 EntryPoint contract deployed for projects that require compatibility with existing 4337 tooling. Consider using native Tempo Transactions for lower gas costs and simpler integration. ## Start with Tempo Transactions To start using Tempo Transactions, see the [Tempo Transactions guide](https://tempo.xyz/developers/docs/guide/tempo-transaction) for SDK integration in TypeScript, Rust, Go, and Python. For the full technical specification, see the [Tempo Transaction Specification](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction). # Comparing Tempo Transactions and EIP-7702 EIP-7702 allows EOAs to delegate to smart contract code temporarily within a transaction. Tempo Transactions build on this foundation and extend it with additional capabilities. ## EIP-7702 Design Goals EIP-7702 enables EOAs to: * **Delegate to contract code**: An EOA can execute as if it were a smart contract * **Batch transactions**: Through delegation to a batching contract * **Maintain EOA properties**: The account remains an EOA after the transaction ## How Tempo Extends EIP-7702 Tempo Transactions support EIP-7702 style delegation through the `aa_authorization_list` field. This field follows EIP-7702 semantics for delegation and execution. ### Extended Signature Support EIP-7702 on Ethereum only supports secp256k1 signatures for the authorization. Tempo extends this to support all Tempo signature types. Authorizations can be signed with secp256k1, P256, or WebAuthn. This enables delegation from passkey-based accounts. ### Native Features Beyond Delegation EIP-7702 provides delegation as a building block. Applications must implement higher-level features through the delegated contract. Tempo Transactions include these features natively: * **Fee sponsorship**: Built into the transaction type, not requiring delegation. See the [fee sponsorship guide](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees). * **Scheduled execution**: Native `validAfter` and `validBefore` timestamp windows for time-bounded transactions. * **Parallelizable nonces**: Multiple nonce keys for concurrent transactions. See the [parallel transactions guide](https://tempo.xyz/developers/docs/guide/payments/send-parallel-transactions). * **Access keys**: Delegate signing to secondary keys with configurable permissions. See the [Account Keychain specification](https://tempo.xyz/developers/docs/protocol/transactions/AccountKeychain). ### Combining Delegation with Native Features Tempo Transactions can use EIP-7702 delegation alongside native features. An application can delegate an EOA to contract code while also using fee sponsorship and batched calls from the protocol. ## Feature Comparison | Feature | EIP-7702 | Tempo Transactions | |---------|----------|-------------------| | EOA delegation | Yes | Yes | | Signature schemes | secp256k1 only | secp256k1, P256, WebAuthn | | Fee sponsorship | Via delegated contract | Native | | Batching | Via delegated contract | Native | | Scheduled execution | Not available | Native | | Concurrent nonces | Not available | Native | ## Implementation The `aa_authorization_list` field in Tempo Transactions contains authorizations that follow EIP-7702 structure. Each authorization delegates an account to a specified implementation contract and is signed by the account authority. For full details on the authorization format, see the [Tempo Transaction Specification](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction). ## Start with Tempo Transactions To start using Tempo Transactions, see the [Tempo Transactions guide](https://tempo.xyz/developers/docs/guide/tempo-transaction) for SDK integration in TypeScript, Rust, Go, and Python. # Account Keychain Precompile **Address:** `0xAAAAAAAA00000000000000000000000000000000` ## Account Keychain overview The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) and admin access keys to provision secondary keys. Limited access keys can have expiry timestamps, recurring or one-time per-TIP20 token spending limits, and explicit call scopes that restrict which targets, selectors, and recipients an Access Key may invoke. `authorizeKey(...)` takes a `KeyRestrictions` tuple that bundles expiry, spending limits, and call scopes. `authorizeAdminKey(...)` provisions an admin key for account key management. Access-key-signed transactions cannot create contracts; use a Root Key for deployment flows. See [gas parameters](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#gas-parameters) for calldata requirements and gas charges. ## Motivation The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a scoped Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting. T6 extends this model with admin access keys for key management. Recurring spending budgets and explicit call scoping make limited keys suitable for subscriptions, connected apps, and session-key-style flows. ## Concepts ### Access Keys Access Keys are secondary signing keys authorized by an account's Root Key or an admin key. Limited access keys can sign transactions on behalf of the account with the following restrictions: * **Expiry**: Unix timestamp when the key becomes invalid. A non-expiring `key_authorization` omits `expiry` at the transaction RLP layer; the protocol translates that omission to `u64::MAX` before calling the precompile. Direct precompile callers should pass `u64::MAX` for a non-expiring key. Literal `0` is treated as past expiry and rejected. * **Spending Limits**: Per-TIP20 token limits that deplete as tokens are spent * Limits are one-time (`period = 0`) or recurring (`period > 0`, in seconds). Recurring limits roll over to `max` when `current_timestamp >= periodEnd`. * Limits can be updated by the Root Key or an active admin access key via `updateSpendingLimit()`. An update resets `remaining` and `max` to `newLimit` but preserves the configured `period` and current `periodEnd`. * Spending limits only apply to TIP20 `transfer()`, `transferWithMemo()`, and `approve()` calls * Spending limits only apply when `msg.sender == tx.origin` (direct EOA calls, not contract calls) * Native value transfers and `transferFrom()` are NOT limited * **Call Scopes**: An Access Key is either unrestricted (`allowAnyCalls = true`) or restricted to an explicit allowlist of `(target, selector, recipients)` tuples. An empty allowlist with `allowAnyCalls = false` means scoped deny-all. * **No Contract Creation**: Access-key-signed transactions cannot perform `CREATE` or `CREATE2`, including via factory contracts. Use a Root Key for deployments. * **Privilege Restrictions**: Limited access keys cannot authorize new keys or modify their own limits or scopes. Admin access keys can manage keys, but cannot carry spending limits, call scopes, or expiry. ### Authorization Hierarchy The protocol enforces a strict hierarchy at validation time: 1. **Root Key**: The account's main key (derived from the account address) * Can call all precompile functions, including the key-management mutators (`authorizeKey`, `authorizeAdminKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) * Has no spending limits or call-scope restrictions 2. **Admin Access Keys**: Secondary authorized keys for account administration * Can call key-management mutators, including `authorizeKey`, `authorizeAdminKey`, and `revokeKey` * Cannot carry spending limits, call scopes, or expiry * Cannot create contracts 3. **Limited Access Keys**: Secondary authorized keys for scoped transactions * Cannot call mutable precompile functions (only view functions are allowed) * Subject to per-TIP20 token spending limits * Subject to call-scope checks during execution * Cannot create contracts * Can have expiry timestamps ## Storage The precompile uses a `keyId` (address) to uniquely identify each access key for an account. **Storage Mappings:** * `keys[account][keyId]` → Packed `AuthorizedKey` struct (signature type, expiry, enforce\_limits, is\_revoked, is\_admin) * `spendingLimits[keccak256(account || keyId)][token]` → `SpendingLimitState { remaining, max, period, periodEnd }` * `keyScopes[keccak256(account || keyId)]` → Tree of `(target, selector, recipients)` allowlists used during call-scope checks * `transactionKey` → Transient storage for the key ID that signed the current transaction (slot 0) **AuthorizedKey Storage Layout (packed into single slot):** * byte 0: signature\_type (u8) * bytes 1-8: expiry (u64, little-endian) * byte 9: enforce\_limits (bool) * byte 10: is\_revoked (bool) * byte 11: is\_admin (bool) ## Interface ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; interface IAccountKeychain { enum SignatureType { Secp256k1, P256, WebAuthn } struct TokenLimit { address token; uint256 amount; uint64 period; } struct SelectorRule { bytes4 selector; address[] recipients; } struct CallScope { address target; SelectorRule[] selectorRules; } struct KeyRestrictions { uint64 expiry; bool enforceLimits; TokenLimit[] limits; bool allowAnyCalls; CallScope[] allowedCalls; } struct KeyInfo { SignatureType signatureType; address keyId; uint64 expiry; bool enforceLimits; bool isRevoked; } event KeyAuthorized(address indexed account, address indexed publicKey, uint8 signatureType, uint64 expiry); event AdminKeyAuthorized(address indexed account, address indexed publicKey); event KeyRevoked(address indexed account, address indexed publicKey); event SpendingLimitUpdated(address indexed account, address indexed publicKey, address indexed token, uint256 newLimit); event AccessKeySpend(address indexed account, address indexed publicKey, address indexed token, uint256 amount, uint256 remainingLimit); error UnauthorizedCaller(); error KeyAlreadyExists(); error KeyNotFound(); error KeyExpired(); error SpendingLimitExceeded(); error InvalidSpendingLimit(); error InvalidSignatureType(); error ZeroPublicKey(); error ExpiryInPast(); error KeyAlreadyRevoked(); error SignatureTypeMismatch(uint8 expected, uint8 actual); error CallNotAllowed(); error InvalidCallScope(); error InvalidKeyId(); error LegacyAuthorizeKeySelectorChanged(bytes4 newSelector); function authorizeKey( address keyId, SignatureType signatureType, KeyRestrictions calldata config ) external; function revokeKey(address keyId) external; function authorizeAdminKey( address keyId, SignatureType signatureType, bytes32 witness ) external; function updateSpendingLimit( address keyId, address token, uint256 newLimit ) external; function setAllowedCalls( address keyId, CallScope[] calldata scopes ) external; function removeAllowedCalls(address keyId, address target) external; function getKey(address account, address keyId) external view returns (KeyInfo memory); function getRemainingLimitWithPeriod( address account, address keyId, address token ) external view returns (uint256 remaining, uint64 periodEnd); function getAllowedCalls( address account, address keyId ) external view returns (bool isScoped, CallScope[] memory scopes); function isAdminKey(address account, address keyId) external view returns (bool); function getTransactionKey() external view returns (address); } ``` ## Behavior ### Key Authorization * `key_authorization` includes an optional trailing `witness: bytes32` field. When present, the witness is included in the signing hash, checked against the account's burned-witness set, and emitted when the key authorization is registered. The protocol otherwise treats it as opaque and application-defined, so apps can bind a single access-key authorization signature to an offchain challenge. :::info[T6 behavior — SDK encoders/decoders] The [T6 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t6) added [admin access keys](https://tempo.xyz/developers/docs/protocol/upgrades/t6#admin-access-keys). For partners maintaining transaction tooling, the wire-level change is two new trailing optional fields on the `KeyAuthorization` RLP payload: ```text rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]) ``` SDK encoders/decoders and hardware-wallet firmware that strictly check field count MUST handle the new optionals on T6 networks. The packed `AuthorizedKey` slot also gains an `is_admin` byte at offset 11. The existing `KeyAuthorized` event is **unchanged**; an additional `AdminKeyAuthorized` event is emitted alongside it when `is_admin == true`, so existing indexers continue to work without changes. Provisioning admin keys uses `authorizeAdminKey(keyId, signatureType, witness)`. See the [admin access keys section of the T6 page](https://tempo.xyz/developers/docs/protocol/upgrades/t6#admin-access-keys) for the full surface. ::: * Creates a new key entry with the specified `signatureType`, `config.expiry`, `config.enforceLimits`, and `isRevoked` set to `false` * If `enforceLimits` is `true`, initializes spending limits for each specified token. Each `TokenLimit` carries a `period` (0 = one-time, non-zero = recurring in seconds). * If `allowAnyCalls` is `false`, stores the `allowedCalls` allowlist in `keyScopes`. `allowAnyCalls = false` with `allowedCalls = []` means scoped deny-all. * Recipient-constrained selector rules are validated before any state is written. * Emits `KeyAuthorized` event **Requirements:** * MUST be called by the Root Key or an active admin access key * MUST be invoked via the canonical selector `0x980a6025` (the `(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))` shape). Other selectors revert. * `keyId` MUST NOT be `address(0)` (reverts with `ZeroPublicKey`) * `keyId` MUST NOT already be authorized with `expiry > 0` (reverts with `KeyAlreadyExists`) * `keyId` MUST NOT have been previously revoked (reverts with `KeyAlreadyRevoked` - prevents replay attacks) * `signatureType` MUST be `0` (Secp256k1), `1` (P256), or `2` (WebAuthn) (reverts with `InvalidSignatureType`) * `config.expiry` MUST be strictly greater than the current block timestamp (reverts with `ExpiryInPast`) * To authorize a non-expiring key, omit `key_authorization.expiry` in the transaction RLP or pass `u64::MAX` when calling the precompile ABI directly. Do not pass `0`. * `enforceLimits` determines whether spending limits are enforced for this key * `limits` are only processed if `enforceLimits` is `true`. Duplicate token entries revert with `InvalidSpendingLimit`. * Invalid call-scope shapes (zero targets, duplicate targets, duplicate selectors, duplicate recipients, malformed recipient-bound rules) revert with `InvalidCallScope`. ### Admin Key Authorization * Creates a new key entry with `is_admin = true`. * Emits the existing `KeyAuthorized` event and the additional `AdminKeyAuthorized` event. * Burns the provided `witness` for replay protection. `bytes32(0)` is valid. * Admin keys can authorize and revoke keys, including other admin keys. * Admin keys cannot carry expiry, spending limits, or call scopes. **Requirements:** * MUST be called by the Root Key or an active admin access key. * `keyId` MUST NOT be the account address (reverts with `InvalidKeyId`). * `keyId` MUST NOT already be authorized (reverts with `KeyAlreadyExists`). * `keyId` MUST NOT have been previously revoked (reverts with `KeyAlreadyRevoked`). * `signatureType` MUST be `0` (Secp256k1), `1` (P256), or `2` (WebAuthn) (reverts with `InvalidSignatureType`). ### Key Revocation * Marks the key as revoked by setting `isRevoked` to `true` and `expiry` to `0` * Once revoked, a `keyId` can NEVER be re-authorized for this account (prevents replay attacks) * Any stored call-scope and periodic-limit state becomes inaccessible. `getAllowedCalls(...)` returns scoped deny-all (`isScoped = true, scopes = []`) for revoked keys. * Key can no longer be used for transactions * Emits `KeyRevoked` event **Requirements:** * MUST be called by the Root Key or an active admin access key * `keyId` MUST exist (key with `expiry > 0`) (reverts with `KeyNotFound` if not found) ### Spending Limit Update * Updates the spending limit for a specific token on an authorized key * Allows the Root Key or an active admin access key to modify limits without revoking and re-authorizing the key * If the key had unlimited spending (`enforceLimits == false`), enables limits * Sets both `remaining` and `max` to `newLimit`. The configured `period` and current `periodEnd` are preserved. * `newLimit` MUST fit within TIP20's `u128` supply range. * Emits `SpendingLimitUpdated` event **Requirements:** * MUST be called by the Root Key or an active admin access key * `keyId` MUST exist and not be revoked (reverts with `KeyNotFound` or `KeyAlreadyRevoked`) * `keyId` MUST not be expired (reverts with `KeyExpired`) * `keyId` MUST not be an admin access key, because admin keys do not carry spending limits (reverts with `InvalidKeyId`) ### Allowed Call Updates * `setAllowedCalls(...)` creates or replaces one or more target scopes for an existing key. * `removeAllowedCalls(...)` removes one stored target scope. * An empty `selectorRules` array means any selector on that target is allowed. * `setAllowedCalls(...)` rejects an empty scope batch, zero targets, duplicate targets, duplicate selectors, duplicate recipients, and invalid recipient-constrained rules (reverts with `InvalidCallScope`). **Requirements:** * MUST be called by the Root Key or an active admin access key * `keyId` MUST exist and not be revoked * `keyId` MUST not be an admin access key, because admin keys do not carry call scopes (reverts with `InvalidKeyId`) ### View Behavior * `getKey(...)` returns key metadata. * `getRemainingLimitWithPeriod(...)` returns the effective `remaining` amount and current `periodEnd` for a key-token pair, accounting for periodic rollover. * `getAllowedCalls(...)` returns `(isScoped, scopes)`. Unrestricted keys return `isScoped = false`. Scoped keys return `isScoped = true` with their `CallScope[]`. Missing, revoked, or expired access keys return `isScoped = true, scopes = []` (scoped deny-all). * `isAdminKey(...)` returns `true` for the Root Key and for active, non-revoked, non-expired admin access keys. * `getTransactionKey()` returns the key used in the current transaction. `address(0)` means the Root Key. * Missing, revoked, or expired keys return zeroed limit values. ## Security Considerations ### Access Key Storage Access Keys should be securely stored to prevent unauthorized access. Call scopes make per-app and per-device key isolation more important, because a mis-scoped key may have a broader allowlist than intended. * **Device and Application Scoping**: Access Keys SHOULD be scoped to a specific client device AND application combination. Access Keys SHOULD NOT be shared between devices or applications, even if they belong to the same user. * **Non-Extractable Keys**: Access Keys SHOULD be generated and stored in a non-extractable format to prevent theft. For example, use WebCrypto API with `extractable: false` when generating Keys in web browsers. * **Secure Storage**: Private Keys MUST never be stored in plaintext. Private Keys SHOULD be encrypted and stored in a secure manner. For web applications, use browser-native secure storage mechanisms like IndexedDB with non-extractable WebCrypto keys rather than storing raw key material. ### Privilege Escalation Prevention Limited access keys cannot escalate their own privileges because: 1. Management functions (`authorizeKey`, `authorizeAdminKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) are restricted to Root Key or active admin-key transactions 2. The protocol sets `transactionKey[account]` during transaction validation to indicate which key signed the transaction 3. These management functions check that the caller is the Root Key or an active admin key before executing 4. Mutable precompile calls also require `msg.sender == tx.origin`, which prevents contract-mediated confused-deputy patterns 5. Limited access keys cannot bypass these checks - transactions will revert with `UnauthorizedCaller` ### Spending Limit Enforcement * Spending limits are only enforced if `enforceLimits == true` for the key * Keys with `enforceLimits == false` have unlimited spending (no limits checked) * Spending limits are enforced by the protocol internally calling `verify_and_update_spending()` during execution * Limits are per-TIP20 token and deplete as TIP20 tokens are spent * Recurring limits (`period > 0`) roll over to `max` when `current_timestamp >= periodEnd`. Callers can observe rollover state via `getRemainingLimitWithPeriod(...)`. * Spending limits only track TIP20 token transfers (via `transfer` and `transferWithMemo`) and approvals (via `approve`) * For approvals: only increases in approval amount count against the spending limit. This means approvals indirectly control `transferFrom` spending, since `transferFrom` requires a prior approval * Non-TIP20 asset movements (ETH, NFTs) are not subject to spending limits * Root keys (`keyId == address(0)`) have no spending limits - the function returns immediately * Missing, revoked, or expired keys have an effective remaining limit of zero * Failed limit checks revert the entire transaction with `SpendingLimitExceeded` ### Call Scope Enforcement * Call-scope checks run on top-level calls signed by an Access Key. * If a key is scoped and a call does not match the stored target, selector, and recipient rules, execution reverts with `CallNotAllowed`. * Access-key-signed transactions cannot create contracts in any configuration — including direct `CREATE`, factory `CREATE`, and internal `CREATE2`. Only Root-Key-signed transactions may perform contract creation. ### Key Expiry * Keys with `expiry > 0` are checked against the current timestamp during validation * Expired keys cause transaction rejection with `KeyExpired` error (checked via `validate_keychain_authorization()`) * New authorizations require a future expiry timestamp * Expiry is checked as: `current_timestamp >= expiry` (key is expired when current time reaches or exceeds expiry) * Expired keys return zeroed limit and call-scope reads. ## Usage Patterns ### First-Time Access Key Authorization 1. User signs Passkey prompt → signs over `key_authorization` for a new Access Key (e.g., WebCrypto P256 key). The signed authorization carries `KeyRestrictions`, allowing the same first-use flow to provision recurring limits and call scopes. 2. User's Access Key signs the transaction 3. Transaction includes the `key_authorization` AND the Access Key `signature` 4. Protocol validates Passkey signature on `key_authorization`, sets `transactionKey[account] = 0`, calls `AccountKeychain.authorizeKey()`, then validates Access Key signature 5. Transaction executes with Access Key's spending limits enforced via internal `verify_and_update_spending()`, plus call-scope checks if the key is scoped ### Subsequent Access Key Usage 1. User's Access Key signs the transaction (no `key_authorization` needed) 2. Protocol validates the Access Key via `validate_keychain_authorization()`, sets `transactionKey[account] = keyId` 3. Transaction executes with spending limit enforcement via internal `verify_and_update_spending()` and call-scope enforcement. Contract creation is rejected. ### Root Key or Admin Key Revoking or Updating an Access Key 1. User signs with the Root Key or an active admin key → signs transaction calling `revokeKey(keyId)`, `updateSpendingLimit(...)`, `setAllowedCalls(...)`, or `removeAllowedCalls(...)` 2. Transaction executes, marking the Access Key as inactive or updating its restrictions 3. Future transactions signed by that Access Key are rejected (after revocation) or evaluated against the updated restrictions # Blockspace overview: understanding payment lanes ## Abstract This specification defines the structure of valid blocks in the Tempo blockchain. ## Motivation Tempo blocks extend the Ethereum block format in multiple ways: there are new header fields to account for payment lanes and shared gas accounting, and system transactions are added to the block body for the fee AMM and other protocol operations. This specification contains all the modifications to the block format. ## Specification ### Header fields Tempo extends an Ethereum header with three extra scalars. ```rust title="Header struct" pub struct Header { pub general_gas_limit: u64, pub shared_gas_limit: u64, pub timestamp_millis_part: u64, pub inner: Header, } ``` * `inner` is the canonical Ethereum header (parent\_hash, state\_root, gas\_limit, etc.). * `general_gas_limit` and `shared_gas_limit` partition the canonical `gas_limit` for payment and non-payment capacity (see [payment lane specification](https://tempo.xyz/developers/docs/protocol/blockspace/payment-lane-specification)). * `timestamp_millis_part` stores the sub‑second component; the full timestamp is `inner.timestamp * 1000 + timestamp_millis_part` . ### Block body The block body in Tempo retains the canonical Ethereum block body structure, with the addition of system transactions. Transactions are ordered in the following sections: 1. Start-of-block system transaction(s). 2. Proposer lane transactions, subject to `general_gas_limit` on non-payment transactions. 3. Remaining transactions that consume the shared gas budget. 4. Protocol-defined end-of-block system transactions, when required. ### System transactions A valid Tempo block must include any required protocol-defined system transactions in the expected order before and after user transactions. # Payment Lane Specification ## Abstract This specification introduces a second consensus gas constraint for **non-payment** transactions. Transactions are classified as either payments or non-payments based solely on their transaction data, without requiring any access to blockchain state. For a block to be valid, total `gas_used` by the block must be less than the `gas_limit`. Non-payment transactions executed in the proposer's lane (i.e. before the gas incentive section) must consume at most `general_gas_limit`, a new field added to the header. Once that budget is exhausted, additional inclusion is constrained by the remaining shared gas budget. ## Motivation Tempo ensures that payment transactions always have available blockspace, even during periods of high network congestion from DeFi activity or complex smart contracts. No action is required by the user to take advantage of this feature. This is achieved through **separate gas limits** for payment and non-payment transactions. When blocks are constructed, validators enforce two separate gas constraints: 1. **`gas_limit`** — The total gas available for all transactions (standard Ethereum behavior) 2. **`general_gas_limit`** — The maximum gas that non-payment transactions can consume Non-payment transactions in the proposer's lane can only fill up to `general_gas_limit`, payment transactions can still use the remaining capacity up. ## Terminology * **Payment transaction:** Determined by a function, `is_payment(tx) -> bool`. This function returns true if the transaction is a payment transaction, false otherwise. * **Non-payment transaction:** `!is_payment(tx)`. ## Specification ### 1. Transaction classification A transaction is classified as a **payment transaction** when: 1. Every call in the transaction matches an allow-listed payment call shape. 2. The transaction `access_list` is empty. 3. The transaction `authorization_list` and `tempo_authorization_list` are empty. 4. If `key_authorization` is present, `len(rlp(key_authorization)) <= 1024` bytes. This classification is performed entirely on the transaction payload, no account state is consulted. The payment call allow-list includes TIP-20 payment and mint/burn/approval calls, plus the `TIP20ChannelReserve` payment-channel precompile calls. Calls with unrecognized selectors, malformed ABI encoding, disallowed targets, non-empty auxiliary payloads, or dynamic calldata above the allow-list size bound are classified as non-payment transactions. See the [payment lane classification specification](https://tips.sh/1045) for the complete selector table and ABI constraints. ### 2. Ordering of Transactions The specification does not require any specific ordering of transactions: payment and non-payment transactions can be intermixed. ### 3. Gas accounting & validity (consensus) Validity of a block requires that, ``` general_gas_limit >= Σ gas_consumed(tx[i]) for all i such that !is_payment(tx[i]) and tx[i] is in the proposer's lane ``` Where `gas_consumed` includes intrinsic gas and gas burned by reverts, as in the existing protocol. # Consensus and finality using Simplex BFT Tempo uses Simplex BFT consensus to provide deterministic, sub-second finality. This page describes the consensus mechanism, finality guarantees, and fault tolerance properties. ## Simplex BFT Consensus Tempo uses Simplex Consensus, implemented by [Commonware](https://www.commonware.xyz/). Simplex is a Byzantine Fault Tolerant consensus protocol optimized for fast finality with graceful degradation under adverse network conditions. ### Block Production Blocks are produced approximately every 600ms under normal network conditions (500ms builder loop plus network latency and block validation). Proposer selection uses a VRF (Verifiable Random Function) for random leader election, providing DoS protection and MEV resistance. Once a block is finalized, it cannot be reverted. ### Deterministic Finality Tempo provides deterministic finality rather than probabilistic finality. When a block is marked as finalized, transactions in that block are guaranteed to remain in the canonical chain. There is no reorg risk after finality. For payment applications, this provides the settlement certainty that operators expect from traditional financial systems. ## Validator Set ### Current Configuration The testnet operates with 4 validators in a permissioned configuration. Mainnet will launch with institutional validators, also permissioned initially. The roadmap includes a path to permissionless validation. ### Fault Tolerance Simplex BFT tolerates Byzantine validators up to a threshold: * The network maintains safety as long as fewer than one-third of validators are Byzantine * The network maintains liveness as long as at least two-thirds of validators are honest and online With 4 validators, the network tolerates 1 Byzantine validator. With 10 validators, the network tolerates 3 Byzantine validators. ## Distributed Validation Tempo uses a distributed validator set rather than a single sequencer. Multiple validators share block production responsibility. Transactions can be included by any proposer, preventing single points of censorship. The network can continue finalizing blocks as long as two-thirds of validators are online and honest. ## Degraded State Behavior Under adverse conditions, Simplex consensus degrades gracefully: | Condition | Behavior | |-----------|----------| | Network partition | Block times may increase but finality guarantees are preserved | | Validator offline within threshold | Network continues with remaining validators | | More than one-third of validators offline | Network halts and resumes when threshold is restored | The protocol prioritizes safety over liveness. It will halt rather than produce conflicting blocks. ## For Integrators Treat finalized blocks as irreversible for settlement purposes. No additional confirmations are needed after finality. Query finalized blocks using `eth_getBlockByNumber` with the `finalized` tag. ## Further Reading * [Commonware Simplex Documentation](https://docs.rs/commonware-consensus/0.0.65/commonware_consensus/simplex/) * [Simplex with BLS12-381 Threshold Scheme](https://docs.rs/commonware-consensus/0.0.65/commonware_consensus/simplex/index.html#schemebls12381_threshold) # Exchanging stablecoins: exchange vs. Fee AMM Tempo features an enshrined decentralized exchange (DEX) designed specifically for trading between stablecoins of the same underlying asset (e.g., USDG to USDT). The exchange provides optimal pricing for cross-stablecoin payments while minimizing chain load from excessive market activity. The exchange operates as a singleton precompiled contract at address `0xdec0000000000000000000000000000000000000`. It maintains an orderbook with separate queues for each price tick, using price-time priority for order matching. :::info **Exchange vs. Fee AMM** — these are two different systems. The Exchange is the user-facing market: you (or your app) trade stablecoins at market prices via swaps and orders against an orderbook. The [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/fee-amm) is protocol fee-conversion liquidity: it converts users' transaction-fee payments into validators' preferred tokens at a fixed price through protocol-driven swaps, and is not a trading venue. See [Exchange vs. Fee AMM](#exchange-vs-fee-amm) below. ::: Trading pairs are determined by each token's quote token. All TIP-20 tokens specify a quote token for trading on the exchange. See [Quote Tokens](https://tempo.xyz/developers/docs/protocol/exchange/quote-tokens) for more information on quote token selection and the optional [pathUSD](https://tempo.xyz/developers/docs/protocol/exchange/quote-tokens#pathusd) stablecoin. See the [Stablecoin DEX Specification](https://tempo.xyz/developers/docs/protocol/exchange/spec) for detailed information on the exchange structure. The exchange supports three types of orders, each with different execution behavior: | Order Type | Description | |------------|-------------| | [**Limit Orders**](https://tempo.xyz/developers/docs/protocol/exchange/providing-liquidity#limit-orders) | Place orders at specific price levels that wait in the book until matched or cancelled. Orders are added to the book immediately when placed. | | [**Flip Orders**](https://tempo.xyz/developers/docs/protocol/exchange/providing-liquidity#flip-orders) | Special orders that automatically reverse to the opposite side when completely filled, acting like a perpetual liquidity pool. When a flip order is fully filled, the same `orderId` is rewritten on the opposite side and emits `OrderFlipped`. | | [**Market Orders**](https://tempo.xyz/developers/docs/protocol/exchange/executing-swaps#swap-functions) | Execute immediately against the best available orders in the book (via swap functions). Swaps and cancellations execute immediately within the transaction. | For the complete execution mechanics, see the [Stablecoin DEX Specification](https://tempo.xyz/developers/docs/protocol/exchange/spec). ## Exchange vs. Fee AMM Tempo has two stablecoin-swapping systems that are easy to confuse. They serve different purposes: | | Exchange (DEX) | [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/fee-amm) | |---|---|---| | **Purpose** | User-facing stablecoin trading | Converting transaction fees into validators' preferred tokens | | **Who initiates swaps** | Users and apps | The protocol, automatically (plus arbitrageurs rebalancing) | | **Pricing** | Market-driven orderbook (price-time priority) | Fixed conversion price | | **Liquidity** | Limit and flip orders resting in the orderbook | LP deposits into per-pair fee pools | | **Contract** | Stablecoin DEX precompile (`0xdec0…0000`) | `FeeManager` precompile (`0xfeec…0000`) | | **Use it for** | Swaps, prices, orderbook depth, fills | Letting users pay fees in any stablecoin | If you want to **trade** stablecoins or read market data (pairs, swaps, orders, prices), use the Exchange. If you want to **enable fee payments** in your stablecoin or provide fee-conversion liquidity, see the [Fee AMM](https://tempo.xyz/developers/docs/protocol/fees/fee-amm). To get started with the exchange, explore these guides: * [Executing Swaps](https://tempo.xyz/developers/docs/protocol/exchange/executing-swaps) — Quote prices and swap between stablecoins * [Providing Liquidity](https://tempo.xyz/developers/docs/protocol/exchange/providing-liquidity) — Place orders and flip orders to earn spreads * [DEX Balance](https://tempo.xyz/developers/docs/protocol/exchange/exchange-balance) — Manage token balances on the DEX to save gas costs :::info For a more complete technical specification including design decisions and details of execution semantics, see the [Stablecoin DEX Specification](https://tempo.xyz/developers/docs/protocol/exchange/spec). ::: # Stablecoin DEX ## Abstract This specification defines an enshrined decentralized exchange for trading between TIP-20 stablecoins. The exchange currently only supports trading between TIP-20 stablecoins with USD as their currency. By only allowing each stablecoin to be paired against its designated "quote token" the exchange enforces that there is only one route for trading between any two tokens. The exchange maintains price‑time priority at each discrete price tick, executes swaps immediately against the active book, and supports auto‑replenishing “flip orders” that recreate themselves on the opposite side after being fully filled. Users maintain internal balances per token on the exchange. Order placement escrows funds from these balances (or transfers from the user if necessary), fills credit makers internally, and withdrawals transfer tokens out. ## Motivation Tempo aims to provide high‑quality execution for cross‑stablecoin payments while avoiding unnecessary chain load and minimizing mid‑block MEV surfaces. A simple, on‑chain, price‑time‑priority orderbook tailored to stable pairs encourages passive liquidity to rest on chain and allows takers to execute deterministically at the best available ticks. Another design goal is to avoid fragmentation of liquidity across many different pairs. By enforcing that each stablecoin only trades against a single quote token, the system guarantees that there is only one path between any two tokens. ## Specification ### Contract and scope The exchange is a singleton contract deployed at `0xdec0000000000000000000000000000000000000`. It exposes functions to create trading pairs, place and cancel orders (including flip orders), execute swaps, produce quotes, and manage internal balances. ### Key concepts #### Internal balances The contract maintains per‑user, per‑token internal balances. Order placement escrows funds from these balances (or transfers any shortfall from the user). When an order fills, the maker is credited internally with the counter‑asset at the order’s tick price. Users can withdraw available balances at any time. #### Flip orders A flip order behaves like a normal resting order until it is fully filled. When filled, the exchange rewrites the order in place under the same `orderId` on the opposite side at a configured `flipTick`. The `flipTick` must be greater than or equal to `tick` for bids and less than or equal to `tick` for asks. The flip emits `OrderFlipped` instead of `OrderPlaced`, and `nextOrderId` does not advance. This enables passive liquidity with flexible strategies. When a flip order flips, it draws escrow exclusively from the maker's internal exchange balance. Unlike initial order placement, the exchange does not fall back to `transferFrom` if the internal balance is insufficient—the flip simply does not occur. This ensures that flip execution is self-contained and does not require additional token approvals or external balance checks at fill time. #### Pairs, ticks, and prices Pairs are identified deterministically from the two token addresses (the base token is any TIP‑20, and its `quoteToken()` function points to the quote token). Prices are discretized into integer ticks with a tick size of 0.1 bps: with `PRICE_SCALE = 100_000`, `price = PRICE_SCALE + tick`. Orders may only be placed at ticks divisible by `TICK_SPACING = 10` (effectively setting a 1 bp tick size). The orderbook tracks best bid (highest active bid tick) and best ask (lowest active ask tick), and uses bitmaps over tick words for efficient discovery of the next initialized tick. #### Quote tokens Each TIP‑20 token specifies a single quote token in its metadata via `quoteToken()`. A trading pair on the Stablecoin DEX exists only between a base token and its designated quote token, and prices for the pair are denominated in units of the quote token. This design reduces liquidity fragmentation by giving every token exactly one paired asset. It also simplifies routing. We require that: 1. each token picks a single other stablecoin as its quote token, and, 2. quote token relationships cannot have circular dependencies. This forces liquidity into a tree structure, which in turn implies that there is only one path between any two stablecoins USD tokens can only choose USD tokens as their quote token. Non-USD TIP-20 tokens can pick any token as their quote token, but currently there is no support for cross-currency trading, or same-currency trading of non-USD tokens, on the DEX. The platform offers a neutral USD stablecoin, [`pathUSD`](https://tempo.xyz/developers/docs/protocol/exchange/quote-tokens#pathusd), as an option for quote token. PathUSD is the first stablecoin deployed on the chain, which means it has no quote token. Use of pathUSD is optional. #### Swaps Swaps execute immediately against the active book. Selling base for quote starts at the current best bid and walks downward as ticks are exhausted; selling quote for base starts at the best ask and walks upward. Within a tick, fills are FIFO and decrement the tick’s total liquidity. When a tick empties, it is de‑initialized. Callers can swap between any two USD TIP-20 tokens. If `tokenIn` and `tokenOut` are not directly paired, the implementation finds the unique path between them through quote‑token relationships, and performs a multi‑hop swap/quote. #### Crossed books Crossed books are permitted; the implementation does not enforce that best bid ≤ best ask. This primarily supports flip‑order scenarios. #### Constraints * Only USD‑denominated tokens are supported, and their quotes must also be USD * Orders must specify ticks within the configured bounds (±2000) * Tick spacing is 10: `tick % 10 == 0` for orders and flip orders * Withdrawals require sufficient internal balance ### Interface Below is the complete on‑chain interface, organized by function. Behavior notes and constraints are included with each function where relevant. #### Constants and pricing ```solidity function PRICE_SCALE() external view returns (uint32); ``` Scaling factor for tick‑based prices. One tick equals 1/PRICE\_SCALE above or below the peg. Current value: `100_000` (0.001% per tick). ```solidity function TICK_SPACING() external view returns (int16); ``` Orders must be placed on ticks divisible by `TICK_SPACING`. Current value: `10` (i.e., 1 bp grid). ```solidity function MIN_TICK() external view returns (int16); function MAX_TICK() external view returns (int16); ``` Inclusive tick bounds for order placement. Current range: ±2000 ticks (±2%). ```solidity function MIN_PRICE() external view returns (uint32); function MAX_PRICE() external view returns (uint32); ``` Price bounds implied by tick bounds and `PRICE_SCALE`. ```solidity function tickToPrice(int16 tick) external pure returns (uint32 price); function priceToTick(uint32 price) external pure returns (int16 tick); ``` Convert between discrete ticks and scaled prices. `priceToTick` reverts if `price` is out of bounds. #### Pairing and orderbook ```solidity function pairKey(address tokenA, address tokenB) external pure returns (bytes32 key); ``` Deterministic key for a pair derived from the two token addresses (order‑independent). ```solidity function createPair(address base) external returns (bytes32 key); ``` Creates the pair between `base` and its `quoteToken()` (from TIP‑20). Both must be USD‑denominated. Reverts if the pair already exists or tokens are not USD. ```solidity function books(bytes32 pairKey) external view returns (address base, address quote, int16 bestBidTick, int16 bestAskTick); ``` Returns pair metadata and current best‑of‑side ticks. Best ticks may be sentinel values when no liquidity exists. ```solidity function getTickLevel(address base, int16 tick, bool isBid) external view returns (uint128 head, uint128 tail, uint128 totalLiquidity); ``` Returns FIFO head/tail order IDs and aggregate liquidity for a tick on a side, allowing indexers to reconstruct the active book. #### Internal balances ```solidity function balanceOf(address user, address token) external view returns (uint128); ``` Returns a user’s internal balance for `token` held on the exchange. ```solidity function withdraw(address token, uint128 amount) external; ``` Transfers `amount` of `token` from the caller’s internal balance to the caller. Reverts if insufficient internal balance. #### Order placement and lifecycle ```solidity function place(address token, uint128 amount, bool isBid, int16 tick) external returns (uint128 orderId); ``` Places a limit order against the pair of `token` and its quote, immediately adding it to the active book. Escrows funds: bids escrow quote at tick price; asks escrow base. Notes: * `tick` must be within `[MIN_TICK, MAX_TICK]` and divisible by `TICK_SPACING` (10). * The maker must be authorized by the TIP-403 transfer policies of both the base and quote tokens. This ensures makers cannot place orders to buy or sell tokens they are not permitted to transfer. * Additionally, the DEX contract itself must be authorized by the TIP-20 transfer policies of both the base and quote tokens. This allows token issuers to prevent their tokens from being traded on the DEX. ```solidity function placeFlip(address token, uint128 amount, bool isBid, int16 tick, int16 flipTick) external returns (uint128 orderId); ``` Like `place`, but marks the order as a flip order. When fully filled, the same `orderId` is rewritten in place on the opposite side at `flipTick` (which must be greater than or equal to `tick` for bids and less than or equal to `tick` for asks). Notes: * Both `tick` and `flipTick` must be within `[MIN_TICK, MAX_TICK]` and divisible by `TICK_SPACING` (10). * When the order flips, escrow is drawn exclusively from the maker's internal exchange balance. If the internal balance is insufficient, the flip silently fails—no `transferFrom` is attempted, even if the maker has sufficient external balance and approval. * The maker must be authorized by the TIP-403 transfer policies of both the base and quote tokens, both at initial placement and when the order flips. If the maker becomes unauthorized before a flip, the flip silently fails and no flipped order is inserted (although the existing order is executed). ```solidity function cancel(uint128 orderId) external; ``` Cancels an order owned by the caller. When canceled, the order is removed from the tick queue, liquidity is decremented, and remaining escrow is refunded to the order owner's exchange balance which can then be withdrawn. ```solidity function cancelStaleOrder(uint128 orderId) external; ``` Cancels an order where the maker is forbidden by the escrowed token's [TIP-403 transfer policy](https://tempo.xyz/developers/docs/protocol/tip403/overview). Unlike `cancel`, this function can be called by anyone—not just the order maker—but only succeeds if the maker is no longer authorized to transfer the escrowed token (e.g., the maker has been blacklisted). This allows third parties to clean up stale orders from the book. When canceled, the order is removed from the tick queue, liquidity is decremented, and remaining escrow is refunded to the order maker's exchange balance. Reverts with `OrderNotStale` if the maker is still authorized. ```solidity function nextOrderId() external view returns (uint128); ``` Monotonic counter for next orderId. #### Swaps and quoting ```solidity function quoteSwapExactAmountIn(address tokenIn, address tokenOut, uint128 amountIn) external view returns (uint128 amountOut); ``` Simulates an exact‑in swap walking initialized ticks and returns the expected output. Reverts if the pair path lacks sufficient liquidity. ```solidity function quoteSwapExactAmountOut(address tokenIn, address tokenOut, uint128 amountOut) external view returns (uint128 amountIn); ``` Simulates an exact‑out swap and returns the required input. Reverts if insufficient liquidity. ```solidity function swapExactAmountIn(address tokenIn, address tokenOut, uint128 amountIn, uint128 minAmountOut) external returns (uint128 amountOut); ``` Executes an exact‑in swap against the active book. Deducts `amountIn` from caller’s internal balance (transferring any shortfall) and transfers output to the caller. Reverts if resulting `amountOut` is below `minAmountOut` or liquidity is insufficient. ```solidity function swapExactAmountOut(address tokenIn, address tokenOut, uint128 amountOut, uint128 maxAmountIn) external returns (uint128 amountIn); ``` Executes an exact‑out swap. Deducts the actual input from the caller’s internal balance (transferring any shortfall from the user) and transfers `amountOut` to the caller. Reverts if required input exceeds `maxAmountIn` or liquidity is insufficient. #### Events ```solidity event PairCreated(bytes32 indexed key, address indexed base, address indexed quote); event OrderPlaced(uint128 indexed orderId, address indexed maker, address indexed token, uint128 amount, bool isBid, int16 tick, bool isFlipOrder, int16 flipTick); event OrderFlipped(uint128 indexed orderId, address indexed maker, address indexed token, uint128 amount, bool isBid, int16 tick, int16 flipTick); event OrderCancelled(uint128 indexed orderId); event OrderFilled(uint128 indexed orderId, address indexed maker, address indexed taker, uint128 amountFilled, bool partialFill); ``` #### Errors ```solidity error Unauthorized(); ``` * Pair creation or usage: `PAIR_EXISTS`, `PAIR_NOT_EXISTS`, `ONLY_USD_PAIRS` * Bounds: `TICK_OUT_OF_BOUNDS`, `FLIP_TICK_OUT_OF_BOUNDS`, `FLIP_TICK_MUST_BE_GREATER_FOR_BID`, `FLIP_TICK_MUST_BE_LESS_FOR_ASK`, "Price out of bounds" * Tick spacing: `TICK_NOT_MULTIPLE_OF_SPACING`, `FLIP_TICK_NOT_MULTIPLE_OF_SPACING` * Liquidity and limits: `INSUFFICIENT_LIQUIDITY`, `MAX_IN_EXCEEDED`, `INSUFFICIENT_OUTPUT` * Authorization: `UNAUTHORIZED` (cancel not by maker) * Stale orders: `ORDER_NOT_STALE` (cancelStaleOrder when maker is still authorized) * Balance: `INSUFFICIENT_BALANCE` (withdraw) # pathUSD on Tempo Mainnet and quote tokens Each USD TIP-20 on Tempo can choose any other USD TIP-20 as its quote token—the token it is paired against on the native decentralized exchange. This guarantees that there is one path between any two tokens, which reduces fragmentation of liquidity and simplifies routing. `pathUSD` is live on Tempo Mainnet. The production asset and the faucet-issued test token on Tempo Testnet use the same predeploy address, but they exist on separate networks and are not interchangeable. ## pathUSD On Tempo Mainnet, pathUSD is a USD-denominated stablecoin that can be used as a quote token on Tempo's decentralized exchange. It is the first stablecoin deployed to the chain, and is used as a fallback gas token when the user or validator does not specify a gas token. Use of pathUSD is optional. ### Issuance & Backing pathUSD is issued by Bridge and is backed 1:1 by high-quality USD-denominated reserves. Please see [bridge.xyz](https://www.bridge.xyz) for more about their reserve allocation practices. pathUSD can be minted by depositing USDC.e and redeemed back to USDC through Bridge. ### Testnet pathUSD Tempo Testnet (Moderato, chain ID `42431`) provides faucet-issued pathUSD for development. Testnet balances have no relationship to production pathUSD on Tempo Mainnet and cannot be treated as mainnet funds. Use the [Tempo faucet](https://tempo.xyz/developers/docs/quickstart/faucet) only for testnet development. ### Why pathUSD? Tempo is designed to be neutral across stablecoin issuers. Rather than defaulting to any single issuer's stablecoin as the quote token, pathUSD provides a neutral option that any token can pair against on Tempo's DEX. This ensures that no single stablecoin issuer has a privileged position in Tempo's liquidity graph and guarantees stablecoin interoperability. pathUSD is not meant to compete as a consumer-facing stablecoin. Use of pathUSD is optional, and tokens are able to list any other token as their quote token if they choose. pathUSD can also be accepted as a fee token by validators. ### Contract pathUSD is a predeployed [TIP-20](https://tempo.xyz/developers/docs/protocol/tip20/spec) at genesis. Since it is the first TIP-20 deployed, its quote token is the zero address. | Property | Value | | -------------- | -------------------------------------------- | | network | Tempo Mainnet (chain ID `4217`) | | address | `0x20c0000000000000000000000000000000000000` | | `name()` | `"pathUSD"` | | `symbol()` | `"pathUSD"` | | `currency()` | `"USD"` | | `decimals()` | `6` | | `quoteToken()` | `address(0)` | ### Use pathUSD as a quote token When creating a USD stablecoin on Tempo, you can set pathUSD as its quote token: ```solidity TIP20 token = factory.createToken( "My Company USD", "MCUSD", "USD", TIP20(0x20c0000000000000000000000000000000000000), // pathUSD msg.sender, bytes32("my-unique-salt") // salt for deterministic address ); ``` This means: * Your token trades against pathUSD on the exchange. * Users can swap between your token and other USD stablecoins that also use pathUSD, or ones connected by a multi-hop path. ### Tree Structure Quote token relationships form a tree structure where all USD stablecoins are connected via multi-hop paths: ``` USDX | pathUSD -- USDY -- USDZ | USDA ``` The tree structure guarantees a single path between any two USD stablecoins. This ensures simple routing, concentrated liquidity, and efficient pricing even for thinly-traded pairs. ### Example: Cross-Stablecoin Payment 1. Market makers provide liquidity for USDX/pathUSD and USDY/pathUSD pairs. 2. A user wants to send USDX to a merchant who prefers USDY. 3. The exchange atomically routes the payment: USDX to pathUSD to USDY. 4. This happens in a single transaction with no manual swaps required. The user and merchant never hold pathUSD directly. It exists only as routing infrastructure. # Executing swaps | Protocol reference ## Swap Functions The exchange provides two primary swap functions: ### Swap Exact Amount In Specify the exact amount of tokens you want to sell, and receive at least a minimum amount: ```solidity function swapExactAmountIn( address tokenIn, address tokenOut, uint128 amountIn, uint128 minAmountOut ) external returns (uint128 amountOut) ``` **Parameters:** * `tokenIn` - The token address you're selling * `tokenOut` - The token address you're buying * `amountIn` - The exact amount of `tokenIn` to sell * `minAmountOut` - Minimum amount of `tokenOut` you'll accept (slippage protection) **Returns:** * `amountOut` - The actual amount of `tokenOut` received **Example:** Swap exactly 1000 USDG for at least 998 USDT: ```solidity uint128 amountOut = exchange.swapExactAmountIn( USDG_ADDRESS, USDT_ADDRESS, 1000e6, // Sell exactly 1000 USDG 998e6 // Receive at least 998 USDT ); ``` ### Swap Exact Amount Out Specify the exact amount of tokens you want to receive, and pay at most a maximum amount: ```solidity function swapExactAmountOut( address tokenIn, address tokenOut, uint128 amountOut, uint128 maxAmountIn ) external returns (uint128 amountIn) ``` **Parameters:** * `tokenIn` - The token address you're selling * `tokenOut` - The token address you're buying * `amountOut` - The exact amount of `tokenOut` to receive * `maxAmountIn` - Maximum amount of `tokenIn` you'll pay (slippage protection) **Returns:** * `amountIn` - The actual amount of `tokenIn` spent **Example:** Receive exactly 1000 USDT by spending at most 1002 USDG: ```solidity uint128 amountIn = exchange.swapExactAmountOut( USDG_ADDRESS, USDT_ADDRESS, 1000e6, // Receive exactly 1000 USDT 1002e6 // Pay at most 1002 USDG ); ``` ## Quoting Prices Before executing a swap, you can query the expected price using view functions that simulate the swap without executing it: ### Quote Exact Amount In ```solidity function quoteSwapExactAmountIn( address tokenIn, address tokenOut, uint128 amountIn ) external view returns (uint128 amountOut) ``` Returns how much `tokenOut` you would receive for a given `amountIn`. ### Quote Exact Amount Out ```solidity function quoteSwapExactAmountOut( address tokenIn, address tokenOut, uint128 amountOut ) external view returns (uint128 amountIn) ``` Returns how much `tokenIn` you would need to spend to receive a given `amountOut`. **Example: Getting a price quote** ```solidity // Check how much USDT you'd get for 1000 USDG uint128 expectedOut = exchange.quoteSwapExactAmountIn( USDG_ADDRESS, USDT_ADDRESS, 1000e6 ); // Only execute if the price is acceptable if (expectedOut >= 998e6) { exchange.swapExactAmountIn(USDG_ADDRESS, USDT_ADDRESS, 1000e6, 998e6); } ``` ## How Swaps Execute When you call a swap function: 1. **Balance Check**: The contract first checks your balance on the DEX 2. **Transfer if Needed**: If your DEX balance is insufficient, tokens are transferred from your wallet 3. **Order Matching**: The DEX walks through orders at each price tick, from best to worst: * Orders are consumed in price-time priority order * Each filled order credits the maker's balance on the DEX * Continues until your swap is complete or limit price is reached 4. **Slippage Check**: Reverts if `minAmountOut` (or `maxAmountIn`) constraints aren't met 5. **Settlement**: Your output tokens are transferred to your wallet :::warning Swaps will revert with an `InsufficientLiquidity` error if there isn't enough liquidity in the orderbook to satisfy your slippage constraints. ::: ## Gas Costs Swap gas costs scale with the number of orders and ticks your trade crosses: * Base swap cost (transfers and setup) * Per-order cost (for each order filled) * Per-tick cost (for each price level crossed) * Per-flip cost (if any flip orders are triggered) Larger swaps that cross more orders will cost more gas, but the cost per unit of volume decreases. ## Token Balances on the DEX The DEX allows you to track token balances directly within the DEX contract, which saves gas by avoiding ERC-20 transfers on every trade. When you execute a swap, the contract first checks your DEX balance and only transfers from your wallet if needed. For complete details on checking balances, depositing, withdrawing, and managing your DEX balance, see the [DEX Balance](https://tempo.xyz/developers/docs/protocol/exchange/exchange-balance) page. # Providing liquidity | Protocol reference Provide liquidity to the DEX by placing limit orders or flip orders in the onchain orderbook. When your orders are filled, you earn the spread between bid and ask prices while helping facilitate trades for other users. You can only place orders on pairs between a token and its designated quote token. All TIP-20 tokens specify a quote token for trading pairs. [pathUSD](https://tempo.xyz/developers/docs/protocol/exchange/quote-tokens#pathusd) can be used as a simple choice for a quote token. ## Orderbook liquidity overview The DEX uses an onchain orderbook where you can place orders at specific price ticks. Orders are matched using price-time priority, meaning better-priced orders fill first, and within the same price, earlier orders fill first. Unlike traditional AMMs, you specify exact prices where you want to buy or sell, giving you more precise control over your liquidity provision strategy. :::info[Storage credits for DEX makers] Storage credits can reduce gas for active DEX makers who repeatedly place, cancel, or fully fill eligible orders. See the [T7 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t7#user-attributed-dex-savings) for the feature details. ::: ## Order Types ### Limit Orders Standard orders that remain in the book at a specific price until filled or cancelled. ```solidity function place( address token, uint128 amount, bool isBid, int16 tick ) external returns (uint128 orderId) ``` **Parameters:** * `token` - The token address you're trading (must trade against its quote token) * `amount` - The amount of the token denominated in `token` * `isBid` - `true` for a buy order, `false` for a sell order * `tick` - The price tick: `(price - 1) * 100_000` where price is in quote token per token **Returns:** * `orderId` - Unique identifier for this order **Example: Place a bid to buy 1000 USDG at $0.9990** ```solidity // tick = (0.9990 - 1) * 100_000 = -10 uint128 orderId = exchange.place( USDG_ADDRESS, 1000e6, // Amount: 1000 USDG true, // isBid: buying USDG -10 // tick: price = $0.9990 ); ``` **Example: Place an ask to sell 1000 USDG at $1.0010** ```solidity // tick = (1.0010 - 1) * 100_000 = 10 uint128 orderId = exchange.place( USDG_ADDRESS, 1000e6, // Amount: 1000 USDG false, // isBid: selling USDG 10 // tick: price = $1.0010 ); ``` ### Flip Orders Special orders that automatically reverse to the opposite side when completely filled, creating perpetual liquidity similar to an automated market maker pool. ```solidity function placeFlip( address token, uint128 amount, bool isBid, int16 tick, int16 flipTick ) external returns (uint128 orderId) ``` **Parameters:** * All parameters from `place()`, plus: * `flipTick` - The price where the order will flip to when filled * Must be greater than or equal to `tick` if `isBid` is true * Must be less than or equal to `tick` if `isBid` is false **Returns:** * `orderId` - Unique identifier for this flip order **Example: Place a flip order providing liquidity on both sides** ```solidity // Place a bid at $0.9990 that flips to an ask at $1.0010 uint128 orderId = exchange.placeFlip( USDG_ADDRESS, 1000e6, // Amount: 1000 USDG true, // isBid: start as a buy order -10, // tick: buy at $0.9990 10 // flipTick: sell at $1.0010 after filled ); ``` When this order is completely filled: 1. You buy 1000 USDG at $0.9990 2. The same order ID automatically rests as an ask for 1000 USDG at $1.0010 and emits `OrderFlipped` 3. When that fills, it flips back to a bid at $0.9990 4. This continues indefinitely, earning the spread each time :::info Flip orders act like a liquidity pool position, automatically providing liquidity on both sides of the market as they're filled back and forth. ::: ## Understanding Ticks Prices are specified using ticks with 0.1 basis point (0.001%) precision: **Tick Formula:** `tick = (price - 1) × 100_000` **Price Formula:** `price = 1 + (tick / 100_000)` Where `price` is the token price in quote token units. ### Example Tick Calculations | Price | Tick | Calculation | |-------|------|-------------| | $0.9990 | -100 | (0.9990 - 1) × 100\_000 = -100 | | $0.9998 | -20 | (0.9998 - 1) × 100\_000 = -20 | | $1.0000 | 0 | (1.0000 - 1) × 100\_000 = 0 | | $1.0002 | 20 | (1.0002 - 1) × 100\_000 = 20 | | $1.0010 | 100 | (1.0010 - 1) × 100\_000 = 100 | :::warning Price ticks are limited to ±2% from peg (±2000 ticks). Orders outside this range will be rejected. ::: ## Bid vs Ask * **Bid (isBid = true)**: An order to *buy* the token using its quote token * **Ask (isBid = false)**: An order to *sell* the token for its quote token For a USDG/USD pair where USD is the quote: * A bid buys USDG with USD at your specified price * An ask sells USDG for USD at your specified price ## Order Execution Timeline Orders follow a specific lifecycle: 1. **Placement**: When you call `place()` or `placeFlip()`: * Tokens are debited from your DEX balance (or transferred if insufficient) * Order is immediately added to the active book and visible to other contracts * Returns an order ID immediately 2. **Filling**: As market orders execute against your order: * Your order fills partially or completely * Proceeds are credited to your DEX balance * If a flip order fills completely, the same `orderId` is immediately rewritten on the opposite side and an `OrderFlipped` event is emitted ## Cancelling Orders Remove an order from the book before it's filled: ```solidity function cancel( uint128 orderId ) external ``` **Example:** ```solidity // Cancel order #12345 exchange.cancel(12345); ``` Cancellations execute immediately, and any unfilled portion of your order is refunded to your [DEX balance](https://tempo.xyz/developers/docs/protocol/exchange/exchange-balance). :::warning You can only cancel your own orders. Attempting to cancel another user's order will revert. ::: ## Flip-order indexing **Indexers and analytics:** * Subscribe to the `OrderFlipped(orderId, newSide, newTick, ...)` event on the DEX * Key order state by `orderId` and let it change side and tick over its lifetime * Do not expect `nextOrderId` to advance per flip **Frontends and SDKs:** * Surface the persistent `orderId` across the flip lifecycle in user-facing order status **Smart contracts and tooling:** * Do not assert that `flipTick != tick`; same-tick flips are valid See the [compatibility section of the specification](https://tips.sh/1056#compatibility) for additional migration guidance on keeping order IDs across flips. # Understanding DEX balances on Tempo The Stablecoin DEX allows you to hold token balances directly using the DEX contract. This eliminates the need for token transfers on every trade, significantly reducing gas costs for active traders and liquidity providers. ## Why DEX Balances? When you trade or provide liquidity on the DEX, constantly transferring tokens between your wallet and the DEX contract wastes gas. By maintaining a balance via the DEX contract, you can: * **Save on gas costs** - Avoid ERC-20 transfer costs for each trade * **Trade more efficiently** - Execute multiple swaps without transfers between each trade * **Receive maker proceeds automatically** - When your limit orders are filled, proceeds are credited to your DEX balance instead of requiring a transfer for each fill ## Checking Your Balance Use the DEX contract to view your balance of any token held on the DEX: ```solidity function balanceOf( address user, address token ) external view returns (uint128) ``` **Example:** ```solidity uint128 balance = exchange.balanceOf(msg.sender, USDG_ADDRESS); ``` ## Using Your DEX Balance Each transaction that you authorize will use your DEX balance before using funds you approve from your wallet. When you execute a swap or place an order, the DEX contract automatically: 1. Checks if you have sufficient balance in the DEX 2. If insufficient, transfers the needed amount from your wallet to your DEX balance 3. Uses your DEX balance for the operation ## Withdrawing from the DEX Transfer tokens from your DEX balance back to your wallet: ```solidity function withdraw( address token, uint128 amount ) external ``` **Parameters:** * `token` - The token address to withdraw * `amount` - The amount to withdraw **Example:** ```solidity // Withdraw 1000 USDG from exchange to your wallet exchange.withdraw(USDG_ADDRESS, 1000e6); ``` :::warning The withdraw function will revert if you attempt to withdraw more than your available balance on the exchange. ::: ## How Balances Work ### When Swapping * **Before swap**: Exchange checks your balance, transfers from wallet if needed * **After swap**: Output tokens are transferred directly to your wallet (not kept on exchange) ### When Placing Orders * **On placement**: Required tokens are debited from your exchange balance (or transferred from wallet if insufficient) * **When filled**: Proceeds are credited to your exchange balance * **On cancellation**: Unfilled portion is refunded to your exchange balance # Tempo Zones: testnet private transaction protocol :::info Tempo Zones are still in early development and available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: A Tempo Zone is a private execution environment attached to Tempo Mainnet. Inside a Tempo Zone, balances, transfers, and transaction history are invisible to block explorers, indexers, and other users on Tempo Mainnet. Each Tempo Zone runs its own sequencer and executes transactions independently. ![Tempo Zones overview](/developers/learn/zones/diagram-overview.svg) Funds deposited into a Tempo Zone are locked in the Zone Portal contract on Tempo Mainnet. [Validity proofs](https://tempo.xyz/developers/docs/protocol/zones/proving) guarantee that the sequencer executed every transaction correctly. The sequencer orders and includes transactions, but cannot steal funds or forge state transitions. Each Tempo Zone operates as a separate chain, so adding more zones increases throughput without congesting Tempo Mainnet. Tempo Zones share liquidity through Tempo Mainnet. A zone can withdraw tokens, swap them on the Stablecoin DEX, and deposit the result into another zone without exposing who placed the trade. See [composable withdrawals](https://tempo.xyz/developers/docs/protocol/zones/bridging#composable-withdrawals) for details. ### Tempo Zones are private Tempo Zones make a key trade-off: Each zone has a sequencer who sees all activity on the zone. Privacy depends on the integrity of whoever is running the sequencer. Thanks to this trade-off, they achieve what few other privacy solutions do: Great privacy with good UX. Most privacy solutions offer either confidentiality (hide the amount) or anonymity (hide the sender). Tempo Zones provide both, and go further. Inside a Tempo Zone, balances, transaction history, and counterparty relationships are all invisible to outside observers. Block explorers and indexers see nothing. Other users cannot query your address. The [accounts specification](https://tempo.xyz/developers/docs/protocol/zones/accounts) describes how balance and allowance reads are restricted at the contract level, and the [RPC specification](https://tempo.xyz/developers/docs/protocol/zones/rpc) covers how the JSON-RPC interface is scoped per account. ![End-to-end privacy flow through a zone](/developers/learn/zones/diagram-privacy.svg) ### Tempo Zones are compliant by design Every TIP-20 token carries its issuer's compliance policy (whitelists, blacklists, freeze controls) via the [TIP-403 registry](https://tempo.xyz/developers/docs/protocol/tip403/overview). When deposited into a Tempo Zone, the policy is provably mirrored. The validity proof commits that every transaction in the batch followed the issuer's rules. ![Policy inheritance from mainnet to zone](/developers/learn/zones/diagram-tip20.svg) ### Tempo Zones are safe from theft Validity proofs guarantee correct state transitions. Sequencers order transactions but cannot steal deposited funds. See the [proving specification](https://tempo.xyz/developers/docs/protocol/zones/proving) for how proofs are constructed and verified. ### Tempo Zones are interoperable Tempo Zones are interoperable with Tempo Mainnet and with each other. Deposits and withdrawals settle in seconds. A Tempo Zone can withdraw tokens, swap them on the Stablecoin DEX, and deposit the result into another Tempo Zone in a single operation. The [bridging specification](https://tempo.xyz/developers/docs/protocol/zones/bridging) covers deposits, withdrawals, encrypted deposits for private on-ramps, and composable withdrawal callbacks for cross-zone transfers. ### GitHub and Specifications The zones repository is available on [GitHub](https://github.com/tempoxyz/zones), which also includes the full Tempo Zones [specification](https://github.com/tempoxyz/zones/blob/main/specs/spec.md). ## Reference * [Architecture](https://tempo.xyz/developers/docs/protocol/zones/architecture) — System design, contract layout, sequencer management, and trust model. * [Accounts](https://tempo.xyz/developers/docs/protocol/zones/accounts) — Private balances, private allowances, and account-scoped access control. * [Bridging](https://tempo.xyz/developers/docs/protocol/zones/bridging) — Deposits, withdrawals, encrypted deposits, and composable withdrawal callbacks. * [RPC](https://tempo.xyz/developers/docs/protocol/zones/rpc) — Authenticated JSON-RPC interface with per-account scoping and timing protections. * [Execution & Gas](https://tempo.xyz/developers/docs/protocol/zones/execution) — Fee tokens, gas accounting, fixed gas costs, and token management. * [Proving](https://tempo.xyz/developers/docs/protocol/zones/proving) — Batch submission, validity proofs, and the state transition function. # Tempo Zone Architecture :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: A Tempo Zone is a dedicated blockchain rooted to Tempo Mainnet where one sequencer controls block production and visibility. No zone data is published on Tempo Mainnet. Instead, the sequencer publishes commitments to the current zone state along with proofs of correct execution. These proofs allow funds to move in and out of the Tempo Zone. This scaling approach is known as a validium. The sequencer can enable any TIP-20 token on the Tempo Zone. Any enabled TIP-20 with USD currency can pay for zone gas. TIP-20 tokens bridge into the zone nearly instantly and bridge out as soon as a validity proof is posted (targeting under 10 seconds). Tempo Zones are tightly integrated with Tempo Mainnet. Withdrawals to Tempo Mainnet are processed by the sequencer and can trigger transfers, trades on Tempo's Stablecoin DEX, or deposits into other Tempo Zones, without further interaction from the user. Tempo Zones are designed for applications that want safe operation guaranteed by validity proofs and privacy from the rest of the world, where users are comfortable trusting a sequencer for liveness and privacy. ## System Architecture Each Tempo Zone runs as a separate Tempo chain with its own Tempo node(s). Tempo Zones are tightly coupled with Tempo Mainnet and have direct, synchronous access to Tempo Mainnet state. Zone contracts can read certain Tempo Mainnet state without any message passing delay, such as deposit queues and TIP-403 policy information. ```mermaid flowchart TD subgraph TN["Tempo Node"] TE["Tempo Execution"] TE --> Z1["Zone 1
USDX, USDY"] TE --> Z2["Zone 2
pathUSD, ..."] end ``` The sequencer runs a Tempo node with one or more zone nodes attached. Each zone node: * Synchronizes the zone's view of Tempo Mainnet each time a Tempo Mainnet block finalizes * Executes zone transactions using privately submitted transactions and the zone's own state * Produces batches proving state transitions on the zone and posts them to Tempo Mainnet * Watches for deposits by monitoring Zone Portal events on Tempo Mainnet, and creates corresponding transactions on the zone once the block finalizes * Watches for withdrawals on the zone and submits transactions to Tempo Mainnet processing them once the batch has been proven ## Contract Architecture The system consists of contracts on both Tempo Mainnet and within each Tempo Zone. ```mermaid flowchart TD ZP["ZonePortal"] -- "deposits" --> ZI["ZoneInbox
(deposits)"] ZO["ZoneOutbox
(withdrawals)"] -- "withdrawals" --> ZP subgraph TEMPO["Tempo Mainnet"] ZF["ZoneFactory
(deploys)"] ZP ZM["ZoneMessenger
(callbacks)"] end subgraph ZONE["Zone"] TS["TempoState
(Tempo Mainnet view)"] ZI ZO end ``` ### Tempo Contracts * **`ZoneFactory`** creates zones and installs a deterministic `ZonePortal` proxy for each one. All portals use the same protocol-managed implementation, verifier, and messenger. * **`ZonePortal`** is the central bridge contract. It locks all deposited tokens, verifies validity proofs, and processes withdrawals. The Zone Portal contract maintains the authoritative state: which deposits have been made, which batches have been proven, and which withdrawals are pending. * **`ZoneMessenger`** is shared by all zones and handles withdrawals that include callbacks. When a user wants to withdraw tokens and trigger a contract call atomically, the messenger executes both operations together. If the callback fails, the entire withdrawal reverts and funds bounce back to the zone. The protocol manages the shared Tempo contracts at these addresses: | Component | Address | |-----------|---------| | `ZoneFactory` | `0x5AF2000000000000000000000000000000000000` | | `ZonePortal` implementation | `0x5AD1000000000000000000000000000000000000` | | Zone verifier | `0x5a56000000000000000000000000000000000000` | | `ZoneMessenger` | `0x5A4d000000000000000000000000000000000000` | ### Zone Predeploys Tempo Zones have four system contract predeploys at fixed addresses: | Contract | Address | Purpose | |----------|---------|---------| | `TempoState` | `0x1c00...0000` | Stores the zone's view of Tempo Mainnet. The sequencer updates this with Tempo Mainnet block headers, allowing zone contracts to read Tempo Mainnet state within proofs. | | `ZoneInbox` | `0x1c00...0001` | Processes incoming deposits. Mints tokens to recipients and validates that processed deposits match what the Zone Portal contract expects. | | `ZoneOutbox` | `0x1c00...0002` | Handles withdrawal requests. Users burn their zone tokens here and specify a Tempo Mainnet recipient. | | `ZoneConfig` | `0x1c00...0003` | Central configuration. Reads sequencer and token registry from Tempo Mainnet. | ## Creating a Zone Zone creation is currently restricted to `ZoneFactory.owner()`. The factory assigns a zone ID and initializes a deterministic `ZonePortal` proxy using the shared implementation, verifier, and messenger. See [TIP-1091](https://tips.sh/1091) for the factory specification and [T11](https://tempo.xyz/developers/docs/protocol/upgrades/t11#duplicate-checks) for role-list validation requirements. ### Chain ID Each Tempo Zone has a unique EIP-155 chain ID derived deterministically from its onchain zone ID: ``` chain_id = 421700000 + zone_id ``` The prefix `4217` corresponds to the Tempo Mainnet chain ID. This ensures replay protection between Tempo Zones. A transaction signed for one zone cannot be replayed on another. ## Sequencer management The portal admin calls `ZonePortal.setSequencerSet(newSequencers, newThreshold)` to replace the sequencer set and settlement threshold atomically. A replacement increments the configuration nonce and invalidates settlement certificates from the previous configuration. Each certificate requires at least `threshold` distinct signatures from the active set. Use the portal ABI for the runtime installed on your network when updating this configuration. The factory's `sequencers` and `threshold` fields describe the initial settlement configuration. ## Trust Model Tempo Zones make explicit tradeoffs between trust and performance: | What You Trust | What Could Go Wrong | |---|---| | Sequencer for liveness | The Tempo Zone halts if the sequencer stops. | | Sequencer for inclusion and ordering | Transactions (including withdrawals) can be excluded or reordered. | | Sequencer for privacy | The sequencer can see all transactions on the Tempo Zone. | | Sequencer for data | Reconstructing the state of the Tempo Zone without the sequencer is impossible. | | Sequencer + verifier for correctness | If a critical safety bug exists in the verifier or proving system, and the sequencer is malicious, they could exploit it to steal funds. | The sequencer cannot steal funds or forge state transitions. Validity proofs prevent this. However, the sequencer can halt the zone entirely, censor specific users, or reorder transactions for MEV. Failed withdrawals always bounce back to the zone `fallbackRecipient`, ensuring users retain their funds. TIP-403 policy changes or token pauses on Tempo Mainnet will cause affected withdrawals to bounce back rather than block the queue. # Accounts and private balances in Tempo Zones on testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Tempo Zones enforce account privacy at two complementary layers: the EVM execution level and the [RPC access control](https://tempo.xyz/developers/docs/protocol/zones/rpc) level. Neither is sufficient alone. * **Execution alone is insufficient.** Without RPC restrictions, a caller could use `eth_getStorageAt` to read TIP-20 balance mapping slots directly, bypassing `balanceOf` access control. * **RPC alone is insufficient.** Without execution-level changes, a caller could use `eth_call` to invoke a contract that reads another account's balance and returns it, bypassing RPC-level filtering. This page covers the execution-level protections. For the RPC layer, see the [RPC specification](https://tempo.xyz/developers/docs/protocol/zones/rpc). ## Private Balances On Tempo Mainnet, anyone can read any account's balance. On a Tempo Zone, `balanceOf(address)` enforces caller restrictions for TIP-20s: * If `msg.sender == account`, the call succeeds and returns the balance. * If `msg.sender` is the sequencer, the call succeeds (required for block production and fee accounting). * Otherwise, the call reverts with `Unauthorized()`. Enforcing this at the contract level (not just the RPC layer) ensures that even onchain composition cannot leak balances. A contract on the Tempo Zone cannot read and emit another account's balance. ## Private Allowances The `allowance(owner, spender)` function is similarly restricted: * If `msg.sender == owner` or `msg.sender == spender`, the call succeeds. * If `msg.sender` is the sequencer, the call succeeds. * Otherwise, the call reverts with `Unauthorized()`. A non-zero allowance reveals that `owner` has interacted with `spender`, a relationship that should be private. Restricting reads to the two parties involved preserves standard TIP-20 (ERC-20) approval flows without leaking relationship information. Public views like `totalSupply()`, `name()`, `symbol()`, and `decimals()` remain unrestricted. ## Related Specifications Tempo Zones also charge fixed gas costs for TIP-20 operations to prevent gas-based side channels. See [Execution & Gas](https://tempo.xyz/developers/docs/protocol/zones/execution#fixed-gas-costs) for details. Tempo Zones currently disable contract creation (`CREATE` and `CREATE2`). See [Execution & Gas](https://tempo.xyz/developers/docs/protocol/zones/execution#contract-creation-disabled) for details. # Zone bridging on Tempo testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: Tempo Zones use Tempo-centric bridging for cross-chain operations: deposits flow from Tempo into a zone, and withdrawals flow from a zone back to Tempo with optional callbacks for composability. ![End-to-end privacy flow through a zone](/developers/learn/zones/diagram-privacy.svg) Above is an example of the type of complex transaction that can remain privacy-preserving via Tempo Zones, while performing operations such as bridging, deposits & sends, and withdrawals. Learn more about [encrypted deposits](#encrypted-deposits) and [verifiable withdrawals](#verifiable-withdrawals) below. ## Deposits (Tempo → Zone) 1. User calls `ZonePortal.deposit(token, to, amount, memo)` on Tempo, specifying which enabled TIP-20 to deposit. 2. The Zone Portal contract validates the token is enabled and deposits are active, deducts the [deposit fee](https://tempo.xyz/developers/docs/protocol/zones/execution#deposit-fees), locks the funds, and appends a deposit to the queue. 3. The sequencer observes `DepositMade` events and processes deposits in order via `ZoneInbox.advanceTempo()`, minting the corresponding zone-side TIP-20 to the recipient. 4. A batch proof must prove the zone correctly processed deposits by validating the Tempo state read inside the proof. ### Encrypted Deposits For privacy-sensitive use cases, users can make encrypted deposits where the recipient and memo are encrypted using the sequencer's public key. Only the sequencer can decrypt and credit the correct recipient on the zone. **What's public vs. private:** | Field | Visibility | Reason | |-------|------------|--------| | `token` | Public | Needed for locked token accounting | | `sender` | Public | Needed for potential refunds if decryption fails | | `amount` | Public | Needed for onchain locked token accounting | | `to` | Encrypted | Only sequencer knows recipient | | `memo` | Encrypted | Only sequencer knows payment context | The encryption uses ECIES with secp256k1: 1. Sequencer publishes a secp256k1 encryption public key via `setSequencerEncryptionKey()` with a proof of possession. 2. User generates an ephemeral keypair and derives a shared secret via ECDH. 3. User encrypts `(to || memo)` with AES-256-GCM using the derived key. 4. User calls `depositEncrypted(token, amount, keyIndex, encryptedPayload)` on the Zone Portal contract. If decryption fails (invalid ciphertext, wrong key), the zone mints tokens to the `sender`'s address on the zone. The Tempo Mainnet funds remain locked in the Zone Portal contract. This ensures chain progress is never blocked by invalid encrypted deposits. ## Withdrawals (Zone → Tempo) Users withdraw by creating a withdrawal request on the zone. Withdrawals are processed in two steps: 1. **Batch submission.** The sequencer calls `finalizeWithdrawalBatch()` at the end of the final block in a batch. This constructs the withdrawal hash chain and writes the `withdrawalQueueHash` and `withdrawalBatchIndex` to state. The proof validates this state and adds withdrawals to Tempo's queue. 2. **Withdrawal processing.** The sequencer calls `processWithdrawal()` on Tempo to process withdrawals from the queue's oldest slot. ### Composable Withdrawals Withdrawals support callbacks to Tempo contracts via the `ZoneMessenger`. When `gasLimit > 0`, the messenger: 1. Transfers tokens from the Zone Portal contract to the target via `transferFrom`. 2. Calls the target with the provided `callbackData`. Both operations are atomic. If the callback reverts, the transfer reverts too. Receiving contracts implement `IWithdrawalReceiver` and verify `msg.sender == zoneMessenger` to authenticate calls. This enables direct composition with DEX swaps, staking, or cross-zone deposits. ```solidity interface IWithdrawalReceiver { function onWithdrawalReceived( bytes32 senderTag, address token, uint128 amount, bytes calldata callbackData ) external returns (bytes4); } ``` ### Withdrawal Failure and Bounce-Back Withdrawals can fail if the token transfer or callback reverts (out of gas, TIP-403 policy, token pause, etc.). When a withdrawal fails, the Zone Portal contract bounces back the funds by re-depositing into the same zone to the withdrawal's `fallbackRecipient`: * The withdrawal is **popped unconditionally** from the queue, even on failure. * A new deposit is enqueued for the `fallbackRecipient` on the zone. * The sequencer keeps the processing fee regardless of success or failure. This ensures failed withdrawals never block the queue and users always retain their funds. ### Verifiable Withdrawals Zone transactions are private: transaction data is not published on Tempo Mainnet. To protect sender privacy during withdrawal processing on Tempo Mainnet, the plaintext `sender` is replaced with a commitment: ``` senderTag = keccak256(abi.encodePacked(sender, txHash)) ``` The `txHash` acts as a blinding factor known only to the sender and sequencer. The sender can selectively disclose their identity by revealing `txHash` to any party, who verifies it against the `senderTag`. For automated disclosure, the sender can specify a `revealTo` public key. The sequencer encrypts `(sender, txHash)` to that key using ECDH, populating the `encryptedSender` field in the Tempo Mainnet-facing withdrawal struct. This enables cross-zone transfers where the destination zone's sequencer can automatically attribute incoming deposits. # Tempo Zone RPC access control on testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: The zone RPC starts from the standard Ethereum JSON-RPC and restricts it to enforce privacy guarantees. Every RPC request must include an authorization token that proves the caller controls a Tempo account and scopes all responses to that account. ## Authorization Tokens Authorization tokens are short-lived credentials (maximum 1 month) signed by the caller's Tempo account key. Tempo accounts support multiple signature types (secp256k1, P256, WebAuthn), and accounts with Access Keys via the `AccountKeychain` precompile can use those keys to authenticate. The signed message includes: * `"TempoZoneRPC"` magic prefix for domain separation * Spec version, zone ID, and chain ID for replay protection (zone 0 can be used to allow access to all zones) * Issuance and expiry timestamps Tokens are sent via the `X-Authorization-Token` HTTP header on every request. ## Method Access Control Each JSON-RPC method falls into one of four categories: Available to any authenticated caller: | Method | Access Type | Notes | |--------|-------------|-------| | `eth_chainId` | Allowed | Zone chain ID | | `eth_blockNumber` | Allowed | Latest block number | | `eth_gasPrice` | Allowed | Current gas price | | `eth_maxPriorityFeePerGas` | Allowed | Current priority fee | | `eth_feeHistory` | Allowed | Fee history | | `eth_getBlockByNumber` | Allowed | Block headers **without transaction details** | | `eth_getBlockByHash` | Allowed | Block headers **without transaction details** | | `eth_subscribe("newHeads")` | Allowed | Block headers with `logsBloom` zeroed | | `eth_syncing` | Allowed | Sync status | | `eth_coinbase` | Allowed | Sequencer address | | `net_version` | Allowed | Network ID | | `net_listening` | Allowed | Node status | | `web3_clientVersion` | Allowed | Client version | | `web3_sha3` | Allowed | Pure Keccak-256 hash | | `eth_getBalance` | Scoped | Returns balance for the authenticated account only. Queries for other accounts return `0x0`. | | `eth_getTransactionCount` | Scoped | Returns nonce for the authenticated account only. Other accounts return `0x0`. | | `eth_call` | Scoped | Executes with `from` set to the authenticated account. [Execution-level privacy](https://tempo.xyz/developers/docs/protocol/zones/accounts) enforces `balanceOf` access control at the contract level. | | `eth_estimateGas` | Scoped | Only allowed when `from` equals the authenticated account. | | `eth_getTransactionByHash` | Scoped | Returns the transaction only if the authenticated account is the sender. Returns `null` otherwise. | | `eth_getTransactionReceipt` | Scoped | Returns the receipt only if the authenticated account is the sender. Logs are filtered (see [Event Filtering](#event-filtering)). | | `eth_sendRawTransaction` | Scoped | Validates that the transaction sender matches the authenticated account. | | `eth_getLogs` | Scoped | Filtered to TIP-20 events where the authenticated account is a relevant party (see [Event Filtering](#event-filtering)). | | `eth_getFilterLogs` | Scoped | Same filtering as `eth_getLogs`. | | `eth_getFilterChanges` | Scoped | Same filtering. Only returns new events since last poll. | | `eth_newFilter` | Scoped | Creates a filter implicitly scoped to the authenticated account. | | `eth_subscribe("logs")` | Scoped | Subscription scoped to the authenticated account. | | `eth_newBlockFilter` | Scoped | Returns new block hashes. | | `eth_uninstallFilter` | Scoped | Removes a previously created filter. | **Error vs. silent response**: Methods where the user explicitly provides a mismatched parameter (`eth_sendRawTransaction` with wrong sender, `eth_call` with wrong `from`) return explicit errors, since the user already knows the address they supplied and the error leaks nothing. Methods that query *about* other accounts return silent dummy values (`0x0`, `null`, empty results) instead of errors; an error would reveal "this data exists but you can't see it." ### Restricted (sequencer-only) | Method | Reason | |--------|--------| | `eth_getStorageAt` | Raw storage reads bypass all access control | | `eth_getCode` | No legitimate non-sequencer use case | | `eth_createAccessList` | Reveals storage layout | | `eth_getBlockByNumber` (with `true`) | Full block with all transactions | | `eth_getBlockByHash` (with `true`) | Full block with all transactions | | `eth_getBlockTransactionCountByNumber` | Transaction counts reveal activity levels | | `eth_getBlockTransactionCountByHash` | Same as above | | `eth_getTransactionByBlockNumberAndIndex` | Arbitrary transaction access | | `eth_getTransactionByBlockHashAndIndex` | Same as above | | `debug_*`, `admin_*`, `txpool_*` | All debug, admin, and txpool namespaces | ### Disabled | Method | Reason | |--------|--------| | `eth_getProof` | Merkle proofs leak state trie structure | | `eth_newPendingTransactionFilter` | Mempool observation | | `eth_subscribe("newPendingTransactions")` | Mempool observation | | Mining-related methods | Tempo Zones have no mining | Any method not explicitly listed returns error code `-32601` (method not found), ensuring new methods are not accidentally exposed. ## Timing Side Channels Scoped methods that fetch data before checking authorization have a mandatory **100 ms minimum response time**. This ensures that `eth_getTransactionByHash` for a non-existent transaction hash and for another user's transaction have indistinguishable response times, preventing existence probing. Methods that need the speed bump: | Method | Reason | |--------|--------| | `eth_getTransactionByHash` | Must fetch the transaction to check if sender matches | | `eth_getTransactionReceipt` | Must fetch the receipt to check the sender | | `eth_getLogs` | Response time correlates with total log volume, not just the caller's logs | | `eth_getFilterLogs` | Same as `eth_getLogs` | | `eth_getFilterChanges` | Same as `eth_getLogs` | Methods that do **not** need the speed bump include `eth_getBalance` and `eth_getTransactionCount` (address checked before any data fetch), `eth_call` and `eth_estimateGas` (`from` validated before execution), and `eth_sendRawTransaction` (sender verified during decoding). ## Block Responses Block headers returned to non-sequencer callers are sanitized: * `transactions` is always an empty array. * `logsBloom` is replaced with a zero Bloom. The real Bloom filter would allow probing whether a specific address had activity in a block. * All other header fields (`number`, `hash`, `gasUsed`, `stateRoot`, etc.) are returned normally. ## Event Filtering Log queries are restricted to TIP-20 events where the authenticated account is a relevant party: | Event | Visible if | |-------|-----------| | `Transfer` | `from == caller` OR `to == caller` | | `Approval` | `owner == caller` OR `spender == caller` | | `TransferWithMemo` | `from == caller` OR `to == caller` | | `Mint` | `to == caller` | | `Burn` | `from == caller` | All other event topics (system events, role events, configuration events) are filtered out. ## Zone-Specific RPC Methods | Method | Access | Description | |--------|--------|-------------| | `zone_getAuthorizationTokenInfo` | Any authenticated | Returns the authenticated account address and token expiry | | `zone_getZoneInfo` | Any authenticated | Returns zone metadata: `zoneId`, `zoneTokens`, `sequencer`, `chainId` | | `zone_getDepositStatus` | Scoped | Returns whether deposits from a given Tempo block have been processed, filtered to the caller's deposits | ## Error Codes | Code | Message | Meaning | |------|---------|---------| | `-32001` | Authorization token required | No authorization token provided | | `-32002` | Authorization token expired | The authorization token has expired | | `-32003` | Transaction rejected | Transaction sender does not match authenticated account | | `-32004` | Account mismatch | The `from` field does not match the authenticated account | | `-32005` | Sequencer only | Method requires sequencer access | | `-32006` | Method disabled | Method is not available on zones | # Execution and gas costs within a Tempo Zone on testnet :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: This page specifies how Tempo Zones handle gas accounting, fee collection, and token management. For deposit and withdrawal flows, see the [bridging specification](https://tempo.xyz/developers/docs/protocol/zones/bridging). For balance visibility and access control rules, see the [accounts specification](https://tempo.xyz/developers/docs/protocol/zones/accounts). ## Fee Tokens Tempo Zones reuse Tempo fee units and gas accounting. Each transaction includes a `feeToken` field. Any enabled TIP-20 token with USD currency is valid for gas payment. The sequencer accepts all enabled tokens directly, so no Fee AMM is needed. ## Deposit Fees Deposits charge a fixed processing fee in the deposited token: ``` fee = FIXED_DEPOSIT_GAS × zoneGasRate ``` `FIXED_DEPOSIT_GAS` is fixed at 100,000 gas. The sequencer configures `zoneGasRate` through `ZonePortal.setZoneGasRate()`. The fee is deducted from the deposit amount and paid to the sequencer on Tempo Mainnet. ## Withdrawal Fees Withdrawals charge a processing fee in the withdrawn token: ``` fee = gasLimit × tempoGasRate ``` The user specifies `gasLimit` to cover processing and any callback execution. The sequencer configures `tempoGasRate` through `ZoneOutbox.setTempoGasRate()`. ## Fixed Gas Costs All user-facing TIP-20 transfer and approval operations cost exactly 100,000 gas. This removes gas-based information leaks tied to storage state. On a standard EVM chain, gas varies based on whether a transfer writes to a previously empty storage slot, revealing whether the recipient has received tokens before. Fixed costs eliminate that side channel. | Function | Gas Cost | |----------|----------| | `transfer(to, amount)` | 100,000 | | `transferFrom(from, to, amount)` | 100,000 | | `transferWithMemo(to, amount, memo)` | 100,000 | | `transferFromWithMemo(from, to, amount, memo)` | 100,000 | | `approve(spender, amount)` | 100,000 | System functions (`systemTransferFrom`, `transferFeePreTx`, `transferFeePostTx`) retain standard gas costs. Only restricted system callers can invoke them, so the gas side channel does not apply. ## Contract Creation Disabled Tempo Zones currently disable the `CREATE` and `CREATE2` opcodes. Each Tempo Zone runs a fixed set of system contracts and predeploys. Any transaction that attempts contract creation reverts. ## Token Management ![Policy inheritance from mainnet to zone](/developers/learn/zones/diagram-tip20.svg) The sequencer manages which TIP-20 tokens are available on a Tempo Zone: | Function | Behavior | |----------|----------| | `enableToken(token)` | Enables a TIP-20 token for bridging and gas payment. Irreversible. | | `pauseDeposits(token)` | Stops new deposits for the token. Withdrawals continue. | | `resumeDeposits(token)` | Restarts deposits for a previously paused token. | Once enabled, a token cannot be disabled. This preserves withdrawals for that token, subject to the token's own compliance policy. Tokens on the Tempo Zone use the same address as their Tempo Mainnet counterpart. `ZoneInbox` mints on deposit, `ZoneOutbox` burns on withdrawal. No mechanism exists to create new tokens on the Tempo Zone. # Planned Tempo Zone proving :::info Tempo Zones is still in early development and is available for testing purposes on Tempo Testnet only. While Tempo Zones are in this stage, expect breaking changes to the design and implementation. Do not use this in production. If you're interested in working with Tempo Labs as a design partner on the development of Tempo Zones, contact us at [tempo.xyz/contact](https://tempo.xyz/contact). ::: :::warning The zone prover is not yet live. This page describes the planned design. The prover will be added in a future release. ::: Zone settlement uses validity proofs to verify correct execution. The prover implements a pure state transition function in Rust with `no_std` compatibility, allowing it to run in both ZKVMs (SP1) and TEEs (SGX/TDX). ## Batch Submission The sequencer posts batches to Tempo Mainnet via `submitBatch` on the portal. Each batch covers one or more zone blocks and includes: | Field | Description | |-------|-------------| | `tempoBlockNumber` | Tempo block the zone committed to (from zone's TempoState) | | `recentTempoBlockNumber` | Optional recent block for ancestry proof (`0` = direct lookup) | | `blockTransition` | Zone block hash transition (`prevBlockHash` → `nextBlockHash`) | | `depositQueueTransition` | Deposit queue processing progress | | `withdrawalQueueHash` | Hash chain of withdrawals for this batch (`0` if none) | | `verifierConfig` | Opaque payload for the verifier (domain separation / attestation) | | `proof` | Validity proof or TEE attestation | The portal verifies that `prevBlockHash` matches the stored `blockHash`, calls the verifier, and on success updates `withdrawalBatchIndex`, `blockHash`, `lastSyncedTempoBlockNumber`, and adds withdrawals to the queue. ## Verifier Interface The verifier is abstracted behind a minimal interface. ZK systems and TEE attesters implement the same contract: ```solidity interface IVerifier { function verify( uint64 tempoBlockNumber, uint64 anchorBlockNumber, bytes32 anchorBlockHash, uint64 expectedWithdrawalBatchIndex, address sequencer, BlockTransition calldata blockTransition, DepositQueueTransition calldata depositQueueTransition, bytes32 withdrawalQueueHash, bytes calldata verifierConfig, bytes calldata proof ) external view returns (bool); } ``` The proof verifies that: 1. Valid state transition from `prevBlockHash` to `nextBlockHash`. 2. Zone committed to `tempoBlockNumber` via TempoState. 3. Anchor block hash matches (direct or ancestry mode). 4. `ZoneOutbox.lastBatch()` has the correct `withdrawalBatchIndex` and `withdrawalQueueHash`. 5. Deposit processing is correct (validated via Tempo state read inside proof). 6. Zone block `beneficiary` matches the registered sequencer. ## State Transition Function The prover takes a complete witness of zone blocks and their dependencies, executes the EVM state transitions, and outputs commitments for on-chain verification: ```rust pub fn prove_zone_batch(witness: BatchWitness) -> Result ``` ### Execution Flow ```mermaid flowchart TD A["Batch witness"] --> B["Verify Tempo state proofs"] B --> C["Initialize zone state from previous block hash"] C --> D{"Next zone block"} D --> E["Check parent hash and block number"] E --> F["Verify beneficiary is the sequencer"] F --> G["Execute advanceTempo system transaction if present"] G --> H["Execute user transactions via revm"] H --> I{"Final block in batch?"} I -- No --> J["Compute simplified zone block hash"] J --> D I -- Yes --> K["Execute finalizeWithdrawalBatch"] K --> L["Compute simplified zone block hash"] L --> M["Extract output commitments"] M --> N["Return batch output for verification"] ``` 1. **Verify Tempo state proofs.** Validate MPT proofs for all Tempo storage reads against Tempo state roots. 2. **Initialize zone state.** Load the zone state from the witness, binding the initial state root to the previous block hash. 3. **Execute zone blocks.** For each block: * Validate parent hash continuity and block number sequencing. * Verify beneficiary matches the registered sequencer. * Execute `advanceTempo()` system transaction (if present) to process deposits. * Execute user transactions via revm. * Execute `finalizeWithdrawalBatch()` in the final block only. * Compute the zone block hash from the simplified header. 4. **Extract output commitments.** Block hash transition, deposit queue transition, withdrawal queue hash, and last batch parameters. ### Deployment Modes **ZKVM (SP1):** The prover runs inside a ZKVM. The witness is read from the ZKVM IO, and the output is committed to the proof. **TEE (SGX/TDX):** The same function runs inside a trusted execution environment. The output is signed by the TEE attestation. ## Ancestry Proofs EIP-2935 provides access to the last ~8,192 block hashes on Tempo. If a zone is inactive longer than this window, `tempoBlockNumber` rotates out of EIP-2935, which would prevent batch submission. The solution verifies ancestry inside the ZK circuit: 1. The portal reads `recentTempoBlockNumber` hash from EIP-2935 (must be recent). 2. The prover includes Tempo headers from `tempoBlockNumber + 1` to `recentTempoBlockNumber` as witness data. 3. The proof verifies the parent hash chain: each header's parent hash must match the previous header's hash. 4. The portal verifies the constant-size proof against the recent block hash. | Mode | Condition | Behavior | |------|-----------|----------| | Direct | `recentTempoBlockNumber = 0` | Portal reads `tempoBlockNumber` hash from EIP-2935 | | Ancestry | `recentTempoBlockNumber > tempoBlockNumber` | Portal reads `recentTempoBlockNumber` hash; proof verifies parent chain | Proving time increases linearly with the block gap (each gap block adds ~1 keccak operation), but on-chain verification cost remains constant. This prevents the zone from becoming stuck after an extended downtime. ## Tempo State Access The zone accesses Tempo state via the TempoState predeploy (`0x1c00...0000`). During batch execution: 1. `ZoneInbox` calls `TempoState.finalizeTempo(header)` to advance the zone's view of Tempo. 2. System contracts read Tempo storage via `TempoState.readTempoStorageSlot()`, restricted to zone system contracts only. 3. The proof includes Merkle proofs for each Tempo account and storage slot accessed during the batch. Tempo state staleness depends on how frequently the sequencer calls `advanceTempo()`. The zone client must only finalize Tempo headers after finality to avoid reorg risk. # T11 Network Upgrade T11 extends expiring-nonce validity to five minutes and updates precompile pricing and validation. :::warning[Breaking change: integration disruption] T11 introduced stricter ABI decoding to address a denial-of-service vulnerability. Rejecting trailing bytes, including extra zeros and attribution data, was an unintended side effect. This can disrupt transfers, read-only `eth_call` requests, gas estimation, and mint/burn workflows that depend on token supply checks. Trailing bytes will be allowed again with [T12](https://github.com/tempoxyz/tempo/pull/7598); other strict ABI checks will remain. ::: :::info[T11 status] T11 is active on testnet and mainnet. [v1.14.0](https://github.com/tempoxyz/tempo/releases/tag/v1.14.0) is the required T11 release. See [Node Operator Updates](https://tempo.xyz/developers/docs/guide/node/network-upgrades#node-operator-updates) for current release guidance. ::: ## Timeline | Network | Activated (UTC) | Unix timestamp | |---------|-----------------|----------------| | Testnet | September 9, 2026 at 14:00 | `1788962400` | | Mainnet | September 10, 2026 at 14:00 | `1789048800` | ## Stricter precompile validation Calldata generated by standard ABI encoders such as Alloy and viem remains compatible when sent unchanged. No ABI migration is needed unless your integration modifies the encoding or appends trailing bytes. For custom or modified calldata, use canonical ABI encoding without gaps, overlaps, or nonzero padding. Until T12 activates on your network, remove trailing bytes and disable options that append data suffixes. Preserve required ABI padding, and move any tracking or attribution data to a supported field or application layer. Test affected reads, simulations, and transactions on the target network using the final calldata your integration sends. ### Duplicate checks T11 charges 20 gas per value processed by duplicate checks in account key permissions and Zone configuration lists. For `ZoneFactory.createZone`, addresses must be unique across `allowedAccounts`, `zoneGateways`, and `sequencers`. T11 newly rejects duplicates within `allowedAccounts` and `zoneGateways`; cross-list overlaps and duplicate sequencers were already invalid. See [Zone creation](https://tempo.xyz/developers/docs/protocol/zones/architecture#creating-a-zone) and [TIP-1105](https://tips.sh/1105). ## Updated gas pricing The precompile input charge rises from 6 to 30 gas per 32-byte word, rounded up over the full calldata including the selector. Below 1 KiB, the increase is at most 768 gas, excluding duplicate-check charges. Estimate gas on the target network and update fixed limits. See [TIP-1100](https://tips.sh/1100). ## Longer transaction submission window Expiring-nonce transactions can now set `validBefore` up to five minutes ahead, instead of 30 seconds. Shorter windows remain supported. Already-signed transactions retain their original expiry; replay protection and other nonce modes are unchanged. See [TIP-1093](https://tips.sh/1093). # T10 Network Upgrade T10 makes zone creation a native Tempo protocol operation. It enshrines `ZoneFactory`, assigns each zone a deterministic `ZonePortal` address, and installs canonical shared runtimes for portals, verification, and messaging. For most partners, T10 matters if you operate a node, create Tempo Zones, or integrate directly with `ZoneFactory` and `ZonePortal`. :::info[T10 status] T10 is active on testnet and mainnet. Release [v1.13.0](https://github.com/tempoxyz/tempo/releases/tag/v1.13.0) is required for T10; see the [Network Upgrades and Releases table](https://tempo.xyz/developers/docs/guide/node/network-upgrades#node-operator-updates) for the current node-operator release status. ::: ## Timeline | Network | Date | Unix timestamp | |---------|------|----------------| | Testnet | Live: August 20, 2026 at 14:00 UTC | `1787234400` | | Mainnet | Live: August 21, 2026 at 14:00 UTC | `1787320800` | Node operators were required to run [v1.13.0](https://github.com/tempoxyz/tempo/releases/tag/v1.13.0) before activation to stay synced. ## T10 upgrade overview T10 introduces three related protocol changes: * **Native zone creation.** `ZoneFactory` becomes a precompile at `0x5AF2000000000000000000000000000000000000`. * **Deterministic zone portals.** Every new zone receives a `ZonePortal` account whose address encodes its zone ID. * **Protocol-managed shared runtimes.** The hardfork installs canonical portal, verifier, and messenger runtimes at reserved addresses. Read the [TIP-1091 specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1091.md). ## Native `ZoneFactory` Zone creation moves from a separately deployed factory contract into the Tempo protocol. The native factory retains the canonical registry behavior, including sequential zone IDs, `zones`, `nextZoneId`, `isZonePortal`, and the `ZoneCreated` event. The initial T10 rollout is permissioned. Only the factory owner can call `createZone`; a later hardfork can open zone creation. A successful `createZone` call consumes at least 15,000,000 gas. An initial TIP-20 token must have an explicit [TIP-403 policy binding](https://tempo.xyz/developers/docs/protocol/upgrades/t9) before the factory can create a zone with it. ## Deterministic `ZonePortal` accounts Each zone receives a portal at a reserved vanity address. The high 12 bytes are the fixed prefix `0x5AD000000000000000000000`, and the low 8 bytes contain the zone ID in big-endian form. For example, zone ID `1` maps to: ```text 0x5AD0000000000000000000000000000000000001 ``` Use `ZoneFactory.isZonePortal(address)` to validate portal addresses instead of reproducing the prefix and zone-ID checks in application code. Each portal is an ERC-1167 proxy to a shared, protocol-managed implementation. Portals keep independent state while using the same canonical logic. ## Protocol-managed Zone runtimes At activation, T10 installs the factory and three shared runtimes atomically: | Component | Address | |-----------|---------| | `ZoneFactory` | `0x5AF2000000000000000000000000000000000000` | | `ZonePortal` implementation | `0x5AD1000000000000000000000000000000000000` | | Zone verifier | `0x5a56000000000000000000000000000000000000` | | Zone messenger | `0x5A4d000000000000000000000000000000000000` | # T9 Network Upgrade T9 records each TIP-20 token's active transfer policy in TIP-403. This is necessary for [Tempo Zones](https://tempo.xyz/developers/docs/protocol/zones#tempo-zones-are-private): zones keep balances, transfers, and account relationships private, but they still need a provable way to know which issuer policy applies to a token. The same registry binding also gives provable contract flows, apps, indexers, and other tooling a single place to check which policy a token is using. In simpler terms, T9 moves the answer to "which transfer rules apply to this token?" into TIP-403, where it can be checked from registry state. For most partners, T9 is only relevant if you issue TIP-20 tokens, run tooling that reads token policy state, or build zone/provable contract flows that depend on TIP-403 policy checks. :::info[T9 status] T9 is active on testnet and mainnet. Release [v1.12.0](https://github.com/tempoxyz/tempo/releases/tag/v1.12.0) is required for T9; see the [Network Upgrades and Releases table](https://tempo.xyz/developers/docs/guide/node/network-upgrades#node-operator-updates) for the current node-operator release status. ::: ## Timeline | Milestone | Date | |-----------|------| | Release published | August 3, 2026 | | Testnet rollout | Live: August 5, 2026 | | Mainnet rollout | Live: August 6, 2026 | Node operators were required to run [v1.12.0](https://github.com/tempoxyz/tempo/releases/tag/v1.12.0) before activation to stay synced. ## Overview T9 has one protocol change: * **TIP-20 policy IDs in TIP-403.** TIP-403 can record which transfer policy a TIP-20 token is currently using. That registry binding is required for zone token enablement and useful for tooling that needs the TIP-403 view. For zones, a ZonePortal can check the TIP-403 binding before enabling a token instead of relying on token-local state. ### Why this matters for zones Tempo Zones keep balances, transfers, and account relationships private. At the same time, tokens in a zone still need to follow the issuer's transfer policy. T9 gives zones a registry-backed way to prove which policy applies to each token before that token is enabled in the zone. ## What changes ### Token policy lookup in TIP-403 TIP-403 already stores transfer policy data. With T9, it can also record which transfer policy a TIP-20 token is using. New TIP-20 tokens write this binding when they are created. Policy changes after T9 keep the binding up to date. Existing tokens only need targeted migration when they need to be enabled in a zone, used by a provable contract flow, or read by tooling that needs their policy ID to be available from TIP-403. Read the [specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1092.md). ### Targeted migration T9 adds a migration path for individual existing TIP-20 tokens. The migration copies the token's current local policy ID into TIP-403. It does not change the token's policy or rules, and it can be run only for the tokens that need the TIP-403 binding. For zone enablement, ZonePortal checks whether the token already has a TIP-403 binding. If it is missing, ZonePortal migrates that token, checks again, and rejects the token if the binding is still missing. ## Compatible release Release notes and binaries are available in the [v1.12.0 release](https://github.com/tempoxyz/tempo/releases/tag/v1.12.0). ## Integration impact ### For TIP-20 issuers and token admins * Migrate an existing token when it needs to be enabled in a zone, used by a provable contract flow, or read by tooling that needs its policy ID from TIP-403. * After activation, policy updates write the TIP-403 binding for the updated token. ### For zones, provable contract flows, and tooling * Treat the TIP-403 binding as required before enabling a token in a zone or using it in a provable contract flow. * Use TIP-403 when your flow needs the registry view of token policy state. * If the binding is missing, migrate the specific token and verify that TIP-403 has the binding before continuing. ### For migration tooling * Migrate only the specific tokens that need a TIP-403 binding. * Batch token lists and verify each binding after migration. # T8 Network Upgrade T8 focuses on clearer validator committee reads, FeeAMM policy behavior, DEX order storage, and the final TIP-20 rewards shutdown. :::info[T8 status] T8 is active on testnet and mainnet. Release [v1.11.0](https://github.com/tempoxyz/tempo/releases/tag/v1.11.0) is required for T8; see the [Network Upgrades and Releases table](https://tempo.xyz/developers/docs/guide/node/network-upgrades#node-operator-updates) for the current node-operator release status. ::: ## Timeline | Network | Date | |---------|------| | Testnet | Live: July 27, 2026 | | Mainnet | Live: July 30, 2026 | Node operators were required to run [v1.11.0](https://github.com/tempoxyz/tempo/releases/tag/v1.11.0) before activation to stay synced. ## Overview T8 focuses on four infrastructure-facing changes: * **Canonical current committee reads.** Contracts and offchain tools can query the committee that consensus is actually using for the current epoch. * **Cleaner fee collection behavior.** Fee collection avoids a redundant FeeManager policy check while public AMM actions remain policy-controlled. * **DEX order storage updates.** New DEX orders use versioned storage, existing orders remain readable, and the Stablecoin DEX supports the compact V2Order layout. * **Rewards cleanup.** T8 completes the TIP-20 rewards shutdown that began in T7. Settled rewards remain claimable, but ordinary balance changes no longer checkpoint rewards after T8. ## Features ### Current committee state T8 adds execution-layer state for the current effective validator committee. In plain terms, this is the committee that consensus is actually using for the current epoch, based on the DKG outcome. This matters because `ValidatorConfigV2.getActiveValidators()` returns the configured validator registry, not necessarily the committee currently active in consensus. Registry changes can happen before they become part of the effective committee, and a failed DKG round can leave the prior committee in place. The new `getCommitteeMembers()` query gives contracts and offchain tools a canonical source for current committee membership, without requiring them to reconstruct it from consensus data or epoch-boundary block data. Read the specification [here](https://tips.sh/1070). ### FeeAMM policy exemptions The FeeAMM and FeeManager help users pay transaction fees in one token while validators receive another. T8 adjusts where token policy checks happen in that flow. During protocol fee collection, the FeeManager address is no longer checked as the recipient. The fee payer is still checked as an authorized sender. This keeps fee collection simpler for block builders and avoids repeating a recipient check on every transaction. Public AMM operations remain policy-controlled. `mint`, `burn`, `rebalanceSwap`, and `distributeFees` continue to enforce the relevant token policies, and `mint` / `burn` gain extra checks that tie authorization to the liquidity provider across the lifecycle of a pool position. Token issuers still control whether their token participates in FeeAMM liquidity by authorizing the FeeManager address where required. Read the specification [here](https://tips.sh/1042). ### DEX order storage T8 introduces a versioned Stablecoin DEX order storage layout. Existing orders stay in the legacy layout and remain readable. New orders use the versioned layout, which stores the same active order state with fewer storage slots. The public `getOrder(uint128)` shape stays compatible. Indexers that reconstruct DEX state from events should not need to change for this storage layout. Tools that read raw DEX storage directly must decode orders by version. The Stablecoin DEX also supports a compact V2Order layout. V2Order stores an orderbook index instead of repeating the full book key on each order. For raw-storage tooling, the main savings are structural: version-1 order records reduce active order storage from six slots to four, and version-2 indexed order records reduce indexed orders from four slots to three. Read the versioned order storage specification [here](https://tips.sh/1062). ### TIP-20 rewards final shutdown T8 completes the reward shutdown that started in T7. After T8 activation, ordinary balance-changing paths such as transfers, mints, burns, and fee refunds stop checkpointing reward accumulators. Rewards that were settled before T8 remain claimable. Lazy rewards that were not checkpointed before activation are forfeited, so rewards integrations should not rely on a post-T8 transfer or balance change to settle older lazy accruals. Read the rewards deprecation specification [here](https://tips.sh/1075). ## Compatible releases The T8-compatible release is published. | Ecosystem | T8-compatible releases | |-----------|------------------------| | Node operators | [v1.11.0](https://github.com/tempoxyz/tempo/releases/tag/v1.11.0) | | Rust SDK crates | `tempo-alloy@1.10.1`, `tempo-primitives@1.10.1`, `tempo-contracts@1.10.1`, `tempo-chainspec@1.10.1`, `tempo-hardfork@1.10.1` | Release notes and binaries are available in the [v1.11.0 release](https://github.com/tempoxyz/tempo/releases/tag/v1.11.0). ## Integration impact ### For validators, monitoring, and contracts * Use `getCommitteeMembers()` when you need the committee consensus is using now. * Keep using `ValidatorConfigV2` when you need the configured validator registry. * Do not treat `getActiveValidators()` as the current effective committee. * After activation on each network, wait for the first epoch-boundary committee update before relying on the new query. ### For FeeAMM, token issuer, and validator tooling * Fee collection no longer checks the FeeManager address as the recipient. * The fee payer is still checked as an authorized sender. * AMM actions remain policy-controlled. * `mint` and `burn` have extra checks that prevent blocked accounts from using liquidity positions to bypass token policies. * If a token does not authorize the FeeManager where required, some fees or liquidity flows may be unavailable or stranded; validator and issuer tooling should surface that clearly. ### For DEX frontends and indexers * Event-driven order indexing should continue to work. * `getOrder(uint128)` remains ABI-compatible. * Raw storage readers must support legacy version `0` orders, compact version `1` orders, and indexed version `2` orders. * Mixed-version order lists are expected, so storage tooling should update each order according to that order's own version. * Existing orderbooks are not automatically scanned or migrated to indexed storage. They continue writing version-1 orders until offchain migration tooling supplies the verified index through `setBookIndex(uint32)`. ### For TIP-20 rewards integrations * Rewards settled before T8 remain claimable. * Ordinary balance changes stop checkpointing reward accumulators after T8. * Do not rely on a post-T8 transfer, mint, burn, or fee refund to settle older lazy rewards. # T7 Network Upgrade T7 makes repeated onchain workflows cheaper. It adds storage savings for DEX order state and TIP-20 payment-channel state, lets the base fee move down when network usage is low, and deprecates new TIP-20 rewards activity. Apps with repeat contract workflows can pass meaningful gas savings to returning users, MPP sessions can reuse channel-state savings for the same payer, and all users can benefit from lower base fees during quieter network periods. :::info[T7 status] T7 is active on testnet and mainnet. Release [v1.10.1](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1) is required for T7; see the [Network Upgrades and Releases table](https://tempo.xyz/developers/docs/guide/node/network-upgrades#node-operator-updates) for the current node-operator release status. ::: ## Timeline | Milestone | Date | |-----------|------| | Testnet rollout | Live: July 2, 2026 | | Mainnet rollout | Live: July 9, 2026 | ## Overview T7 focuses on five partner-facing changes: * **Reusable storage savings.** Contracts that create storage, clear it, and later create storage again in the same contract can lower the cost of the next eligible storage write. * **Savings tied to the right user.** Shared systems such as the StablecoinDEX can keep those savings attached to the maker who earned them, instead of letting the next user spend them by accident. * **Payment-channel savings.** MPP payment channels can keep storage credits attached to the payer who earned them, so repeated session lifecycles can reuse channel-state savings. * **Lower fees during quiet periods.** The base fee can fall when block gas usage is below the target threshold. * **Rewards cleanup.** New Tempo Token Rewards opt-ins and distributions stop after activation, while already-accrued rewards remain claimable. These changes make costs easier to reason about for partners building high-throughput payment, liquidity, and exchange experiences. The main opportunity is to identify workflows with temporary state and decide whether the contract should allocate storage-credit savings per user, payer, maker, or account. ## Why it matters | Partner impact | What changes | |----------------|--------------| | Active DEX makers can benefit on repeat order placement | Maker-attributed storage credits let reusable order-storage savings stay attached to the maker who earned them. | | Apps can avoid random gas discounts | Shared contracts can track which user earned credits and spend them only for that same user. | | MPP sessions can get cheaper over repeated channel lifecycles | The credited reopen path, `open_new_channel_with_storage_credit`, is 60,225 gas in the channel-reserve gas snapshot. | | All integrators get a simpler low-fee story | The T7 base fee cap is 40% lower than the pre-T7 fixed base fee, and quiet periods can be up to 20x cheaper than the cap. | ## Features ### Reusable storage savings Storage credits lower fees for workflows that repeatedly create and clear temporary storage. A contract earns a credit when it clears eligible storage, then can use that credit to reduce the cost of creating eligible storage later. This is not a blanket gas discount. It matters most when a workflow has a natural lifecycle: create state, clear it, then create more state in the same contract. ### User-attributed DEX savings The StablecoinDEX uses the same storage-credit idea but adds maker-level accounting. If a maker cancels or fully fills an eligible order, the DEX can keep the reusable order-storage savings attached to that maker. When the same maker places a later eligible order, the DEX can apply those savings. ### Payer-scoped payment-channel savings T7 applies the storage-credit pattern to MPP payment channels through the `TIP20ChannelReserve` precompile. When a payer closes or withdraws a finished channel, the reserve records a channel storage credit for that payer. When the same payer opens a later channel, the reserve uses that payer's credit. This keeps channel savings attached to the payer who earned them. A different payer cannot spend another payer's channel credit. For MPP partners, the benefit is focused on repeated session lifecycles. Per-request vouchers already stay off-chain; T7 can lower the net onchain lifecycle cost when a close or withdraw is followed by a later open from the same payer. The channel-reserve gas snapshot for the credited reopen path is below. | Channel reserve snapshot | T7 gas | |--------------------------|-------:| | `open_new_channel_with_storage_credit` | 60,225 | Read [Accept pay-as-you-go payments](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) for the MPP session flow. ### Dynamic base fee T7 replaces the fixed base fee with a bounded dynamic base fee. The cap is 40% lower than the pre-T7 fixed fee. When block gas usage is below target, the base fee can fall toward a floor that is one twentieth of the cap. For a simple fee example, a 50,000 gas transfer costs about $0.0006 at the new cap and about $0.00003 at the floor. | Example transaction | Pre-T7 fixed fee | T7 cap | T7 quiet-period floor | |---------------------|------------------:|-------:|----------------------:| | 50,000 gas transfer | $0.0010 | $0.0006 | $0.00003 | | 1,000,000 gas transaction | $0.0200 | $0.0120 | $0.0006 | At the quiet-period floor, the same transaction is 20x cheaper than the T7 cap and about 33x cheaper than the pre-T7 fixed fee. After activation, the base fee moves with block usage within the T7 cap and floor. ### Deprecate TIP-20 rewards T7 deprecates new Tempo Token Rewards opt-ins and reward distributions so partners do not need to model new reward accrual after the upgrade. Already-accrued rewards remain claimable through the existing `claimRewards()` flow. Apps that show reward information should keep already-accrued rewards separate from post-T7 balances, and should stop presenting new rewards as accruing from post-T7 reward distributions. ## Technical references | Feature | What changes | Reference | |---------|--------------|-----------| | Reusable storage savings | Contracts can earn credits when they clear eligible storage and use those credits to lower later storage-creation gas | [TIP-1060: Storage Credits](https://tips.sh/1060) | | User-attributed DEX savings | The DEX can keep reusable order-storage credits attached to the maker who earned them | [TIP-1064: StablecoinDEX Order Storage Credits](https://tips.sh/1064) | | Payer-scoped payment-channel savings | MPP channel credits stay attached to the payer who earned them and can be reused by that payer on a later channel open | [TIP-1066: TIP-20 Channel Storage Credits](https://tips.sh/1066) | | Dynamic base fee | The base fee can move within a bounded range instead of staying fixed | [TIP-1067: Dynamic Base Fee](https://tips.sh/1067) | ## Partner benefits | Partner type | What T7 can help with | |--------------|-----------------------| | DEX makers and liquidity providers | Lower gas when a maker cancels or fully fills orders, then places eligible orders later | | MPP and pay-as-you-go providers | Lower onchain channel lifecycle costs when the same payer closes or withdraws a channel, then opens another channel later | | Shared contract developers | A clear pattern for passing storage savings to the user who earned them | | Wallets, checkout teams, and consumer apps | Lower base fees during periods of low network usage | | Rewards integrators | Clear migration timing for stopping new Tempo Token Rewards activity | ## Benchmark highlights The [v1.10.1 release notes](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1) include the headline gas benchmarks below. Treat these as protocol-level benchmarks: they show where T7 lowers base fees and storage-related costs, while partner-specific flows should still be tested end to end. The benchmark values are listed in the table below. | Area | T6 / before | T7 / after | What changes | |------|-------------|------------|--------------| | Base-fee ceiling for a 50,000 gas transfer | $0.001 | Cap $0.0006; quiet-period floor $0.00003 | The cap is 40% lower, and the floor is 20x below the cap. | | Credited storage creation (`SSTORE 0 -> x`) | 250,000 gas | 5,000 residual + up to 245,000 creditable gas | When credits are available, most of the storage-creation cost can be offset. | | Credited channel reopen (`open_new_channel_with_storage_credit`) | n/a | 60,225 gas | Payer-scoped credit path for repeated MPP sessions. | | Observed transfer-like costs before T7 | Average $0.0037857 and median $0.0011855 across 1,000 transactions; average $0.0008975 and median $0.0007657 across 598 steady-state transfers | T7 lowers the per-gas component through the new cap and floor | Useful baseline context, not a post-T7 production average. | ## Compatible releases The following releases support the T7 feature set: | Ecosystem | T7-compatible releases | |-----------|------------------------| | Node operators | [`v1.10.1`](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1) | | Rust | `tempo-alloy@1.8.1`, `tempo-primitives@1.8.1`, `tempo-contracts@1.8.1`, `tempo-chainspec@1.8.2` | Release notes and binaries are available in the [v1.10.1 release](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1). ## Integration impact ### For storage savings * Storage credits are included for the core primitive, DEX order storage, and TIP-20 channel storage. * Shared contracts should avoid global credit pools when savings should stay attached to a specific user, maker, payer, or account. ### For dynamic fees Wallets, checkout flows, and infrastructure that display fees should expect the base fee to move instead of staying fixed. * Fees can fall when block gas usage is below target and rise back toward the cap when usage increases. * Fee estimators should handle movement between the floor and cap. * Fee analytics should compare pre-T7 fixed-fee periods separately from post-T7 dynamic-fee periods. ### For TIP-20 rewards deprecation Apps that show TIP-20 reward information should stop presenting new rewards as accruing from post-T7 reward distributions. * Wallets and dashboards should separate already-accrued rewards from post-T7 balances. # T6 Network Upgrade T6 gives partners more control over account safety and user key management. It adds account-level receive policies so users can prevent unwanted TIP-20 deposits to their account, and it adds admin access keys so users and apps can manage passkeys and delegated keys without relying on the root wallet for every change. :::info[T6 status] T6 is active on both testnet and mainnet. Testnet activation occurred on June 18, 2026 4pm CEST. Mainnet activation occurred on June 23, 2026 4pm CEST. ::: ## Timeline | Network | Date | Timestamp | |---------|------|-----------| | Testnet | June 18, 2026 4pm CEST | 1781791200 | | Mainnet | June 23, 2026 4pm CEST | 1782223200 | Node operators were required to run the T6-compatible release before their network's activation timestamp to stay synced. ## Overview T6 focuses on two partner needs: * **Safer receiving flows.** Accounts can define what tokens they are willing to receive, and which addresses they are willing to receive from. This is especially useful for accountable receivers: institutions, exchanges, custodians, on/off-ramps, etc. who need to prevent deposits of unsupported or undesirable tokens, and/or control over what counterparties they receive from. * **Better key management.** Admin access keys make passkey onboarding, device recovery, delegated access, and account-controlled signing flows easier to build. Both features are designed to improve user experience without requiring partners to replace their current account model. Existing TIP-20 behavior and existing non-admin access keys continue to work, while partners can adopt the new controls where they add value. ## Features ### Account-level receive policies Receive policies let an account specify which TIP-20 tokens it wants to receive and which addresses can send tokens to it. This is useful for users such as exchanges, custodians, on/off-ramps, payment processors, and treasury systems that need to keep unsupported or unwanted assets out of user balances, or limit who can send to it. An account wishing to opt-in to having a receive policy configures three things: | Setting | What it controls | |---------|------------------| | Tokens | An allowlist or blocklist of TIP-20 tokens the account can receive | | Senders | An allowlist or blocklist of addresses that can send to the account | | Recovery authority | Who can move funds if a send is held | A sender still starts a normal stablecoin transfer or mint and does not need to be aware of the policy. If the receiver's policy accepts that token and sender, the funds are credited normally. If the receiver's policy does not accept them, the funds are held in a new protocol precompile, `ReceivePolicyGuard`, and a receipt records enough context for later recovery. That means partners can offer a clearer recovery path for blocked transfers while still protecting the receiving account. Practical benefits: * Deposit addresses can accept only the assets they are meant to handle. * Wallets can help users avoid receiving unsupported or unwanted tokens (e.g., memecoins). * Regulated or risk-managed accounts can restrict accepted senders. * Embedded-wallet and wallet-as-a-service providers can expose receive policies as business controls for their customers. * Support teams can surface blocked transfers for recovery instead of treating every mismatch as a lost deposit. Try the [receive policies demo](https://tempo.xyz/receive-policies) to see the credited, held, and recovery flow. Read the [technical specification](https://tips.sh/1028). ### Admin access keys Admin access keys let an account delegate key management without requiring the root wallet for every key-management action. This helps partners build smoother passkey and delegated-access flows while keeping key management tied to the user's account. Practical benefits: * A user can add a new device or passkey from an existing admin key. * Wallets and wallet-as-a-service providers can show a clear distinction between root keys, admin keys, and limited access keys. * Apps can support account-controlled admin signing checks onchain. * Recovery and delegated-access flows can reduce dependence on the root EOA. * Contracts can use canonical keychain verification through `verifyKeychain` and `verifyKeychainAdmin`. Admin keys are only for key management and admin verification. They can authorize and revoke other keys, but they cannot carry spending limits, call scopes, or expiry settings. Read the [technical specification](https://tips.sh/1049). ## Compatible releases The following releases support the T6 feature set: | Ecosystem | T6-compatible releases | |-----------|------------------------| | Node operators | [`v1.9.0`](https://github.com/tempoxyz/tempo/releases/tag/v1.9.0) | | Rust | `tempo-alloy@1.9.0`, `tempo-primitives@1.9.0`, `tempo-contracts@1.9.0`, `tempo-chainspec@1.9.0` | ## What operators and integrators should know T6 is mostly additive. Integrators, indexers, wallets, and partner infrastructure should review the notes below when supporting T6 behavior. ### For receive policies Wallets, explorers, and indexers should treat held sends as their own delivery state. In user interfaces, distinguish: * **Failed:** the normal token checks failed and the transaction reverted. * **Credited:** the receiver accepted the send and received the funds. * **Held:** the transaction succeeded, but the receiver's policy held the funds for recovery instead of crediting the receiver. For receivers and integrators, the operating flow is: 1. Decide which tokens and senders the account should accept. 2. Choose who can recover held funds. 3. Set the receive policy on the account, or on the master account for virtual-address deposit flows. 4. Warn users before sends that are likely to be held. 5. Index blocked events and store the receipt fields. 6. Review, claim, or reroute held receipts when needed. * Receive policies are configured on the TIP-403 precompile with `setReceivePolicy(...)` and enforced through `validateReceivePolicy(...)`. The token's existing TIP-403 policy still runs first and still reverts on failure. * Blocked inbound `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `systemTransferFrom`, `mint`, and `mintWithMemo` calls still succeed, but delivery is redirected to `ReceivePolicyGuard` at `0xB10C000000000000000000000000000000000000`. * Senders observing a successful `Transfer` event from the host TIP-20 to `0xB10C000000000000000000000000000000000000` should not assume the receiver was credited. Wallets and accounting systems should read the guard receipt to track redirected funds. * Blocked-transfer receipts are not enumerable onchain. Wallets and dashboards that surface claimable funds need indexers that listen for the `TransferBlocked` event emitted by `ReceivePolicyGuard`. * Virtual addresses are resolved to the master address before any receive-policy check. Receipts are recorded against the master while preserving the original `to` for attribution. * `approve`, `permit`, and `burn` are unaffected. Fee deposits and refunds (`transfer_fee_pre_tx` / `transfer_fee_post_tx`) are also unaffected. ### For admin access keys Wallets, account dashboards, SDKs, and contracts should show admin keys as a distinct account-management role. In user interfaces, distinguish: * **Root key:** the account's root EOA key. * **Admin key:** an access key that can manage other keys and satisfy admin verification checks. * **Limited access key:** an access key scoped to spending limits, allowed calls, or expiry. For account and app integrators, the operating flow is: 1. Show whether each key can manage other keys. 2. Make admin-key creation and revocation visible in account activity. 3. Avoid presenting admin keys as spending-limited keys; they are for key management, not scoped spending. 4. Use `verifyKeychainAdmin` when a contract needs "root key or admin key of this account signed this" semantics. 5. Domain-separate any digest passed to `verifyKeychainAdmin` with context such as chain ID, contract address, and account address. * The `AuthorizedKey` packed storage slot gains a new `is_admin` boolean at byte 11. * Admin keys cannot carry spending limits, call scopes, or expiry. Authorizations that combine `admin = true` with non-default `enforceLimits`, `allowedCalls`, or `expiry` are rejected. * `AccountKeychain` adds `authorizeAdminKey(keyId, signatureType, witness)` for provisioning admin keys. The existing `KeyAuthorized` event is unchanged, and a new `AdminKeyAuthorized(account, publicKey)` event is emitted alongside it when `admin = true`. * Signature verification through `SignatureVerifier` adds `verifyKeychain(account, digest, sig)` and `verifyKeychainAdmin(account, digest, sig)`. These methods combine signature recovery with key-status checks. * The RLP `KeyAuthorization` encoding gains an optional trailing `is_admin` flag and `account` field in the signed payload. SDK encoders and decoders should update in lockstep. * `verifyKeychainAdmin` does not bind the `account` argument into the signed `digest`. Callers should include a replay domain, such as chain ID, contract address, and account address, in the digest they ask users or keys to sign. # T5 Network Upgrade T5 is Tempo's latest network upgrade. It lowered costs for session-based payments, improved DEX flip-order tracking, simplified fee-token routing, added on-chain token logos, and added witness binding for key authorization flows. :::info[T5 status] T5 is live on testnet and mainnet. ::: ## Timeline | Network | Date | Timestamp | |---------|------|-----------| | Testnet | June 3, 2026 4pm CEST | 1780495200 | | Mainnet | June 9, 2026 4pm CEST | 1781013600 | Mainnet node operators needed to upgrade to the T5-compatible release (v1.8.0) before the mainnet activation timestamp. ## Overview T5 focuses on four integration areas, plus a storage correctness fix: * **Cheaper payment sessions.** The `TIP20ChannelReserve` precompile gives MPP and other session-based payment apps a protocol-native reserve path. * **Clearer DEX state.** Flip orders can use the same tick on both sides and keep the same `orderId` after each flip. * **Simpler fee-token liquidity.** FeeAMM routing can use two hops, so issuers usually do not need a direct pool against every validator payout token. * **Better token and key metadata.** TIP-20 tokens can expose an on-chain `logoURI`, listed precompiles can pull TIP-20 tokens without a separate approval, and key authorizations can include an app-defined witness digest. * **Storage correctness fix.** Shrinking writes to dynamic precompile storage clear their stale tail slots at the T5 hardfork boundary. Most T5 changes are additive: existing TIP-20 tokens, MPP contracts, and non-flip DEX flows continue to work. ## Features ### Enshrined TIP-20 reserve channel The reserve precompile lives at [`0x4D50500000000000000000000000000000000000`](https://explore.tempo.xyz/address/0x4D50500000000000000000000000000000000000) (ASCII `MPP`). It replaces the application-level MPP reserve contract for new integrations, while the existing contract continues to work. Measured gas savings versus the legacy reserve contract: | Operation | Legacy contract | Enshrined reserve precompile | Gas reduction | |---|---:|---:|---:| | Open channel, existing reserve balance | 1,055,229 | 294,425 | 72% | | Open channel, first reserve balance | 1,302,429 | 791,625 | 39% | | Close existing channel | 85,118 | 62,913 | 26% | | Top up existing channel | 53,724 | 46,805 | 13% | | Top up and cancel close request | 58,785 | 48,680 | 17% | These numbers cover only the channel operation itself. Under the legacy path, first-time users also had to send a separate `approve` transaction before opening a channel. With the precompile on the Implicit Approvals List, that approval round trip and allowance storage write are removed. Read the [technical specification](https://tips.sh/1034). ### Payment lane classification T5 moved payment-lane eligibility from local builder policy into consensus. The allow-list covers TIP-20 calls and the new `TIP20ChannelReserve` precompile, so reserve-channel transactions are classified consistently across the network. Read the [technical specification](https://tips.sh/1045). ### DEX flip-order improvements The upgrade changed flip orders in two ways: * `flipTick == tick` is valid. * A filled flip order keeps the same `orderId` and emits `OrderFlipped` instead of creating a new order with `OrderPlaced`. This makes a market-making strategy easier to track over time. Indexers should treat `OrderFlipped` as the latest active state for that `orderId`. Read the technical specifications for [same-tick flip orders](https://tips.sh/1030) and [keeping order IDs across flips](https://tips.sh/1056). ### Multihop FeeAMM routing FeeAMM can now route through two pools when there is no usable direct pool between the user's fee token and the validator's payout token. For token issuers, this usually means pairing against one liquid quote token, such as `pathUSD`, instead of provisioning direct pools against every validator payout token. Read the [technical specification](https://tips.sh/1033). ### Optional on-chain logoURI TIP-20 tokens can expose an optional `logoURI` field. Wallets and explorers can read the official token icon directly from the token contract instead of relying only on the tokenlist registry. The field is capped at 256 bytes, and non-empty values must use an allowed URI scheme: `https`, `http`, `ipfs`, or `data`. Read the [technical specification](https://tips.sh/1026). ### Implicit approvals Listed protocol precompiles can pull TIP-20 tokens without a prior `approve`. This removes an extra wallet prompt and avoids the allowance storage write for native flows such as DEX orders, DEX swaps, FeeAMM fee collection, and reserve-channel operations. The internal transfer path is not part of the public TIP-20 ABI and cannot be called by EOAs or external contracts. It still enforces balance checks, TIP-403 transfer policies, AccountKeychain spending limits, and emits the standard TIP-20 `Transfer` event. Read the [technical specification](https://tips.sh/1035). ### Witness digest in key authorizations `key_authorization` now supports an optional `witness: bytes32` field. Apps can bind one key-authorization signature to an application challenge, which removes the need for a separate challenge signature in login or delegated-access flows. Read the [technical specification](https://tips.sh/1053). ### Storage correctness fix T5 gates one precompile storage fix at the hardfork boundary. Overwriting a `Vec`, `String`, or `Bytes` value with a shorter one used to leave the trailing slots populated, so reads past the new length returned stale data. T5 clears those slots on shrinking writes. This changes storage state and gas at the activation boundary, but no public ABI changes and no integrator action is required. Read the [technical specification](https://tips.sh/1057). ## Compatible SDK releases The following releases support the full T5 feature set. New integrations should prefer the `TIP20ChannelReserve` precompile over the legacy MPP reserve contract. | Ecosystem | T5-compatible releases | | ---------- | ---------------------- | | TypeScript | [`mppx@0.7.0`](https://github.com/wevm/mppx/releases/tag/mppx%400.7.0), [`viem@2.52.2`](https://github.com/wevm/viem/releases/tag/viem%402.52.2) | | Rust | [`tempo-alloy@1.8.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.8.0), [`tempo-primitives@1.8.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-primitives%401.8.0), [`tempo-contracts@1.8.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-contracts%401.8.0) | | Foundry | [nightly](https://getfoundry.sh) (T5 hardfork-aware decoding) | ## What operators and integrators should know T5 is mostly additive. Integrators, indexers, wallets, explorers, and partner infrastructure should review the notes below. ### For MPP and payment-session integrators * Keep supporting the legacy reserve contract during the transition. * Prefer the `TIP20ChannelReserve` precompile for new SDK releases. * Update monitoring to recognize both reserve surfaces. * Show channel-open, top-up, and close flows from the precompile path. ### For DEX indexers and frontends * Index `OrderFlipped`. * Treat `OrderFlipped` as the active state for the same `orderId`. * Do not assume a filled flip order receives a new `orderId`. * Remove checks that reject `flipTick == tick`. * See the [flip-order indexing notes](https://tempo.xyz/developers/docs/protocol/exchange/providing-liquidity#flip-order-indexing). ### For FeeAMM integrators * Account for two-pool routes when quoting or explaining fee conversion. * Show route availability from direct pools and multihop paths. * Make clear that one conversion may reserve and consume liquidity from two pools. ### For token issuers, wallets, and explorers * Read `logoURI()` directly from TIP-20 contracts when available. * Continue using the tokenlist for richer metadata and fallback icons. * Watch `LogoURIUpdated(address indexed updater, string newLogoURI)`. * Use square, single-frame PNG or WebP images for token icons. ### For key-management and auth flows * Add the optional `witness` field to key-authorization encoding and decoding. * Include the witness in signing and verification flows. * Treat the witness as opaque application data. # T4 Network Upgrade This page summarizes T4 scope. :::info[T4 status] T4 is active on both testnet and mainnet. ::: ## Timeline | Network | Date | Timestamp | |---------|------|-----------| | Testnet | May 14, 2026 4pm CEST | `1778767200` | | Mainnet | May 18, 2026 4pm CEST | `1779112800` | Node operators needed to upgrade to the T4-compatible release (v1.7.0, released May 11, 2026 4pm CEST) before the testnet activation timestamp. ## Overview T4 introduced the following changes: * [Consensus context in block headers](https://tips.sh/1031) to unlock deferred verification and reduce finalization latency * [T4 network upgrade](https://tips.sh/1046) bug fixes and security hardening ## Compatible SDK releases | SDK | T4-compatible release | |-----|-----------------------| | [Rust](https://github.com/tempoxyz/tempo) | [`tempo-alloy@1.7.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.7.0), [`tempo-primitives@1.7.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-primitives%401.7.0), [`tempo-contracts@1.7.0`](https://github.com/tempoxyz/tempo/releases/tag/tempo-contracts%401.7.0) | | [Foundry](https://github.com/foundry-rs/foundry) | nightly (T4 hardfork-aware decoding) | See [Developer tools](https://tempo.xyz/developers/docs/quickstart/developer-tools) for the broader SDK ecosystem. ## Related docs * [Network upgrades and releases](https://tempo.xyz/developers/docs/guide/node/network-upgrades) * [Consensus context in block headers](https://tips.sh/1031) * [T4 network upgrade](https://tips.sh/1046) ## Feature TIPs ### Consensus context in block headers The [specification](https://tips.sh/1031) added an optional `Context` field (epoch, view, parent view, leader) as the last field of `TempoHeader`. This commits consensus metadata to the block hash so Tempo blocks implement Commonware's `CertifiableBlock` trait, enabling deferred verification (optimistic notarization with async background verification) and reduced finalization latency. Pre-T4 headers are unchanged; post-T4 headers require the field, and validators reject mismatched context. # T3 Network Upgrade T3 introduced enhanced access keys, a standard signature verification precompile, and virtual addresses for TIP-20 deposit routing. :::info[T3 status] T3 is active on both testnet and mainnet. ::: ## Timeline | Network | Date | Timestamp | |---------|------|-----------| | Testnet | April 21, 2026 4pm CEST | `1776780000` | | Mainnet | April 27, 2026 4pm CEST | `1777298400` | Node operators needed to upgrade to the T3-compatible release before the testnet activation timestamp. ## Overview | TIP | What it does | Who should review | |-----|-------------|-------------------| | [Enhanced access key permissions](https://tips.sh/1011) | Periodic spending limits, call scoping, and a ban on access-key contract creation | Wallets, account SDKs, apps using connected apps or subscriptions | | [Signature verification](https://tips.sh/1020) | Signature verification precompile for secp256k1, P256, and WebAuthn | Smart contract teams, account integrators, wallet SDKs | | [Virtual addresses](https://tips.sh/1022) | [Virtual addresses](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses) for TIP-20 deposit forwarding | Exchanges, ramps, custodians, payment processors, explorers, indexers | ## Breaking changes These breaking changes only affect access-key integrations. You need to update your integration if you create new access keys, manually encode `key_authorization` or `authorizeKey(...)`, or rely on access-key-signed deployment flows. Existing authorized keys continue to work. ### Access-key authorization ABI Integrations that directly call `AccountKeychain.authorizeKey(...)` or manually encode `key_authorization` must use the tuple-form ABI for enhanced access key permissions. The legacy selector `0x54063a55` no longer works — legacy calls fail with `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. If you use an updated SDK, this is mostly a tooling upgrade. If you hand-encode calldata, use the exact tuple-form signature from the [Account Keychain precompile spec](https://tempo.xyz/developers/docs/protocol/transactions/AccountKeychain). ### Access-key contract creation Access-key-signed transactions can no longer create contracts in any configuration — including direct CREATE, factory CREATE, and internal CREATE2. Move those flows to a root key path. ### Migration checklist * Upgrade to a T3-compatible SDK release listed below * Regenerate contract bindings or replace handcrafted encoders for `authorizeKey(...)` * Move any access-key contract-creation flows to a root key path * If you adopt virtual addresses, collapse the two-hop `Transfer` pair into one logical deposit to the registered master wallet rather than treating the virtual address hop as a separate transfer * Test key creation, key rotation, and recovery flows on testnet Most integrators only needed to upgrade tooling. Existing authorized access keys keep working. ## Supporting new features ### Virtual addresses for explorers and indexers Each virtual-address deposit emits two `Transfer` events in one transaction: `Transfer(sender, virtualAddress, amount)` then `Transfer(virtualAddress, masterWallet, amount)`. Collapse these into one logical deposit to the master wallet. The virtual address is for attribution only — `balanceOf(virtualAddress)` is always zero. See [Virtual addresses for TIP-20 deposits](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses) for the full routing model and event sequences. ### Signature verification precompile Signature verification is additive — existing verifier setups keep working. Teams that want a standard onchain verification surface can adopt the precompile instead of maintaining custom verifier contracts. See the [Signature Verification with Foundry](https://tempo.xyz/developers/docs/sdk/foundry/signature-verifier) guide or read the [technical specification](https://tips.sh/1020). ## Compatible SDK releases Tempo's broader tooling ecosystem is available in [Developer tools](https://tempo.xyz/developers/docs/quickstart/developer-tools). | SDK | T3-compatible release | |-----|-----------------------| | [TypeScript](https://github.com/wevm/viem) | [`viem@2.47.11`](https://github.com/wevm/viem/releases/tag/viem%402.47.11), [`ox@0.14.13`](https://github.com/wevm/ox/releases/tag/ox%400.14.13) | | [Rust](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.5.1) | [`tempo-alloy@1.5.1`](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.5.1) | | [Go](https://github.com/tempoxyz/tempo-go) | [`v0.4.0`](https://github.com/tempoxyz/tempo-go/releases/tag/v0.4.0) | | [Python](https://github.com/tempoxyz/pytempo) | [`0.5.0`](https://github.com/tempoxyz/pytempo/releases/tag/pytempo%400.5.0) | | [Foundry](https://github.com/foundry-rs/foundry) | [`v1.7.0`](https://github.com/foundry-rs/foundry/releases/tag/v1.7.0) | ## Related docs * [Virtual addresses for TIP-20 deposits](https://tempo.xyz/developers/docs/protocol/tip20/virtual-addresses) * [Virtual addresses](https://tips.sh/1022) * Coordinating meta TIP: [tempoxyz/tempo#3273](https://github.com/tempoxyz/tempo/pull/3273) # T2 Network Upgrade This page summarises the features that shipped in the T2 network upgrade. :::info[T2 status] T2 is active on both testnet and mainnet. ::: ## Timeline | Network | Date | Timestamp | |---------|------|-----------| | Testnet | Thursday, 26th March 4pm CET | `1774537200` | | Mainnet | Tuesday, 31st March 4pm CEST | `1774965600` | Node operators needed to upgrade to the T2-compatible release before the testnet activation timestamp. ## Overview T2 built on T1 and introduced the following features: * [Compound transfer policies](https://tips.sh/1015) * [Permit](https://tips.sh/1004) support for TIP-20 tokens * A new [Validator Config V2](https://tips.sh/1017) precompile * [T2 network upgrade](https://tips.sh/1036) security improvements ## Feature TIPs ### Compound transfer policies Read the [technical specification](https://tips.sh/1015). **TLDR:** Extended TIP-403 policies so token issuers can set different authorization rules for senders, recipients, and mint recipients. Previously a single policy applied to both sides of a transfer. **Customer use case:** Issuers of regulated or closed-loop tokens need to enforce different rules for senders vs recipients. For example: KYC required to on-ramp into a stablecoin, but anyone in the approved set can spend. Without compound policies, issuers had to apply the same restrictions to both sides, which broke real-world Commerce patterns and Tokenized Asset distribution flows. **What this enables:** * If you integrate with TIP-403 policies: new `isAuthorizedSender()`, `isAuthorizedRecipient()`, and `isAuthorizedMintRecipient()` functions are available. The existing `isAuthorized()` still works (returns `senderCheck && recipientCheck`). * If you issue TIP-20 tokens: you can now create compound policies for use cases like vendor credits, directional KYC, or asymmetric compliance. ### Validator Config V2 Read the [technical specification](https://tips.sh/1017). **Operator guide:** [Controlling validator lifecycle](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle) **TLDR:** New precompile for managing consensus validators. Adds Ed25519 signature verification at registration, append-only history, and stable validator indices. **Customer use case:** Before V2, node operators depended on Tempo to make any validator configuration changes. With V2, operators can self-service IP rotations, key rotations, and ownership transfers without waiting on Tempo — reducing operational dependency and enabling faster incident response. **What this enables:** * **Node operators:** Node operators didn't need to take action for the migration itself. Operators can now self-service IP updates, key rotation, and ownership transfers. * **Minimal nodes for validators:** Validator V2 unlocks minimal nodes for validators. Previously, validators had to keep ~64,000 blocks of history to reconstruct validator sets, requiring hundreds of GB of storage. V2 removes that requirement, letting validators query the set from the latest state only. ### Permit for TIP-20 Read the [technical specification](https://tips.sh/1004). **TLDR:** Added EIP-2612 compatible `permit()` to all TIP-20 tokens, enabling gasless approvals via off-chain signatures. **Customer use case:** Any Ramp or Commerce platform that sponsors gas for end users needs `permit()` to avoid forcing two separate transactions. **What this enables:** * Users can use `permit()` to combine approve + action in a single transaction. ### Meta TIP: Security Improvements Read the [technical specification](https://tips.sh/1036). # Tempo CLI: installation and usage The `tempo` binary covers three core workflows: * **Tempo Wallet CLI (`tempo wallet`)** — use Tempo Wallet from the terminal: balances, funding, access keys, service discovery, and agent-ready wallet operations * **`tempo request`** — make paid HTTP requests with automatic [MPP](https://mpp.dev/overview) payment negotiation * **`tempo node`** / **`tempo download`** — run and sync a Tempo node ## Install the Tempo CLI :::code-group ```bash [Terminal] curl -fsSL https://tempo.xyz/install | bash ``` ::: To update later, run `tempoup`. ## Teach your agent to use Tempo Paste this into your AI agent to set up Tempo Wallet CLI and start making paid requests: :::code-group ```bash [Claude Code] claude -p "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ```bash [Amp] amp --execute "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ```bash [Codex CLI] codex exec "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ::: All commands support `--help` for documentation and `--describe` for JSON command schemas. For scripts and agents, pass `-t` (`--toon-output`) to get compact, machine-readable output, and use `--dry-run` or `--max-spend` before paid calls. ## Tempo CLI commands Dive into each command: * [tempo download](https://tempo.xyz/developers/docs/cli/node) — Fetch chain snapshots for faster initial sync * [Wallet CLI](https://tempo.xyz/developers/docs/cli/wallet) — Use Tempo Wallet CLI from the command line for agents and scripts * [tempo request](https://tempo.xyz/developers/docs/cli/request) — Make HTTP requests that pay automatically via MPP * [tempo node](https://tempo.xyz/developers/docs/cli/node) — Run and configure a Tempo RPC or validator node # Tempo Wallet CLI: setting up and using it Tempo Wallet CLI is the command-line way to use [Tempo Wallet](https://wallet.tempo.xyz) from scripts, terminals, and AI agents. Use `tempo wallet` to authenticate, manage balances and access keys, discover [MPP](https://mpp.dev/overview) services, fund token or credit balances, and manage payment sessions. Use [`tempo request`](https://tempo.xyz/developers/docs/cli/request) when you want the CLI to make the paid HTTP call. * [Download](#download-the-tempo-wallet-cli) — Install the Tempo launcher and wallet extensions * [Authenticate](#authenticate-with-tempo-wallet-cli) — Connect to your Tempo Wallet * [Agent Mode](#agent-and-script-mode) — Use TOON output, dry runs, and spend caps * [Balances](#balances-and-credits) — View your address, balances, credits, and key state * [Add Funds](#add-wallet-funds) — Fund your wallet or transfer tokens * [Discover Services](#discover-services) — Browse MPP-registered service providers * [Manage Sessions](#manage-payment-sessions) — Track and close payment sessions ## Download the Tempo Wallet CLI ```bash curl -fsSL https://tempo.xyz/install | bash ``` The launcher manages `tempo wallet` and `tempo request` extensions. To update later, run `tempoup`. Verify with `tempo --version`. ## Authenticate with Tempo Wallet CLI ```bash tempo wallet login ``` Opens a browser flow to connect to your [Tempo Wallet](https://wallet.tempo.xyz). If you don't have one, the flow creates it. For remote hosts where the browser is on another device: ```bash tempo wallet login --no-browser ``` Once logged in, verify everything works: ```bash tempo wallet whoami ``` If `ready=true`, the wallet is ready for [`tempo request`](https://tempo.xyz/developers/docs/cli/request). Refresh a stale access key without logging out: ```bash tempo wallet refresh ``` To disconnect: ```bash tempo wallet logout --yes ``` ## Balances and credits ```bash tempo wallet whoami tempo wallet whoami --credits ``` Shows your address, token balances, key state, and optional MPP Credits balance. Credits are separate from token balances and only apply to eligible one-time MPP charges. ## Manage access keys ```bash tempo wallet keys tempo wallet revoke --dry-run tempo wallet revoke ``` Each wallet can have multiple access keys with independent spending limits. Use scoped keys to constrain what an agent or script can spend, and revoke keys that are stale or no longer needed. ## Add wallet funds ```bash tempo wallet fund tempo wallet fund --credits tempo wallet fund --crypto ``` On testnet, the default flow opens the faucet. On mainnet, it opens available funding options. `--crypto` opens direct crypto funding. `--credits` opens MPP Credits purchase for eligible one-time services. To transfer tokens to another address: ```bash tempo wallet transfer --dry-run tempo wallet transfer ``` For more options, see [Getting Funds on Tempo](https://tempo.xyz/developers/docs/guide/getting-funds). ## Discover services ```bash tempo wallet services tempo wallet services --search tempo wallet services ``` The [Machine Payments Protocol](https://mpp.dev/overview) (MPP) lets HTTP endpoints accept payments inline. The service directory indexes MPP-registered providers. Each entry shows endpoint URLs, HTTP methods, pricing, whether credits are supported, and request schemas. Use it to find the exact URL and payload for [`tempo request`](https://tempo.xyz/developers/docs/cli/request). Agents that support MCP can also use the read-only services MCP server at `https://mpp.dev/mcp/services`. See [Discover MPP services](https://tempo.xyz/developers/docs/guide/machine-payments/discover-services) for MCP setup, JSON-RPC examples, and the public catalog API. Agent workflow: ```bash tempo wallet -t services --search ai tempo wallet -t services tempo request -t --dry-run -X POST --json '{"input":"hello"}' / tempo request -t -X POST --json '{"input":"hello"}' / ``` ## Manage payment sessions When you use [pay-as-you-go](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) services, MPP opens a [session](https://mpp.dev/payment-methods/tempo/session) — a payment channel where your wallet deposits funds into a reserve contract, then pays per request using signed [vouchers](https://mpp.dev/protocol/credentials) off-chain. This avoids an on-chain transaction for every request, giving sub-100ms latency and near-zero per-request fees. Tempo Wallet CLI tracks session state locally: ```bash tempo wallet sessions list tempo wallet sessions sync tempo wallet sessions close --all tempo wallet sessions close --orphaned tempo wallet sessions close --finalize ``` `sync` reconciles local records with onchain state. `close --orphaned` cleans up sessions whose counterparty is unreachable. `close --finalize` finalizes channels already pending close. Use `--dry-run` with any close command to preview before executing. ## Agent and script mode Use [TOON](https://toonformat.dev/) output for compact, machine-readable responses: ```bash tempo wallet -t whoami tempo wallet -t services --search ai tempo wallet -t services ``` Before spending, preview and cap paid requests: ```bash tempo request -t --dry-run -X POST --json '{"input":"hello"}' / tempo request -t --max-spend 1.00 -X POST --json '{"input":"hello"}' / ``` If a service supports MPP Credits, inspect the challenge first, then pay with credits: ```bash headers="$(mktemp)" tempo request -t --dry-run -D "$headers" -X POST --json '{"input":"hello"}' / tempo wallet -t transfer --credits --dry-run --mpp-challenge-file "$headers" tempo wallet -t transfer --credits --mpp-challenge-file "$headers" ``` ## Tempo Wallet CLI command reference ### Wallet authentication commands | Command | Description | | --- | --- | | `tempo wallet login` | Connect or create a wallet via browser auth | | `tempo wallet login --no-browser` | Print an auth URL for remote-host login | | `tempo wallet refresh` | Refresh the current access key | | `tempo wallet logout` | Disconnect and clear local credentials | | `tempo wallet whoami` | Print readiness, address, balances, and key state | | `tempo wallet whoami --credits` | Print MPP Credits balance | ### Wallet key commands | Command | Description | | --- | --- | | `tempo wallet keys` | List keys and their spending limits | | `tempo wallet revoke --dry-run` | Preview access-key revocation | | `tempo wallet revoke ` | Revoke an access key | ### Wallet funding commands | Command | Description | | --- | --- | | `tempo wallet fund` | Fund wallet (faucet on testnet, bridge on mainnet) | | `tempo wallet fund --crypto` | Open direct crypto funding | | `tempo wallet fund --credits` | Buy MPP Credits for eligible one-time services | | `tempo wallet transfer ` | Transfer tokens to another address | | `tempo wallet transfer --credits --mpp-challenge-file ` | Pay an MPP challenge with credits | ### Wallet service discovery commands | Command | Description | | --- | --- | | `tempo wallet services` | List all registered services | | `tempo wallet services --search ` | Filter services by keyword | | `tempo wallet services ` | Show endpoints, methods, and request schemas | ### Wallet session commands | Command | Description | | --- | --- | | `tempo wallet sessions list` | List active payment sessions | | `tempo wallet sessions sync` | Reconcile local sessions with onchain state | | `tempo wallet sessions close --all` | Close all sessions | | `tempo wallet sessions close --orphaned` | Close sessions whose counterparty is unreachable | | `tempo wallet sessions close --finalize` | Finalize channels pending close | ### Advanced wallet commands | Command | Description | | --- | --- | | `tempo wallet debug` | Collect debug info for support | | `tempo wallet completions ` | Generate shell completions | ## Learn more about Tempo Wallet CLI * [Tempo Wallet](https://wallet.tempo.xyz) — Open the web wallet to manage your account * [MPP overview](https://mpp.dev/overview) — How agentic payments work with the Machine Payments Protocol * [tempo request](https://tempo.xyz/developers/docs/cli/request) — Make paid HTTP requests from the terminal * [Source code](https://github.com/tempoxyz/wallet-cli) — tempoxyz/wallet-cli on GitHub # `tempo request`: CLI command reference A curl-compatible HTTP client that handles [Machine Payments Protocol](https://mpp.dev/overview) negotiation transparently. When a server responds with [`402 Payment Required`](https://mpp.dev/protocol/http-402), `tempo request` reads the [challenge](https://mpp.dev/protocol/challenges), signs and submits the payment onchain, then retries with the [credential](https://mpp.dev/protocol/credentials) in one command. Requires [`tempo wallet login`](https://tempo.xyz/developers/docs/cli/wallet) first. Use [`tempo wallet services`](https://tempo.xyz/developers/docs/cli/wallet#discover-services) before calling paid endpoints so agents and scripts use registered URLs, methods, pricing, and request schemas instead of guessing. ## `tempo request` usage | Command | Description | | --- | --- | | `tempo request ` | Make an HTTP request with automatic payment | | `tempo request --dry-run ` | Preview cost without executing payment | | `tempo request --max-spend ` | Set a hard cap for cumulative payment spend | | `tempo request -X POST --json '{...}'` | Send a JSON body | | `tempo request -H 'Header: Value'` | Add a custom header | ## `tempo request` flags | Flag | Description | | --- | --- | | `-X ` | HTTP method (`GET`, `POST`, etc.) | | `--json ` | Send a JSON body (implies `-X POST`) | | `-H
` | Add a custom header | | `--dry-run` | Preview the payment cost and validate the request without spending | | `--max-spend ` | Stop if the request would exceed a spend cap | | `-D ` / `--dump-header ` | Write response headers, useful for inspecting MPP challenges | | `-t` / `--toon-output` | Compact machine-readable output for scripts and agents | ## `tempo request` examples Preview cost before paying: ```bash tempo request --dry-run -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` Execute a paid request: ```bash tempo request -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` Capture headers for an agent-controlled MPP Credits flow: ```bash headers="$(mktemp)" tempo request -t --dry-run -D "$headers" -X POST \ --json '{"input":"hello"}' \ / ``` Discover the right URL and request schema first with [`tempo wallet services`](https://tempo.xyz/developers/docs/cli/wallet#discover-services). ## Learn more about paid Tempo requests * [MPP protocol](https://mpp.dev/protocol) — Challenges, credentials, and receipts * [Wallet CLI](https://tempo.xyz/developers/docs/cli/wallet) — Authenticate and discover services * [MPP SDKs](https://mpp.dev/sdk) — TypeScript, Python, and Rust libraries * [Accept payments](https://tempo.xyz/developers/docs/guide/machine-payments/server) — Add MPP to your own API # `tempo download`: CLI command reference Download chain snapshots for faster initial sync. Fetches execution state, static files, and consensus data, and generates a `reth.toml` prune config for the target data directory. Current releases resolve the default data directory and snapshot manifest from the selected chain. Running `tempo download` without a snapshot profile opens an interactive component selector. Passing a profile flag such as `--minimal` or `--archive` skips the selector. Validators should use `--minimal`. RPC providers, indexers, and other workloads that need complete historical data should use `--archive`. ## `tempo download` usage ```bash tempo download [flags] ``` ## `tempo download` flags | Flag | Description | | --- | --- | | `--chain ` | Target network (`mainnet`, `moderato`) | | `--datadir ` | Data directory for downloaded state | | `--consensus.datadir ` | Destination for consensus snapshot data. Defaults to `/consensus`; match this to the node's `--consensus.datadir` when using a separate volume. | | `-u, --url ` | Download a single legacy snapshot archive URL | | `--manifest-url ` | Download a specific modular snapshot manifest URL | | `--list` | List available snapshots | | `--print-plan-json` | Print the selected execution and consensus archive plan without downloading archives or modifying the data directory. Select a profile such as `--minimal`. | | `--resumable[=]` | Download to disk before extraction so interrupted downloads can resume. Enabled by default. | | `--minimal` | Download the minimal component set without opening the interactive selector. Validators should use this profile. | | `--full` | Download the full node component set. | | `--archive`, `--all` | Download all available components without opening the interactive selector. Recommended for RPC providers and indexers. | | `-y, --non-interactive` | Skip the interactive selector and download the minimal component set unless explicit component flags are provided. | | `--force` | Replace existing snapshot state, including the entire consensus directory. Preserves `discovery-secret` and `known-peers.json`. | ## Validator Migration Guidance Validators should use the minimal snapshot profile, even if the previous validator data directory used a different snapshot profile. Migrating from archive v1 to minimal v2 reduces validator node size approximately as follows: | Network | Before | After | Reduction | | --- | ---: | ---: | ---: | | Mainnet | 27 GB | 10 GB | 2.7x | | Moderato testnet | 1 TB | 100 GB | 10x | ### What is Minimal Mode? Minimal Mode is a Reth storage profile for nodes that need to follow the chain and serve recent state, such as validators. It keeps disk usage low by pruning older historical data. If the node serves historical RPC, indexing, archive, or tracing workloads, use `--archive` instead. See Reth's [Minimal Storage Mode](https://reth.rs/run/storage/minimal/) docs for the storage trade-offs. ### Am I Running a Minimal Node? To check whether an existing validator has already migrated, inspect the node startup logs for the `Loaded storage settings` line and its `pruning_mode` field. If `pruning_mode` is `minimal`, no migration is needed unless you are replacing the node. If you are unsure which configuration your validator is running, reach out to the Tempo team before replacing snapshot data. To migrate or replace a mainnet validator snapshot, run: ```bash tempo download --chain mainnet --minimal --force ``` To migrate or replace a testnet validator snapshot, run: ```bash tempo download --chain moderato --minimal --force ``` `--force` removes the execution databases, static files, `reth.toml`, and the entire consensus directory before installing the new snapshot. It preserves `discovery-secret` and `known-peers.json`. :::note[Unsure which profile to use?] If you are unsure which pruning configuration your validator is running, reach out to the Tempo team before replacing snapshot data. ::: ## `tempo download` examples Preview a validator snapshot download before making changes: ```bash tempo download --chain mainnet --minimal --print-plan-json ``` The JSON plan includes archive URLs, sizes, and checksums when supplied by the manifest. It includes the consensus archive from v1.12.0 onward. Planning still fetches snapshot metadata. Open the interactive selector for mainnet: ```bash tempo download --chain mainnet ``` Download an archive snapshot for an RPC node: ```bash tempo download --chain mainnet --archive ``` List available snapshots: ```bash tempo download --list ``` List available snapshots for a specific chain: ```bash tempo download --list --chain moderato ``` If the data directory has limited free disk space, disable resumable downloads and stream the snapshot directly into extraction: ```bash tempo download --chain mainnet --minimal --resumable=false ``` Use the [snapshots viewer](https://snapshots.tempo.xyz/) to compare snapshot profiles and copy generated commands. Then start your node with [`tempo node`](https://tempo.xyz/developers/docs/cli/node). # `tempo node`: CLI command reference Run a Tempo node. For faster initial sync, first download a snapshot with [`tempo download`](https://tempo.xyz/developers/docs/cli/download). For operational setup guides — system requirements, systemd configs, monitoring, validator onboarding — see [Run a Tempo Node](https://tempo.xyz/developers/docs/guide/node). Flags grouped by function: ### Network | Flag | Description | | --- | --- | | `--follow` | Run as a full node following a trusted RPC endpoint | | `--chain ` | Target network (`mainnet`, `moderato`) | | `--datadir ` | Data directory for chain state | | `--trusted-peers ` | Comma-separated enode URLs or ENRs to add as trusted execution P2P peers | | `--trusted-only` | Connect to and accept execution P2P connections from trusted peers only | | `--disable-discovery` | Disable DNS, discv4, and discv5 peer discovery | ### RPC server | Flag | Description | | --- | --- | | `--http` | Enable the JSON-RPC HTTP server | | `--http.port ` | JSON-RPC port (default: `8545`) | | `--http.addr ` | JSON-RPC bind address | | `--http.api ` | Enabled API namespaces (e.g., `eth,net,web3,txpool,trace`) | ### Consensus (validators) | Flag | Description | | --- | --- | | `--consensus.signing-key ` | Path to validator signing key | | `--consensus.secret ` | Path to the secret used to decrypt an encrypted validator signing key. Prefer a named pipe (FIFO) or shell process substitution. | | `--consensus.datadir ` | Separate volume for consensus data | :::warning `--consensus.fee-recipient` was removed in `v1.7.0`. Remove it from startup commands and [update the fee recipient on-chain](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#update-the-fee-recipient). ::: ### Transaction pool | Flag | Description | | --- | --- | | `--txpool.filter ` | Optional address filter, available since v1.14.0. Accepts comma-separated addresses or a plain-text file with comma/newline-separated addresses. Rejects transactions matching the sender or any direct call target at pool admission. | See [Transaction address filtering](https://tempo.xyz/developers/docs/guide/node/validator-setup#transaction-address-filtering) for examples, file reload behavior, and the limits of this local policy. ### Observability | Flag | Description | | --- | --- | | `--telemetry-url ` | Unified metrics and logs export endpoint | | `--telemetry-metrics-interval ` | Metrics push interval (default: `10s`) | | `--metrics ` | Enable Prometheus metrics on this port | ## `tempo node` examples Download a snapshot and start an RPC node: ```bash tempo node \ --follow \ --http --http.port 8545 \ --http.api eth,net,web3,txpool,trace ``` Start a validator: ```bash tempo node --datadir /data/tempo \ --chain mainnet \ --consensus.signing-key /etc/tempo/key \ --consensus.secret /run/tempo/consensus-secret ``` ## Learn more about Tempo node operations * [tempo download](https://tempo.xyz/developers/docs/cli/download) — Download snapshots for faster initial sync * [Run a Tempo Node](https://tempo.xyz/developers/docs/guide/node) — System requirements, systemd, monitoring * [Become a validator](https://tempo.xyz/developers/docs/guide/node/validator) — Stake and operate a validator node * [Source code](https://github.com/tempoxyz/tempo) — tempoxyz/tempo on GitHub # Tempo Wallet CLI: overview Tempo Wallet CLI uses Tempo Mainnet for wallet balances, funding, transfers, and MPP settlement. Mainnet has been live since March 18, 2026, and pathUSD in Tempo Wallet is the production mainnet asset. Use the separate Moderato testnet only for explicitly testnet development workflows. Tempo Wallet CLI is the command-line way to use Tempo Wallet from scripts, terminals, and AI agents. It has two command families: * **`tempo wallet`**: manages your onchain identity: authentication, key management, balances, funding, transfers, MPP Credits, service discovery, and payment sessions. It also provides a service directory for discovering [MPP](https://mpp.dev)-compatible endpoints and their request schemas. * **`tempo request`**: a curl-like HTTP client that handles [Machine Payments Protocol](https://mpp.dev) negotiation automatically. It sends your request, intercepts `402 Payment Required` challenges, signs and submits the payment onchain, then retries with the credential, all in a single command. Together they let you browse paid APIs, preview costs, and execute paid requests from a terminal, script, or AI agent without writing any integration code. ## Install Tempo Wallet CLI ```bash curl -fsSL https://tempo.xyz/install | bash ``` ## Authenticate with Tempo Wallet CLI ```bash tempo wallet login tempo wallet login --no-browser tempo wallet whoami tempo wallet whoami --credits ``` `login` opens a browser flow that creates or connects a Tempo Wallet. Use `--no-browser` on remote hosts. `whoami` confirms readiness, prints your address, and shows token balances. `whoami --credits` shows MPP Credits separately from token balances. ## Discover and call a paid API ```bash # Find services tempo wallet services --search ai # Inspect a service's endpoints and request schema tempo wallet services # Preview cost without paying tempo request --dry-run -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev # Execute the paid request tempo request -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` Always build request URLs from `tempo wallet services ` output. Service entries include URLs, methods, pricing, request schemas, and credit support when available. ## Scripting and agents Tempo Wallet CLI is designed for non-interactive use. Three features matter here: * **`-t` ([TOON](https://toonformat.dev/) output)**: compact, machine-readable output that minimizes token usage when consumed by an LLM or parsed by a script. * **`--dry-run`**: previews the payment cost and validates the request shape without spending funds. Useful for agents that need to confirm cost before committing. * **`--max-spend`**: caps paid `tempo request` workflows so an agent cannot exceed a caller-provided budget. To set up an AI agent (Claude Code, Amp, Codex) with wallet and request capabilities: :::code-group ```bash [Claude Code] claude -p "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ```bash [Amp] amp --execute "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ```bash [Codex CLI] codex exec "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ::: This installs `tempo-wallet` and `tempo-request` skills automatically. The agent can then discover services, preview costs, and make paid requests within scoped spending limits. ## Next Tempo Wallet steps * [Wallet CLI](https://tempo.xyz/developers/docs/cli/wallet): canonical docs and command reference * [Reference](https://tempo.xyz/developers/docs/wallet/reference): legacy command reference # Tempo Wallet CLI: core flow recipes ## Tempo Wallet core flow ```bash tempo wallet -t whoami tempo wallet -t whoami --credits tempo wallet services --search ai tempo wallet services tempo request --dry-run --json '{"input":"hello"}' tempo request --max-spend 1.00 --json '{"input":"hello"}' ``` ## Tempo Wallet operations ```bash # Wallet readiness and balances tempo wallet whoami tempo wallet whoami --credits # Key and spending-limit state tempo wallet keys tempo wallet revoke --dry-run # Fund wallet tempo wallet fund tempo wallet fund --credits # Transfer tokens tempo wallet transfer --dry-run tempo wallet transfer ``` For machine-readable output: ```bash tempo wallet -t whoami tempo wallet -t whoami --credits tempo wallet -t keys ``` Use access-key spending limits and `tempo request --max-spend` for agent workflows. Credits are separate from token balances and only work with eligible one-time MPP charges. ## Tempo Wallet service discovery ```bash # Search services by keyword tempo wallet services --search ai # Inspect one service to see exact endpoints tempo wallet services ``` Tip: copy endpoint URL, method, and payload shape directly from service details. Service details also show pricing, payment mode, and whether `supportsCredits: true` is available. ## Tempo Wallet request execution Preview before paying: ```bash tempo request --dry-run --json '{"input":"hello"}' ``` Execute paid request: ```bash tempo request --max-spend 1.00 --json '{"input":"hello"}' ``` Capture headers for a credits flow: ```bash headers="$(mktemp)" tempo request -t --dry-run -D "$headers" -X POST --json '{"input":"hello"}' tempo wallet -t transfer --credits --dry-run --mpp-challenge-file "$headers" tempo wallet -t transfer --credits --mpp-challenge-file "$headers" ``` ## Tempo Wallet session management ```bash # List sessions tempo wallet sessions list # Reconcile local state against on-chain state tempo wallet sessions sync # Preview close operations first tempo wallet sessions close --dry-run --all # Close orphaned sessions tempo wallet sessions close --orphaned # Finalize channels pending close tempo wallet sessions close --finalize ``` Sessions are MPP payment channels used by pay-as-you-go services. One-time charges may use direct token payment or MPP Credits when the service supports credits. ## Tempo Wallet agent and script mode Use TOON output (`-t`) when command output is consumed by agents or scripts. ```bash tempo wallet -t whoami tempo wallet -t services --search ai tempo wallet -t services tempo request -t --dry-run --max-spend 1.00 --json '{"input":"hello"}' ``` ## Tempo Wallet failure recovery shortcuts ```bash # Wallet not ready / auth missing tempo wallet login tempo wallet -t whoami # Suspected key issue tempo wallet logout --yes tempo wallet login tempo wallet keys tempo wallet refresh # Request failing due to payload/path mismatch tempo wallet services # Insufficient funds tempo wallet fund # Credit-eligible one-time MPP charge tempo wallet -t whoami --credits tempo wallet fund --credits ``` If issues persist, continue with [Troubleshooting](https://tempo.xyz/developers/docs/cli/wallet). ## Tempo Wallet end-to-end script pattern ```bash # 1) Ensure wallet is ready tempo wallet -t whoami # 2) Discover service details tempo wallet -t services --search ai # 3) Preview cost tempo request --dry-run --json '{"input":"hello"}' # 4) Execute with a spend cap tempo request --max-spend 1.00 --json '{"input":"hello"}' ``` ## Related Tempo Wallet docs 1. [Reference](https://tempo.xyz/developers/docs/wallet/reference) 2. [Wallet CLI Reference](https://tempo.xyz/developers/docs/cli/wallet) 3. [Use with Agents](https://tempo.xyz/developers/docs/wallet/use-with-agents) # Tempo Wallet CLI Reference ## `tempo wallet` Manages your onchain identity and provides service discovery for [MPP](https://mpp.dev) endpoints. ### Wallet auth commands | Command | Description | | --- | --- | | `tempo wallet login` | Connect or create wallet via browser auth | | `tempo wallet login --no-browser` | Print an auth URL for remote-host login | | `tempo wallet refresh` | Refresh your access key without logging out | | `tempo wallet logout` | Disconnect wallet and clear local credentials | | `tempo wallet whoami` | Print readiness, address, balances, and key state | | `tempo wallet whoami --credits` | Show MPP Credits balance | ### Wallet key commands Each wallet can have multiple access keys with independent spending limits. This is how you constrain what an agent or script can spend. | Command | Description | | --- | --- | | `tempo wallet keys` | List keys and their spending limits | | `tempo wallet revoke --dry-run` | Preview access-key revocation | | `tempo wallet revoke ` | Revoke an access key | ### Wallet funding commands | Command | Description | | --- | --- | | `tempo wallet fund` | Fund wallet (faucet on testnet, bridge on mainnet) | | `tempo wallet fund --crypto` | Open direct crypto funding | | `tempo wallet fund --credits` | Buy credits for eligible one-time MPP charges | | `tempo wallet transfer ` | Transfer tokens to another address | | `tempo wallet transfer --dry-run` | Preview a token transfer | | `tempo wallet transfer --credits --mpp-challenge-file ` | Pay a captured MPP challenge with credits | ### Wallet service commands The service directory indexes [MPP](https://mpp.dev)-registered providers. Each service entry includes endpoint URLs, HTTP methods, pricing, request schemas, payment mode, and credit support when available. | Command | Description | | --- | --- | | `tempo wallet services` | List all registered services | | `tempo wallet services --search ` | Filter services by keyword | | `tempo wallet services ` | Show a service's endpoints, methods, and request schemas | ### Wallet session commands Sessions are the local state for [pay-as-you-go](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) payment channels. Tempo Wallet CLI tracks them locally and can reconcile against onchain state. | Command | Description | | --- | --- | | `tempo wallet sessions list` | List active payment sessions | | `tempo wallet sessions sync` | Reconcile local sessions with onchain state | | `tempo wallet sessions close --all` | Close all sessions | | `tempo wallet sessions close --orphaned` | Close sessions whose counterparty is unreachable | | `tempo wallet sessions close --finalize` | Finalize channels pending close | | `tempo wallet sessions close --cooperative ` | Use cooperative close for a target session | ### Agent setup and diagnostics | Command | Description | | --- | --- | | `tempo wallet debug` | Collect debug info for support | | `tempo wallet completions ` | Generate shell completions | ## `tempo request` A curl-like HTTP client that handles [MPP](https://mpp.dev) payment negotiation transparently. On a `402 Payment Required` response, it reads the challenge, signs and submits the payment, then retries with the credential. | Command | Description | | --- | --- | | `tempo request ` | Make an HTTP request with automatic payment | | `tempo request --dry-run ` | Preview cost without executing payment | | `tempo request --max-spend ` | Cap cumulative payment spend | | `tempo request --json '{...}'` | Send a JSON body (implies `-X POST`) | | `tempo request -H 'Header: Value'` | Add a custom header | | `tempo request -D ` | Write response headers to a file | ## Tempo Wallet global flags | Flag | Scope | Description | | --- | --- | --- | | `-t` / `--toon-output` | `tempo wallet`, `tempo request` | Compact machine-readable output for scripts and agents | | `--dry-run` | `tempo request`, `tempo wallet transfer`, `tempo wallet sessions close` | Preview the action without executing | | `--max-spend` | `tempo request` | Hard cap for cumulative payment spend | | `--help` | all commands | Show command documentation | | `--describe` | supported commands | Output command schema as JSON for programmatic tooling | | `--schema` | supported built-in setup commands | Output command schema for programmatic tooling | ## Tempo Wallet source * Repository: [`tempoxyz/wallet-cli`](https://github.com/tempoxyz/wallet-cli) * Full help: `tempo wallet --help`, `tempo request --help` # Use Tempo Wallet CLI with Agents Tempo Wallet CLI uses Tempo Mainnet for balances, funding, transfers, and MPP settlement. Agents should treat pathUSD in Tempo Wallet as the live mainnet asset, not as a testnet faucet token. ## Agent wallet quickstart Paste this into your agent to set up Tempo Wallet CLI: :::code-group ```bash [Claude Code] claude -p "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ```bash [Amp] amp --execute "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ```bash [Codex CLI] codex exec "Read https://tempo.xyz/SKILL.md and set up tempo" ``` ::: ## Auto-installed Tempo wallet skills When you run the setup prompt, your agent installs Tempo's built-in skills automatically. No manual skill wiring required. * `tempo-wallet`: gives your agent wallet-aware capabilities like readiness checks, balances, service discovery, and session/funding actions. * `tempo-request`: gives your agent paid HTTP request capabilities with payment preview (`--dry-run`) and execution support. This works in supported skill-enabled agents including **Claude Code**, **Amp**, **Codex**, and similar environments. ## Tempo Wallet is agent-ready by design 1. **TOON output mode (`-t` / `--toon-output`)** gives compact, machine-readable, token-efficient output so agent tooling can parse command responses reliably. 2. **Built-in service discovery** (`tempo wallet services`) lets agents search providers, inspect endpoint details, and use verified method/path metadata instead of guessing URLs or payload shapes. 3. **`--dry-run` payment previews** let agents validate endpoint reachability, request shape, and expected payment cost before committing funds. 4. **`--max-spend` and scoped access keys** let you enforce budgets per request and per key. 5. **MPP Credits support** lets agents pay eligible one-time charges from a separate credits balance after checking `tempo wallet -t whoami --credits`. ## Tempo Wallet agent pattern ```bash tempo wallet -t whoami tempo wallet -t services --search tempo wallet -t services tempo request -t --dry-run --max-spend 1.00 -X POST --json '{"input":"hello"}' / tempo request -t --max-spend 1.00 -X POST --json '{"input":"hello"}' / ``` For credit-eligible one-time charges: ```bash tempo wallet -t whoami --credits headers="$(mktemp)" tempo request -t --dry-run -D "$headers" -X POST --json '{"input":"hello"}' / tempo wallet -t transfer --credits --dry-run --mpp-challenge-file "$headers" ``` ## Troubleshoot agent wallet flows If agent runs fail, continue with [Troubleshooting](https://tempo.xyz/developers/docs/cli/wallet). # Server utilities: Relay and Fee Payer Handler Server utilities are distributed through the Tempo API library, [`tapimo`](https://npm.im/tapimo). They work with frameworks that support either: * The [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) through `handler.fetch` * The [Node.js `RequestListener` API](https://nodejs.org/api/http.html#http_class_http_serverrequestlistener) through `handler.listener` ## Connect a handler to your server Create a handler once, then expose it through your server framework: ```ts twoslash // @noErrors import { createServer } from 'node:http' import { Handler } from 'tapimo' import { privateKeyToAccount } from 'viem/accounts' const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY, feePayer: { account: privateKeyToAccount('0x...'), }, }) createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve(handler.fetch) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ## Handlers * [Relay & Fee Payer Handler](https://tempo.xyz/developers/docs/server/relay-handler) — Proxy RPC requests with fee sponsorship, automatic swaps, and simulation metadata. # Relay & Fee Payer Handler Use `Handler.relay` to proxy RPC requests through your backend. The handler can enrich `eth_fillTransaction` with conditional fee sponsorship, fee-token selection, balance simulation, fee estimates, and automatic Stablecoin DEX swaps. ## Create a relay handler Create the handler with an optional [Tempo API key](https://tempo.xyz/developers/docs/api/authentication), then expose either its Fetch API or Node.js listener entrypoint: ```ts import { Handler } from 'tapimo' const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY, }) ``` By default, the relay configures Viem clients for Tempo mainnet and testnet and loads fee-token candidates from the [Tempo API verified-token list](https://tempo.xyz/developers/docs/api/verified-tokens#gettokenlist). Pass `apiKey` to authenticate the built-in RPC and verified-token requests. When you omit it, the clients use each chain's default Viem transport and the verified-token request is unauthenticated. :::info The built-in client defaults to mainnet when a request does not specify a chain. Use testnet chain ID `42431` with Sandbox API keys. ::: ```ts twoslash // @noErrors import { createServer } from 'node:http' import { Handler } from 'tapimo' const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY }) // ---cut--- createServer(handler.listener) // Node.js Bun.serve({ fetch: handler.fetch }) // Bun Deno.serve(handler.fetch) // Deno app.all('*', c => handler.fetch(c.request)) // Elysia app.use(handler.listener) // Express app.use(c => handler.fetch(c.req.raw)) // Hono export const GET = handler.fetch // Next.js export const POST = handler.fetch // Next.js ``` ## Relay handler features * [Sponsorship](#sponsorship) — Approve transactions and sign their fee payer payload with a server-controlled account. * [Automatic swaps](#auto-swap) — Insert Stablecoin DEX calls when an account lacks a required token. * [Fee-token selection](#best-fee-tokens) — Resolve a suitable fee token from account preferences and balances. * [Balance diffs](#balance-diffs) — Return simulated token balance changes for the transaction sender. * [Fee estimates](#fee-derivation) — Return fee estimates in raw token units and human-readable form. * [Funding requirements](#require-funds) — Describe the exact token deficit when an account needs more funds. ### Fee sponsorship Set [`feePayer`](#feepayer) to have the relay sign the fee payer payload for approved transactions. Add [`feePayer.validate`](#feepayervalidate) when your sponsorship policy should accept only specific senders or transactions. If validation rejects a request, the relay fills it again for self-payment. The configured fee payer also handles approved `eth_signRawTransaction`, `eth_sendRawTransaction`, and `eth_sendRawTransactionSync` requests. :::code-group ```ts twoslash [server.ts] // @noErrors import { createServer } from 'node:http' import { Handler } from 'tapimo' import { isAddressEqual } from 'viem' import { privateKeyToAccount } from 'viem/accounts' const blockedAddress = '0x000000000000000000000000000000000000dead' // ---cut--- const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY, feePayer: { account: privateKeyToAccount('0x...'), name: 'My App', url: 'https://myapp.com', // Optional sponsorship policy. validate: (request) => request.from !== undefined && !isAddressEqual(request.from, blockedAddress), }, }) createServer(handler.listener).listen(3000) ``` ```ts twoslash [client.ts] // @noErrors import { Provider } from 'accounts' const provider = Provider.create({ relay: 'http://localhost:3000' }) const [account] = await provider.request({ method: 'eth_requestAccounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.sponsored // true result.capabilities.sponsor // { // address: '0x1234567890abcdef1234567890abcdef12345678', // name: 'My App', // url: 'https://myapp.com', // } ``` ::: :::warning Without `feePayer.validate`, the handler sponsors every eligible request it receives. Add authentication, a sponsorship policy, rate limits, and logging before deploying a funded fee payer. ::: ### Automatic swaps When an account lacks a token required by the transaction, the relay can prepend `approve` and `buy` calls through the [Stablecoin DEX](https://tempo.xyz/developers/docs/guide/stablecoin-dex). The response describes the injected calls and swap limits in `capabilities.autoSwap`. Swap-related changes are excluded from `capabilities.balanceDiffs`. Enable this behavior with [`autoSwap`](#autoswap) or [`features: 'all'`](#features): :::code-group ```ts [server.ts] import { Handler } from 'tapimo' const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY, autoSwap: { slippage: 0.05 }, }) ``` ```ts [response.ts] result.capabilities.autoSwap // { // calls: [ // { to: '0x20c0...', data: '0x095ea7b3...' }, // { to: '0x20c0...', data: '0x...' }, // ], // maxIn: { // decimals: 6, // formatted: '105.000000', // name: 'pathUSD', // symbol: 'pathUSD', // token: '0x20c0000000000000000000000000000000000000', // value: '0x6422c40', // }, // minOut: { // decimals: 6, // formatted: '100.000000', // name: 'USDC.e', // symbol: 'USDC.e', // token: '0x20c000000000000000000000b9537d11c60e8b50', // value: '0x5f5e100', // }, // slippage: 0.05, // } ``` ::: ### Fee-token selection For sponsored requests, `feePayer.feeToken` takes precedence over the request's explicit `feeToken`. If neither is set, the relay and upstream fill resolve a token from the available candidates. The relay does not inspect the sender's balances because the sponsor pays the fee. For self-paid requests with fee-token resolution enabled, the relay selects a token in this order: 1. Use the request's explicit `feeToken`. 2. Use the account's onchain preference when the account has a balance. When [`cache`](#cache) is configured, the relay caches this lookup for about 60 seconds. 3. Choose the highest-balance token returned by [`resolveTokens`](#resolvetokens). The relay also considers TIP-20 tokens called by the transaction. 4. Leave `feeToken` unset for the upstream fill when no candidate has a positive balance. Set `features: 'all'` to enable fee-token resolution with the Tempo API verified-token list for mainnet and testnet. Passing `resolveTokens` also enables fee-token resolution and replaces these candidates. Self-paid fills also consider TIP-20 call targets. ```ts import { Handler } from 'tapimo' const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY, features: 'all', // [!code focus] }) ``` ### Balance diffs Set `features: 'all'` to simulate filled transactions and return token changes for the transaction sender in `capabilities.balanceDiffs`: ```ts [response.ts] result.capabilities.balanceDiffs // { // '0x1234567890abcdef1234567890abcdef12345678': [{ // address: '0x20c000000000000000000000b9537d11c60e8b50', // decimals: 6, // direction: 'outgoing', // formatted: '100.000000', // name: 'USDC.e', // recipients: ['0xcafebabecafebabecafebabecafebabecafebabe'], // symbol: 'USDC.e', // value: '0x5f5e100', // }], // } ``` Pass `capabilities.balanceDiffs: false` in a request when you need fee metadata but want to skip the balance simulation. ### Fee estimates With `features: 'all'`, the relay derives the transaction fee from the filled gas fields and resolved fee token. It returns raw token units alongside formatted metadata: ```ts [response.ts] result.capabilities.fee // { // amount: '0x6b86', // decimals: 6, // formatted: '0.027526', // symbol: 'USDC.e', // } ``` ### Funding requirements Set `capabilities.errors: true` to receive structured fill failures. For an `InsufficientBalance` error, the relay adds `capabilities.requireFunds` with the exact token deficit and returns optimistic balance diffs when available. ```ts twoslash // @noErrors import { Provider } from 'accounts' const provider = Provider.create({ relay: 'http://localhost:3000' }) const [account] = await provider.request({ method: 'eth_requestAccounts' }) // ---cut--- const result = await provider.request({ method: 'eth_fillTransaction', params: [{ from: account, capabilities: { errors: true }, // [!code focus] calls: [{ to: '0x20c000000000000000000000b9537d11c60e8b50', data: '0xa9059cbb000000000000000000000000cafebabecafebabecafebabecafebabecafebabe0000000000000000000000000000000000000000000000000000000005f5e100', }], }], }) result.capabilities.requireFunds // { // amount: '0x3938700', // decimals: 6, // formatted: '60.000000', // token: '0x20c000000000000000000000b9537d11c60e8b50', // symbol: 'USDC.e', // } ``` ### Enable all enrichment features By default, the relay enables only the features you configure directly. Use `features: 'all'` to enable fee-token resolution, automatic swaps, balance simulation, and fee estimates together. This does not enable sponsorship; configure [`feePayer`](#feepayer) separately. ```ts import { Handler } from 'tapimo' const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY, features: 'all', // [!code focus] }) ``` This adds network requests for token balances and simulation, so enable the complete feature set only when your application uses the returned metadata. ## API Reference ### `apiKey` * **Type:** `string` * **Optional** Routes the built-in mainnet and testnet clients through the Tempo API and authenticates verified-token requests. When you omit it, the clients use each chain's default Viem transport and verified-token requests are unauthenticated. ```ts const handler = Handler.relay({ apiKey: process.env.TEMPO_API_KEY, // [!code focus] }) ``` ### `getClient` * **Type:** `(chainId?: number) => Client` * **Optional** Overrides the built-in Viem clients for Tempo mainnet and testnet. With `apiKey`, the built-in clients use the Tempo API; without one, they use each chain's default Viem transport. Provide `getClient` for custom transports or additional chains. The relay calls it without an argument for the default client and with the request's resolved chain ID when available. ```ts twoslash import { Handler } from 'tapimo' import { createClient, http } from 'viem' import { tempoMainnet, tempoTestnet } from 'viem/tempo/chains' const mainnetClient = createClient({ chain: tempoMainnet, transport: http() }) const testnetClient = createClient({ chain: tempoTestnet, transport: http() }) const getClient = (chainId: number = tempoMainnet.id) => { // [!code focus] if (chainId === tempoMainnet.id) return mainnetClient // [!code focus] if (chainId === tempoTestnet.id) return testnetClient // [!code focus] throw new Error(`Unsupported chain: ${chainId}`) // [!code focus] } // [!code focus] const handler = Handler.relay({ getClient }) ``` When you provide `getClient`, `apiKey` does not change the custom clients' transports. It still authenticates the built-in verified-token resolver unless you also provide `resolveTokens`. ### `autoSwap` * **Type:** `false | { slippage?: number }` * **Optional** Controls Stablecoin DEX swaps for insufficient balances. An options object enables automatic swaps. Set it to `false` to disable them when `features: 'all'` is enabled. ```ts const handler = Handler.relay({ autoSwap: { slippage: 0.02 }, // [!code focus] }) ``` #### `autoSwap.slippage` * **Type:** `number` * **Default:** `0.05` (5%) Sets the maximum swap slippage. The relay calculates `maxAmountIn` from the token deficit and this percentage. ### `features` * **Type:** `'all'` * **Optional** Set this to `'all'` to enable fee-token resolution, automatic swaps, balance diffs, and fee estimates. Fee-token candidates come from the Tempo API verified-token list by default. Passing `resolveTokens` also enables fee-token resolution and replaces the default candidates. Configure `feePayer` separately to enable sponsorship. ```ts const handler = Handler.relay({ features: 'all', // [!code focus] }) ``` ### `feePayer` * **Type:** `object` * **Optional** Configures transaction sponsorship. When present, the relay can sign the filled transaction's `feePayerSignature`. ```ts import { Handler } from 'tapimo' import { privateKeyToAccount } from 'viem/accounts' const handler = Handler.relay({ feePayer: { // [!code focus] account: privateKeyToAccount('0x...'), // [!code focus] feeToken: '0x20c0000000000000000000000000000000000001', // [!code focus] name: 'My App', // [!code focus] url: 'https://myapp.com', // [!code focus] }, // [!code focus] }) ``` #### `feePayer.account` * **Type:** `LocalAccount` * **Required** The local account that signs fee payer payloads. #### `feePayer.feeToken` * **Type:** `Address` * **Optional** The token the sponsor prefers to use for fees. This value overrides the transaction sender's requested `feeToken` during sponsorship. #### `feePayer.onSponsored` * **Type:** `(event: SponsoredEvent) => void | Promise` * **Optional** Runs after fee-payer signing and before the relay returns or broadcasts the transaction. The awaited event includes the chain ID, method, sender, signing payload, serialized transaction, and transaction hash when available. #### `feePayer.name` * **Type:** `string` * **Optional** A sponsor name returned in response metadata. #### `feePayer.url` * **Type:** `string` * **Optional** A sponsor URL returned in response metadata. #### `feePayer.validate` * **Type:** `(request: TransactionRequest & { chainId?: number | Hex }) => Validation | Promise` * **Optional** Approves or rejects sponsorship for a Tempo transaction. Return `false` or a named refusal reason to reject sponsorship. Named reasons are `billing_past_due`, `billing_required`, `fee_token_unsupported`, `spend_limit_exceeded`, and `tx_fee_limit_exceeded`. When omitted, the configured fee payer sponsors every eligible request. ```ts import { Handler } from 'tapimo' import { isAddressEqual } from 'viem' import { privateKeyToAccount } from 'viem/accounts' const allowedSender = '0x000000000000000000000000000000000000dead' const handler = Handler.relay({ feePayer: { account: privateKeyToAccount('0x...'), validate: (request) => // [!code focus] request.from !== undefined && isAddressEqual(request.from, allowedSender), // [!code focus] }, }) ``` ### `multisig` * **Type:** `Multisig.Options` * **Optional** Collects native multisig approvals and broadcasts after the configured threshold is reached. Provide an enumerable state store; use a shared store with atomic compare-and-swap in production. ```ts import { Handler, Store } from 'tapimo' const handler = Handler.relay({ multisig: { store: Store.memory() }, // [!code focus] }) ``` ### `cache` * **Type:** `Store.Store` * **Optional** Caches TIP-20 metadata and account fee-token preferences. When omitted, the relay reads this data upstream on every fill. ```ts import { Handler, Store } from 'tapimo' const handler = Handler.relay({ cache: Store.memory(), // [!code focus] }) ``` ### `onRequest` * **Type:** `(request: RpcRequest) => Promise` * **Optional** Runs before the relay handles each request. Use it for logging, rate limits, or request-level validation. ```ts const handler = Handler.relay({ onRequest: async (request) => { // [!code focus] console.log('Processing request:', request.method) // [!code focus] }, // [!code focus] }) ``` ### `path` * **Type:** `string` * **Default:** `'/'` Sets the route where the handler accepts requests. ```ts const handler = Handler.relay({ path: '/relay', // [!code focus] }) ``` When you use [`Provider.relay`](https://accounts.tempo.xyz/docs/api/provider#relay), route both the base path and its chain-qualified children to the handler. The provider sends JSON-RPC requests to `/relay/:chainId`. ### `resolveTokens` * **Type:** `(chainId: number) => readonly Address[] | Promise` * **Optional** Returns candidate token addresses for fee-token resolution. By default, the relay loads the [Tempo API verified-token list](https://tempo.xyz/developers/docs/api/verified-tokens#gettokenlist) for mainnet and testnet. Pass a resolver to replace these candidates. For additional chains, provide both `getClient` and `resolveTokens`. ```ts const handler = Handler.relay({ resolveTokens: () => [ // [!code focus] '0x20c0000000000000000000000000000000000000', // [!code focus] '0x20c000000000000000000000b9537d11c60e8b50', // [!code focus] ], // [!code focus] }) ``` A custom resolver replaces only token lookup. `apiKey` still configures the built-in RPC clients. ### `cors` * **Type:** `boolean | Handler.from.Cors` * **Default:** `true` Configures CORS headers for every response. This option is inherited from `Handler.from`. ### `headers` * **Type:** `Headers | Record` * **Optional** Adds headers to every response. This option is inherited from `Handler.from`. # Tempo RPC reference: the `tempo_` namespace Tempo nodes expose all standard [Ethereum JSON-RPC methods](https://ethereum.org/developers/docs/apis/json-rpc/) (`eth_`, `net_`, `web3_`, `txpool_`, `trace_`, `debug_`) plus Tempo-specific namespaces for fork scheduling, consensus data, and node administration. Connect to a public RPC endpoint or [run your own node](https://tempo.xyz/developers/docs/guide/node/rpc): | Network | RPC URL | |---------|---------| | Mainnet | `https://rpc.tempo.xyz` | | Testnet | `https://rpc.moderato.tempo.xyz` | :::info Not all methods listed below are available on public endpoints. Method availability depends on node role and configuration. ::: | Method group | Availability | |---|---| | `tempo_forkSchedule` | All Tempo nodes | | `tempo_fundAddress` | Faucet-enabled testnet endpoints only | | `consensus_*` | Validator nodes only | | `consensus_subscribe` | Validator nodes over WebSocket only | | `admin_validatorKey` | Self-hosted nodes with `admin` API enabled | ## Tempo-specific `tempo_` namespace ### `tempo_forkSchedule` Returns the Tempo fork schedule and the currently active fork at the chain head. Each entry includes the fork name, activation timestamp, whether it is active, and an [EIP-2124](https://eips.ethereum.org/EIPS/eip-2124) fork hash. **Parameters:** None. **Returns:** | Field | Type | Description | |-------|------|-------------| | `schedule` | `ForkInfo[]` | Ordered list of Tempo forks | | `active` | `string` | Name of the currently active fork | Each `ForkInfo`: | Field | Type | Description | |-------|------|-------------| | `name` | `string` | Fork name (e.g. `"T0"`, `"T1"`, `"T2"`) | | `activationTime` | `number` | Unix timestamp of activation | | `active` | `boolean` | Whether this fork is active at the chain head | | `forkId` | `string` | EIP-2124 fork hash (e.g. `"0x471a451c"`). Omitted for forks that are not yet active | ```bash cast rpc tempo_forkSchedule --rpc-url https://rpc.tempo.xyz ``` **Example response:** ```json { "schedule": [ { "name": "T0", "activationTime": 0, "active": true, "forkId": "0xfde57c3e" }, { "name": "T1", "activationTime": 1770908400, "active": true, "forkId": "0x9e6fe384" }, { "name": "T1A", "activationTime": 1770908400, "active": true, "forkId": "0x9e6fe384" }, { "name": "T1B", "activationTime": 1771858800, "active": true, "forkId": "0x73a4f670" }, { "name": "T1C", "activationTime": 1773327600, "active": true, "forkId": "0x2a3ee80d" }, { "name": "T2", "activationTime": 1774965600, "active": true, "forkId": "0x471a451c" }, { "name": "T3", "activationTime": 1777298400, "active": true, "forkId": "0xd2087b77" } ], "active": "T3" } ``` ### `tempo_fundAddress` Available on faucet-enabled testnet endpoints only. Mints test stablecoins to the given address. On the public Moderato testnet endpoint, this currently mints pathUSD, AlphaUSD, BetaUSD, and ThetaUSD. **Parameters:** | Name | Type | Description | |------|------|-------------| | `address` | `Address` | Recipient address | **Returns:** `B256[]` — array of transaction hashes, one per token minted. ```bash cast rpc tempo_fundAddress 0xYOUR_ADDRESS \ --rpc-url https://rpc.moderato.tempo.xyz ``` ## Tempo consensus `consensus_` namespace Available on validator nodes only. Provides real-time consensus state for explorers, bridges, and indexers. ### `consensus_getFinalization` Get a finalized block by height or latest. **Parameters:** | Name | Type | Description | |------|------|-------------| | `query` | `"latest"` or `{"height": number}` | Which finalization to retrieve | **Returns:** `CertifiedBlock | null` | Field | Type | Description | |-------|------|-------------| | `epoch` | `number` | Consensus epoch | | `view` | `number` | Consensus view | | `digest` | `B256` | Block digest | | `certificate` | `string` | Hex-encoded BLS finalization certificate | | `block` | `Block` | The full Tempo block | ```bash # Latest finalization cast rpc consensus_getFinalization '"latest"' --rpc-url # By height cast rpc consensus_getFinalization '{"height": 1000000}' --rpc-url ``` ### `consensus_getLatest` Returns the current consensus state snapshot: the latest finalized block and the latest notarized block (if not yet finalized). **Parameters:** None. **Returns:** | Field | Type | Description | |-------|------|-------------| | `finalized` | `CertifiedBlock \| null` | Latest finalized block | | `notarized` | `CertifiedBlock \| null` | Latest notarized block (if ahead of finalized) | ```bash cast rpc consensus_getLatest --rpc-url ``` ### `consensus_subscribe` / `consensus_unsubscribe` WebSocket-only subscription to consensus events. Emits events when blocks are notarized, finalized, or views are nullified. **Event types:** | Type | Fields | Description | |------|--------|-------------| | `notarized` | `epoch`, `view`, `digest`, `certificate`, `block`, `seen` | A block was notarized | | `finalized` | `epoch`, `view`, `digest`, `certificate`, `block`, `seen` | A block was finalized | | `nullified` | `epoch`, `view`, `seen` | A view was nullified (no block produced) | The `seen` field is a Unix timestamp in milliseconds. **Example event:** ```json { "type": "finalized", "epoch": 42, "view": 387213, "digest": "0x6baa8fa8...", "certificate": "0x...", "block": { ... }, "seen": 1775134536000 } ``` ### `consensus_getIdentityTransitionProof` Returns DKG (Distributed Key Generation) identity transition proofs. Useful for light client verification and bridge implementations. **Parameters:** | Name | Type | Description | |------|------|-------------| | `from_epoch` | `number \| null` | Epoch to search from (defaults to latest finalized) | | `full` | `boolean \| null` | If `true`, return all transitions back to genesis; if `false` (default), only the most recent | **Returns:** | Field | Type | Description | |-------|------|-------------| | `identity` | `string` | Hex-encoded BLS public key at the requested epoch | | `transitions` | `IdentityTransition[]` | Transitions ordered newest to oldest | Each `IdentityTransition`: | Field | Type | Description | |-------|------|-------------| | `transitionEpoch` | `number` | Epoch where the DKG ceremony occurred | | `oldIdentity` | `string` | BLS public key before the transition | | `newIdentity` | `string` | BLS public key after the transition | | `proof` | `object` | Block header + finalization certificate. Omitted for genesis (epoch 0) | ```bash # Most recent transition cast rpc consensus_getIdentityTransitionProof null null --rpc-url # Full chain back to genesis cast rpc consensus_getIdentityTransitionProof null true --rpc-url ``` ## Tempo admin `admin_` namespace Requires the `admin` API to be enabled on a self-hosted node (`--http.api admin`). ### `admin_validatorKey` Returns the node's ed25519 validator public key if configured, or `null` for non-validator nodes. **Parameters:** None. **Returns:** `B256 | null` ```bash cast rpc admin_validatorKey --rpc-url http://localhost:8545 ``` ## Tempo-modified `eth_` methods Tempo is fully [EVM compatible](https://tempo.xyz/developers/docs/quickstart/evm-compatibility), but the following standard methods behave differently due to the lack of a native gas token: ### `eth_getBalance` Always returns a large constant (`0x9612084f0316e0ebd5182f398e5195a51b5ca47667d4c9b26c9b26c9b26c9b2`) rather than an actual balance. Tempo has no native token — use TIP-20 `balanceOf` to query token balances. ### `eth_estimateGas` Gas estimation accounts for TIP-20 fee token balances instead of native ETH. The gas allowance is calculated from the effective fee payer's selected fee token balance. ### `eth_sendRawTransaction` Accepts both standard EVM transaction types and [Tempo Transactions](https://tempo.xyz/developers/docs/protocol/transactions) (type `0x54`). Transactions targeting a subblock proposer are routed directly to the consensus layer when submitted to the matching validator node; other nodes reject them. # Tempo SDKs: available languages and tools Tempo is building clients in multiple languages to make integration as easy as possible. * [TypeScript](https://tempo.xyz/developers/docs/sdk/typescript) — Build on Tempo using TypeScript * [Go](https://tempo.xyz/developers/docs/sdk/go) — Build on Tempo using Go * [Foundry](https://tempo.xyz/developers/docs/sdk/foundry) — Build on Tempo using Foundry * [Rust](https://tempo.xyz/developers/docs/sdk/rust) — Build on Tempo using Rust # TypeScript SDKs: Viem extension and Account SDK Tempo distributes TypeScript SDKs for: * [Viem](https://viem.sh): TypeScript interface for EVM blockchains * [Wagmi](https://wagmi.sh): React Hooks (and reactive primitives) for EVM blockchains The Tempo extensions cover common chain operations such as querying state, sending Tempo Transactions, and managing tokens and AMM pools. * [Viem Setup](https://viem.sh/tempo) — Set up a Viem client to interact with Tempo * [Wagmi Setup](https://wagmi.sh/tempo) — Set up a Wagmi configuration with Tempo :::tip **When should I use Wagmi vs. Viem?** * **Viem** is best suited for **libraries, tooling, servers, scripting, etc** – a low-level and stateless interface for the EVM * **Wagmi** is best suited for **applications & wallets** – a high-level and stateful interface for the EVM (React Hooks, Vanilla JS, etc) ::: ## Viem SDK for Tempo Use Viem for Node.js scripts, servers, and other headless applications. ### Connect to Tempo Testnet and send a stablecoin Install Viem: ```bash npm install viem ``` Connect to Tempo Testnet, fund an account, read its pathUSD balance, and send a confirmed transfer. ```ts twoslash [testnet.ts] filename="testnet.ts" import { Account, createClient } from 'viem/tempo' const privateKey = '0x...' const account = Account.fromSecp256k1(privateKey) const client = createClient({ account, testnet: true }) await client.faucet.fundSync({ account }) const token = '0x20c0000000000000000000000000000000000000' // pathUSD const balance = await client.token.getBalance({ token }) const { receipt } = await client.token.transferSync({ amount: { formatted: '1' }, to: '0x742d35cc6634c0532925a3b844bc9e7595f0bebb', token, }) ``` :::warning This example is testnet-only. For mainnet, use a funded mainnet account and token, omit `testnet: true`, and remove the faucet call. ::: `balance` includes the amount in base units, the token's decimals, and a formatted value. `transferSync` waits for the transaction to be included; `receipt.transactionHash` identifies the transaction. The same client includes standard Viem methods such as `getBlockNumber`, `getTransaction`, `getTransactionReceipt`, `getLogs`, `readContract`, and `simulateContract`. ### Tempo Transactions | Goal | Viem input | Guide | | --- | --- | --- | | Send multiple calls together | `calls` | [Batch calls](https://tempo.xyz/developers/docs/guide/tempo-transaction#batch-calls) | | Pay fees with a chosen token | `feeToken` | [Configurable fee tokens](https://tempo.xyz/developers/docs/guide/tempo-transaction#configurable-fee-tokens) | | Let another account pay the fees | `feePayer` | [Fee sponsorship](https://tempo.xyz/developers/docs/guide/tempo-transaction#fee-sponsorship) | | Send independent transaction sequences | `nonceKey` | [Concurrent transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction#concurrent-transactions) | | Limit when a transaction can run | `validAfter` and `validBefore` | [Scheduled transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction#scheduled-transactions) | ### Tempo guides and API reference * [Payments](https://tempo.xyz/developers/docs/guide/payments) — Send and accept stablecoins, attach memos, and configure payment controls * [Stablecoin Issuance](https://tempo.xyz/developers/docs/guide/issuance) — Launch and operate a TIP-20 stablecoin * [Stablecoin Exchange](https://tempo.xyz/developers/docs/guide/stablecoin-dex) — Trade stablecoins and provide liquidity * [Tempo Transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction) — Use fee tokens, sponsorship, batching, access keys, and concurrent transactions * [Actions](https://viem.sh/tempo/actions) — Viem Actions for querying data, sending transactions, managing tokens & AMM pools, and more ## Wagmi SDK for Tempo * [Hooks](https://wagmi.sh/tempo/hooks) — Wagmi React Hooks for building apps on Tempo * [Connectors](https://wagmi.sh/tempo/connectors) — Wagmi Connectors for connecting between wallets, apps, and Tempo * [Actions](https://wagmi.sh/tempo/actions) — Wagmi Actions for querying data, sending transactions, managing tokens & AMM pools, and more # Tempo Go SDK: installing and using it Tempo distributes a Go SDK for building application clients. The SDK provides packages for RPC communication, transaction signing, and key management. The Tempo Go SDK can be used to perform common operations with the chain, such as: sending Tempo Transactions, batching multiple calls, fee sponsorship, and more. ::::steps ## Install the Go SDK To install the Tempo Go SDK: ```bash [go] go get github.com/tempoxyz/tempo-go@v0.3.0 ``` :::tip The SDK requires Go 1.21 or higher. ::: ## Create a Go RPC client To interact with Tempo, first create an RPC client connected to a Tempo node: ```go [main.go] package main import ( "context" "fmt" "github.com/tempoxyz/tempo-go/pkg/client" ) func main() { c := client.New("https://rpc.tempo.xyz") ctx := context.Background() blockNum, _ := c.GetBlockNumber(ctx) fmt.Printf("Connected to Tempo at block %d\n", blockNum) } ``` For authenticated RPC endpoints: ```go [main.go] c := client.New("https://rpc.tempo.xyz", client.WithAuth("username", "password"), ) ``` ## Create a Go transaction signer Create a signer to sign transactions. The signer manages your private key and generates signatures: ```go [main.go] package main import ( "fmt" "github.com/tempoxyz/tempo-go/pkg/signer" ) func main() { s, err := signer.NewSigner("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") if err != nil { panic(err) } fmt.Printf("Address: %s\n", s.Address().Hex()) } ``` ## Send a Tempo transaction with Go Build and send a transaction using the builder pattern: ```go [main.go] package main import ( "context" "log" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/client" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { c := client.New("https://rpc.tempo.xyz") s, _ := signer.NewSigner("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, s.Address().Hex()) // [!code hl] recipient := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") // [!code hl:10] tx := transaction.NewBuilder(big.NewInt(4217)). // Tempo mainnet SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(20000000000)). // 20 gwei base fee SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(recipient, big.NewInt(0), []byte{}). Build() transaction.SignTransaction(tx, s) serialized, _ := transaction.Serialize(tx, nil) hash, _ := c.SendRawTransaction(ctx, serialized) // [!code hl] log.Printf("Transaction hash: %s", hash) } ``` :::: ## Go SDK examples ### Read chain data with Go Query the blockchain for basic information: ```go [read.go] ctx := context.Background() blockNum, _ := c.GetBlockNumber(ctx) chainID, _ := c.GetChainID(ctx) nonce, _ := c.GetTransactionCount(ctx, "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") fmt.Printf("Block: %d, Chain: %d, Nonce: %d\n", blockNum, chainID, nonce) ``` ### Send a token transfer with Go Send a TIP-20 token transfer using go-ethereum's ABI encoding: ```go [transfer.go] import "github.com/ethereum/go-ethereum/accounts/abi" erc20ABI, _ := abi.JSON(strings.NewReader(`[{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}]}]`)) recipient := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") amount := big.NewInt(100_000_000) // 100 tokens (6 decimals) transferData, _ := erc20ABI.Pack("transfer", recipient, amount) tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(transaction.AlphaUSDAddress, big.NewInt(0), transferData). Build() ``` ### Send a memo transfer with Go Include a memo for payment reconciliation: ```go [memo.go] tip20ABI, _ := abi.JSON(strings.NewReader(`[{"name":"transferWithMemo","type":"function","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"},{"name":"memo","type":"bytes32"}]}]`)) recipient := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") amount := big.NewInt(100_000_000) memo := [32]byte{} copy(memo[:], "INV-12345") memoData, _ := tip20ABI.Pack("transferWithMemo", recipient, amount, memo) tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(transaction.AlphaUSDAddress, big.NewInt(0), memoData). Build() ``` ### Batch multiple calls with Go Execute multiple operations atomically in a single transaction: ```go [batch.go] tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(200000). SetMaxFeePerGas(big.NewInt(10000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(addr1, big.NewInt(0), transfer1Data). // [!code hl] AddCall(addr2, big.NewInt(0), transfer2Data). // [!code hl] AddCall(addr3, big.NewInt(0), contractCallData). // [!code hl] Build() transaction.SignTransaction(tx, s) ``` ### Parallel Transactions (2D Nonces) Send multiple transactions concurrently using different nonce keys: ```go [parallel.go] tx1 := transaction.NewBuilder(big.NewInt(4217)). SetNonceKey(big.NewInt(1)). // Sequence A // [!code hl] SetNonce(0). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(recipient1, big.NewInt(0), data1). Build() tx2 := transaction.NewBuilder(big.NewInt(4217)). SetNonceKey(big.NewInt(2)). // Sequence B (parallel) // [!code hl] SetNonce(0). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(recipient2, big.NewInt(0), data2). Build() transaction.SignTransaction(tx1, s) transaction.SignTransaction(tx2, s) // Send both in parallel go func() { c.SendRawTransaction(ctx, serialize(tx1)) }() go func() { c.SendRawTransaction(ctx, serialize(tx2)) }() ``` ### Fee sponsorship with Go Have another account pay for transaction fees: ```go [feepayer.go] tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). SetSponsored(true). // Mark as awaiting fee payer // [!code hl] AddCall(recipient, big.NewInt(0), data). Build() transaction.SignTransaction(tx, userSigner) transaction.AddFeePayerSignature(tx, feePayerSigner) ``` ### Transaction validity windows in Go Set a time window during which the transaction is valid: ```go [validity.go] now := time.Now() tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). SetValidAfter(uint64(now.Unix())). // [!code hl] SetValidBefore(uint64(now.Add(1 * time.Hour).Unix())). // [!code hl] AddCall(recipient, big.NewInt(0), data). Build() ``` ### Batch RPC requests with Go Send multiple RPC calls efficiently in a single HTTP request: ```go [batch_rpc.go] batch := client.NewBatchRequest() batch.Add("eth_blockNumber"). Add("eth_chainId"). Add("eth_getBalance", "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", "latest") responses, _ := c.SendBatch(ctx, batch) for _, resp := range responses { fmt.Printf("Result: %v\n", resp.Result) } ``` ## Account Keychain in the Go SDK The `keychain` package provides typed helpers for Tempo's [Account Keychain precompile](https://tempo.xyz/developers/docs/protocol/transactions/AccountKeychain), enabling access key management and signing directly from Go. :::info Enhanced access key features — periodic spending limits and call scoping — require the [T3 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t3). ::: ```go [keychain_manage.go] package main import ( "context" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/tempoxyz/tempo-go/pkg/client" "github.com/tempoxyz/tempo-go/pkg/keychain" "github.com/tempoxyz/tempo-go/pkg/signer" "github.com/tempoxyz/tempo-go/pkg/transaction" ) func main() { c := client.New("https://rpc.tempo.xyz") s, _ := signer.NewSigner("0xYOUR_PRIVATE_KEY") ctx := context.Background() nonce, _ := c.GetTransactionCount(ctx, s.Address().Hex()) accessKeyAddr := common.HexToAddress("") // Authorize a new access key (secp256k1, no expiry): restrictions := keychain.NewKeyRestrictions(0) call, _ := keychain.AuthorizeKey(accessKeyAddr, keychain.SignatureTypeSecp256k1, restrictions) tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(200000). SetMaxFeePerGas(big.NewInt(20000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(call.To, big.NewInt(0), call.Data). Build() // Authorize with a spending limit: token := common.HexToAddress("") restrictions = keychain.NewKeyRestrictions(0). WithLimits([]keychain.TokenLimit{{Token: token, Amount: big.NewInt(1_000_000)}}) call, _ = keychain.AuthorizeKey(accessKeyAddr, keychain.SignatureTypeSecp256k1, restrictions) // Authorize with call scopes (restrict to specific contracts/functions): scope := keychain.NewCallScopeBuilder(token). Transfer(nil). Approve(nil). Build() restrictions = keychain.NewKeyRestrictions(0). WithAllowedCalls([]keychain.CallScope{scope}) call, _ = keychain.AuthorizeKey(accessKeyAddr, keychain.SignatureTypeSecp256k1, restrictions) // Full example: 24h expiry + spending limit + call scope: expiry := uint64(time.Now().Add(24 * time.Hour).Unix()) restrictions = keychain.NewKeyRestrictions(expiry). WithLimits([]keychain.TokenLimit{{Token: token, Amount: big.NewInt(1_000_000)}}). WithAllowedCalls([]keychain.CallScope{ keychain.NewCallScopeBuilder(token).Transfer(nil).Build(), }) call, _ = keychain.AuthorizeKey(accessKeyAddr, keychain.SignatureTypeSecp256k1, restrictions) // Revoke an access key (permanent, cannot be re-authorized): call, _ = keychain.RevokeKey(accessKeyAddr) // Update spending limit for a key-token pair: call, _ = keychain.UpdateSpendingLimit(accessKeyAddr, token, big.NewInt(2_000_000)) // Replace all call scopes for a key: call, _ = keychain.SetAllowedCalls(accessKeyAddr, []keychain.CallScope{ keychain.NewCallScopeBuilder(token).Transfer(nil).Build(), }) // Remove a target contract from allowed call list: call, _ = keychain.RemoveAllowedCalls(accessKeyAddr, token) _ = ctx _ = tx _ = call } ``` ### Sign with an access key in Go Use `keychain.SignWithAccessKey` to sign a transaction as an access key holder: ```go [access_key_sign.go] accessKeySigner, _ := signer.NewSigner("") rootAccount := common.HexToAddress("") tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(20000000000)). SetMaxPriorityFeePerGas(big.NewInt(1000000000)). AddCall(recipient, big.NewInt(0), data). Build() keychain.SignWithAccessKey(tx, accessKeySigner, rootAccount) // [!code hl] serialized, _ := transaction.Serialize(tx, nil) hash, _ := c.SendRawTransaction(ctx, serialized) ``` ### Query remaining spending limits with Go ```go [query_limit.go] calldata := keychain.EncodeGetRemainingLimitCalldata( common.HexToAddress(""), common.HexToAddress(""), common.HexToAddress(""), ) result, _ := c.Call(ctx, keychain.GetKeychainAddress().Hex(), calldata) remaining := keychain.ParseRemainingLimitResult(result) fmt.Printf("Remaining: %s\n", remaining.String()) ``` ## Go SDK packages | Package | Description | | --- | --- | | `transaction` | TempoTransaction encoding, signing, and validation | | `client` | RPC client for interacting with Tempo nodes | | `signer` | Key management and signature generation | | `keychain` | Account Keychain precompile: access key management and signing | ## Next steps for the Go SDK After setting up the Go SDK, you can: * Follow a guide on how to [make payments](https://tempo.xyz/developers/docs/guide/payments), [issue stablecoins](https://tempo.xyz/developers/docs/guide/issuance), [exchange stablecoins](https://tempo.xyz/developers/docs/guide/stablecoin-dex), and [more](https://tempo.xyz/developers/docs). * View the [examples on GitHub](https://github.com/tempoxyz/tempo-go/tree/main/examples) # Using Foundry with Tempo Tempo is supported as a first-class citizen in [Foundry](https://github.com/foundry-rs/foundry): the leading Ethereum development toolkit. Install the latest Foundry release to access Tempo's [protocol-level features](https://tempo.xyz/developers/docs/protocol) in `forge`, `cast`, `anvil`, and `chisel`, and to build, test, and deploy contracts that go [beyond the limits of standard EVM chains](https://tempo.xyz/developers/docs/quickstart/evm-compatibility). :::warning[`tempo-foundry` is deprecated] `tempo-foundry` and `foundryup -n tempo` are deprecated. Switch to the latest upstream Foundry release with `foundryup`. ::: For general information about Foundry, see the [Foundry documentation](https://getfoundry.sh/). ## Get started with Foundry Install the latest Foundry release to get Tempo support. ::::steps ### Install `foundryup` for Tempo If you don't have `foundryup` installed yet: ```bash curl -L https://foundry.paradigm.xyz | bash ``` ### Install or update Foundry ```bash foundryup ``` This installs the latest versioned release of [`forge`](https://getfoundry.sh/forge/overview#forge), [`cast`](https://getfoundry.sh/cast/overview#cast), [`anvil`](https://getfoundry.sh/anvil/overview#anvil), and [`chisel`](https://getfoundry.sh/chisel/overview#chisel). :::tip To install a specific version, replace `` with the desired release tag: ```bash foundryup --install ``` ::: ### Create a new Foundry project Initialize a new Foundry project with the Tempo template: ```bash forge init -n tempo my-project && cd my-project ``` This gives you a Tempo-ready starter project, including the Tempo `Mail` example template. If you're adding Tempo support to an existing Foundry project, install [`tempo-std`](https://github.com/tempoxyz/tempo-std) manually: ```bash forge install tempoxyz/tempo-std ``` :::: ## Configure `foundry.toml` for Tempo The Tempo template gives you a working starting point, but it is often useful to make Tempo explicit in `foundry.toml`. ### Configure Tempo RPC aliases Set a default Tempo RPC alias and keep a separate alias for Moderato testnet: ```toml [profile.default] eth_rpc_url = "tempo" [rpc_endpoints] tempo = "${TEMPO_RPC_URL}" moderato = "${TEMPO_TESTNET_RPC_URL}" ``` With this config, commands that use the default RPC pick up `tempo` automatically, and you can still switch explicitly with `--rpc-url moderato`. ### Activate Tempo features explicitly For most projects, the most flexible option is to enable Tempo network features directly: ```toml [profile.default] tempo = true ``` This network flag enables Tempo-specific network behavior while still letting Foundry resolve the right semantics from the chain you are targeting. If you need advanced testing against historical network behavior, pin a specific Tempo hardfork explicitly in `foundry.toml` or via inline config. Most projects should avoid hardfork pinning so local tests track the current Tempo rules for the network they target. ### Configure Tempo contract verification Tempo's contract verifier is Sourcify-compatible. Configure verification with `VERIFIER_URL=https://contracts.tempo.xyz` or `--verifier-url https://contracts.tempo.xyz`. The `[etherscan]` table in `foundry.toml` is for Etherscan-style verifiers, not Tempo's verifier. ### Use `foundry-toolchain` in Tempo CI Use the [`foundry-toolchain`](https://github.com/foundry-rs/foundry-toolchain) GitHub Action to install Foundry in your CI. Tempo support is included in the latest Foundry release, so no special configuration is needed. ```yaml - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 ``` ## Use Foundry for Tempo workflows All standard Foundry commands are supported out of the box. ### Test and deploy Tempo contracts locally with `forge` ```bash # Build your contracts forge build # Run all tests locally forge test # Run deployment scripts locally forge script script/Mail.s.sol ``` ### Test and deploy with `forge` on Tempo Testnet ```bash # Set environment variables export TEMPO_RPC_URL=https://rpc.moderato.tempo.xyz export VERIFIER_URL=https://contracts.tempo.xyz # Optional: create a new keypair and request some testnet tokens from the faucet. cast wallet new cast rpc tempo_fundAddress --rpc-url https://rpc.moderato.tempo.xyz # Run all tests on Tempo's testnet forge test # Deploy and verify a simple contract forge create src/Mail.sol:Mail \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --broadcast \ --verify \ --constructor-args 0x20c0000000000000000000000000000000000001 # Deploy a simple contract with custom fee token forge create src/Mail.sol:Mail \ --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --broadcast \ --verify \ --constructor-args 0x20c0000000000000000000000000000000000001 # Set a salt for deterministic contract address derivation # The salt is passed to TIP20_FACTORY.createToken() which uses it with the sender # address to compute a deterministic deployment address via getTokenAddress(sender, salt) export SALT="my-unique-salt" # Run a deployment script and verify forge script script/Mail.s.sol \ --sig "run(string)" $SALT \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --sender \ --broadcast \ --verify # Run a deployment script with custom fee token and verify forge script script/Mail.s.sol \ --sig "run(string)" $SALT \ --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive \ --sender \ --broadcast \ --verify # Batch multiple calls into a single atomic transaction forge script script/Deploy.s.sol \ --broadcast --batch \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY ``` Use a root key for `forge create`. Access keys can sign calls but not deployments. For more verification options including verifying existing contracts and API verification, see [Contract Verification](https://tempo.xyz/developers/docs/quickstart/verify-contracts). :::warning[Batch Transaction Rules] * **Atomic execution**: If any call reverts, the entire batch reverts * **Single CREATE allowed**: At most one contract deployment per batch * **CREATE must be first**: Deployment must be the first operation * **Value must be zero**: Since Tempo has no native token, value must be 0 * **Silent failures**: Calling a non-existent function without a fallback succeeds silently ::: ### Interact and debug Tempo contracts with `cast` ```bash # Check that your contract is deployed: cast code \ --rpc-url $TEMPO_RPC_URL # Interact with the contract, retrieving the token address: cast call "token()" \ --rpc-url $TEMPO_RPC_URL # Get the name of an ERC20 token: cast erc20 name \ --rpc-url $TEMPO_RPC_URL # Check the ERC20 token balance of your address: cast erc20 balance \ --rpc-url $TEMPO_RPC_URL # Transfer some of your ERC20 tokens: cast erc20 transfer \ --rpc-url $TEMPO_RPC_URL \ --interactive # Transfer some of your ERC20 tokens with custom fee token: cast erc20 transfer \ --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive # Send a transaction with custom fee token: cast send \ --tempo.fee-token \ --rpc-url $TEMPO_RPC_URL \ --interactive # Replay a transaction by hash: cast run \ --rpc-url $TEMPO_RPC_URL # Send a batch transaction with multiple calls: cast batch-send \ --call "::increment()" \ --call "::setNumber(uint256):500" \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Batch with pre-encoded calldata: ENCODED=$(cast calldata "setNumber(uint256)" 200) cast batch-send \ --call "::$ENCODED" \ --call "::setNumber(uint256):101" \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Sponsored transaction (gasless for sender): # Step 1: Get the fee payer signature hash FEE_PAYER_HASH=$(cast mktx 'increment()' --rpc-url $TEMPO_RPC_URL --private-key $SENDER_KEY --tempo.print-sponsor-hash) # Step 2: Sponsor signs the hash SPONSOR_SIG=$(cast wallet sign --private-key $SPONSOR_KEY "$FEE_PAYER_HASH" --no-hash) # Step 3: Send with sponsor signature cast send 'increment()' --rpc-url $TEMPO_RPC_URL --private-key $SENDER_KEY --tempo.sponsor-signature "$SPONSOR_SIG" # Send with 2D nonce (parallel tx submission): cast send 'increment()' \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 1 # Send with expiring nonce (time-bounded tx, max 30s): VALID_BEFORE=$(($(date +%s) + 25)) cast send 'increment()' \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY \ --tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE # Send with access key (delegated signing): # First authorize the key via Account Keychain precompile cast send 0xAAAAAAAA00000000000000000000000000000000 \ 'authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))' \ $ACCESS_KEY_ADDR 0 '(1893456000,false,[],true,[])' \ --rpc-url $TEMPO_RPC_URL \ --private-key $ROOT_PRIVATE_KEY # Then send using the access key cast send 'increment()' \ --rpc-url $TEMPO_RPC_URL \ --tempo.access-key $ACCESS_KEY_PRIVATE_KEY \ --tempo.root-account $ROOT_ADDRESS ``` If the access key will be used with passkey or WebAuthn signatures, pass `2` for `SignatureType`. `1` is only for raw P256 signatures. Access-key transactions cannot create contracts, so use a root key for deployments or other flows that perform `CREATE`. ### Local Tempo development with Anvil Anvil supports Tempo mode for local testing and forking Tempo networks: ```bash # Start anvil in Tempo mode anvil --tempo # Fork a live Tempo network for local testing anvil --tempo --fork-url $TEMPO_RPC_URL # Test transactions on local anvil fork cast send 'increment()' \ --tempo.fee-token \ --rpc-url http://127.0.0.1:8545 \ --private-key $PRIVATE_KEY # 2D nonce on anvil fork cast send 'increment()' \ --tempo.fee-token \ --rpc-url http://127.0.0.1:8545 \ --private-key $PRIVATE_KEY \ --nonce 0 --tempo.nonce-key 100 # Expiring nonce on anvil fork cast send 'increment()' \ --tempo.fee-token \ --rpc-url http://127.0.0.1:8545 \ --private-key $PRIVATE_KEY \ --tempo.expiring-nonce --tempo.valid-before $(($(date +%s) + 25)) # Batch transactions on anvil fork cast batch-send \ --tempo.fee-token \ --rpc-url http://127.0.0.1:8545 \ --call "::increment()" \ --call "::increment()" \ --private-key $PRIVATE_KEY ``` ## Tempo-specific Foundry CLI flags The following flags are available for `cast` and `forge script` for Tempo-specific features: | Flag | Description | Example | |------|-------------|---------| | `--tempo.fee-token
` | Specify the TIP-20 token to pay transaction fees | `--tempo.fee-token 0x20c0...0001` | | `--tempo.nonce-key ` | 2D nonce key for parallel transaction submission | `--tempo.nonce-key 1` | | `--tempo.expiring-nonce` | Enable expiring nonce for time-bounded transactions | `--tempo.expiring-nonce` | | `--tempo.valid-before ` | Unix timestamp before which tx must execute (max 30s from now) | `--tempo.valid-before 1704067200` | | `--tempo.valid-after ` | Unix timestamp after which tx can execute | `--tempo.valid-after 1704067100` | | `--tempo.sponsor-signature ` | Pre-signed sponsor signature for gasless transactions | `--tempo.sponsor-signature 0x...` | | `--tempo.print-sponsor-hash` | Print fee payer signature hash and exit (for sponsor to sign) | `--tempo.print-sponsor-hash` | | `--tempo.access-key ` | Private key for delegated signing via access key | `--tempo.access-key $ACCESS_KEY_PRIVATE_KEY` | | `--tempo.root-account
` | Root account address when using an access key | `--tempo.root-account $ROOT_ADDRESS` | Ledger and Trezor wallets are not yet compatible with any `--tempo.*` option. ## `cast keychain` for Tempo access keys `cast keychain` provides a CLI interface to Tempo's [Account Keychain precompile](https://tempo.xyz/developers/docs/protocol/transactions/AccountKeychain). Prefer this over hand-encoding `authorizeKey(...)` calldata when you are working from the CLI. :::info `cast keychain` only works on Tempo networks. ::: `cast keychain` authorization takes a future expiry timestamp, `webauthn` for passkey-backed access keys, optional `TOKEN:AMOUNT:PERIOD_SECONDS` limits for recurring budgets, and `--scope` for target, selector, and recipient restrictions. `cast keychain` sends the Account Keychain ABI directly, so a non-expiring key uses `18446744073709551615` (`type(uint64).max`) as the expiry value. This differs from tx-level `key_authorization`, where a non-expiring key is represented by omitting `expiry`. Do not pass `0`. ```bash # Access keys must be authorized with a future expiry timestamp. EXPIRY=$(($(date +%s) + 86400)) # For a non-expiring key via direct precompile ABI / cast keychain, use: NEVER_EXPIRES=18446744073709551615 # Authorize a new access key (signature types: secp256k1, p256, webauthn): cast keychain authorize secp256k1 $EXPIRY \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Authorize with a spending limit (TOKEN:AMOUNT or TOKEN:AMOUNT:PERIOD_SECONDS): cast keychain authorize secp256k1 $EXPIRY \ --limit :1000000 \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Authorize with call scopes (restrict to specific contracts/functions): cast keychain authorize secp256k1 $EXPIRY \ --scope :transfer,approve \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Authorize with call scope restricted to a specific recipient: cast keychain authorize secp256k1 $EXPIRY \ --scope :transfer@ \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Full example: 24h expiry + spending limit + call scope: cast keychain authorize secp256k1 $EXPIRY \ --limit :1000000 \ --scope :transfer \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Revoke an access key (permanent, cannot be re-authorized): cast keychain revoke \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Update spending limit for a key-token pair: cast keychain update-limit \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Replace all call scopes for a key: cast keychain set-scope \ --scope :transfer \ --scope \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Remove a target contract from allowed call list: cast keychain remove-scope \ --rpc-url $TEMPO_RPC_URL \ --private-key $PRIVATE_KEY # Query key provisioning status (read-only): cast keychain check \ --rpc-url $TEMPO_RPC_URL # Query remaining spending limit via the precompile directly: cast call 0xAAAAAAAA00000000000000000000000000000000 \ 'getRemainingLimitWithPeriod(address,address,address)(uint256,uint64)' \ \ --rpc-url $TEMPO_RPC_URL ``` # Using MPP with Foundry Foundry includes native MPP support on Tempo. When an RPC endpoint returns `402 Payment Required`, Foundry automatically handles the payment challenge with no wrapper scripts, middleware, or code changes. :::warning[`tempo-foundry` is deprecated] `tempo-foundry` and `foundryup -n tempo` are deprecated. Install the latest Foundry release with `foundryup`. ::: Every Foundry tool works transparently with MPP-gated endpoints: * **`cast`** — queries and transactions * **`forge`** — scripts and forked tests * **`anvil`** — local forks of paid endpoints * **`chisel`** — interactive REPL sessions ## How MPP works with Foundry When you point any Foundry tool at an MPP-gated RPC URL, the built-in transport intercepts `402` responses and resolves them using MPP's [session flow](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go): 1. **First request** — Foundry sends a normal JSON-RPC request to the endpoint. 2. **402 challenge** — The server responds with `402 Payment Required` and a `WWW-Authenticate: Payment` header describing the price. 3. **Key discovery** — Foundry reads your signing key from `$TEMPO_HOME/wallet/keys.toml` (default `~/.tempo/wallet/keys.toml`) or the `TEMPO_PRIVATE_KEY` env var. If the server offers multiple payment challenges (e.g. different chains or currencies), Foundry automatically picks the one matching your key's chain ID and spending allowance. 4. **Channel open** — If no payment channel exists, Foundry opens one on-chain with a deposit (default: `100,000` base units). This is a one-time on-chain lockup — unused balance remains in the channel. 5. **Voucher payment** — Foundry signs an off-chain voucher against the open channel and retries the request with an `Authorization: Payment` header. 6. **Auto top-up** — When a channel's deposit is exhausted, Foundry sends a top-up transaction. The server accepts it with `204 No Content`, then Foundry signs a fresh voucher and retries automatically. 7. **Channel reuse** — Subsequent requests reuse the same channel. Channel state is persisted to `$TEMPO_HOME/foundry/channels.json` (default `~/.tempo/foundry/channels.json`) across process invocations. :::tip Channel reuse means the first call to an MPP endpoint has roughly one confirmation of overhead (~500ms on Tempo), but all subsequent calls add near-zero latency. ::: ## Foundry MPP setup :::note Some endpoints use a one-shot `charge` intent instead of session-based channels. Foundry handles both — charge payments sign a single TIP-20 transfer without opening a channel. ::: :::steps ### Install the Tempo CLI ```bash curl -fsSL https://tempo.xyz/install | bash ``` ### Install Foundry Tempo support now ships in the latest Foundry releases: ```bash foundryup ``` All standard Foundry commands work as before — MPP activates only when an endpoint returns `402`. ### Configure your wallet ```bash tempo wallet login ``` This creates `~/.tempo/wallet/keys.toml` with your signing key. Foundry discovers this key automatically on the first `402` response. Alternatively, set the `TEMPO_PRIVATE_KEY` environment variable: ```bash export TEMPO_PRIVATE_KEY=0xabc…123 ``` ### Use MPP endpoints Point any Foundry tool at an MPP-gated RPC URL. No additional flags or config needed. ```bash cast block-number --rpc-url https://rpc.mpp.tempo.xyz ``` ::: ## Foundry MPP examples ### MPP requests with `cast` Query chain state through a paid endpoint: ```bash # Get latest block number cast block-number --rpc-url https://rpc.mpp.tempo.xyz # Read a contract cast call 0x20c0000000000000000000000000000000000000 \ "balanceOf(address)(uint256)" 0xYourAddress \ --rpc-url https://rpc.mpp.tempo.xyz ``` ### MPP requests in `forge script` Run deployment or read scripts against a paid endpoint: ```solidity // script/ReadBlock.s.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Script.sol"; contract ReadBlock is Script { function run() public view { console.log("block", block.number); console.log("chain", block.chainid); } } ``` ```bash forge script script/ReadBlock.s.sol --rpc-url https://rpc.mpp.tempo.xyz ``` ### MPP requests in forked `forge test` Fork a paid endpoint in tests using `vm.createSelectFork`: ```solidity // test/MppFork.t.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Test.sol"; contract MppForkTest is Test { function test_fork_via_mpp() public { vm.createSelectFork("https://rpc.mpp.tempo.xyz"); assertGt(block.number, 0); assertEq(block.chainid, 4217); } } ``` ```bash forge test --match-test test_fork_via_mpp -vvv ``` ### MPP requests with Anvil Fork a paid endpoint locally. Local RPC calls stay local, but any upstream fetches Anvil makes to the fork URL go through MPP: ```bash anvil --fork-url https://rpc.mpp.tempo.xyz ``` ### MPP requests with Chisel Interactive REPL against a paid endpoint: ```bash chisel --fork-url https://rpc.mpp.tempo.xyz ``` ``` ➜ block.number Type: uint256 ├ Hex: 0x... └ Decimal: 1234567 ``` ## Foundry MPP configuration ### MPP deposit amount Set the fallback deposit amount used when the server does not suggest one: ```bash export MPP_DEPOSIT=500000 cast block-number --rpc-url https://rpc.mpp.tempo.xyz ``` The deposit determines how many RPC calls you can make before the channel needs a top-up. When a channel is exhausted, Foundry automatically tops it up. ### MPP key discovery Foundry discovers MPP signing keys in this order: 1. **`TEMPO_PRIVATE_KEY`** env var — highest priority, no keychain metadata 2. **`$TEMPO_HOME/wallet/keys.toml`** — created by `tempo wallet login`, includes keychain signing mode and authorized signer metadata Within `keys.toml`, the key selection priority is: * Passkey entries first * Entries with an inline private key second * First entry as fallback Foundry needs a usable inline private key — entries without one are skipped. When the server offers multiple chains or currencies, Foundry picks the first key that matches both the chain ID and currency from the challenge. ### MPP channel persistence Open channels are saved to `$TEMPO_HOME/foundry/channels.json` (default `~/.tempo/foundry/channels.json`). This allows channel reuse across process invocations — you won't re-open a channel every time you run `cast` or `forge`. Channels are automatically evicted when fully spent or closed. If the server restarts and returns `410 Gone`, Foundry clears stale local state and opens a fresh channel on the next request. ## Tempo MPP testnet workflow Use the Moderato testnet MPP endpoint for development: ```bash cast block-number --rpc-url https://rpc.mpp.moderato.tempo.xyz # Mainnet cast block-number --rpc-url https://rpc.mpp.tempo.xyz ``` Fund your testnet wallet with `tempo wallet fund` before making requests. ### MPP gas sponsorship Some MPP endpoints sponsor gas fees on behalf of the caller. When the server's challenge includes a `feePayer` flag, Foundry delegates gas payment to the server, so no native balance is needed for gas. ## Troubleshoot MPP with Foundry | Error | Cause | Fix | |---|---|---| | `tempo: command not found` | Tempo CLI not installed | Run `curl -fsSL https://tempo.xyz/install \| bash` | | `no supported MPP challenge` | Missing wallet key or wrong chain/currency | Run `tempo wallet login` or check `keys.toml` | | `410 Gone` | Stale local channel state | Re-run the command — Foundry clears stale state and opens a fresh channel | | `access key does not exist` | Signing key not yet provisioned on-chain | Foundry retries automatically with a key provisioning bundle — no action needed | ## Next steps for MPP with Foundry * [Client quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/client) — Handle payment-gated resources with the TypeScript SDK * [Agent quickstart](https://tempo.xyz/developers/docs/guide/machine-payments/agent) — Make paid requests from a terminal or AI agent * [Pay-as-you-go](https://tempo.xyz/developers/docs/guide/machine-payments/pay-as-you-go) — Session-based billing with off-chain vouchers # Signature Verification with Foundry The [`SignatureVerifier` precompile](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md) is available on Tempo. It provides signature verification for secp256k1, P256, and WebAuthn through a single interface — no custom verifier contracts needed. The Foundry project template for Tempo ships with a working example that demonstrates signature verification in a relayed mail contract. Initialize it with: ```bash forge init --template tempo my-project && cd my-project ``` ## How signature verification works in Foundry The template's `Mail` contract supports two modes: 1. **Direct** — call `sendMail()` yourself (`msg.sender` is the sender). 2. **Relayed** — sign a mail off-chain and let anyone deliver it on-chain. Relayed mode uses the `SignatureVerifier` precompile to verify the sender's signature. Unlike Ethereum's `ecrecover`, the precompile: * Supports secp256k1, P256, and WebAuthn signature types * Reverts on invalid signatures instead of returning `address(0)` * Maintains forward compatibility with future Tempo account types :::info[T6 keychain verification] On T6 networks, `SignatureVerifier` also exposes `verifyKeychain(account, digest, signature)` and `verifyKeychainAdmin(account, digest, signature)` for contracts that need to check whether a signature came from an active access key, root key, or admin key for an account. Include chain ID, contract address, and account address in the digest you ask users or keys to sign. ::: ## Contract example The key pattern is a single `verify()` or `recover()` call on the precompile: ```solidity import {StdPrecompiles} from "tempo-std/StdPrecompiles.sol"; // Option 1: verify — returns true/false, reverts on malformed signatures require( StdPrecompiles.SIGNATURE_VERIFIER.verify(from, hash, signature), "invalid signature" ); // Option 2: recover — returns the signer address, reverts on malformed signatures require( StdPrecompiles.SIGNATURE_VERIFIER.recover(hash, signature) == from, "invalid signature" ); ``` The full `Mail` contract in the template combines this with a per-sender nonce to prevent replay: ```solidity contract Mail { ITIP20 public token; mapping(address => uint256) public nonces; /// @notice Send mail on behalf of `from` using their off-chain Tempo signature. function sendMail( address from, address to, string memory message, Attachment memory attachment, bytes calldata signature ) external { bytes32 hash = getDigest(from, to, message, attachment); require( StdPrecompiles.SIGNATURE_VERIFIER.verify(from, hash, signature), "invalid signature" ); nonces[from]++; token.transferFromWithMemo(from, to, attachment.amount, attachment.memo); emit MailSent(from, to, message, attachment); } function getDigest(address from, address to, string memory message, Attachment memory attachment) public view returns (bytes32) { return keccak256( abi.encode(address(this), block.chainid, from, to, message, attachment, nonces[from]) ); } } ``` ## Test signature verification in Foundry The template includes tests for both signature types. Tempo support is enabled by the template's Foundry config. If you copy this pattern into an existing project, make sure `foundry.toml` enables Tempo mode: ```toml [profile.default] tempo = true ``` ### secp256k1 ```solidity contract MailRelayTest is MailTest { uint256 internal constant ALICE_PK = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; function test_SendMailWithSecp256k1Signature() public { bytes32 digest = mail.getDigest(ALICE, BOB, message, attachment); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ALICE_PK, digest); mail.sendMail(ALICE, BOB, message, attachment, abi.encodePacked(r, s, v)); assertEq(mail.nonces(ALICE), 1); } } ``` ### P256 ```solidity uint256 internal constant CAROL_P256_PK = 0x1; function setUp() public override { super.setUp(); (uint256 x, uint256 y) = vm.publicKeyP256(CAROL_P256_PK); carolPubX = bytes32(x); carolPubY = bytes32(y); CAROL = address(uint160(uint256(keccak256(abi.encodePacked(x, y))))); } function test_SendMailWithP256Signature() public { bytes32 digest = mail.getDigest(CAROL, BOB, message, attachment); (bytes32 r, bytes32 s) = vm.signP256(CAROL_P256_PK, digest); s = _normalizeP256S(s); // low-s normalization required by the precompile bytes memory sig = abi.encodePacked(carolPubX, carolPubY, r, s); mail.sendMail(CAROL, BOB, message, attachment, sig); assertEq(mail.nonces(CAROL), 1); } ``` ## Run the tests ```bash forge test -vvv ``` The secp256k1 and P256 relay tests use Tempo mode through the template's Foundry config. ## Related signature verification docs * [Signature verification specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md) * [T6 Network Upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t6) * [Foundry for Tempo](https://tempo.xyz/developers/docs/sdk/foundry) # Tempo Python SDK: installing and using it Tempo distributes a Python SDK as a [web3.py](https://web3py.readthedocs.io/) extension. The SDK adds native support for Tempo Transactions, including call batching, fee sponsorship, and access key management. The Tempo Python SDK can be used to perform common operations with the chain, such as: sending Tempo Transactions, batching multiple calls, fee sponsorship, and more. ::::steps ## Install the Python SDK To install the Tempo Python SDK: ```bash [pip] pip install pytempo ``` :::tip The SDK requires Python 3.9 or higher and web3.py 7.0+. ::: ## Create a Python client To interact with Tempo, create a web3.py client connected to a Tempo node: ```python [main.py] from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) # [!code hl] block_number = w3.eth.block_number print(f"Connected to Tempo at block {block_number}") ``` ## Send a Tempo transaction with Python Build and send a transaction using the `TempoTransaction` class: ```python [main.py] import os from web3 import Web3 from pytempo import Call, TempoTransaction # [!code hl] w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) private_key = os.environ["PRIVATE_KEY"] account = w3.eth.account.from_key(private_key) # [!code hl:12] tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=100_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=( Call.create(to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), ), ) signed_tx = tx.sign(private_key) # [!code hl] tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) # [!code hl] receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print(f"Transaction hash: {tx_hash.hex()}") ``` :::: ## Python SDK examples ### Send a token transfer with Python Send a TIP-20 token transfer using pytempo's typed contract helpers: ```python [transfer.py] from pytempo import TempoTransaction from pytempo.contracts import TIP20, ALPHA_USD tx = TempoTransaction.create( chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, nonce=w3.eth.get_transaction_count(account.address), calls=( TIP20(ALPHA_USD).transfer( # [!code hl] to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8", # [!code hl] amount=100_000_000, # 100 tokens (6 decimals) # [!code hl] ), # [!code hl] ), ) ``` ### Pay fees in a stablecoin with Python Use a TIP-20 token to pay for transaction fees instead of the native token: ```python [fee_token.py] from pytempo import TempoTransaction, Call from pytempo.contracts import ALPHA_USD tx = TempoTransaction.create( chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, nonce=w3.eth.get_transaction_count(account.address), fee_token=ALPHA_USD, # [!code hl] calls=( Call.create(to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), ), ) ``` ### Batch multiple calls with Python Execute multiple operations atomically in a single transaction: ```python [batch.py] from pytempo import TempoTransaction from pytempo.contracts import TIP20, ALPHA_USD token = TIP20(ALPHA_USD) tx = TempoTransaction.create( chain_id=4217, gas_limit=300_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, nonce=w3.eth.get_transaction_count(account.address), calls=( token.transfer(to="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", amount=100_000_000), # [!code hl] token.transfer(to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8", amount=50_000_000), # [!code hl] token.transfer(to="0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", amount=25_000_000), # [!code hl] ), ) signed_tx = tx.sign(private_key) tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ### Parallel Transactions (2D Nonces) Send multiple transactions concurrently using different nonce keys: ```python [parallel.py] tx1 = TempoTransaction.create( chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, nonce_key=1, # Sequence A // [!code hl] nonce=0, calls=(Call.create(to=recipient1, data=data1),), ) tx2 = TempoTransaction.create( chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, nonce_key=2, # Sequence B (parallel) // [!code hl] nonce=0, calls=(Call.create(to=recipient2, data=data2),), ) # Sign and send both in parallel signed_tx1 = tx1.sign(private_key) signed_tx2 = tx2.sign(private_key) w3.eth.send_raw_transaction(signed_tx1.encode()) w3.eth.send_raw_transaction(signed_tx2.encode()) ``` ### Fee sponsorship with Python Have another account pay for transaction fees: ```python [fee_payer.py] # User creates and signs a transaction marked for fee sponsorship tx = TempoTransaction.create( chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, awaiting_fee_payer=True, # [!code hl] calls=(Call.create(to=recipient, data=data),), ) signed_by_user = tx.sign(user_private_key) final_tx = signed_by_user.sign(fee_payer_private_key, for_fee_payer=True) # [!code hl] w3.eth.send_raw_transaction(final_tx.encode()) ``` ### Transaction validity windows in Python Set a time window during which the transaction is valid: ```python [validity.py] import time now = int(time.time()) tx = TempoTransaction.create( chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, nonce=nonce, valid_after=now, # [!code hl] valid_before=now + 3600, # 1 hour from now // [!code hl] calls=(Call.create(to=recipient, data=data),), ) ``` ## Account Keychain in the Python SDK The `AccountKeychain` class provides typed helpers for Tempo's [Account Keychain precompile](https://tempo.xyz/developers/docs/protocol/transactions/AccountKeychain), enabling access key management directly from Python. :::info Enhanced access key features — periodic spending limits and call scoping — require the [T3 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t3). ::: ```python [keychain.py] from pytempo import ( TempoTransaction, Call, KeyRestrictions, SignatureType, TokenLimit, CallScope, ) from pytempo.contracts import AccountKeychain, ALPHA_USD from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) # Authorize a new access key (secp256k1, no expiry): call = AccountKeychain.authorize_key( key_id="", signature_type=SignatureType.SECP256K1, restrictions=KeyRestrictions(expiry=0), ) tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=200_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(account.address), calls=(call,), ) # Authorize with a spending limit: call = AccountKeychain.authorize_key( key_id="", signature_type=SignatureType.SECP256K1, restrictions=KeyRestrictions( expiry=0, limits=[TokenLimit(token=ALPHA_USD, limit=1_000_000)], ), ) # Authorize with call scopes (restrict to specific contracts/functions): call = AccountKeychain.authorize_key( key_id="", signature_type=SignatureType.SECP256K1, restrictions=KeyRestrictions( expiry=0, allowed_calls=[ CallScope.transfer(target=ALPHA_USD), CallScope.approve(target=ALPHA_USD), ], ), ) # Full example: 24h expiry + spending limit + call scope: import time expiry = int(time.time()) + 86400 call = AccountKeychain.authorize_key( key_id="", signature_type=SignatureType.SECP256K1, restrictions=KeyRestrictions( expiry=expiry, limits=[TokenLimit(token=ALPHA_USD, limit=1_000_000)], allowed_calls=[CallScope.transfer(target=ALPHA_USD)], ), ) # Revoke an access key (permanent, cannot be re-authorized): call = AccountKeychain.revoke_key(key_id="") # Update spending limit for a key-token pair: call = AccountKeychain.update_spending_limit( key_id="", token=str(ALPHA_USD), new_limit=2_000_000, ) # Replace all call scopes for a key: call = AccountKeychain.set_allowed_calls( key_id="", scopes=[ CallScope.transfer(target=ALPHA_USD), CallScope.unrestricted(target=""), ], ) # Remove a target contract from allowed call list: call = AccountKeychain.remove_allowed_calls( key_id="", target="", ) # Query key info (read-only): key_info = AccountKeychain.get_key( w3, account_address="", key_id="", ) print(key_info) # {'signature_type': 0, 'key_id': '0x...', 'expiry': 1893456000, ...} # Query remaining spending limit: remaining = AccountKeychain.get_remaining_limit( w3, account_address="", key_id="", token_address=str(ALPHA_USD), ) print(f"Remaining: {remaining}") ``` ### Sign with an access key in Python Use `sign_access_key` to sign a transaction as an access key holder: ```python [access_key_sign.py] from pytempo import TempoTransaction, Call tx = TempoTransaction.create( chain_id=w3.eth.chain_id, gas_limit=100_000, max_fee_per_gas=w3.eth.gas_price * 2, max_priority_fee_per_gas=w3.eth.gas_price, nonce=w3.eth.get_transaction_count(root_account_address), calls=(Call.create(to=""),), ) signed_tx = tx.sign_access_key( # [!code hl] access_key_private_key="", # [!code hl] root_account="", # [!code hl] ) # [!code hl] tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) ``` ## Next steps for the Python SDK After setting up the Python SDK, you can: * Follow a guide on how to [make payments](https://tempo.xyz/developers/docs/guide/payments), [issue stablecoins](https://tempo.xyz/developers/docs/guide/issuance), [exchange stablecoins](https://tempo.xyz/developers/docs/guide/stablecoin-dex), and [more](https://tempo.xyz/developers/docs). * View the [source on GitHub](https://github.com/tempoxyz/pytempo) * View the [package on PyPI](https://pypi.org/project/pytempo/) # Tempo Rust SDK: installing and using it Tempo distributes a Rust SDK in the form of an [Alloy](https://alloy.rs) crate. Alloy is a popular Rust crate for interacting with EVM-compatible blockchains. The Tempo Alloy crate can be used to perform common operations with the chain, such as: querying the chain, sending Tempo Transactions, managing tokens & their AMM pools, and more. ::::steps ## Install the Rust SDK To install the Tempo extension, you will need to install [Alloy](https://alloy.rs) and Tempo: ```bash [cargo] cargo add alloy tokio cargo add tempo-alloy --git https://github.com/tempoxyz/tempo --tag tempo-alloy@1.10.1 ``` :::tip We use [`tokio`](https://tokio.rs) in this example, but you can use any async runtime. ::: ## Configure a Rust provider To use the Tempo extension crate, you will need to create a [`Provider`] using the [`TempoNetwork`](https://tempoxyz.github.io/tempo/tempo_alloy/struct.TempoNetwork.html). This will enable the usage of Tempo specific types on the [`Provider`] instance. For more information about network types, see the Alloy [documentation](https://alloy.rs/guides/interacting-with-multiple-networks#interacting-with-multiple-networks). ```rs [main.rs] use alloy::providers::ProviderBuilder; // [!code focus] use tempo_alloy::TempoNetwork; // [!code focus] #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() // [!code focus] .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) // [!code focus] .await?; // [!code focus] println!("Provider connected successfully"); // [!code focus] println!("Chain ID: {provider:?}"); // [!code focus] Ok(()) } ``` ## Use Tempo actions in Rust Now we are ready to use the provider to interact with the network. We can use the provider to send transactions, read data, and more. ```rs [main.rs] use alloy::providers::{Provider, ProviderBuilder}; use tempo_alloy::TempoNetwork; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) .await?; // [!code focus:2] println!("{}", provider.get_block_number().await?); // @log: 421045 Ok(()) } ``` See the Alloy [documentation](https://alloy.rs) or the [`Provider`] docs for more examples. [`Provider`]: https://docs.rs/alloy/latest/alloy/providers/trait.Provider.html :::: # System Requirements These are the minimum and recommended system requirements for running a validator/RPC node. It is likely, that the nodes will not require as much resources at the beginning of the chain, but we still highly recommend to follow the recommended specifications. This will allow for future growth and scalability. :::danger[Execution storage requirement] Validators must run execution state on local NVMe / direct-attached storage. Network-attached volumes (EBS, GCP Persistent Disk, Azure Managed Disk, NAS, SAN) are **not supported** for execution. ::: Consensus state can live on a lower performance volume (for example, EBS), but execution state needs NVMe storage. If you want to separate them, use `--datadir` for execution data and `--consensus.datadir` for consensus data. ## RPC Node | Component | Minimum | Recommended | |-----------|---------|-------------| | **CPU** | 16 cores | 32+ cores | | **RAM** | 32 GB | 64 GB | | **Storage** | 1000 GB NVMe | 2000 GB NVMe | | **Network** | 1 Gbps | 10 Gbps | ## Validator Node | Component | Minimum | Recommended | |-----------|---------|-------------| | **CPU** | 8 cores | 16+ cores | | **RAM** | 16 GB | 32 GB | | **Storage** | 100 GB NVMe | 1 TB NVMe | | **Network** | 1 Gbps | 1 Gbps | ## Cloud provider recommendations These dedicated servers meet or exceed the recommended specs for both RPC and validator nodes: | Provider | Server | CPU | RAM | Storage | |----------|--------|-----|-----|---------| | OVH | Advance-4 | Intel Xeon-E 2386G (6c/12t) | 32 GB | 2× 512 GB NVMe | | Hetzner | AX42 | AMD Ryzen 5 3600 (6c/12t) | 64 GB | 2× 512 GB NVMe | | AWS | `c6id.8xlarge` | 32 vCPUs | 64 GB | 1.9 TB NVMe | :::warning Cloud instances with network-attached storage (e.g., AWS EBS) do not provide sufficient I/O performance. Use dedicated servers or instances with local NVMe storage. ::: ## Time Synchronization Tempo validates that block timestamps are not in the future. If your system clock drifts even slightly, your node may reject valid blocks or produce blocks that other validators reject — leading to consensus errors and missed proposals. :::warning `systemd-timesyncd` (the default on many minimal VMs) is **not sufficient**. Use `chrony` or `ntpd` for reliable sub-millisecond synchronization. ::: ### Install and enable chrony (recommended) ```bash sudo apt install chrony sudo systemctl enable --now chronyd ``` ### Verify synchronization ```bash chronyc tracking ``` Check that **System time** offset is under a few milliseconds and **Leap status** is `Normal`. You can also verify with: ```bash timedatectl ``` Confirm `System clock synchronized: yes` and `NTP service: active`. ### Cloud providers Most cloud providers (AWS, Hetzner, OVH) pre-configure NTP, but minimal VM images may ship without a proper NTP daemon. Always verify that `chrony` or `ntpd` is installed and running after provisioning a new machine. ## Security For network configuration, key management, release verification, and other security best practices, see the dedicated [Node Security](https://tempo.xyz/developers/docs/guide/node/security) page. ## Network Tuning We recommend enabling TCP **BBR** congestion control with the **fq** packet scheduler for better P2P and consensus performance. Add the following to your sysctl configuration (e.g. `/etc/sysctl.d/99-tempo-network.conf` or equivalent): ```ini net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr ``` Apply and verify: ```bash sudo sysctl --system sysctl net.ipv4.tcp_congestion_control # should print: bbr sysctl net.core.default_qdisc # should print: fq ``` Restart the node after applying for the changes to take effect. ## Ports | Port | Protocol | Purpose | Expose | |------|----------|---------|--------| | 30303 | TCP/UDP | Execution P2P | Public | | 8000 | TCP | Consensus P2P | Validators only | | 8545 | TCP | HTTP RPC | Optional (internal for validators) | | 8546 | TCP | WebSocket RPC | Optional (internal for validators) | | 9000 | TCP | Metrics | Internal | # Install and configure a Tempo node We provide three different installation paths — installing a pre-built binary, building from source, or using our provided Docker image. For the full CLI command reference, see [`tempo node`](https://tempo.xyz/developers/docs/cli/node). ## Versions The required node version may differ across networks. See [Network Upgrades](https://tempo.xyz/developers/docs/guide/node/network-upgrades) for the current version for each network. ## Pre-built Binary ```bash /dev/null/download.sh#L1-4 curl -L https://tempo.xyz/install | bash tempo --version ``` To update Tempo in the future, simply run `tempoup`. ## Build from Source ```bash /dev/null/build.sh#L1-10 # Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env # Build and install from source using cargo cargo install --git https://github.com/tempoxyz/tempo.git tempo --root /usr/local tempo --version ``` ## Docker You can find the latest tagged version of Tempo at [the Tempo GHCR package](https://github.com/tempoxyz/tempo/pkgs/container/tempo). ```bash /dev/null/docker.sh#L1-4 # Pull the latest Docker image docker pull ghcr.io/tempoxyz/tempo: # Run the Docker container docker run -d --name tempo ghcr.io/tempoxyz/tempo: --version docker logs tempo ``` ## Snapshots Downloading a snapshot lets your node skip syncing from genesis and start participating much faster. Choose the snapshot profile based on what the node does: * Validators should use `--minimal`. * RPC providers, indexers, and other workloads that need complete historical data should use `--archive`. ### What is Minimal Mode? Minimal Mode is a Reth storage profile for nodes that need to follow the chain and serve recent state, such as validators. It keeps disk usage low by pruning older historical data. If the node serves historical RPC, indexing, archive, or tracing workloads, use `--archive` instead. See Reth's [Minimal Storage Mode](https://reth.rs/run/storage/minimal/) docs for the storage trade-offs. :::code-group ```bash [Validator mainnet] tempo download --chain mainnet --minimal ``` ```bash [Validator testnet] tempo download --chain moderato --minimal ``` ```bash [Archive mainnet] tempo download --chain mainnet --archive ``` ```bash [Archive testnet] tempo download --chain moderato --archive ``` ::: Use [snapshots.tempo.xyz](https://snapshots.tempo.xyz/) to compare snapshot profiles or copy generated `tempo download` commands. ### Preview a snapshot download Inspect the execution and consensus archives before downloading them or changing your data directory: ```bash tempo download --chain mainnet --minimal --print-plan-json ``` This fetches snapshot metadata and prints a JSON plan. See the [`tempo download` reference](https://tempo.xyz/developers/docs/cli/download) for flags, including `--consensus.datadir` when consensus data lives on a separate volume. Current validators require consensus finalization certificates at startup. Official Tempo snapshots include the required consensus archive, and `tempo download` restores it by default. :::info[Keep execution and consensus data in sync] Restore execution and consensus data from the same snapshot and update them in lockstep, even when stored on separate volumes. Independent updates can work but are brittle. Tempo is working on making this flow possible. ::: :::note[Replacing existing snapshot data] When replacing snapshot data in an existing data directory, add `--force` after selecting the right profile. `--force` removes the execution databases, static files, `reth.toml`, and the entire consensus directory before installing the new snapshot. It preserves `discovery-secret` and `known-peers.json`. ::: ## Verifying Releases All release artifacts are cryptographically signed. We recommend verifying signatures before running any binary. ### Binary Signatures (GPG) Release binaries are signed with GPG. The `tempoup` installer checks the archive checksum, then verifies GitHub release provenance when authenticated `gh` is available, or falls back to GPG signature verification. Install and authenticate `gh`, or install `gpg`, before using the installer. For GPG verification, [`tempoup`](https://github.com/tempoxyz/tempo/blob/main/tempoup/tempoup) embeds the expected fingerprint, not the public key. It uses the key in your local GPG keyring if present; otherwise, it fetches it from `keyserver.ubuntu.com` over HTTPS. The public key is also included below. To verify manually, import the key and compare its full fingerprint with the one below before checking the signature. To independently confirm that the fingerprint belongs to Tempo, confirm it with the Tempo team through a trusted channel. ```bash # Import the Tempo release signing key gpg --keyserver keyserver.ubuntu.com --recv-keys EE3C5D41EA963E896F310EC3CBBFA54B20D33446 # Inspect the imported key fingerprint gpg --fingerprint EE3C5D41EA963E896F310EC3CBBFA54B20D33446 # Verify a downloaded binary gpg --verify tempo-v1.13.2-x86_64-unknown-linux-gnu.tar.gz.asc \ tempo-v1.13.2-x86_64-unknown-linux-gnu.tar.gz ``` A successful verification reports `Good signature`. Confirm that the signing key matches the fingerprint below. **Fingerprint:** `EE3C 5D41 EA96 3E89 6F31 0EC3 CBBF A54B 20D3 3446`
Public Key ``` -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEaYXmJhYJKwYBBAHaRw8BAQdAa+cO3zz4+YQuPgUCNXSW7ApNTCAIwx9wBfPc lXyZBw20Xlp5Z2ltYW50YXMgTWFnZWxpbnNrYXMgKFRlbXBvIHJlbGVhc2Ugc2ln bmluZyBrZXkgZm9yIDIwMjYgYW5kIG9ud2FyZHMpIDx6eWdpbWFudGFzQHRlbXBv Lnh5ej6IkwQTFgoAOxYhBO48XUHqlj6JbzEOw8u/pUsg0zRGBQJpheYmAhsDBQsJ CAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEMu/pUsg0zRGjJABAP8dy+gWx/E8 EqzkKEUkEfLiRZ6n8APsc0aI5gqwfVAuAP99147oAq9cWVkNMh5PQmvdSG8MIx7Z G4OIGIHqFwKSCA== =o2TA -----END PGP PUBLIC KEY BLOCK----- ```
### Docker Image Signatures (Cosign) Docker images are signed with [Cosign](https://docs.sigstore.dev/cosign/signing/overview/) using keyless signing via GitHub Actions OIDC. To verify a Docker image: ```bash # Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/ cosign verify ghcr.io/tempoxyz/tempo:latest \ --certificate-identity-regexp="https://github.com/tempoxyz/tempo/" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" ``` This verifies that the image was built and signed by the official Tempo CI pipeline. ### SHA256 Checksums Every release archive includes a `.sha256` checksum file: ```bash # Download the checksum file curl -sSfLO https://github.com/tempoxyz/tempo/releases/download/v1.1.0/tempo-v1.1.0-x86_64-unknown-linux-gnu.tar.gz.sha256 # Verify shasum -a 256 -c tempo-v1.1.0-x86_64-unknown-linux-gnu.tar.gz.sha256 ``` # Running RPC and Standby Nodes RPC nodes provide API access to the Tempo network without participating in consensus. ## Quick Start > **Note** > > All RPC nodes are trustless by default. ```bash /dev/null/quickstart.sh#L1-15 # Download snapshot (this will help you sync much faster) tempo download --chain mainnet --archive # Run node tempo node \ --follow \ --http --http.port 8545 \ --http.api eth,net,web3,txpool,trace ``` An RPC node running in follow mode is a **full node**. It first verifies consensus finalization certificates from the upstream before accepting followed blocks. Those certificates prove the data is backed by a validator quorum, not just the upstream node's local view of the chain. After verification, it fetches blocks from the upstream RPC endpoint, executes every transaction locally through the EVM, validates each block after execution, and stores complete block data with full state. All execution and validation happens locally on your machine. By default, RPC nodes run in archive mode, meaning they do not prune historical state. The upstream must serve consensus finalization certificates. For RPC-to-RPC following, follow a validator-backed RPC node or another RPC node that is already serving certified consensus data. ## Example Systemd Service ```bash /dev/null/systemd.sh#L1-55 sudo tee /etc/systemd/system/tempo.service > /dev/null < \\ --follow \\ --http \\ --http.addr 0.0.0.0 \\ --http.port 8545 \\ --http.api eth,net,web3,txpool,trace \\ --metrics 9000 \\ Restart=always RestartSec=10 StandardOutput=journal StandardError=journal SyslogIdentifier=tempo LimitNOFILE=infinity [Install] WantedBy=multi-user.target EOF # Enable and start sudo systemctl daemon-reload sudo systemctl enable tempo sudo systemctl start tempo # Check status sudo systemctl status tempo # View logs sudo journalctl -u tempo -f ``` ## Monitoring Once you've set up your node (whether it's with Systemd or Docker), you can verify that it's running correctly using these commands (`cast` requires installation of [Foundry](https://tempo.xyz/developers/docs/sdk/foundry)): ```bash /dev/null/monitor.sh#L1-11 # Check service status sudo systemctl status tempo # Check peer connections (should be non-zero) cast rpc net_peerCount --rpc-url http://localhost:8545 # Check block height (should be steadily increasing) cast block-number --rpc-url http://localhost:8545 cast block --rpc-url http://localhost:8545 # Search logs sudo journalctl -u tempo -n 1000 | grep -i "error" ``` In a production setting, you should monitor the [Reth metrics port](https://reth.rs/run/monitoring) using a tool like Prometheus or Grafana. # Running a validator node Validator nodes secure Tempo by validating blocks and participating in consensus. :::info The active validator set is currently permissioned. If you are interested in becoming a validator, please [get in touch](https://tempo.xyz/contact) with the Tempo team. See [Initial setup](https://tempo.xyz/developers/docs/guide/node/validator-setup) for technical details. ::: * [Initial Setup](https://tempo.xyz/developers/docs/guide/node/validator-setup) — Register with the Tempo team, generate your signing key, download a snapshot, and start your validator for the first time * [Checking Validator Status](https://tempo.xyz/developers/docs/guide/node/validator-status) — Understand state transitions, check participation via metrics, and query on-chain status * [Validator Network Topology](https://tempo.xyz/developers/docs/guide/node/validator-topology) — Isolate the validator and route execution P2P through redundant follower RPC nodes * [Controlling Validator Lifecycle](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle) — Start, stop, register, rotate, deactivate, and transfer ownership of your validator * [Managing Validator Keys](https://tempo.xyz/developers/docs/guide/node/validator-keys) — Key hierarchy, generation, rotation, and signing share recovery * [Monitoring a Validator](https://tempo.xyz/developers/docs/guide/node/validator-monitoring) — Monitor consensus and execution health, metrics glossary, Grafana dashboards, and log management * [Troubleshooting and FAQ](https://tempo.xyz/developers/docs/guide/node/validator-troubleshooting) — Common issues and solutions # Validator onboarding :::info The active validator set is currently permissioned. If you are interested in becoming a validator, please [get in touch](https://tempo.xyz/contact) with the Tempo team. ::: This guide walks through registering, generating your signing key, and starting your validator node for the first time. Before proceeding, make sure you have completed [system requirements](https://tempo.xyz/developers/docs/guide/node/system-requirements) and [installation](https://tempo.xyz/developers/docs/guide/node/installation). ## Initial registration Registering a validator takes three steps: 1. **[Generate a signing keypair](#step-1-generate-a-signing-keypair)** — Create an encrypted ed25519 private key and derive the public key. 2. **[Create the add-validator signature](#step-2-create-the-add-validator-signature)** — Prove ownership of the signing key by producing a signature over your registration details. 3. **[Submit registration details](#step-3-submit-registration-details)** — Provide the Tempo team with all required values to add your validator on-chain. ### Step 1: Generate a signing keypair :::warning Never share your private signing key. Anyone with access to it can impersonate your validator. The Tempo team will never ask for your private key. ::: Generate an encrypted ed25519 keypair. The `--secret` argument points to a file-like input that contains the encryption key. Prefer a named pipe (FIFO) or shell process substitution for this path: a FIFO lets one process stream bytes directly to another process without storing those bytes as a regular file, and it keeps the secret out of environment variables and command-line arguments. See [Why FIFOs and not env vars](https://tempo.xyz/developers/docs/guide/node/validator-keys#why-fifos-and-not-env-vars). ```bash mkfifo /run/tempo/consensus-secret > /run/tempo/consensus-secret & tempo consensus generate-signing-key \ --output \ --secret /run/tempo/consensus-secret ``` Verify the public key: ```bash > /run/tempo/consensus-secret & tempo consensus show-verification-key \ --private-key \ --secret /run/tempo/consensus-secret ``` The public key should match the output of the `generate-signing-key` command. `` should be a command that retrieves the encryption key from your KMS or secret manager and writes the raw secret to stdout. ### Step 2: Create the add-validator signature The signature proves ownership of the ed25519 key being registered. Generate it with: :::code-group ```bash [Mainnet] tempo consensus create-add-validator-signature \ --signing-key \ --validator-address \ --public-key \ --ingress \ --egress \ --fee-recipient \ --chain-id-from-rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus create-add-validator-signature \ --signing-key \ --validator-address \ --public-key \ --ingress \ --egress \ --fee-recipient \ --chain-id-from-rpc-url https://rpc.testnet.tempo.xyz ``` ::: ### Step 3: Submit registration details Provide the following values along with the signature to the Tempo team: | Value | Format | Description | |-------|--------|-------------| | **Validator operator address** | Ethereum address (`0x…`) | The control address for your validator. Used to authorize on-chain operations (IP updates, rotation, ownership transfer). | | **Public key** | `0x`-prefixed 32-byte hex | Your ed25519 identity key (from [Step 1](#step-1-generate-a-signing-keypair)). | | **Ingress** | `IP:port` | The inbound address other validators use to reach your node. Must be unique across all active validators. | | **Egress** | `IP` | The outbound IP address your node uses to connect to other validators. | | **Fee recipient** | Ethereum address (`0x…`) | The address that receives transaction fees from blocks your validator proposes. If you are not prepared to accept fees, use `0x0000000000000000000000000000000000000000`. | | **Signature** | `0x`-prefixed hex | The ed25519 signature proving you control the signing key (from [Step 2](#step-2-create-the-add-validator-signature)). | Once the Tempo team adds your validator on-chain, it will enter the active set in the [next epoch](https://tempo.xyz/developers/docs/guide/node/validator-status#state-transitions). ## Running the validator The process for running a validator node is very similar to [running a full node](https://tempo.xyz/developers/docs/guide/node/rpc). You should start by downloading the latest snapshot. Validators should use `--minimal` when migrating to minimal snapshots, and should keep using `--minimal` for future validator replacements. :::code-group ```bash [Mainnet] tempo download --chain mainnet --minimal ``` ```bash [Testnet] tempo download --chain moderato --minimal ``` ::: If you are replacing snapshot data in an existing data directory, add `--force` after the profile flag. This removes the execution databases, static files, `reth.toml`, and the entire consensus directory before installing the new snapshot. It preserves `discovery-secret` and `known-peers.json`. If you are unsure which pruning configuration your validator is running, reach out to the Tempo team before replacing snapshot data. To check whether an existing validator is already migrated, inspect the node startup logs for the `Loaded storage settings` line and its `pruning_mode` field. If `pruning_mode` is `minimal`, no action is needed unless you are replacing the node. Once you've downloaded the snapshot and have been whitelisted on-chain, you can proceed to run the validator node: :::code-group ```bash [Mainnet] tempo node --datadir \ --chain mainnet \ --consensus.signing-key \ --consensus.secret \ --telemetry-url ``` ```bash [Testnet] tempo node --datadir \ --chain moderato \ --consensus.signing-key \ --consensus.secret \ --telemetry-url ``` ::: The notable difference between RPC nodes and validator nodes is the omission of the `--follow` argument and the addition of the `--consensus.signing-key` argument. If the signing key is encrypted, provide the encryption key with `--consensus.secret`. Once your node is up, it may not start syncing immediately. This is because your node might not be part of the active set. In most cases, your validator will enter the active set in under 6 hours after the on-chain addition of the validator identity. ### Optional flags | Flag | Description | |------|-------------| | `--telemetry-url ` | Unified metrics and logs export. See [Telemetry endpoint](#telemetry-endpoint) below for details. **We ask all validators to configure this so we can support troubleshooting.** | | `--telemetry-metrics-interval ` | Interval for pushing metrics (default: `10s`). | | `--consensus.datadir ` | Store consensus data on a separate volume (e.g., AWS EBS) while keeping execution state on high-performance local disks. Migrate by copying `/consensus` to the new location. | | `--consensus.secret ` | Read the encrypted signing-key secret from a FIFO, process-substitution path, or regular file. Prefer FIFO or process substitution so the secret is streamed just in time and kept out of the process environment. | | `--txpool.filter ` | Reject transactions whose sender or direct call target matches an operator-supplied address list. See [Transaction address filtering](#transaction-address-filtering). | ### Transaction address filtering From [v1.14.0](https://github.com/tempoxyz/tempo/releases/tag/v1.14.0), you can configure `--txpool.filter` to exclude addresses from your node's transaction pool without maintaining a custom node build. Filtering is optional and disabled by default. You supply and maintain the address list. Append one of these forms to your existing `tempo node` command: :::code-group ```bash [Comma-separated addresses] --txpool.filter "0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002" ``` ```bash [Address file] --txpool.filter /etc/tempo/filtered-addresses.txt ``` ::: The file is plain text, with addresses separated by commas, newlines, or both: ```text 0x0000000000000000000000000000000000000001 0x0000000000000000000000000000000000000002 ``` Whitespace, blank entries, and duplicate addresses are ignored. An empty list, an invalid address, or an unreadable file prevents startup. The file is read at startup; restart the node after changing its contents. Remove the flag to disable filtering. The node rejects the entire transaction at pool admission when its recovered sender or any direct call target appears in the list. For batched Tempo transactions, every direct call target is checked. A rejection reports `Transaction address check failed for {address}`. This is a local pool policy. It does not change consensus validation or reject otherwise-valid blocks proposed by other validators. It does not inspect internal contract calls or addresses encoded in calldata, such as a token transfer's recipient. Contract-creation calls have no direct target to check; their sender is still checked. ### Telemetry endpoint The `--telemetry-url` flag enables unified telemetry export. It pushes two types of data to a Tempo-operated metrics backend: * **Prometheus metrics** — execution-layer (reth) and consensus-layer metrics in Prometheus text format, pushed at a configurable interval (default every 10 seconds). * **Structured logs** — operational logs exported via OTLP at `debug` level, covering consensus events, block processing, and sync progress. The URL must include credentials: `--telemetry-url https://user:pass@metrics.example.com` #### What is collected | Category | Examples | |----------|---------| | Execution metrics | Block processing times, transaction pool size, peer count, sync status, database stats | | Consensus metrics | Epoch and view progress, DKG ceremony status, proposal and finalization counts | | Operational logs | Consensus state transitions, block proposals, sync progress, error events | | Hardware metadata | CPU vendor, model and frequency, physical and logical core counts, total memory, and filesystem types for node storage, reported by `tempo_hardware_info` since v1.11.0 | All consensus metrics are namespaced under a `consensus` prefix. #### What is **not** collected The `tempo_hardware_info` metric omits hostnames, IP addresses, disk names, mount sources, and filesystem paths. Node operational logs are exported separately and can include the node's configured paths and peer information; the hardware metric's exclusions do not apply to all logs. # Validator network topology Run a production validator in an isolated network segment. Allow consensus traffic between validators, but route execution P2P and all transaction ingress through redundant trusted follower RPC nodes. :::info These trusted RPC nodes can also provide internal access to chain state as an alternative to `https://rpc.tempo.xyz`. ::: ## Why validators need an isolation layer Do not submit transactions directly to a validator. Submit them to a trusted RPC node, which checks transaction signatures before forwarding valid transactions to the validator. This prevents invalid transactions from consuming the validator's compute resources. Do not establish execution devp2p connections between validators. A malicious or compromised validator or public peer could otherwise send spurious transactions directly to another validator and attempt to exhaust its resources. With the recommended topology, transactions are gossiped only through the trusted RPC layer, while validators communicate directly with one another only over consensus P2P. :::info[Run trusted RPC nodes on separate instances] Run trusted RPC nodes on machines or instances separate from the validator so they do not share CPU, memory, disk, or network resources with it. The isolation boundary is ineffective if abusive traffic against an RPC node can still exhaust the validator host. ::: ## Recommended validator topology The validator has two types of network relationships: * **Consensus P2P:** The validator communicates directly with other Tempo validators. * **Execution P2P:** The validator peers only with its operator's trusted RPC nodes over TCP and UDP. It does not establish execution P2P connections with other validators or public peers. The execution P2P layer is open, so place trusted RPC nodes between the validator and that network. Configure each trusted RPC node with `--follow ws://:8546` so it syncs in lockstep with the validator over a private WebSocket connection. Run at least two trusted RPC nodes for each validator to avoid a single point of failure for execution P2P and transaction ingress: Two separate Validator units each contain a trusted RPC group and its validator: R1 with V1, and R2 with V2. A client outside both units submits a transaction to R1 over JSON-RPC. R1 and R2 each connect to Public Nodes and to each other over bidirectional execution P2P. R1 follows V1 and R2 follows V2 using --follow over WebSocket; each pair also has a bidirectional execution P2P connection. V1 and V2 communicate over bidirectional consensus P2P, with no execution P2P connection between them. No validator P2P or RPC port should be directly accessible from the internet. Do not allow execution P2P between `V1` and `V2`; transactions cross the validator isolation boundary only through `R1` or `R2`. ## Validator firewall policy Enforce the topology at the network firewall even when the node's peer configuration restricts discovery or has a fixed peer list. Firewall for the validator (default ports): | Traffic | Source or destination | Port | Policy | | --- | --- | --- | --- | | Consensus P2P | Other Tempo validators | `8000/TCP` | Allow | | Execution P2P | Trusted RPC hosts | `30303/TCP` and `30303/UDP` | Allow | | WebSocket JSON-RPC | Trusted RPC hosts | `8546/TCP` | Allow | | HTTP JSON-RPC management | Validator host only (loopback) | `8545/TCP` | Deny remote access | | All other inbound traffic | Any source | Any port | Deny | Restrict the validator's execution peer configuration to the trusted follower RPC nodes. Do not include other validators in this allowlist, and do not rely on peer discovery alone to provide isolation. ## Configure validator execution peers Each follower needs a stable and unique execution P2P identity. On each follower, print its `enode://` URL from the discovery secret that the node uses: ```bash tempo p2p enode ``` Run the command once for each trusted RPC node. Each node must have a unique discovery secret that it uses to identify itself. Use the RPC node's private IP that the validator uses to reach it. Concatenate these `enode://` URLs, passed to the validator command: ```bash tempo node \ --disable-discovery \ --trusted-only \ --trusted-peers "enode://@:30303,enode://@:30303" \ --ws \ --ws.addr \ --ws.port 8546 \ --ws.api eth,consensus ``` Each flag serves a specific purpose: | Flag | Effect on the validator | | --- | --- | | `--trusted-peers` | Static list of trusted peers that the node accepts connections from or tries to connect to. | | `--trusted-only` | Connects to and accepts execution P2P connections from trusted peers only. | | `--disable-discovery` | Disables DNS, discv4, and discv5 peer discovery. | Keep the firewall allowlist in place. The enode allowlist authenticates the followers' execution P2P identities, while the firewall restricts which private IPs can reach the validator's P2P port. The `--ws` flags enable the validator's WebSocket endpoint for followers. Follow mode uses `consensus` RPC subscriptions and finalization data, plus `eth` RPC calls to fetch blocks. `--http` alone does not enable WebSocket subscriptions. Bind WebSocket RPC to the validator's private interface and allow port `8546/TCP` only from trusted RPC hosts. ### Add or remove trusted RPC nodes without restarting Enable the `admin` RPC namespace on a localhost-only HTTP management endpoint at validator startup to change the in-memory trusted peer set while the validator is running. Add these flags to the validator command above, keeping its private WebSocket endpoint enabled: ```bash tempo node \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 \ --http.api eth,net,web3,admin ``` Here, "trusted" means the RPC node is allowed to exchange execution P2P traffic with the validator. It does not grant permission to administer the validator. `--trusted-peers` and `--trusted-only` control execution P2P connections; they do not authenticate or restrict JSON-RPC callers. A compromised follower must not be able to change the validator's peer allowlist. Keep `admin` on `http://127.0.0.1:8545`, accessible only from the validator host. Never include it in `--ws.api` on the follower-facing endpoint or expose the management endpoint through a proxy to followers, clients, or the public internet. The two endpoints belong to the same validator process but use separate bind addresses, ports, and RPC namespace lists. If the validator already enables other HTTP namespaces on this localhost-only endpoint, preserve them when adding `admin`. Enabling the namespace after startup requires a validator restart. Run the following management commands on the validator host. Add a trusted RPC node immediately: ```bash cast rpc admin_addTrustedPeer \ '"enode://@:30303"' \ --rpc-url http://127.0.0.1:8545 ``` To remove a trusted RPC node, first remove it from the trusted set, then disconnect its active peer session: ```bash cast rpc admin_removeTrustedPeer \ '"enode://@:30303"' \ --rpc-url http://127.0.0.1:8545 cast rpc admin_removePeer \ '"enode://@:30303"' \ --rpc-url http://127.0.0.1:8545 ``` These methods update only the validator's in-memory peer set. Update the durable `--trusted-peers` configuration separately so the intended peers remain after a restart. Update the firewall allowlist at the same time. ## Configure trusted RPC nodes Place each trusted RPC node on a separate machine or instance from the validator, preferably in a separate network segment. Configure each node to: * Run in follow mode with `--follow` pointed at the validator's private WebSocket RPC endpoint. * Participate in the public Tempo execution P2P network. * Accept transaction submissions from clients or an internal load balancer instead of exposing the validator's JSON-RPC endpoint. On startup, RPC nodes discover a starting set of peers from bootnodes and connect to new peers through P2P discovery. Their P2P ports must be reachable by the public execution network, subject to the operator's host-hardening and traffic-control policy. ### Follow the validator over WebSocket On each trusted RPC host, add an explicit validator URL to the node's existing chain, data directory, and discovery-secret configuration: ```bash tempo node \ --follow ws://:8546 \ --http \ --http.addr \ --http.port 8545 \ --http.api eth,net,web3 ``` Use the same chain as the validator and the discovery secret used to derive that follower's allowlisted enode. Replace an existing bare `--follow` with this explicit URL so the node follows your validator instead of the chain's default upstream. Keep peer discovery enabled on the followers; the `--disable-discovery` and `--trusted-only` flags above apply to the validator. The follower opens an outbound WebSocket connection to the validator. It does not need its own `--ws` server enabled to follow. Its `--http` flags expose a separate endpoint for clients or an internal load balancer; restrict access to that endpoint to the intended callers. For setup and snapshot instructions, see [running RPC and standby nodes](https://tempo.xyz/developers/docs/guide/node/rpc). ## Failure and maintenance considerations Both follower RPC nodes are on the validator's transaction-ingress path. Monitor their execution peer counts, block heights, resource utilization, transaction-submission health, and private links to the validator. Run the followers independently so either node can continue accepting and relaying transactions when the other is unavailable. Configure clients or an internal load balancer to remove an unhealthy follower from rotation. Verify that failure of either follower does not interrupt transaction submission through the remaining node. ## Validate the isolation boundary After deployment or a firewall change, confirm that: * Other validators can reach the validator's consensus port. * The validator's execution peer count exactly matches the number of trusted RPC nodes. * Internet hosts cannot reach the validator on any ports. * The validator cannot establish execution P2P connections to internet peers or other validators. * Each follower is configured with `--follow` against the validator and remains synced. * Each follower can reach the validator's private WebSocket endpoint on port `8546`, but cannot reach its localhost HTTP management endpoint on port `8545` or call `admin` methods over WebSocket. * Transactions submitted through either follower reach the validator. * Clients cannot submit transactions directly to the validator. * Transaction submission continues after either follower is removed from service. * Monitoring reaches both hosts only through the private monitoring network. See [system requirements and ports](https://tempo.xyz/developers/docs/guide/node/system-requirements#ports), [validator monitoring](https://tempo.xyz/developers/docs/guide/node/validator-monitoring), and [node security](https://tempo.xyz/developers/docs/guide/node/security) for the related operational controls. # Checking validator status Your validator moves through different states after registration. Understanding these states helps you verify that your node is healthy and participating in consensus. ## Look up your validator You can query a single validator by its consensus public key (the ed25519 key generated during [initial setup](https://tempo.xyz/developers/docs/guide/node/validator-setup#step-1-generate-a-signing-keypair) — see also [Managing validator keys](https://tempo.xyz/developers/docs/guide/node/validator-keys#generating-a-signing-key)): :::code-group ```bash [Mainnet] tempo consensus validator --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus validator --rpc-url https://rpc.testnet.tempo.xyz ``` ::: Example output: ```json { "current_epoch": 18, "current_height": 5733680, "onchain_address": "0x1234567890abcdef1234567890abcdef12345678", "public_key": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b", "inbound_address": "203.0.113.10:9000", "outbound_address": "203.0.113.10:9001", "fee_recipient": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", "index": 5, "active": true, "is_dkg_dealer": true, "is_dkg_player": true, "in_committee": true } ``` | Field | Description | |-------|-------------| | `current_epoch` | The epoch at the time of the query | | `current_height` | The block height at the time of the query | | `onchain_address` | Validator control address | | `public_key` | Ed25519 consensus public key (hex) | | `inbound_address` | Inbound address (`IP:port`) for incoming connections | | `outbound_address` | Outbound IP address for firewall whitelisting | | `fee_recipient` | Address that receives block proposal fees | | `index` | Index of the validator (constant under rotation) | | `active` | Whether the validator is active in the current contract state | | `is_dkg_dealer` | Whether the validator is a dealer (distributing shares) in the current epoch | | `is_dkg_player` | Whether the validator is a player (receiving shares) in the current epoch | | `in_committee` | Whether the validator is in the committee for the current epoch | ## State transitions Every state transition happens on epoch boundaries. Currently on mainnet and testnet, the epoch length is around 3 hours. ```mermaid stateDiagram-v2 [*] --> Registration: Add / Rotate in Registration --> Player: Epoch boundary Player --> Dealer: Epoch boundary Dealer --> Active: Fully synced Active --> Exiting: Deactivate / Rotate out Exiting --> Exited: Epoch boundary Exited --> [*] state "Registration — Epoch E" as Registration state "Player — Epoch E+1 Receiving signing shares" as Player state "Dealer — Epoch E+2 Distributing shares" as Dealer state "Active — Voter & Proposer Proposing and voting on blocks" as Active state "Exiting — Epoch E+1 Dealer only, no longer a player" as Exiting state "Exited — Epoch E+2 Fully out of committee" as Exited ``` ### Registration (epoch E) Once your validator is added to the on-chain contract, it is immediately registered on the p2p peer set and starts syncing blocks from the network. ### Player (epoch E+1) Your validator is receiving consensus signing shares from dealers during the DKG ceremony. ### Dealer / Validator (epoch E+2) Your validator is distributing consensus signing shares to other validators during the ceremony. Once your node is fully synced, it will also be able to propose blocks and vote on other validators' proposals. ### Exiting (epoch E+1 after deactivation) After deactivation in epoch E, your validator is still a **dealer** (distributing shares) during epoch E+1, but is **no longer a player** (not receiving new shares). It is in the process of being removed from the committee. ### Exited (epoch E+2 after deactivation) Your validator is fully out of the committee, assuming no DKG failures occurred in E+1. It is safe to shut down the node once `in_committee` is `false`. :::info The on-chain contract represents which validators should eventually be members of the committee, not which ones currently are. Every epoch, the Tempo network performs a distributed key generation ceremony, distributing keys to validators that should be committee members in the next epoch. So adding, deactivating, or rotating validator entries in epoch `E` can only be taken into account for the next DKG ceremony that runs during epoch `E+1`, and take effect in epoch `E+2`. ::: ## Checking state via metrics Monitor these metrics to track your validator's state: ```bash # Is your validator registered with consensus peers? # This should be >0 in at most 3 hours after your validator's addition. curl -s localhost:8002/metrics | grep consensus_engine_peer_manager_peers # How many times YOUR node has been a dealer (distributing shares) # This metric should be >0 over 6 hours. curl -s localhost:8002/metrics | grep consensus_engine_dkg_manager_how_often_dealer # How many times YOUR node has been a player (receiving shares) # This metric should be >0 over 6 hours. curl -s localhost:8002/metrics | grep consensus_engine_dkg_manager_how_often_player # Successful ceremonies (should increase every ~3 hours) curl -s localhost:8002/metrics | grep consensus_engine_dkg_manager_ceremony_successes_total # Failed ceremonies (should stay at 0 or increase rarely) curl -s localhost:8002/metrics | grep consensus_engine_dkg_manager_ceremony_failures_total ``` If `how_often_dealer` or `how_often_player` is increasing, your node is actively participating in DKG ceremonies. After your validator has been added to the network, you should alert on these metrics, as they indicate that your validator is actively participating in the network. If your validator is not registered with consensus peers, but at least 3 epochs have passed, check that your node is properly configured — e.g. firewall settings are open to other peers. If you have reset your validator's state, your validator might have been blocked due to double-signing a block. In that case, please reach out to the Tempo team — even with the Tempo team, resolving this requires coordinating a new validator identity. # Controlling validator lifecycle This guide covers operational actions for managing a Tempo validator — from initial registration to deactivation. ## Starting and stopping Use `SIGINT` (Ctrl+C) or `SIGTERM` to gracefully stop the node: ```bash # If running directly kill -INT # If running via systemd sudo systemctl stop tempo ``` The node will finish processing the current block before shutting down. Avoid using `SIGKILL` as it may corrupt the database. ## Resetting your validator's data :::danger **You cannot reset a validator's data and continue with the same identity.** Doing so risks inconsistent voting, which can cause irrecoverable network safety failures. ::: If you need to reset your validator's data, you must rotate to a new validator identity. This requires coordinating with the Tempo team to deactivate your old identity and register a new one. Don't hesitate to reach out — even with the Tempo team, this is a routine operation. :::info Self-service data resets are coming soon. Once available, you will be able to rotate to a new identity and reset your data without coordinating with the Tempo team. See [Rotate validator identity](#rotate-validator-identity). ::: ## Managing your validator ValidatorConfig operations below submit transactions from the validator operator address, which does not need to be a plaintext EOA key. Use the CLI signer backend that matches your custody setup; see [validator operator address custody](https://tempo.xyz/developers/docs/guide/node/validator-keys#validator-operator-address-custody). [Fee-token selection](#choose-the-validator-fee-token) uses the fee-recipient address instead. :::info[Preview or confirm transactions] Use `--dry-run` to print the target and calldata without signing or sending; it does not simulate execution. Use `--yes` to skip the confirmation prompt when sending. ::: ### Rotate validator identity The ed25519 key can be changed while keeping your validator index stable. This is useful for key rotation or recovery without leaving and re-joining the committee. [Generate a new signing key](https://tempo.xyz/developers/docs/guide/node/validator-keys#generating-a-signing-key) first. ::::danger[Do not shut down the old validator] Unlike Ethereum, you must **keep your old validator running** after rotation. The rotated-out validator is still a dealer in the committee for at least one more epoch. Shutting it down early will degrade network liveness. See the [exit timeline](#exit-timeline) for the full epoch-by-epoch breakdown. Keep the old validator running until it shows `in_committee: false`: :::code-group ```bash [Mainnet] tempo consensus validator --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus validator --rpc-url https://rpc.testnet.tempo.xyz ``` ::: :::: \::: :::code-group ```bash [Mainnet] tempo consensus rotate-validator \ --validator-address \ --public-key \ --ingress \ --egress \ --signing-key \ --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus rotate-validator \ --validator-address \ --public-key \ --ingress \ --egress \ --signing-key \ --rpc-url https://rpc.testnet.tempo.xyz ``` ::: If self-service rotation is not yet enabled for your validator, use `tempo consensus create-rotate-validator-signature` to generate the signature and provide it to the Tempo team. :::info Rotation preserves your validator index and active validator count. The old entry is appended to history as deactivated, and the entry at your index is updated in place. You must use a different ingress address (changing the port is sufficient). ::: After rotation, your validator goes through the [standard state transitions](https://tempo.xyz/developers/docs/guide/node/validator-status#state-transitions) with the new identity. ### Update IP addresses If your node's network endpoints change, update them on-chain. The change takes effect at the next finalized block. :::code-group ```bash [Mainnet] tempo consensus set-validator-ip-address
\ --ingress \ --egress \ --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus set-validator-ip-address
\ --ingress \ --egress \ --rpc-url https://rpc.testnet.tempo.xyz ``` ::: :::warning Ingress addresses must be unique across all active validators. The transaction will revert if another active validator already uses the same `IP:port`. ::: ### Update the fee recipient The fee recipient used by your validator when constructing block proposals is managed on-chain. This can be updated and takes effect on the next finalized block. :::code-group ```bash [Mainnet] tempo consensus set-validator-fee-recipient
\ --fee-recipient \ --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus set-validator-fee-recipient
\ --fee-recipient \ --rpc-url https://rpc.testnet.tempo.xyz ``` ::: ### Choose the validator fee token From [v1.12.0](https://github.com/tempoxyz/tempo/releases/tag/v1.12.0), `tempo consensus set-validator-token` lets you select the USD-denominated TIP-20 token used for validator fees. This calls the FeeManager and sets the preference for the **transaction sender**. Submit from your validator's configured fee-recipient address, which can differ from the validator operator address used by the other lifecycle commands. List verified tokens on your network without a signer: :::code-group ```bash [Mainnet] tempo consensus set-validator-token --list --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus set-validator-token --list --rpc-url https://rpc.testnet.tempo.xyz ``` ::: Preview a selection using a token address, symbol, or name from the list: ```bash tempo consensus set-validator-token \ --rpc-url https://rpc.tempo.xyz \ --dry-run ``` For a raw token address, `--no-fetch-verified-tokens` skips the metadata lookup; on-chain token validation still applies. The token must be a deployed USD-denominated TIP-20. The FeeManager rejects a preference change in a block whose fee recipient is the caller. This command changes the preferred token, not the fee-recipient address; see [Update the fee recipient](#update-the-fee-recipient) to change that address. ### Transfer validator ownership Rebind your validator entry to a new control address: :::code-group ```bash [Mainnet] tempo consensus transfer-validator-ownership
\ --rpc-url https://rpc.tempo.xyz \ --new-private-key ``` ```bash [Testnet] tempo consensus transfer-validator-ownership
\ --rpc-url https://rpc.testnet.tempo.xyz \ --new-private-key ``` ::: The `--new-private-key` argument is used to derive the replacement operator address. That new address must not already be used by another active validator. ### Deactivate your validator Deactivate your validator when you want to leave the active set. :::warning[Do not shut down your node immediately after deactivating] Your validator remains a dealer in the committee for at least one more epoch after deactivation. Shutting it down early will degrade network liveness. See the [exit timeline](#exit-timeline) below. ::: :::code-group ```bash [Mainnet] tempo consensus deactivate-validator
\ --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus deactivate-validator
\ --rpc-url https://rpc.testnet.tempo.xyz ``` ::: #### Exit timeline Deactivation is not instant — your validator is phased out over two epochs: | Epoch | State | `is_dealer` | `is_player` | `in_committee` | |-------|-------|:-----------:|:-----------:|:--------------:| | **E** (deactivation) | Active | `true` | `true` | `true` | | **E+1** | Exiting | `true` | `false` | `true` | | **E+2** | Exited | `false` | `false` | `false` | Keep your node running until your validator shows `in_committee: false`. Use [validator lookup](https://tempo.xyz/developers/docs/guide/node/validator-status#look-up-your-validator) to check — it is safe to shut down the node at that point. # Managing validator keys Tempo validators use several keys and addresses. This page explains what each one does, how sensitive it is, and how to manage it. :::warning Never share your private signing key. Anyone with access to it can impersonate your validator. The Tempo team will never ask for your private key. Store keys securely and restrict file permissions. Use different signing keys and operator keys for testnet and mainnet. A testnet compromise should never put your mainnet validator at risk. ::: ## Key and address overview | Key / Address | Type | What it does | Sensitivity | How to change | |---|---|---|---|---| | **Signing key** | Ed25519 keypair | Identifies your validator in the consensus protocol. Used for DKG participation, block proposals, and voting. | **Critical** — anyone with this key can impersonate your validator. | [Rotate validator identity](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#rotate-validator-identity) | | **Validator operator address** | Ethereum address (`0x…`) | The control address that authorizes on-chain operations: IP updates, fee-recipient updates, key rotation, ownership transfer, and deactivation. | **High** — controls all validator configuration. | [Transfer validator ownership](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#transfer-validator-ownership) | | **Fee recipient** | Ethereum address (`0x…`) | Receives transaction fees from blocks your validator proposes. | **Low** — changing it only redirects future fee revenue, no security impact. | [Update fee recipient](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#update-the-fee-recipient) | | **Signing share** | BLS12-381 key share | A share of the committee's threshold signing key, used to sign block notarizations and finalizations. | **Managed automatically** — updated every DKG ceremony (~3 hours). Lost shares are recovered from the network on restart. | Automatic (see [recovery](#signing-share-recovery)) | ## Generating a signing key :::warning Never share your private signing key. Anyone with access to it can impersonate your validator. The Tempo team will never ask for your private key. Use a different key for each network rather than reusing the same validator identity on testnet and mainnet. ::: Generate an encrypted ed25519 keypair. The `--secret` argument points to a file-like input that contains the encryption key. Prefer a named pipe (FIFO) or shell process substitution for this path: a FIFO lets one process stream bytes directly to another process without storing those bytes as a regular file, and it keeps the secret out of environment variables and command-line arguments. See [Why FIFOs and not env vars](#why-fifos-and-not-env-vars). ```bash mkfifo /run/tempo/consensus-secret > /run/tempo/consensus-secret & tempo consensus generate-signing-key \ --output \ --secret /run/tempo/consensus-secret ``` Verify the public key: ```bash > /run/tempo/consensus-secret & tempo consensus show-verification-key \ --private-key \ --secret /run/tempo/consensus-secret ``` The verification key should match the output of the `generate-signing-key` command. `` should be a command that retrieves the encryption key from your KMS or secret manager and writes the raw secret to stdout. ### Encrypting an existing signing key If you already have an unencrypted ed25519 signing key, encrypt it with `tempo consensus encrypt-signing-key`. The command reads the existing plaintext key, reads the encryption key from `--secret`, and writes a new encrypted key file. ```bash tempo consensus encrypt-signing-key \ --input \ --output \ --secret <() ``` Verify the encrypted key before replacing the old file: ```bash tempo consensus show-verification-key \ --private-key \ --secret <() ``` After the verification key matches the old signing key's public key, update `--consensus.signing-key` to point at the encrypted file and start `tempo node` with `--consensus.secret `. Once the encrypted key is verified and backed up, delete the old unencrypted key file. On Linux, you can use `shred` to overwrite and remove the file: ```bash shred --remove --zero ``` ### Why FIFOs and not env vars `--secret` accepts any filesystem path. The `tempo node` binary does not place extra restrictions on where it reads the secret from, so a regular file works, but a named pipe (FIFO) or process-substitution path is preferred. A FIFO is a special filesystem entry used to pass data between processes; it has a path, but the bytes written to it are streamed through the kernel rather than stored as regular file contents. Use a FIFO so the encryption key is provided to `tempo` only when it is needed, without putting the key in the process environment or command-line arguments. Environment variables can be inherited by child processes and may be exposed through process inspection, crash dumps, shell history, or service-manager diagnostics. A FIFO also avoids leaving the secret behind in a regular file, though the producing command and `tempo` may still hold the value briefly in process memory while handling it. :::warning The `printf` example below is only a demonstration of how a FIFO works. Do not put production encryption keys directly in shell commands, shell history, scripts, or environment variables. ::: ```bash mkfifo /run/tempo/consensus-secret printf '%s' '' > /run/tempo/consensus-secret & tempo consensus show-verification-key \ --private-key \ --secret /run/tempo/consensus-secret ``` With shell process substitution and a KMS-backed secret command: ```bash tempo consensus generate-signing-key \ --output \ --secret <() ``` ## Signing key rotation The ed25519 signing key can be rotated while preserving your validator index and committee slot. See [Rotate validator identity](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#rotate-validator-identity) for the full procedure. ## Validator operator address custody The validator operator address is an Ethereum address, but it does not need to be controlled by a plaintext EOA private key. The Tempo CLI can submit validator on-chain operations with a local wallet key, a hardware wallet, or a remote KMS signer. | Signer | CLI flag | Notes | |---|---|---| | Local wallet key file | `--wallet-key ` | Use only where local key custody is appropriate | | Ledger | `--ledger` | Uses the first Ledger Live account | | Trezor | `--trezor` | Uses the first Trezor Live account | | AWS KMS | `--aws` | Requires `AWS_KMS_KEY_ID` and AWS credentials in the environment | | GCP KMS | `--gcp` | Requires `GCP_PROJECT_ID`, `GCP_LOCATION`, `GCP_KEY_RING`, `GCP_KEY_NAME`, and `GCP_KEY_VERSION` | Use these signer flags for CLI commands that submit transactions to the validator contract, including rotation, IP updates, fee-recipient updates, deactivation, and ownership transfer. :::info The validator identity signature and the transaction signer are different: * The ed25519 consensus signing key proves ownership of the validator public key during registration and rotation. * The Ethereum transaction signer controls the validator operator address. It can be backed by a hardware wallet or remote KMS. ::: ## Signing share recovery :::warning[Deleting consensus data can halt the network] A node eventually recovers its signing share, but do not delete the consensus directory lightly. If too many validators delete their shares in the same epoch, the network halts. ::: If the signing share is lost — for example by deleting `/consensus` — the node will recover a new share in the following epochs from the network when it restarts. ## ValidatorConfig V2 precompile All key and identity operations are executed through the [Validator Config V2 precompile](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1017.md): ```solidity address constant VALIDATOR_CONFIG_V2 = 0xCCCCCCCC00000000000000000000000000000001; ``` Public validator write operations require the validator operator address. # Validator failover: preparing a follower node This runbook covers a warm failover where a follower is promoted into a validator. A follower running in certified mode stores the same consensus finalization data used to bootstrap the validator. :::danger[Prevent double-signing] Only one node may run with a validator signing key at a time. Before promoting the follower, fence the old validator. Do not start the replacement while the old validator can still sign. ::: ## Prepare a follower :::steps ### Run the follower Use a dedicated data directory for the follower. Trustless RPC-to-RPC following is the default, and the upstream must be a validator or RPC node that exposes the `consensus` RPCs. If following a validator, the upstream validator must also expose WebSockets with `--ws`. ```bash tempo node --datadir \ --chain \ --follow ws://example.url:8546 \ --http --http.port 8545 \ --http.api eth,net,web3,txpool,trace,consensus ``` Do not pass `--consensus.signing-key` while the standby is running as a follower. The standby is syncing certified consensus state, not participating in consensus. ### Verify the follower is current Check that execution height is advancing and the certified consensus feed is available: ```bash cast block-number --rpc-url http://localhost:8545 cast rpc consensus_getLatest --rpc-url http://localhost:8545 ``` If `consensus_getLatest` is unavailable, the follower is not syncing consensus state. Confirm that the upstream endpoint is certified and exposes the `consensus` namespace. ### Keep validator networking ready The promoted node must be reachable at the ingress and egress addresses registered onchain. A setup that allows a floating IP or load balancer address that can move from the old validator to the standby will require no onchain updates. If failover uses a different address, update the validator's onchain IP configuration after fencing the old validator. Follow the [update IP addresses](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#update-ip-addresses) procedure. ::: ## Promote the follower ::::steps ### Fence the old validator Stop the old validator or isolate it from the network before starting the replacement. If you use a floating IP, move it to the standby before validator startup. ```bash sudo systemctl stop tempo ``` ### Stop the follower process Stop the standby follower cleanly so the same data directory can be reopened in validator mode. ```bash sudo systemctl stop tempo-follower ``` If the follower is not managed by systemd, send `SIGINT` or `SIGTERM` and wait for the process to exit. ### Restart the standby in validator mode Restart the same data directory without `--follow`, and add the validator signing key. Use the same consensus signing key as the validator you are failing over from. This procedure moves an existing validator identity to the standby; it does not rotate the identity. To use a different signing key, the validator's onchain configuration must be rotated. Follow the [rotate the validator identity](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#rotate-validator-identity) procedure. :::code-group ```bash [Mainnet] tempo node --datadir \ --chain mainnet \ --consensus.signing-key \ --consensus.listen-address 0.0.0.0: \ --http --http.port 8545 \ --http.api eth,net,web3,txpool,trace,consensus \ --telemetry-url ``` ```bash [Testnet] tempo node --datadir \ --chain testnet \ --consensus.signing-key \ --consensus.listen-address 0.0.0.0: \ --http --http.port 8545 \ --http.api eth,net,web3,txpool,trace,consensus \ --telemetry-url ``` ::: Do not delete or resync the data directory during failover. The follower's datadir contains the necessary consensus and execution state that allows the node to restart as a validator without a fresh bootstrap. ### Verify validator participation Check that the node's consensus RPC is live and progressing, then verify the validator status from a public RPC endpoint: ```bash cast rpc consensus_getLatest --rpc-url http://localhost:8545 ``` :::code-group ```bash [Mainnet] tempo consensus validator --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus validator --rpc-url https://rpc.testnet.tempo.xyz ``` ::: The validator should remain `in_committee: true` if it was already active. If you changed IP addresses during failover, wait for the IP update to finalize and for peer discovery to pick up the new address. :::: ## After failover Treat the promoted node as the active validator until you intentionally move the role again. Use the [validator status metrics](https://tempo.xyz/developers/docs/guide/node/validator-status#checking-state-via-metrics) to confirm the promoted node is connected to peers and participating in consensus. # Monitoring a validator This guide covers how to monitor your Tempo validator's health, diagnose issues, and manage logs. ## Consensus states ### Proposer Your validator is currently the leader and proposing blocks. Check proposal activity: ```bash # Number of blocks your node has built and resolved curl -s localhost:9000/metrics | grep reth_payloads_resolved_block ``` If this counter is increasing, your validator is actively proposing blocks. ### Voter Your validator is voting on blocks proposed by others (notarization and finalization). This is the most common state. ```bash # Inbound voting messages (should increase steadily) curl -s localhost:8002/metrics | grep consensus_engine_epoch_manager_simplex_batcher_inbound_messages_total | grep data_0 ``` This counter should increase steadily when your node is participating in consensus. ## Execution states ### Catching up Your node is syncing historical blocks. ```bash # Check sync stage curl -s localhost:6060/metrics | grep reth_sync_checkpoint ``` If `reth_sync_checkpoint` shows stages other than `Finish`, you're still syncing. ### Up to sync Your node is fully synced and processing new blocks in real-time. ```bash # Processed height should match or be close to finalized height curl -s localhost:8002/metrics | grep -E "marshal_finalized_height|marshal_processed_height" ``` Both values should be nearly equal and increasing together. ## Metrics glossary | Metric Name | Description | When to alert? | Meaning | | --- | --- | --- | --- | | `consensus_engine_dkg_manager_ceremony_successes_total` | Number of successful DKG ceremonies | Critical when it hasn't increased in 12 hours | Your node has participated in a successful DKG ceremony | | `consensus_engine_dkg_manager_ceremony_failures_total` | Number of failed DKG ceremonies | Warning when it increases | Your node has failed to participate in a DKG ceremony | | `consensus_engine_dkg_manager_how_often_dealer` | How many times your node has been a dealer (distributing shares) | Warning when it hasn't increased in 6 hours | Your node is distributing signing shares to other validators | | `consensus_engine_dkg_manager_how_often_player` | How many times your node has been a player (receiving shares) | Warning when it hasn't increased in 6 hours | Your node is receiving signing shares from dealers | | `consensus_engine_marshal_finalized_height` | Latest finalized height your node is aware of | Warning when it hasn't increased in 1 hour, critical when it hasn't increased in 3 hours | Your node is aware of the latest finalized height | | `consensus_engine_marshal_processed_height` | Latest height your node has processed | Critical when it hasn't increased in an hour, warning when it's behind finalized height | Your node is processing blocks | | `consensus_engine_peer_manager_peers` | Number of peers registered with the consensus peer manager | Warning when it's below the expected validator count, critical when it's 0 | Your node is registered with consensus peers | | `consensus_engine_epoch_manager_simplex_batcher_inbound_messages_total` | Number of inbound messages related to voting on the consensus layer | Warning when it's not increasing and your node is synced | Your node is receiving voting messages from the consensus layer | | `consensus_application_parent_ahead_of_local_time` | Number of times the parent block timestamp was ahead of local time | Warning when it increases frequently | Your node's clock may be out of sync — check NTP configuration | | `reth_sync_checkpoint` | Current sync progress | Warning on when there are no changes in the `Finish` stage | Your node is syncing with the network | | `reth_payloads_resolved_block` | Number of built and resolved payloads | Warning when it hasn't increased in 12 hours | Your node has built and resolved blocks | ## Grafana dashboard We provide a pre-built Grafana dashboard for monitoring your validator. It visualizes key metrics including node status, sync progress, voting activity, consensus latency, and execution performance. ### Importing the dashboard 1. Download the dashboard JSON from the [Tempo repository](https://github.com/tempoxyz/tempo/blob/main/contrib/grafana/dashboards/validator-health.json) 2. In Grafana, go to **Dashboards → Import** 3. Either paste the JSON content or upload the file 4. Select your Prometheus and Loki datasources when prompted 5. Click **Import** ### Dashboard variables The dashboard uses these template variables: | Variable | Description | | --- | --- | | `datasource` | Prometheus datasource | | `loki_ds` | Loki datasource (for log-based panels) | | `network_name` | Filter by network/validator job name | Make sure your Prometheus is scraping metrics from your validator's metrics endpoint (default: `localhost:8002/metrics`). ## Log management ### Parsing logs Tempo logs include ANSI escape codes for colors. To strip them for grep/awk: ```bash # Strip colors when searching sudo journalctl -u tempo | sed 's/\x1b\[[0-9;]*m//g' | grep "error" # Or disable colors at runtime RUST_LOG_STYLE=never tempo node ... ``` If you're using Loki, you can also use the `decolorize` filter to strip colors: ``` {job="tempo-node"} | decolorize ``` ### Log levels Control verbosity with `RUST_LOG`: ```bash # Default (info) RUST_LOG=info # Debug consensus only RUST_LOG=info,tempo_commonware_node::consensus=debug # Quiet mode (warnings and errors only) RUST_LOG=warn ``` # Tempo validator troubleshooting and FAQ ## My node is not proposing blocks Once your node is proposing blocks, it will start emitting logs like these: ``` INFO handle_propose{epoch=18 view=387213 parent.view=387212 parent.digest=0x43885416b4a7ae7550c615ad4dc702045cd26540b78fa2bd75abdcbfbef02d9d}: tempo_commonware_node::consensus::application::actor: constructed proposal proposal.digest=0x6baa8fa813beea491cf598024d8b31cb2927e7ba1b92a29899a795dbafd682c6 proposal.height=5733680 ``` If you do not see logs like these: * Check that your validator is part of the active set, and is both [a player and a dealer](https://tempo.xyz/developers/docs/guide/node/validator-status). * Check that your node is synced up to the [latest block height](https://explorer.tempo.xyz/). * Check that your outgoing IP address matches the one whitelisted on the validator smart contract: :::code-group ```bash [Mainnet] tempo consensus validators-info --rpc-url https://rpc.tempo.xyz ``` ```bash [Testnet] tempo consensus validators-info --rpc-url https://rpc.testnet.tempo.xyz ``` ::: ## My node is not connecting to peers If `consensus_engine_peer_manager_peers` remains at `0` for more than 3 hours after your validator was added on-chain: * Verify your firewall allows inbound connections on the ingress port you registered. * Verify your egress IP matches the one registered on-chain — check with `tempo consensus validator --rpc-url https://rpc.tempo.xyz`. * If you have reset your validator's data without rotating to a new identity, your node may have been blocked due to double-signing. In that case, [reach out to the Tempo team](https://tempo.xyz/contact) to coordinate a new validator identity. ## My node's DKG metrics are not increasing If `how_often_dealer` and `how_often_player` are not increasing after 6 hours: * Confirm your node is connected to peers (see above). * Check that your validator has progressed past the [Syncer state](https://tempo.xyz/developers/docs/guide/node/validator-status#state-transitions) — it takes at least one full epoch (~3 hours) after on-chain addition before your node participates in DKG. * Check DKG failure count: if `consensus_engine_dkg_manager_ceremony_failures_total` is increasing, your node may be failing to complete ceremonies. Enable debug logging for more detail: ```bash RUST_LOG=info,tempo_commonware_node::dkg=debug ``` ## My node is rejecting blocks or missing proposals If your node logs block validation errors (e.g. "block timestamp is in the future") or you notice missed proposals, a potential root cause is **clock drift**. Check whether `parent_ahead_of_local_time` is increasing in your metrics — if so, your system clock is behind the network. To fix: 1. **Check your current sync status:** ```bash timedatectl status ``` Confirm `System clock synchronized: yes` and `NTP service: active`. 2. **Install chrony** (if not already installed): ```bash sudo apt install chrony sudo systemctl enable --now chronyd ``` 3. **Verify the offset is acceptable:** ```bash chronyc tracking ``` The **System time** offset should be under a few milliseconds. 4. **Restart your node** after fixing the clock to clear any cached invalid block state. See [Time Synchronization](https://tempo.xyz/developers/docs/guide/node/system-requirements#time-synchronization) for full setup details. ## I accidentally deleted my consensus data directory Current validators require consensus finalization certificates to start. Contact the Tempo team to coordinate restoring a consistent snapshot; do not assume restarting with an empty consensus directory is sufficient. Once startup data is restored, [signing-share recovery](https://tempo.xyz/developers/docs/guide/node/validator-keys#signing-share-recovery) can reconstruct a missing share, or the node can obtain one in a future successful DKG ceremony. :::danger Do **not** delete the entire data directory and attempt to re-sync with the same signing key. This risks double-signing and will require [rotating to a new identity](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#resetting-your-validators-data). ::: ## How long does it take for my validator to become active? After on-chain registration, your validator follows the [state transition timeline](https://tempo.xyz/developers/docs/guide/node/validator-status#state-transitions): 1. **Epoch E** (immediate) — registered on the p2p network, starts syncing. 2. **Epoch E+1** (~3 hours) — becomes a player, receives signing shares. 3. **Epoch E+2** (~6 hours) — becomes a dealer/validator, can propose and vote once synced. In most cases, your validator will be fully active within 6 hours. ## How long does it take for my validator to exit? After deactivation, your validator is phased out over two epochs: 1. **Epoch E** — deactivation transaction submitted. Validator remains fully active. 2. **Epoch E+1** (~3 hours) — still a dealer but no longer a player. In the process of being removed. 3. **Epoch E+2** (~6 hours) — fully out of the committee (assuming no DKG failures). Keep your node running until `in_committee: false` — check with [validator lookup](https://tempo.xyz/developers/docs/guide/node/validator-status#look-up-your-validator). ## Can I register my validator without the Tempo team? No — the active validator set is currently permissioned. Only the contract owner can add validators on-chain. To get started, [contact the Tempo team](https://tempo.xyz/contact). The onboarding process: 1. **You** generate your signing key and registration signature ([Steps 1–2](https://tempo.xyz/developers/docs/guide/node/validator-setup#initial-registration)). 2. **You** provide the required values to the Tempo team ([Step 3](https://tempo.xyz/developers/docs/guide/node/validator-setup#step-3-submit-registration-details)). 3. **The Tempo team** adds your validator on-chain. 4. **You** download a snapshot and start your node ([Running the validator](https://tempo.xyz/developers/docs/guide/node/validator-setup#running-the-validator)). ## What can I do without the Tempo team? Once your validator is registered, most operations are self-service: * [Rotate your signing key](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#rotate-validator-identity) * [Update IP addresses](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#update-ip-addresses) * [Update your fee recipient](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#update-the-fee-recipient) * [Transfer validator ownership](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#transfer-validator-ownership) * [Deactivate your validator](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#deactivate-your-validator) Only **initial registration** and **reactivation** require the Tempo team. ## How do I check which version I'm running? ```bash tempo --version ``` Compare with the latest release on the [network upgrades](https://tempo.xyz/developers/docs/guide/node/network-upgrades) page to ensure you're on a supported version. # Node security: best practices for operators This page covers security best practices for Tempo node operators. Following these recommendations helps protect your validator from impersonation, unauthorized access, and operational mistakes. ## Key management Your signing key is the most sensitive asset on your validator. Anyone with access to it can impersonate your node in consensus. * **Restrict file permissions** — set `chmod 600` on key files so only the node process user can read them. * **Never share your private key** — the Tempo team will never ask for it. * **Use different keys for testnet and mainnet** — do not reuse signing keys or operator keys across networks; a testnet compromise should never put your mainnet validator at risk. * **Rotate keys periodically** — use [key rotation](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#rotate-validator-identity) to swap to a new ed25519 key without leaving the committee. * **Separate the operator address** — the Ethereum address that controls on-chain operations (IP updates, rotation, ownership transfer) should be a dedicated address, not a general-purpose hot wallet. See [Managing validator keys](https://tempo.xyz/developers/docs/guide/node/validator-keys) for the full key hierarchy and generation instructions. ## Network configuration :::info Deploy validators using the [validator network topology](https://tempo.xyz/developers/docs/guide/node/validator-topology). Isolate the validator, allow direct consensus traffic only with other validators, and route execution P2P through internal follower RPC nodes. ::: Tempo's networking layer includes built-in protections, making a cloud firewall or NAT gateway unnecessary in most setups. The node: * Only accepts consensus connections from validators that can prove their identity * Only accepts connections from trusted IP addresses (active validators on-chain) * Rate-limits connections and messages in both consensus and execution layers ### Recommendations * **Expose only the required ports** — see [Ports](https://tempo.xyz/developers/docs/guide/node/system-requirements#ports) for which ports need public access vs. internal-only. * **Keep ingress addresses unique** — the on-chain contract enforces uniqueness, but verify your registered `IP:port` is correct after infrastructure changes. * **Use a dedicated machine** — avoid running other internet-facing services on the same host as your validator. ## Release verification Always verify release artifacts before running them. Tempo signs all binaries and Docker images — see [Verifying Releases](https://tempo.xyz/developers/docs/guide/node/installation#verifying-releases) for GPG, Cosign, and SHA256 verification instructions. ## Time synchronization An unsynchronized clock can cause your node to reject valid blocks or produce blocks that other validators reject. Use `chrony` or `ntpd` — not `systemd-timesyncd`. See [Time Synchronization](https://tempo.xyz/developers/docs/guide/node/system-requirements#time-synchronization) for setup instructions. ## Data integrity * **Never delete the data directory and re-sync with the same signing key** — this risks double-signing and will require [rotating to a new identity](https://tempo.xyz/developers/docs/guide/node/validator-lifecycle#resetting-your-validators-data). Deleting the `consensus` subdirectory also removes certificates required at startup; coordinate snapshot recovery with the Tempo team. [Signing-share recovery](https://tempo.xyz/developers/docs/guide/node/validator-keys#signing-share-recovery) does not replace those certificates. * **Back up your signing key** — if the key file is lost and no backup exists, you will need to rotate to a new key and coordinate with the Tempo team. ## Staying up to date * Subscribe to [Tempo GitHub releases](https://github.com/tempoxyz/tempo/releases) for security patches and upgrade announcements. * Review the [Upgrade Cadence](https://tempo.xyz/developers/docs/guide/node/upgrade-cadence) to understand notification timelines. * Apply Required upgrades before the activation block — failing to do so will fork your node off the network. # Upgrade cadence and rollout timeline Tempo ships protocol upgrades on a **bi-weekly to monthly cadence**. Each upgrade bundles one or more protocol changes ([TIPs](https://tips.sh/)) into a named hardfork (T1, T2, T3, …). ## Rollout timeline Every upgrade follows the same two-stage rollout: | Stage | Lead time | Details | |-------|-----------|---------| | **Testnet** (Moderato) | Announced at least **3 days** before activation | The upgrade activates on the Moderato testnet first so operators and integrators can validate. | | **Mainnet** (Presto) | Announced at least **7 days** before activation | After a successful testnet activation, the same release is scheduled for mainnet — typically one week later. | Operators should use the window between testnet and mainnet activation to upgrade their testnet node, verify it syncs past the activation block, and confirm their applications work against the new protocol rules. ## Patch releases Between hardforks, Tempo publishes patch releases (e.g. v1.5.1, v1.5.2) for security fixes, performance improvements, and non-consensus bug fixes. Patch releases do not require a hardfork and can be adopted at your own pace, subject to the priority level. ## Upgrade priority levels Each release on the [Network Upgrades and Releases](https://tempo.xyz/developers/docs/guide/node/network-upgrades) page carries a priority badge: | Badge | Meaning | |-------|---------| | **Required** | Consensus-breaking change — nodes **must** upgrade before the activation block or they will fork off the network. | | **Recommended** | Non-consensus improvement (performance, hardening, bug fixes). Nodes continue to function without upgrading, but the update is strongly encouraged. | | **RPC only** | Only affects RPC-serving nodes (e.g. security patches for public endpoints). Validators without public RPC exposure can skip. | ## How you will be notified * **GitHub releases** — every release is published to [tempoxyz/tempo releases](https://github.com/tempoxyz/tempo/releases) with a full changelog. * **Operator channels** — the Tempo team shares activation timestamps and migration checklists in dedicated operator channels ahead of each upgrade. * **Docs** — the [Network Upgrades and Releases](https://tempo.xyz/developers/docs/guide/node/network-upgrades) page is updated with dates, TIP references, and priority badges as soon as an upgrade is scheduled. ## Operator checklist :::steps ### Review the release Read the changelog and upgrade page to understand what is changing and whether any migration steps are required. ### Upgrade your testnet node Update your Moderato node to the new release and confirm it syncs past the testnet activation block. ### Validate your integration Run your application or test suite against the upgraded testnet to catch any breaking changes before mainnet. ### Upgrade your mainnet node Update your mainnet node **before** the mainnet activation timestamp. ### Monitor after activation Watch logs and metrics after the activation block to confirm normal operation. ::: # Network Upgrades and Releases Tempo uses scheduled network upgrades to introduce protocol changes. Each upgrade goes through testnet activation before mainnet. This page also tracks important releases that node operators should be aware of. For detailed release notes and binaries, see the [Changelog](https://tempo.xyz/developers/docs/changelog). ## Node Operator Updates | Release | Date | Network | Description | Priority | |---------|------|---------|-------------|----------| | [v1.14.0](https://github.com/tempoxyz/tempo/releases/tag/v1.14.0) | Sep 7, 2026 | Testnet + Mainnet | Current required release for both networks ([T11](https://tempo.xyz/developers/docs/protocol/upgrades/t11)). Extends expiring-nonce validity and changes precompile pricing and ABI validation. Nodes running v1.13.x must upgrade. | **Required for T11** | | [v1.13.2](https://github.com/tempoxyz/tempo/releases/tag/v1.13.2) | Aug 27, 2026 | Testnet + Mainnet | Snapshot download and consensus startup fixes. Superseded by v1.14.0 for T11. | **Superseded** | | [v1.13.1](https://github.com/tempoxyz/tempo/releases/tag/v1.13.1) | Aug 20, 2026 | Testnet + Mainnet | Security release that hardens precompile input processing and improves Zone validation performance. Superseded by v1.14.0 for T11. | **Superseded** | | [v1.13.0](https://github.com/tempoxyz/tempo/releases/tag/v1.13.0) | Aug 17, 2026 | Testnet + Mainnet | Introduced [T10](https://tempo.xyz/developers/docs/protocol/upgrades/t10): native ZoneFactory, deterministic ZonePortal addresses, and shared zone runtimes. Superseded by v1.14.0 for T11. | **Superseded** | | [v1.12.0](https://github.com/tempoxyz/tempo/releases/tag/v1.12.0) | Aug 3, 2026 | Testnet + Mainnet | Required for T9. Adds TIP-403 storage for TIP-20 token policy bindings used by zones/provable contract flows, plus a targeted migration path for existing tokens that need one. T9 is active on testnet and mainnet. | **Required** | | [v1.11.0](https://github.com/tempoxyz/tempo/releases/tag/v1.11.0) | Jul 22, 2026 | Testnet + Mainnet | Required for T8. Includes current committee state, FeeAMM policy changes, versioned Stablecoin DEX order storage, DEX V2Order support, and final TIP-20 rewards deprecation. T8 is active on testnet and mainnet. | **Required** | | [v1.10.2](https://github.com/tempoxyz/tempo/releases/tag/v1.10.2) | Jul 17, 2026 | Testnet + Mainnet | Patch release with Alloy dependency updates. Operators running v1.10.x should upgrade to v1.10.2. | **High priority** | | [v1.10.1](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1) | Jun 29, 2026 | Testnet + Mainnet | Required for T7; includes storage credits for DEX order storage and TIP-20 channel storage, dynamic base fee behavior, and TIP-20 rewards deprecation. | **Required** | | [v1.9.1](https://github.com/tempoxyz/tempo/releases/tag/v1.9.1) | Jun 19, 2026 | Testnet + Mainnet | Patch release with follow-mode stability fixes, payload-builder latency fixes, Reth/Rust dependency updates, and transaction-pool/RPC improvements. | **Recommended** | | [v1.9.0](https://github.com/tempoxyz/tempo/releases/tag/v1.9.0) | Jun 15, 2026 | Testnet + Mainnet | Required for T6; adds account-level receive policies for safer TIP-20 deposits and admin access keys for smoother passkey, device, and delegated-key management. Operators were required to run this release before the T6 activation timestamp on each network. | **Required** | | [v1.8.2](https://github.com/tempoxyz/tempo/releases/tag/v1.8.2) | Jun 8, 2026 | Testnet + Mainnet | High-priority patch release that fixes a `--minimal` node issue when requesting historical blocks through the commonware marshal interface. | **Recommended** | | [v1.8.1](https://github.com/tempoxyz/tempo/releases/tag/v1.8.1) | Jun 1, 2026 | Testnet + Mainnet | Required for T5; reverts builder prewarming and execution cache sharing defaults from v1.8.0 to avoid stale-state validation errors while retaining T5 compatibility. | **Required** | | [v1.8.0](https://github.com/tempoxyz/tempo/releases/tag/v1.8.0) | May 28, 2026 | Testnet + Mainnet | Required for T5; introduces the enshrined TIP-20 reserve channel precompile, payment lane classification, DEX flip-order improvements, multihop FeeAMM routing, optional on-chain TIP-20 `logoURI`, implicit approvals, and key authorization witnesses. Operators should use v1.8.2 for the latest patch fixes. | **Required** | | [v1.7.1](https://github.com/tempoxyz/tempo/releases/tag/v1.7.1) | May 21, 2026 | Moderato + Mainnet | Adds validator migration support for minimal snapshots, trustless RPC certificate checks on Moderato, and automatic pruning for finalized consensus blocks. | **Recommended** | | [v1.7.0](https://github.com/tempoxyz/tempo/releases/tag/v1.7.0) | Mon, May 11, 2026 | Moderato + Mainnet | Required for T4; embeds consensus context into block headers to unlock deferred verification and bundles T4 bug fixes and security hardening. | **Required** | | [v1.6.0](https://github.com/tempoxyz/tempo/releases/tag/v1.6.0) | Apr 16, 2026 | Moderato + Mainnet | Required for T3; implements enhanced access key permissions with periodic limits and call scoping, signature verification, and virtual addresses for TIP-20 deposit forwarding. | **Required** | | [v1.5.3](https://github.com/tempoxyz/tempo/releases/tag/v1.5.3) | Apr 9, 2026 | Moderato + Mainnet | Patch release that restores OTLP HTTPS telemetry and fixes an epoch-transition consensus race that could incorrectly block straggling peers. Validators that skipped v1.5.2 should upgrade directly to this release. | **Recommended** | | [v1.5.2](https://github.com/tempoxyz/tempo/releases/tag/v1.5.2) | Apr 8, 2026 | Moderato + Mainnet | Maintenance release with the latest reth update, payload builder and RPC improvements, plus transaction validation and mempool hardening. | **Recommended** | | [v1.5.1](https://github.com/tempoxyz/tempo/releases/tag/v1.5.1) | Mar 29, 2026 | Moderato + Mainnet | Security patch for RPC endpoints that accept `stateOverride`, including `eth_call` and `debug_traceCall`. This release is required for RPC providers and other public RPC nodes, and low priority for validators. | **RPC only** | | [v1.5.0](https://github.com/tempoxyz/tempo/releases/tag/v1.5.0) | Mar 26, 2026 | Moderato + Mainnet | Required for T2; implements compound transfer policies, permit support for TIP-20, Validator Config V2, and 14 audit-driven bug fixes. | **Required** | | [v1.4.3](https://github.com/tempoxyz/tempo/releases/tag/v1.4.3) | Mar 18, 2026 | Moderato + Mainnet | Fixes gas price oracle poisoning that caused inflated fee estimates for wallet transactions | **Recommended** | | [v1.4.2](https://github.com/tempoxyz/tempo/releases/tag/v1.4.2) | Mar 16, 2026 | Moderato + Mainnet | Strict payment calldata validation in the transaction pool and block builder, rejecting malformed payment transactions earlier | **Recommended** | | [v1.4.1](https://github.com/tempoxyz/tempo/releases/tag/v1.4.1) | Mar 12, 2026 | Moderato + Mainnet | Transaction pool DoS-hardening, consensus resilience improvements (DKG recovery), hardfork-aware gas estimation, and payload builder enhancements | **Recommended** | | [v1.4.0](https://github.com/tempoxyz/tempo/releases/tag/v1.4.0) | Mar 5, 2026 | Moderato + Mainnet | Required for T1C; introduces keychain signature migration so only V2 signatures and hashes are accepted after activation | **Required** | | [v1.3.1](https://github.com/tempoxyz/tempo/releases/tag/v1.3.1) | Feb 22, 2026 | Testnet + Mainnet | Fixes high-load issues and finalizes T1A/T1B hardening for expiring nonce replay protection and keychain precompile gas handling | **Required** | | [v1.2.0](https://github.com/tempoxyz/tempo/releases/tag/v1.2.0) | Feb 13, 2026 | Mainnet only | Fixes validation bug rejecting transactions with gas limits above ~16.7M, blocking large contract deployments | **Required** | ## T10 | | | |---|---| | **Scope** | Native ZoneFactory, deterministic ZonePortal accounts, and protocol-managed shared zone runtimes | | **TIPs** | [Enshrined ZoneFactory](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1091.md) | | **Details** | [T10 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t10) | | **Release** | [v1.13.0](https://github.com/tempoxyz/tempo/releases/tag/v1.13.0) | | **Testnet** | Live: August 20, 2026 at 14:00 UTC (`1787234400`) | | **Mainnet** | Live: August 21, 2026 at 14:00 UTC (`1787320800`) | | **Priority** | **Required** | T10 is active on testnet and mainnet and supported by [v1.13.0](https://github.com/tempoxyz/tempo/releases/tag/v1.13.0), published on August 17, 2026. Node operators were required to run the T10-compatible release before activation to stay synced. *** ## T9 | | | |---|---| | **Scope** | TIP-20 policy IDs in TIP-403 for zones/provable contract flows, plus targeted migration for existing TIP-20 tokens that need a TIP-403 binding | | **TIPs** | [TIP-20 Policy IDs in TIP-403](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1092.md) | | **Details** | [T9 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t9) | | **Release** | [v1.12.0](https://github.com/tempoxyz/tempo/releases/tag/v1.12.0) | | **Testnet** | Live: August 5, 2026 | | **Mainnet** | Live: August 6, 2026 | | **Priority** | **Required** | T9 is active on testnet and mainnet and supported by [v1.12.0](https://github.com/tempoxyz/tempo/releases/tag/v1.12.0), published on August 3, 2026. Node operators were required to run the T9-compatible release before activation to stay synced. *** ## T8 | | | |---|---| | **Scope** | current committee state; FeeAMM policy exemptions; versioned Stablecoin DEX order storage; DEX V2Order support; final TIP-20 rewards deprecation | | **References** | [current committee state specification](https://tips.sh/1070), [FeeAMM policy exemptions specification](https://tips.sh/1042), [versioned DEX order storage specification](https://tips.sh/1062), [TIP-20 rewards deprecation](https://tips.sh/1075), [V2 DEX order storage](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1087.md) | | **Details** | [T8 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t8) | | **Release** | [v1.11.0](https://github.com/tempoxyz/tempo/releases/tag/v1.11.0) | | **Testnet** | Live: July 27, 2026 | | **Mainnet** | Live: July 30, 2026 | | **Priority** | **Required** | T8 is active on testnet and mainnet and supported by [v1.11.0](https://github.com/tempoxyz/tempo/releases/tag/v1.11.0), published on July 22, 2026. Node operators were required to run the T8-compatible release before activation to stay synced. *** ## T7 | | | |---|---| | **Scope** | Storage credits for DEX order storage and TIP-20 channel storage; lower the base fee when gas is below the target threshold; deprecate new TIP-20 rewards | | **TIPs** | [Storage credits](https://tips.sh/1060), [StablecoinDEX order storage credits](https://tips.sh/1064), [TIP-20 channel storage credits](https://tips.sh/1066), [Dynamic base fee](https://tips.sh/1067), TIP-20 rewards deprecation | | **Details** | [T7 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t7) | | **Release** | [v1.10.1](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1) | | **Testnet** | Live: July 2, 2026 | | **Mainnet** | Live: July 9, 2026 | | **Priority** | **Required** | T7 is active on testnet and mainnet and supported by [v1.10.1](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1), published on June 29, 2026. Node operators were required to run the T7-compatible release before their network's activation timestamp to stay synced. *** ## T6 | | | |---|---| | **Scope** | Account-level receive policies for safer TIP-20 deposits; admin access keys for smoother passkey, device, and delegated-key management | | **TIPs** | [Receive policies](https://tips.sh/1028), [Admin access keys](https://tips.sh/1049) | | **Details** | [T6 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t6) | | **Release** | [v1.9.0](https://github.com/tempoxyz/tempo/releases/tag/v1.9.0) | | **Testnet** | June 18, 2026 4pm CEST (unix: 1781791200) | | **Mainnet** | June 23, 2026 4pm CEST (unix: 1782223200) | | **Priority** | **Required** | ### Who is affected? Node operators were required to run v1.9.0 before the T6 activation timestamp on each network. Non-upgraded nodes fall out of consensus once T6 activates on their network. Integrators, indexers, wallets, explorers, and SDK maintainers should review the [T6 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t6) page for feature benefits and integration notes. *** ## T5 | | | |---|---| | **Scope** | Enshrined TIP-20 reserve channel precompile; payment lane classification; DEX same-tick flip orders and persistent order IDs across flips; multihop FeeAMM routing; optional on-chain TIP-20 `logoURI`; implicit approvals; and key authorization witnesses | | **TIPs** | [Payment-channel reserve](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1034.md), [Payment lane classification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1045.md), [Same-tick flip orders](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1030.md), [Keep order IDs across flips](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1056.md), [Multihop FeeAMM routing](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1033.md), [Logo URI](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1026.md), [Implicit approvals](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1035.md), [Witnesses in key authorizations](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1053.md) | | **Details** | [T5 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t5) | | **Release** | [v1.8.1](https://github.com/tempoxyz/tempo/releases/tag/v1.8.1) | | **Testnet** | June 3, 2026 16:00 CEST (unix: 1780495200) | | **Mainnet** | June 9, 2026 16:00 CEST (unix: 1781013600) | | **Priority** | **Required** | ### Who is affected? All node operators needed to upgrade before the T5 activation timestamp. Integrators, indexers, wallets, explorers, and SDK maintainers should review the [T5 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t5) page for the T5 surfaces and migration notes. *** ## T4 | | | |---|---| | **Scope** | Embed consensus context into the block header to unlock deferred verification, plus T4 bug fixes and security hardening | | **TIPs** | [Consensus context in block headers](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1031.md), [T4 network upgrade](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1046.md) | | **Details** | [T4 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t4) | | **Release** | [v1.7.0](https://github.com/tempoxyz/tempo/releases/tag/v1.7.0) | | **Testnet** | Moderato: May 14, 2026 16:00 CEST (unix: 1778767200) | | **Mainnet** | Presto: May 18, 2026 16:00 CEST (unix: 1779112800) | | **Priority** | **Required** | ### Who is affected? All node operators needed to upgrade before the T4 activation timestamp. Adding consensus context to block headers changed how blocks are produced and verified; non-upgraded nodes have their proposals rejected and have fallen out of consensus. Smart contract developers and integrators are not directly affected by consensus context in block headers, but should review the [T4 network upgrade specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1046.md) for bundled gas accounting changes and access-key scope validation updates that may affect transaction gas usage post-T4. *** ## T3 | | | |---|---| | **Scope** | Enhanced access keys with periodic limits, call scoping, and an authorization ABI update; signature verification precompile; and virtual addresses for TIP-20 deposit forwarding | | **TIPs** | [Enhanced access key permissions](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1011.md), [Signature verification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1020.md), [Virtual addresses](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1022.md) | | **Details** | [T3 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t3) | | **Release** | [v1.6.0](https://github.com/tempoxyz/tempo/releases/tag/v1.6.0) | | **Testnet** | Moderato: Apr 21, 2026 16:00 CEST (unix: 1776780000) | | **Mainnet** | Presto: Apr 27, 2026 16:00 CEST (unix: 1777298400) | | **Priority** | **Required** | See the [T3 network upgrade](https://tempo.xyz/developers/docs/protocol/upgrades/t3) page for breaking changes, migration checklist, and integration guidance. *** ## T2 | | | |---|---| | **Scope** | Compound transfer policies, ValidatorConfig V2, and audit-driven bug fixes | | **TIPs** | [Compound transfer policies](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1015.md), [Permit](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1004.md), [Validator Config V2](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1017.md), [T2 network upgrade](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1036.md) | | **Release** | v1.5.0 | | **Testnet** | Moderato: Mar 26, 2026 16:00 CET (unix: 1774537200) | | **Mainnet** | Mar 31, 2026 16:00 CEST (unix: 1774965600) | | **Priority** | **Required** | *** ## T1C | | | |---|---| | **Scope** | Security hardening, includes breaking change on keychain signatures | | **Release** | [v1.4.0](https://github.com/tempoxyz/tempo/releases/tag/v1.4.0) | | **Testnet** | Moderato: Mar 9, 2026 15:00 UTC (unix: 1773068400) | | **Mainnet** | Mar 12, 2026 15:00 UTC (unix: 1773327600) | | **Priority** | **Required** | *** ## T1A / T1B | | | |---|---| | **Scope** | T1A removes the 16.7M per-transaction gas limit in favor of Tempo's 30M cap; T1B adds keychain precompile gas metering and expiring nonce replay-protection hardening | | **TIPs** | T1A: [Mainnet gas parameters](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1010.md); T1B: upgrade hardening release | | **Release** | [v1.3.1](https://github.com/tempoxyz/tempo/releases/tag/v1.3.1) | | **Testnet** | T1A + T1B: Feb 23, 2026 15:00 UTC (unix: 1771858800) | | **Mainnet** | T1A: Feb 12, 2026 15:00 UTC (unix: 1770908400); T1B: Feb 23, 2026 15:00 UTC (unix: 1771858800) | | **Priority** | **Required** | *** ## T1 (Bach) | | | |---|---| | **Scope** | Mainnet-ready gas economics, expiring nonces, and security hardening | | **TIPs** | [State creation costs](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1000.md), [Expiring nonces](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1009.md), [Mainnet gas parameters](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1010.md) | | **Testnet** | Feb 5, 2026 15:00 UTC (unix: 1770303600) — Release: [v1.1.0](https://github.com/tempoxyz/tempo/releases/tag/v1.1.0) | | **Mainnet** | Feb 12, 2026 15:00 UTC (unix: 1770908400) — Release: [v1.1.1](https://github.com/tempoxyz/tempo/releases/tag/v1.1.1) | | **Priority** | **Required** | *** ## T0 (Genesis) | | | |---|---| | **Scope** | Initial mainnet launch | | **Mainnet** | Jan 16, 2026 (genesis) — Release: [v1.0.0](https://github.com/tempoxyz/tempo/releases/tag/v1.0.0) | | **Priority** | **Required** | # Changelog Tempo publishes the 20 most recent network releases here. ## v1.14.0 — Release v1.14.0 (2026-09-07) > \[!IMPORTANT] > This release is required for the T11 network upgrade scheduled for testnet on September 9, 2026 16:00 CEST (`1788962400`) and mainnet on September 10, 2026 16:00 CEST (`1789048800`). Node operators must update before activation. > > This release contains a breaking change for integrators & developers, please check the release notes below. T11 extends the expiring-nonce validity window (TIP-1093) and hardens precompile pricing and input decoding (TIP-1100, TIP-1105). #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes running an earlier release can fall out of sync at the T11 activation timestamp. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | September 9, 2026 16:00 CEST (14:00 UTC) | `1788962400` | | Mainnet | September 10, 2026 16:00 CEST (14:00 UTC) | `1789048800` | #### TIPs included with T11 1. **[TIP-1093: Extend Expiring Nonce Window to Five Minutes](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1093.md)**: Extends the maximum validity window for expiring-nonce transactions from 30 seconds to five minutes and grows the circular replay-protection buffer from 300,000 to 3,000,000 entries, preserving the TIP-1009 sizing target of 10,000 expiring-nonce transactions per second. 2. **[TIP-1100: Increase Precompile Input Gas Cost](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1100.md)** ([#7281](https://github.com/tempoxyz/tempo/pull/7281)): Raises the calldata input charge for Tempo precompiles from 6 to 30 gas per 32-byte word, bringing worst-case ABI decoding throughput from roughly 200 MGas/s to 1 GGas/s while adding less than 768 gas for calls below 1 KiB. 3. **[TIP-1105: T11 Pricing Hardening Meta TIP](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1105.md)**: * **Strict ABI decoding** ([#7444](https://github.com/tempoxyz/tempo/pull/7444)): Every Tempo precompile dispatcher enables Alloy's strict ABI decoder ([Alloy Core v1.7.2](https://github.com/alloy-rs/core/releases/tag/v1.7.2)) while retaining the 16 MiB decoder memory limit. * **Priced duplicate validation** ([#7298](https://github.com/tempoxyz/tempo/pull/7298), [#7447](https://github.com/tempoxyz/tempo/pull/7447)): Checking precompile input lists for duplicate entries is now ~4x faster: median validation of a worst-case 65,536-entry list drops from 2.40 ms to 0.57 ms. AccountKeychain call-scope and recipient duplicate checks switch from hash-set insertion to sort-and-scan, and ZoneFactory rejects duplicate addresses within or across its allowed-account, gateway, and sequencer role lists. *** ### Operators #### What's Changed * **`tempo download` fix** (reth [#26934](https://github.com/paradigmxyz/reth/pull/26934)): Downloaded snapshots now resolve the configured minimal pruning defaults. `tempo download → tempo node` no longer requires additional flags after a snapshot restore. * **Txpool address filter** ([#7363](https://github.com/tempoxyz/tempo/pull/7363)): Adds `--txpool.filter` to reject transactions by sender or direct call target at pool admission. Accepts comma-separated addresses or a file. * **`tempo-localnet` image** ([#7050](https://github.com/tempoxyz/tempo/pull/7050)): Publishes a bootstrapped localnet container with each release, preconfigured with faucet, fee liquidity, and exchange setup. *** ### Developers #### Breaking behavior at T11 * Precompile calldata must be strict, canonical ABI: gaps, overlaps, trailing data, and non-zero padding are rejected. Standard tooling (alloy/ethers/viem) is unaffected. * Precompile calldata charge increases from 6 to 30 gas per 32-byte word. Re-check gas estimates and hardcoded limits for keychain, DEX, and zone flows. * Duplicate entries in AccountKeychain call scopes, recipient lists, and ZoneFactory role lists are rejected. * Expiring-nonce validity window extends from 30 seconds to five minutes. **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.13.2...v1.14.0 ## v1.13.2 — Release v1.13.2 (2026-08-27) This is a quality-of-life patch release for operators. > \[!IMPORTANT]\ > Operators using `tempo download` to start nodes from snapshots please upgrade ### Update Priority | User Class | Priority | |------------|----------| | Validators | Recommended | | RPC Nodes | Recommended | ### What's Changed **UX Improvements** * **Fixes `tempo download --force`** [#7256](https://github.com/tempoxyz/tempo/pull/7256): `tempo download --force` now deletes the entire consensus directory. * **Fixes `tempo download` not fetching required consensus data** [#7154](https://github.com/tempoxyz/tempo/pull/7154): operators no longer need to provide `tempo download --skip-consensus=false` to download required consensus data. This now is the default. * **Fixes `tempo download` requiring explicit `--datadir` and `--manifest-url` arguments** [#7264](https://github.com/tempoxyz/tempo/pull/7264): operators no longer need to provide these arguments (unless desired). With v1.13.2 the node uses the Tempo snapshot API to determine which manifest to download and correctly resolves the datadir. **Bug Fixes:** * **Fix rare race condition leaving consensus metadata invalid** [#7203](https://github.com/tempoxyz/tempo/pull/7203): never observed in real deployments but identified as a potential issue that would require operator intervention. * **Fresh or stale nodes can participate in consensus faster** [#6950](https://github.com/tempoxyz/tempo/pull/6950): validator nodes that have been offline and are restarted with a fresh snapshot, or nodes that are freshly spun up, will now attempt to reconstruct their signing share by reading their last known epoch. If successful, they will participate in consensus immediately instead of waiting for a full cycle. * **Fix nodes refusing to start up because they cannot find boundary headers** [#7244](https://github.com/tempoxyz/tempo/pull/7244): in certain scenarios nodes running with `tempo node --minimal` tried to read already pruned boundary blocks and shut down even though boundary headers were available and contained the desired data. * **Noisily rejects startup on invalid data** [#7255](https://github.com/tempoxyz/tempo/pull/7255): due to manual intervention a node could be brought into a state where its available finalization certificates were older than persisted node metadata. This state is now detected early and a node shuts down with a clear error message. **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.13.1...v1.13.2 ## v1.13.1 — Release v1.13.1 (2026-08-20) > \[!IMPORTANT] > **v1.13.1 is a security release. The coordinated operator rollout has been completed. Independent node operators should upgrade promptly.** This patch hardens precompile input processing. No configuration changes or data migrations are required. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | ### What’s Changed * Hardened precompile input processing. * Improved Zone validation performance. ## v1.13.0 — Release v1.13.0 (2026-08-17) > \[!IMPORTANT] > **This release is required for the T10 network upgrade scheduled for testnet on August 20, 2026 16:00 CEST (`1787234400`) and mainnet on August 21, 2026 16:00 CEST (`1787320800`).** Node operators must update before activation. T10 activates TIP-1091, adding the native ZoneFactory and installing the canonical Zone Portal, Verifier, and Messenger runtimes. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes running an earlier release can diverge when T10 installs the Zone contracts or processes a native ZoneFactory call. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | August 20, 2026 16:00 CEST (14:00 UTC) | `1787234400` | | Mainnet | August 21, 2026 16:00 CEST (14:00 UTC) | `1787320800` | #### TIPs included with T10 1. **[TIP-1091: Enshrined ZoneFactory](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1091.md)**: Adds an owner-gated native ZoneFactory at `0x5AF2000000000000000000000000000000000000`. Zones receive deterministic portal addresses in the reserved `0x5AD0` namespace, while shared Portal, Verifier, and Messenger runtimes are installed at protocol-managed addresses. Zone creation validates the initial sequencer set, settlement threshold, TIP-403 token-policy binding, access roles, token metadata, and initial token-enablement commitment. *** ### Developers #### SDK Crate Versions No new SDK crate versions accompany this binary release. #### T10 Protocol Changes * **Native ZoneFactory and deterministic portals** ([#6874](https://github.com/tempoxyz/tempo/pull/6874), [#6990](https://github.com/tempoxyz/tempo/pull/6990)): Activates TIP-1091 at T10, installs the factory and shared Zone runtimes atomically, and creates portals as minimal proxies at deterministic `0x5AD0` addresses. * **Zone Portal leadership and initial token commitment** ([#6987](https://github.com/tempoxyz/tempo/pull/6987), [#7054](https://github.com/tempoxyz/tempo/pull/7054), [#7150](https://github.com/tempoxyz/tempo/pull/7150)): Records leader transitions, commits the initial token-enablement event into portal state, and applies token metadata and same-block enablement bounds during native zone creation. * **Portal authorization and pause state** ([#7175](https://github.com/tempoxyz/tempo/pull/7175), [#7171](https://github.com/tempoxyz/tempo/pull/7171)): Uses one role mapping for sequencers, accounts, and gateways; adds a pause guardian and typed capabilities; and allows proof-verified batch submission while asset flows are paused. * **Canonical Zone runtimes** ([#6986](https://github.com/tempoxyz/tempo/pull/6986), [#6996](https://github.com/tempoxyz/tempo/pull/6996), [#7151](https://github.com/tempoxyz/tempo/pull/7151)): Updates the embedded Portal, Verifier, and Messenger bytecode installed at T10, including bounded callback revert-data handling in the Messenger. #### Consensus, EVM, and SDK Integration * **Revm and Reth update** [#6946](https://github.com/tempoxyz/tempo/pull/6946): Updates Revm to `42.0.1`, `revm-inspectors` to `0.41.2`, and Reth to `2.4.0`. This brings block building and validation performance improvements of up to 10%. ### Operators * **Commonware Upgrade** ([#7009](https://github.com/tempoxyz/tempo/pull/7009)). Upgrade to commonware `2026.7.0`. Better tracing, perf improvements, `--strict-startup` is now the default * Validation nodes require finalization certificates to anchor their start; certs are now provided with all tempo published snapshots. * **Notarized tip driven execution** ([#7057](https://github.com/tempoxyz/tempo/pull/7057)). Validators now proactively reach the pending notarized tip. * When load testing multi-region setups some validators would struggle to reach the pending notarized tip and be unable to contribute to consensus. * Validator block sync is now forced through Consensus Layer P2P only. * As a side effect, if a validator falls behind too much (3 epochs, 64800 blocks), it needs to now be restarted from a fresh snapshot with (see `$ tempo download` to download one) * **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.12.0...v1.13.0 ## v1.12.0 — Release v1.12.0 (2026-08-03) > \[!IMPORTANT] > **This release is required for the T9 network upgrade scheduled for testnet on August 5, 2026 16:00 CEST (`1785938400`) and mainnet on August 6, 2026 16:00 CEST (`1786024800`).** Node operators must update before activation. T9 activates TIP-1092, introducing TIP-403 transfer-policy bindings for TIP-20 tokens. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | After activation, nodes running an earlier release can diverge when a block uses T9 policy-binding behavior or the new TIP-403 interfaces. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | August 5, 2026 16:00 CEST (14:00 UTC) | `1785938400` | | Mainnet | August 6, 2026 16:00 CEST (14:00 UTC) | `1786024800` | #### TIPs included with T9 The T9 network upgrade includes: 1. **[TIP-1092: TIP-20 Policy IDs in TIP-403](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1092.md)**: Adds token-to-policy bindings to TIP-403 so transfer-policy selection can be proven from registry state. New TIP-20 tokens register their policy during creation. Existing tokens continue to use their token-local policy until a permissionless migration copies it into TIP-403, or an administrator changes the policy after activation. Zone and provable-contract flows must require `tokenTransferPolicyId(token).isSet == true`; an unset lookup can still return the token's legacy policy ID. ### Operators #### What's Changed * **Reliable finalized-block feeds** ([#6968](https://github.com/tempoxyz/tempo/pull/6968), [#6989](https://github.com/tempoxyz/tempo/pull/6989)): Drives consensus RPC state and subscriptions from persisted marshal finalizations, including while a node is syncing, and removes reorg-prone notarized block delivery. * **Follower RPC lifetime fix** ([#6920](https://github.com/tempoxyz/tempo/pull/6920)): Keeps the execution node alive for the full follower-engine lifetime so HTTP RPC and other execution services do not shut down after startup. * **Validator fee-token command** ([#6906](https://github.com/tempoxyz/tempo/pull/6906)): Adds `tempo consensus set-validator-token`, with verified-token lookup by symbol or name, raw-address support, and `--list` output for the connected chain. * **Snapshot restore planning** ([#6929](https://github.com/tempoxyz/tempo/pull/6929)): Extends `tempo download --print-plan-json` to report the selected consensus archive together with Reth's execution-layer archives without downloading or modifying data directories. * **Consensus status output** ([#6933](https://github.com/tempoxyz/tempo/pull/6933)): Adds the number of blocks remaining before the next epoch to `tempo consensus info`. * **Signing-key file permissions** ([#6840](https://github.com/tempoxyz/tempo/pull/6840)): Creates encrypted signing-key files with owner-only permissions on Unix and tightens permissions when replacing an existing file. ### Developers #### SDK Crate Versions No new SDK crate versions accompany this binary release. | Package | Version | Notes | |---------|---------|-------| | `tempo-alloy` | `1.10.1` | Unchanged from v1.11.0. | | `tempo-primitives` | `1.10.1` | Unchanged from v1.11.0. | | `tempo-contracts` | `1.10.1` | Unchanged from v1.11.0. | | `tempo-chainspec` | `1.10.1` | Unchanged from v1.11.0. | | `tempo-hardfork` | `1.10.1` | Unchanged from v1.11.0. | #### T9 Protocol Changes * **TIP-403 token-policy bindings** ([#6846](https://github.com/tempoxyz/tempo/pull/6846), [#6935](https://github.com/tempoxyz/tempo/pull/6935)): Adds the T9-gated `tokenTransferPolicyId(address)` and `migrateTransferPolicyIds(address[])` interfaces, distinguishes an unset binding from policy ID `0`, and preserves legacy fallback for unmigrated tokens. * **Token creation, updates, and migration** ([#6846](https://github.com/tempoxyz/tempo/pull/6846)): New T9 tokens write their initial binding atomically. Permissionless migration skips invalid or already-bound addresses, copies the current local policy, and deletes the local slot. An administrator policy update writes the new registry binding while leaving the legacy slot untouched. #### EVM, Transaction Pool, and Genesis Tooling * **Custom fee-manager integration** ([#6908](https://github.com/tempoxyz/tempo/pull/6908), [#6917](https://github.com/tempoxyz/tempo/pull/6917), [#6928](https://github.com/tempoxyz/tempo/pull/6928), [#6938](https://github.com/tempoxyz/tempo/pull/6938)): Lets downstream nodes supply their configured EVM to transaction-pool validation, customize fee-token resolution and validation, and programmatically disable only the FeeAMM liquidity admission check. Tempo's default USD-only validation remains unchanged. * **Reusable TIP-20 mutation APIs** ([#6927](https://github.com/tempoxyz/tempo/pull/6927), [#6930](https://github.com/tempoxyz/tempo/pull/6930)): Exposes checked balance mutation and transfer helpers for downstream fee managers without requiring direct TIP-20 storage writes. * **EIP-2935 genesis provisioning** ([#6925](https://github.com/tempoxyz/tempo/pull/6925)): Adds the canonical block-hash history account to genesis state generated by `tempo-xtask`, allowing Zone deployments to anchor batches to L1 block hashes. #### Testing, Benchmarks, and Tooling * Added focused driver, resolver, and executor actor coverage for follower synchronization ([#6896](https://github.com/tempoxyz/tempo/pull/6896), [#6899](https://github.com/tempoxyz/tempo/pull/6899), [#6900](https://github.com/tempoxyz/tempo/pull/6900)). * Added StablecoinDEX gas and overflow snapshots across historical hardforks through T9 ([#6967](https://github.com/tempoxyz/tempo/pull/6967)). * Added a stable public benchmark preset and synchronized multi-region benchmark workflows with GCP support ([#6905](https://github.com/tempoxyz/tempo/pull/6905), [#6977](https://github.com/tempoxyz/tempo/pull/6977)). **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.11.0...v1.12.0 ## v1.11.0 — Release v1.11.0 (2026-07-22) > \[!IMPORTANT] > **This release is required for the T8 network upgrade scheduled for testnet on July 27, 2026 16:00 CEST (`1785160800`) and mainnet on July 30, 2026 16:00 CEST (`1785420000`).** Node operators must update before activation or their nodes will fall out of sync. T8 activates versioned StablecoinDEX order storage, FeeAMM policy changes, current-committee persistence, and the final phase of TIP-20 reward deprecation. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes that are not updated will fall out of sync at the T8 activation timestamp. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | July 27, 2026 16:00 CEST (14:00 UTC) | `1785160800` | | Mainnet | July 30, 2026 16:00 CEST (14:00 UTC) | `1785420000` | #### TIPs included with T8 The T8 network upgrade includes: 1. **[TIP-1042 FeeAMM TIP-403 Policy Exemptions](https://tips.sh/1042)**: Fee collection checks the fee payer as an authorized sender without requiring the FeeManager to be an authorized recipient. Public FeeAMM operations retain policy enforcement, with explicit authorization requirements for liquidity providers and recipients on `mint` and `burn`. Existing pools are grandfathered and are not revalidated or migrated at activation. 2. **[TIP-1062 Versioned DEX Order Storage](https://tips.sh/1062)**: Introduces version-dispatched StablecoinDEX order storage. Legacy version-0 orders remain readable, while T8 writes compact version-1 records that reduce an order from six storage slots to four. Post-T8 flip-order rewrites migrate the rewritten record through the versioned storage path. 3. **[TIP-1070 Current Committee State](https://tips.sh/1070)**: Activates the `CurrentCommittee` precompile at `0xC077E00000000000000000000000000000000000`. An epoch-boundary system call persists the committee selected by the finalized DKG outcome, including the fallback outcome when DKG does not complete, so contracts can read the committee that is actually effective for consensus. 4. **[TIP-1075 Deprecate TIP-20 Rewards](https://tips.sh/1075)**: Completes the two-stage reward shutdown begun at T7. After T8, transfers, mints, burns, fee refunds, and other balance-changing paths stop checkpointing reward accumulators. Rewards settled before T8 remain claimable indefinitely; lazy rewards that were not checkpointed before activation are forfeited. 5. **[TIP-1087 V2 DEX Order Storage with Book Indexes](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1087.md)**: Adds version-2 orders that replace the repeated 32-byte book key with a compact index, reducing indexed orders from four slots to three. New orderbooks persist their index automatically. Pre-existing orderbooks remain functional with version-1 writes until their index is supplied by off-chain migration tooling through the new `setBookIndex` API. #### Breaking Changes * **Consensus-breaking upgrade**: T8 changes protocol state-transition rules. Nodes that do not run this release before activation will reject or produce invalid blocks after the T8 timestamp. * **TIP-20 pending reward behavior changes**: T8 disables reward-accumulator updates on ordinary balance changes. Only reward balances settled before activation remain claimable; integrations must not rely on a post-T8 transfer to checkpoint older lazy accruals. * **StablecoinDEX now contains mixed order layouts**: Readers and writers must use the versioned order-storage abstraction. Direct assumptions about the legacy six-slot order layout are invalid for T8-created orders, and unknown versions must be rejected. * **DEX index migration is off-chain assisted**: Existing orderbooks are not scanned or migrated automatically. They continue writing version-1 orders until `setBookIndex(uint32)` records a verified index; tooling may also use `bookIndexForKey(bytes32)` and `bookKeyForIndex(uint32)`. * **FeeAMM policy authorization changes**: Fee collection no longer checks FeeManager recipient authorization, while T8 liquidity `mint` and `burn` paths enforce the TIP-1042 participant checks. Existing pools are not revalidated. * **New reserved precompile surface**: The `CurrentCommittee` precompile becomes active at `0xC077E00000000000000000000000000000000000`. Tooling and contracts must treat this address as reserved protocol space. * **Consensus snapshot archive layout changed**: Snapshot manifests are written at the archive root instead of under `consensus/`. Snapshot producers and consumers that depend on the old prefix must be updated. *** ### Operators #### What's Changed * **T8 activation schedule** ([#6866](https://github.com/tempoxyz/tempo/pull/6866), [#6888](https://github.com/tempoxyz/tempo/pull/6888)): Adds the T8 chainspec configuration and schedules activation for July 27 on testnet and July 30 on mainnet. * **Consensus-enabled snapshot startup** ([#6536](https://github.com/tempoxyz/tempo/pull/6536), [#6714](https://github.com/tempoxyz/tempo/pull/6714), [#6796](https://github.com/tempoxyz/tempo/pull/6796)): When a snapshot includes consensus certificates, starts fresh nodes from its smallest certified height; otherwise the existing execution-finalized fallback remains in place. This also relaxes archive contents to hash/size and safe-unpack constraints, moves the manifest to the archive root, adds certification-anchor recovery scenarios, and passes `--force` through to the wrapped Reth download command. * **CL-to-EL backfill and startup floors** ([#6517](https://github.com/tempoxyz/tempo/pull/6517), [#6723](https://github.com/tempoxyz/tempo/pull/6723), [#6872](https://github.com/tempoxyz/tempo/pull/6872)): Simplifies the DKG/executor startup path, backfills CL blocks to the finalized floor before the main executor loop, treats an unfillable archive backfill as a hard error, and preserves a nonzero marshal startup floor across restarts. * **Execution/consensus reconciliation** ([#6790](https://github.com/tempoxyz/tempo/pull/6790), [#6801](https://github.com/tempoxyz/tempo/pull/6801)): Makes hybrid gap tracking aware of the Reth finalized watermark and permits EL to be ahead of CL when the overlapping finalized block hashes agree, avoiding unnecessary rewinds while still rejecting inconsistent state. * **Finalized archive observability** ([#6775](https://github.com/tempoxyz/tempo/pull/6775)): Reports the contents of the finalized block archive to make snapshot and recovery state easier to diagnose. * **DKG correctness and diagnostics** ([#6809](https://github.com/tempoxyz/tempo/pull/6809), [#6834](https://github.com/tempoxyz/tempo/pull/6834)): Corrects invalid dealer-log diagnostics and preserves first-write-wins finalized dealer logs while filling incomplete caches, keeping validators on the same DKG input. * **Consensus proposal metrics** ([#6831](https://github.com/tempoxyz/tempo/pull/6831)): Counts canonical self-proposals even when EL is already at or ahead of the CL finalized block. * **Regenesis validator-state safety** ([#6599](https://github.com/tempoxyz/tempo/pull/6599), [#6745](https://github.com/tempoxyz/tempo/pull/6745), [#6820](https://github.com/tempoxyz/tempo/pull/6820)): Synchronizes `ValidatorConfigV2` during regenesis, fully replaces its block-0 current and historical state when validator counts change, streams large changesets to avoid OOMs, and fixes a process-seed-dependent static-file provider self-deadlock. * **Follower reliability** ([#6392](https://github.com/tempoxyz/tempo/pull/6392), [#6754](https://github.com/tempoxyz/tempo/pull/6754), [#6783](https://github.com/tempoxyz/tempo/pull/6783)): Enforces websocket upstreams, adds keepalive pings and reconnect behavior, and uses a follower-specific executor that verifies finalizations, advances the marshal, and maintains a one-epoch sync window. * **Consensus operations and tooling** ([#6229](https://github.com/tempoxyz/tempo/pull/6229), [#6545](https://github.com/tempoxyz/tempo/pull/6545), [#6633](https://github.com/tempoxyz/tempo/pull/6633), [#6678](https://github.com/tempoxyz/tempo/pull/6678)): Lowers noisy cut-short event logs, defines a three-epoch `--minimal` sync window, moves identity transitions from node RPC to an xtask, and exercises strict-startup behavior in end-to-end testing. * **Hardware telemetry** ([#6814](https://github.com/tempoxyz/tempo/pull/6814)): Exposes a static anonymized hardware metric covering CPU, core count, memory, and filesystem types while omitting disk names and mount sources. * **Reth and Alloy updates** ([#6797](https://github.com/tempoxyz/tempo/pull/6797), [#6878](https://github.com/tempoxyz/tempo/pull/6878)): Updates the pinned Reth revision and Alloy core dependencies used by the node. *** ### Developers #### SDK Crate Versions | Package | Version | Notes | |---------|---------|-------| | `tempo-alloy` | `1.10.1` | Published since the previous binary release. | | `tempo-primitives` | `1.10.1` | Published since the previous binary release. | | `tempo-contracts` | `1.10.1` | Includes T8 ABI/address surfaces. | | `tempo-chainspec` | `1.10.1` | Includes the standalone hardfork dependency. | | `tempo-hardfork` | `1.10.1` | New crate for Tempo hardfork identifiers and activation schedules. | #### T8 Protocol Changes * **FeeAMM TIP-403 policy exemptions** ([#6604](https://github.com/tempoxyz/tempo/pull/6604), [#6605](https://github.com/tempoxyz/tempo/pull/6605), [#6808](https://github.com/tempoxyz/tempo/pull/6808)): Implements TIP-1042's T8 fee-collection exemption, adds the liquidity lifecycle authorization gates, updates txpool policy invalidation to stop tracking the exempt FeeManager recipient side, and documents that existing pools are grandfathered. * **Versioned StablecoinDEX order storage** ([#4075](https://github.com/tempoxyz/tempo/pull/4075), [#6546](https://github.com/tempoxyz/tempo/pull/6546)): Adds version detection and dispatch for legacy and compact order layouts, routes reads and mutations through the storage abstraction, and safely rewrites filled flip orders into the active T8 format. * **V2 indexed DEX orders** ([#6682](https://github.com/tempoxyz/tempo/pull/6682), [#6691](https://github.com/tempoxyz/tempo/pull/6691), [#6767](https://github.com/tempoxyz/tempo/pull/6767)): Adds compact `bookIndex` storage, the orderbook index migration/read ABI, and mixed V0/V1/V2 linked-list handling. Unindexed legacy books deliberately fall back to V1. * **Current committee persistence** ([#5215](https://github.com/tempoxyz/tempo/pull/5215), [#6209](https://github.com/tempoxyz/tempo/pull/6209), [#6819](https://github.com/tempoxyz/tempo/pull/6819)): Adds the `ICurrentCommittee` ABI and T8 precompile, persists the DKG outcome at epoch boundaries, and disables TIP-1060 accounting for system-only committee writes so committee shrink/regrowth cannot leave storage credits behind. * **TIP-20 rewards final shutdown** ([#5433](https://github.com/tempoxyz/tempo/pull/5433)): Stops reward hooks in ordinary balance-changing paths at T8 while preserving claims for settled balances and leaving legacy reward storage intact. * **T8 invariant and integration coverage** ([#6804](https://github.com/tempoxyz/tempo/pull/6804), [#6805](https://github.com/tempoxyz/tempo/pull/6805)): Updates FeeAMM invariants for T8 and adds storage-credit invariants around the new system behavior. #### EVM, Transactions, RPC, and Performance * **Protocol fee extensibility** ([#6879](https://github.com/tempoxyz/tempo/pull/6879)): Adds context-aware protocol-fee hooks so downstream EVMs can install a custom `StorageCtx` without duplicating Tempo's handler logic. * **FeeAMM error diagnostics** ([#6698](https://github.com/tempoxyz/tempo/pull/6698)): Reports the user-token to validator-token pair when a fee swap fails for insufficient liquidity, with a generic fallback when the pair cannot be resolved. * **Account-abstraction RPC and pool fixes** ([#6859](https://github.com/tempoxyz/tempo/pull/6859), [#6871](https://github.com/tempoxyz/tempo/pull/6871)): Shares AA request detection between transaction construction and simulation so gas estimation uses the correct intrinsic cost, and removes stale ordering keys during live two-dimensional-nonce replacements. * **Precompile downstream integration** ([#6650](https://github.com/tempoxyz/tempo/pull/6650), [#6845](https://github.com/tempoxyz/tempo/pull/6845), [#6869](https://github.com/tempoxyz/tempo/pull/6869)): Exposes dispatch helpers, supports custom downstream dispatch errors, and makes the generated inner helpers reusable by downstream precompiles. * **Speculative parallel payload building** ([#6238](https://github.com/tempoxyz/tempo/pull/6238), [#6641](https://github.com/tempoxyz/tempo/pull/6641), [#6736](https://github.com/tempoxyz/tempo/pull/6736)): Adds default-off speculative prewarming and storage-action replay for payment transactions, ensures payload construction always performs an initial build, and keeps the experimental builder flag out of normal help output. * **Storage-action replay and DEX hot paths** ([#6502](https://github.com/tempoxyz/tempo/pull/6502), [#6645](https://github.com/tempoxyz/tempo/pull/6645), [#6649](https://github.com/tempoxyz/tempo/pull/6649), [#6662](https://github.com/tempoxyz/tempo/pull/6662), [#6666](https://github.com/tempoxyz/tempo/pull/6666), [#6670](https://github.com/tempoxyz/tempo/pull/6670), [#6768](https://github.com/tempoxyz/tempo/pull/6768)): Expands replayable storage actions, validates cached reads and writes without recording replay writes again, adds FeeAMM swap replay, and removes redundant DEX deletes and maker reads. #### Testing, Benchmarks, and Tooling * Added certification-anchor snapshot scenarios and strict-startup end-to-end coverage ([#6796](https://github.com/tempoxyz/tempo/pull/6796), [#6678](https://github.com/tempoxyz/tempo/pull/6678)). * Added multi-region benchmark workflows, scheduled runs, telemetry wiring, and neobank/parameterized transaction presets ([#6726](https://github.com/tempoxyz/tempo/pull/6726), [#6821](https://github.com/tempoxyz/tempo/pull/6821), [#6839](https://github.com/tempoxyz/tempo/pull/6839), [#6740](https://github.com/tempoxyz/tempo/pull/6740), [#6595](https://github.com/tempoxyz/tempo/pull/6595)). * Added StablecoinDEX microbenchmarks and storage-action snapshots for two-dimensional nonces and FeeAMM flows ([#6618](https://github.com/tempoxyz/tempo/pull/6618), [#6627](https://github.com/tempoxyz/tempo/pull/6627), [#6497](https://github.com/tempoxyz/tempo/pull/6497)). **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.10.1...v1.11.0 ## v1.10.2 — Release v1.10.2 (2026-07-17) This patch release updates the v1.10.x release line with alloy dependency updates. ### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Operators running v1.10.x should upgrade to v1.10.2. ## v1.10.1 — Release v1.10.1 (2026-06-29) > \[!IMPORTANT] > **This release is required for the T7 network upgrade scheduled for testnet on July 2, 2026 16:00 CEST (`1783000800`) and mainnet on July 9, 2026 16:00 CEST (`1783605600`).** Node operators must update before activation or their nodes will fall out of sync. This release activates T7 support for dynamic base fees, storage credits, and reusable storage accounting across contract, DEX, and channel-reserve state, with the goal of lowering ordinary Tempo transaction costs and making repeated storage use cheaper. > \[!NOTE] > **v1.10.1 supersedes v1.10.0.** The v1.10.0 release was published without release assets after the release workflow failed the Cargo workspace-version check. v1.10.1 uses the same T7 release line with the Cargo workspace version corrected and includes the generated release assets. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes that are not updated will fall out of sync at the T7 activation timestamp. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | July 2, 2026 16:00 CEST | `1783000800` | | Mainnet | July 9, 2026 16:00 CEST | `1783605600` | #### TIPs included with T7 The T7 network upgrade includes: 1. **[TIP-1060 Storage Credits](https://tips.sh/1060)**: Replaces one-time storage-clearing refunds with per-account storage credits that can offset later storage creations by the same account. Adds the `StorageCredits` precompile at `0x1060000000000000000000000000000000000000`. 2. **[TIP-1064 StablecoinDEX Order Storage Credits](https://tips.sh/1064)**: Adds maker-attributed reusable storage accounting for StablecoinDEX order records, so makers receive credit for reusable order storage they previously freed. 3. **[TIP-1067 Dynamic Base Fee](https://tips.sh/1067)**: Replaces the fixed base fee with a bounded EIP-1559-style controller capped at `12_000_000_000` attodollars per gas, floored at `600_000_000`, with a `10_000_000` gas target. 4. **TIP-1066 Channel Storage Credits**: Adds payer-scoped reusable storage credits for TIP-20 channel reserve state, including the `storageCredits(address)` channel-reserve view. #### Gas Cost and Benchmarking Highlights | Area | T6 | T7 | Diff | Notes | |------|----|----|------|-------| | Base-fee ceiling (50k-gas transfer) | `$0.001` | cap `$0.0006` · floor `$0.00003` | cap `−40%` · floor `−97%` | The T7 cap is `12_000_000_000` attodollars per gas; the floor is `600_000_000`, 20x below the cap. | | Observed transfer-like costs | avg `$0.0037857` · median `$0.0011855` (1k txs); avg `$0.0008975` · median `$0.0007657` (598 steady-state) | — | — | Removing state-creation-like txs (`gas_used >= 250k`) left 598 steady-state transfers; excluding two IQR outliers gives `$0.0008929` avg. T7's lower base-fee cap/floor reduces the per-gas component further for equivalent gas profiles. | | Credited storage creation (SSTORE `0→x`) | `250,000` gas | `5,000` residual + `245,000` creditable | `−98%` | TIP-1060 splits the previous creation component into a `5,000` gas residual plus a `245,000` gas creditable portion. | | Channel reserve gas | open-existing `1,055,229` · open-first `1,302,429` | open-existing `294,425` · open-first `791,625` | open-existing `−72.1%` · open-first `−39.2%` | Call-level numbers exclude separate approval gas; implicit approvals for MPP/DEX/FeeAMM improve user-total comparisons for flows that previously required explicit approvals. | #### Breaking Changes * **Consensus-breaking upgrade**: T7 changes protocol state transition rules. Nodes that do not run this release before activation will reject or produce invalid blocks after the T7 timestamp. * **Base fee is no longer fixed**: After T7, `baseFeePerGas` is computed from parent gas usage and clamped to the T7 floor/cap range. Wallets, fee estimators, transaction builders, tests, and monitoring that assume the T1 fixed base fee of `20_000_000_000` attodollars per gas must handle a changing base fee. * **Storage gas and refund semantics changed**: T7 removes the legacy storage-clearing refund, splits the TIP-1000 SSTORE creation cost into a `5_000` gas residual plus a `245_000` gas creditable portion, and removes the EIP-3529 one-fifth refund cap for T7 refunds. Contracts and tests that depend on exact SSTORE gas/refund behavior should be revalidated. * **TIP-20 reward mutators are disabled at T7**: `setRewardRecipient` and `distributeReward` become no-ops after T7. Integrators relying on new reward opt-ins or reward distributions must adjust their flows before activation. * **New reserved precompile surface**: The `StorageCredits` system precompile becomes active at T7. Contracts and tooling should treat `0x1060000000000000000000000000000000000000` as reserved protocol space. * **RPC, standby, follow nodes require certificates**: with the `tempo node --follow` flag nodes are now verifying finalization certificates before executing blocks against their local state. When tracking the official mainnet or testnet default RPCs operators will not see a change. If tracking their local validator, operators need to ensure that validator exposes websocket via `--ws`. For more info, see https://tempo.xyz/developers/docs/guide/node/rpc/ and https://tempo.xyz/developers/docs/guide/node/validator-failover/ *** ### Operators #### What's Changed * **T7 activation timestamps** ([#6396](https://github.com/tempoxyz/tempo/pull/6396)): Adds testnet and mainnet T7 timestamps to chainspec and genesis configuration. * **Dynamic base fee** ([#5153](https://github.com/tempoxyz/tempo/pull/5153)): Seeds the T7 activation block at the lowered cap of `12_000_000_000` attodollars per gas, then adjusts each child block from parent gas usage with a floor of `600_000_000` and a `10_000_000` gas target. * **Snapshot and recovery improvements** ([#5789](https://github.com/tempoxyz/tempo/pull/5789), [#6203](https://github.com/tempoxyz/tempo/pull/6203), [#6207](https://github.com/tempoxyz/tempo/pull/6207), [#6208](https://github.com/tempoxyz/tempo/pull/6208)): Bundles consensus finalized-block archives with execution snapshots and preserves recovered block handles through consensus lookup paths. * **Consensus and payload performance** ([#5421](https://github.com/tempoxyz/tempo/pull/5421), [#5998](https://github.com/tempoxyz/tempo/pull/5998), [#6176](https://github.com/tempoxyz/tempo/pull/6176)): Caches encoded execution blocks, reduces payload transaction iterator cloning, and avoids cloning subblock pool transactions on the RPC path. * **Payload builder configuration** ([#6387](https://github.com/tempoxyz/tempo/pull/6387), [#6306](https://github.com/tempoxyz/tempo/pull/6306)): Resolves builder gas limits from CLI arguments and chain defaults, and threads the skip-state-root benchmark setting through the Tempo payload builder. * **Installer reliability** ([#6274](https://github.com/tempoxyz/tempo/pull/6274)): Installs the macOS `libusb` runtime dependency when required, verifies the installed binary before replacing backups, and adds installer regression tests. * **Reth and dependency updates** ([#5876](https://github.com/tempoxyz/tempo/pull/5876), [#6134](https://github.com/tempoxyz/tempo/pull/6134), [#6211](https://github.com/tempoxyz/tempo/pull/6211), [#6270](https://github.com/tempoxyz/tempo/pull/6270)): Updates Reth from upstream main and refreshes selected runtime dependencies. *** ### Developers #### SDK Crate Versions | Package | Version | Notes | |---------|---------|-------| | `tempo-alloy` | `1.8.1` | Unchanged in this binary patch release; SDK crate version bumps will follow separately. | | `tempo-primitives` | `1.8.1` | Unchanged in this binary patch release; SDK crate version bumps will follow separately. | | `tempo-contracts` | `1.8.1` | Unchanged in this binary patch release; SDK crate version bumps will follow separately. | | `tempo-chainspec` | `1.8.2` | Unchanged in this binary patch release; SDK crate version bumps will follow separately. | #### T7 Protocol Changes * **Storage credits precompile** ([#5228](https://github.com/tempoxyz/tempo/pull/5228), [#4016](https://github.com/tempoxyz/tempo/pull/4016)): Adds per-account persistent storage credit balances and transaction-local `Refund`, `Preserve`, and `Direct` creation modes. Refund mode remains the default so gas limits do not depend on credit balance at inclusion time. * **Storage-credit safety fixes** ([#6206](https://github.com/tempoxyz/tempo/pull/6206), [#6233](https://github.com/tempoxyz/tempo/pull/6233), [#6237](https://github.com/tempoxyz/tempo/pull/6237), [#6310](https://github.com/tempoxyz/tempo/pull/6310)): Excludes fee/keychain bookkeeping slots from unbacked credit minting, avoids recreating exhausted periodic spending-limit slots, disables minting during fee distribution, and decodes the T7 zero-remaining sentinel consistently. * **StablecoinDEX storage credits** ([#5305](https://github.com/tempoxyz/tempo/pull/5305), [#4082](https://github.com/tempoxyz/tempo/pull/4082)): Credits reusable order-record storage to the maker that owned the cleared order slot and exposes `storageCredits(address)` for DEX credit balances. * **TIP-20 channel reserve storage credits** ([#5935](https://github.com/tempoxyz/tempo/pull/5935)): Credits terminal channel close/withdraw storage deletion to the channel payer and consumes payer-scoped credits on later channel opens. * **TIP-20 rewards deprecation path** ([#5433](https://github.com/tempoxyz/tempo/pull/5433)): T7 disables new reward-recipient changes and reward distributions while preserving existing reward state for lazy checkpointing ahead of the later full-disable phase. * **Dynamic base fee constants and validation** ([#5153](https://github.com/tempoxyz/tempo/pull/5153)): Adds T7 base-fee floor/cap constants, fixed `10_000_000` gas target, and block-header validation for the computed dynamic base fee. #### EVM, Transaction, and Fee Semantics * **Generic protocol fee manager** ([#6279](https://github.com/tempoxyz/tempo/pull/6279)): Routes fee token resolution and pre/post-transaction fee collection through a generic `TempoFeeManager`, preserving current L1 behavior while allowing alternate fee managers for Tempo Zones. * **Storage action recording** ([#5423](https://github.com/tempoxyz/tempo/pull/5423), [#5605](https://github.com/tempoxyz/tempo/pull/5605), [#6075](https://github.com/tempoxyz/tempo/pull/6075)): Adds SLOAD/SSTORE action recording and storage delta helpers while avoiding default allocations when recording is disabled. * **Transaction pool and raw transaction recovery** ([#6175](https://github.com/tempoxyz/tempo/pull/6175)): Optimizes raw Tempo AA transaction recovery by threading the precomputed expiring nonce hash through sender recovery. * **Precompile storage and block environment plumbing** ([#6286](https://github.com/tempoxyz/tempo/pull/6286), [#6394](https://github.com/tempoxyz/tempo/pull/6394)): Exposes epoch-length mapping and the full `TempoBlockEnv` to precompile storage providers. * **Serde and RPC shape fixes** ([#6252](https://github.com/tempoxyz/tempo/pull/6252)): Serializes sealed blocks through the plain Tempo block shape for consensus RPC and execution data. #### Testing, Benchmarks, and Tooling * Added T7 gas-estimation and hardfork matrix coverage for upcoming devnet hardforks ([#6194](https://github.com/tempoxyz/tempo/pull/6194), [#6217](https://github.com/tempoxyz/tempo/pull/6217)). * Added storage-credit integration and gas snapshot coverage for StablecoinDEX and TIP-20 channel reserve flows ([#6297](https://github.com/tempoxyz/tempo/pull/6297), [#5935](https://github.com/tempoxyz/tempo/pull/5935), [#4082](https://github.com/tempoxyz/tempo/pull/4082)). * Improved bench-e2e presets, comments, gas-limit controls, metadata, and T7/T8 hardfork support ([#5959](https://github.com/tempoxyz/tempo/pull/5959), [#6232](https://github.com/tempoxyz/tempo/pull/6232), [#6261](https://github.com/tempoxyz/tempo/pull/6261), [#6283](https://github.com/tempoxyz/tempo/pull/6283), [#6391](https://github.com/tempoxyz/tempo/pull/6391), [#6393](https://github.com/tempoxyz/tempo/pull/6393), [#6395](https://github.com/tempoxyz/tempo/pull/6395)). **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.9.1...v1.10.1 ## v1.10.0 — v1.10.0 (superseded by v1.10.1) (2026-06-29) ### Release v1.10.0 > \[!WARNING] > **v1.10.0 is superseded by v1.10.1.** The v1.10.0 release was published without release assets after the release workflow failed the Cargo workspace-version check. Use [v1.10.1](https://github.com/tempoxyz/tempo/releases/tag/v1.10.1) for the T7 release artifacts. > \[!IMPORTANT] > **This release is required for the T7 network upgrade scheduled for testnet on July 2, 2026 16:00 CEST (`1783000800`) and mainnet on July 9, 2026 16:00 CEST (`1783605600`).** Node operators must update before activation or their nodes will fall out of sync. This release activates T7 support for dynamic base fees, storage credits, and reusable storage accounting across contract, DEX, and channel-reserve state, with the goal of lowering ordinary Tempo transaction costs and making repeated storage use cheaper. ##### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes that are not updated will fall out of sync at the T7 activation timestamp. ##### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | July 2, 2026 16:00 CEST | `1783000800` | | Mainnet | July 9, 2026 16:00 CEST | `1783605600` | ##### TIPs included with T7 The T7 network upgrade includes: 1. **[TIP-1060 Storage Credits](https://tips.sh/1060)**: Replaces one-time storage-clearing refunds with per-account storage credits that can offset later storage creations by the same account. Adds the `StorageCredits` precompile at `0x1060000000000000000000000000000000000000`. 2. **[TIP-1064 StablecoinDEX Order Storage Credits](https://tips.sh/1064)**: Adds maker-attributed reusable storage accounting for StablecoinDEX order records, so makers receive credit for reusable order storage they previously freed. 3. **[TIP-1067 Dynamic Base Fee](https://tips.sh/1067)**: Replaces the fixed base fee with a bounded EIP-1559-style controller capped at `12_000_000_000` attodollars per gas, floored at `600_000_000`, with a `10_000_000` gas target. 4. **TIP-1066 Channel Storage Credits**: Adds payer-scoped reusable storage credits for TIP-20 channel reserve state, including the `storageCredits(address)` channel-reserve view. ##### Gas Cost and Benchmarking Highlights | Area | T6 | T7 | Diff | Notes | |------|----|----|------|-------| | Base-fee ceiling (50k-gas transfer) | `$0.001` | cap `$0.0006` · floor `$0.00003` | cap `−40%` · floor `−97%` | The T7 cap is `12_000_000_000` attodollars per gas; the floor is `600_000_000`, 20x below the cap. | | Observed transfer-like costs | avg `$0.0037857` · median `$0.0011855` (1k txs); avg `$0.0008975` · median `$0.0007657` (598 steady-state) | — | — | Removing state-creation-like txs (`gas_used >= 250k`) left 598 steady-state transfers; excluding two IQR outliers gives `$0.0008929` avg. T7's lower base-fee cap/floor reduces the per-gas component further for equivalent gas profiles. | | Credited storage creation (SSTORE `0→x`) | `250,000` gas | `5,000` residual + `245,000` creditable | `−98%` | TIP-1060 splits the previous creation component into a `5,000` gas residual plus a `245,000` gas creditable portion. | | Channel reserve gas | open-existing `1,055,229` · open-first `1,302,429` | open-existing `294,425` · open-first `791,625` | open-existing `−72.1%` · open-first `−39.2%` | Call-level numbers exclude separate approval gas; implicit approvals for MPP/DEX/FeeAMM improve user-total comparisons for flows that previously required explicit approvals. | ##### Breaking Changes * **Consensus-breaking hardfork**: T7 changes protocol state transition rules. Nodes that do not run this release before activation will reject or produce invalid blocks after the T7 timestamp. * **Base fee is no longer fixed**: After T7, `baseFeePerGas` is computed from parent gas usage and clamped to the T7 floor/cap range. Wallets, fee estimators, transaction builders, tests, and monitoring that assume the T1 fixed base fee of `20_000_000_000` attodollars per gas must handle a changing base fee. * **Storage gas and refund semantics changed**: T7 removes the legacy storage-clearing refund, splits the TIP-1000 SSTORE creation cost into a `5_000` gas residual plus a `245_000` gas creditable portion, and removes the EIP-3529 one-fifth refund cap for T7 refunds. Contracts and tests that depend on exact SSTORE gas/refund behavior should be revalidated. * **TIP-20 reward mutators are disabled at T7**: `setRewardRecipient` and `distributeReward` become no-ops after T7. Integrators relying on new reward opt-ins or reward distributions must adjust their flows before activation. * **New reserved precompile surface**: The `StorageCredits` system precompile becomes active at T7. Contracts and tooling should treat `0x1060000000000000000000000000000000000000` as reserved protocol space. * **RPC, standby, follow nodes require certificates**: with the `tempo node --follow` flag nodes are now verifying finalization certificates before executing blocks against their local state. When tracking the official mainnet or testnet default RPCs operators will not see a change. If tracking their local validator, operators need to ensure that validator exposes websocket via `--ws`. For more info, see https://tempo.xyz/developers/docs/guide/node/rpc/ and https://tempo.xyz/developers/docs/guide/node/validator-failover/ *** #### Operators ##### What's Changed * **T7 activation timestamps** ([#6396](https://github.com/tempoxyz/tempo/pull/6396)): Adds testnet and mainnet T7 timestamps to chainspec and genesis configuration. * **Dynamic base fee** ([#5153](https://github.com/tempoxyz/tempo/pull/5153)): Seeds the T7 activation block at the lowered cap of `12_000_000_000` attodollars per gas, then adjusts each child block from parent gas usage with a floor of `600_000_000` and a `10_000_000` gas target. * **Snapshot and recovery improvements** ([#5789](https://github.com/tempoxyz/tempo/pull/5789), [#6203](https://github.com/tempoxyz/tempo/pull/6203), [#6207](https://github.com/tempoxyz/tempo/pull/6207), [#6208](https://github.com/tempoxyz/tempo/pull/6208)): Bundles consensus finalized-block archives with execution snapshots and preserves recovered block handles through consensus lookup paths. * **Consensus and payload performance** ([#5421](https://github.com/tempoxyz/tempo/pull/5421), [#5998](https://github.com/tempoxyz/tempo/pull/5998), [#6176](https://github.com/tempoxyz/tempo/pull/6176)): Caches encoded execution blocks, reduces payload transaction iterator cloning, and avoids cloning subblock pool transactions on the RPC path. * **Payload builder configuration** ([#6387](https://github.com/tempoxyz/tempo/pull/6387), [#6306](https://github.com/tempoxyz/tempo/pull/6306)): Resolves builder gas limits from CLI arguments and chain defaults, and threads the skip-state-root benchmark setting through the Tempo payload builder. * **Installer reliability** ([#6274](https://github.com/tempoxyz/tempo/pull/6274)): Installs the macOS `libusb` runtime dependency when required, verifies the installed binary before replacing backups, and adds installer regression tests. * **Reth and dependency updates** ([#5876](https://github.com/tempoxyz/tempo/pull/5876), [#6134](https://github.com/tempoxyz/tempo/pull/6134), [#6211](https://github.com/tempoxyz/tempo/pull/6211), [#6270](https://github.com/tempoxyz/tempo/pull/6270)): Updates Reth from upstream main and refreshes selected runtime dependencies. *** #### Developers ##### Compatible Tooling Versions | Package | Version | Notes | |---------|---------|-------| | `tempo-alloy` | `1.10.0` | Includes SDK helpers for receive-policy/admin-key flows and T7-compatible ABI bindings. | | `tempo-primitives` | `1.10.0` | Includes T7 hardfork, block, and base-fee primitives. | | `tempo-contracts` | `1.10.0` | Includes `StorageCredits`, StablecoinDEX, and channel-reserve ABI updates. | | `tempo-chainspec` | `1.10.0` | Includes T7 activation timestamps and dynamic base-fee constants. | ##### T7 Protocol Changes * **Storage credits precompile** ([#5228](https://github.com/tempoxyz/tempo/pull/5228), [#4016](https://github.com/tempoxyz/tempo/pull/4016)): Adds per-account persistent storage credit balances and transaction-local `Refund`, `Preserve`, and `Direct` creation modes. Refund mode remains the default so gas limits do not depend on credit balance at inclusion time. * **Storage-credit safety fixes** ([#6206](https://github.com/tempoxyz/tempo/pull/6206), [#6233](https://github.com/tempoxyz/tempo/pull/6233), [#6237](https://github.com/tempoxyz/tempo/pull/6237), [#6310](https://github.com/tempoxyz/tempo/pull/6310)): Excludes fee/keychain bookkeeping slots from unbacked credit minting, avoids recreating exhausted periodic spending-limit slots, disables minting during fee distribution, and decodes the T7 zero-remaining sentinel consistently. * **StablecoinDEX storage credits** ([#5305](https://github.com/tempoxyz/tempo/pull/5305), [#4082](https://github.com/tempoxyz/tempo/pull/4082)): Credits reusable order-record storage to the maker that owned the cleared order slot and exposes `storageCredits(address)` for DEX credit balances. * **TIP-20 channel reserve storage credits** ([#5935](https://github.com/tempoxyz/tempo/pull/5935)): Credits terminal channel close/withdraw storage deletion to the channel payer and consumes payer-scoped credits on later channel opens. * **TIP-20 rewards deprecation path** ([#5433](https://github.com/tempoxyz/tempo/pull/5433)): T7 disables new reward-recipient changes and reward distributions while preserving existing reward state for lazy checkpointing ahead of the later full-disable phase. * **Dynamic base fee constants and validation** ([#5153](https://github.com/tempoxyz/tempo/pull/5153)): Adds T7 base-fee floor/cap constants, fixed `10_000_000` gas target, and block-header validation for the computed dynamic base fee. ##### EVM, Transaction, and Fee Semantics * **Generic protocol fee manager** ([#6279](https://github.com/tempoxyz/tempo/pull/6279)): Routes fee token resolution and pre/post-transaction fee collection through a generic `TempoFeeManager`, preserving current L1 behavior while allowing alternate fee managers for Tempo Zones. * **Storage action recording** ([#5423](https://github.com/tempoxyz/tempo/pull/5423), [#5605](https://github.com/tempoxyz/tempo/pull/5605), [#6075](https://github.com/tempoxyz/tempo/pull/6075)): Adds SLOAD/SSTORE action recording and storage delta helpers while avoiding default allocations when recording is disabled. * **Transaction pool and raw transaction recovery** ([#6175](https://github.com/tempoxyz/tempo/pull/6175)): Optimizes raw Tempo AA transaction recovery by threading the precomputed expiring nonce hash through sender recovery. * **Precompile storage and block environment plumbing** ([#6286](https://github.com/tempoxyz/tempo/pull/6286), [#6394](https://github.com/tempoxyz/tempo/pull/6394)): Exposes epoch-length mapping and the full `TempoBlockEnv` to precompile storage providers. * **Serde and RPC shape fixes** ([#6252](https://github.com/tempoxyz/tempo/pull/6252)): Serializes sealed blocks through the plain Tempo block shape for consensus RPC and execution data. ##### Testing, Benchmarks, and Tooling * Added T7 gas-estimation and hardfork matrix coverage for upcoming devnet hardforks ([#6194](https://github.com/tempoxyz/tempo/pull/6194), [#6217](https://github.com/tempoxyz/tempo/pull/6217)). * Added storage-credit integration and gas snapshot coverage for StablecoinDEX and TIP-20 channel reserve flows ([#6297](https://github.com/tempoxyz/tempo/pull/6297), [#5935](https://github.com/tempoxyz/tempo/pull/5935), [#4082](https://github.com/tempoxyz/tempo/pull/4082)). * Improved bench-e2e presets, comments, gas-limit controls, metadata, and T7/T8 hardfork support ([#5959](https://github.com/tempoxyz/tempo/pull/5959), [#6232](https://github.com/tempoxyz/tempo/pull/6232), [#6261](https://github.com/tempoxyz/tempo/pull/6261), [#6283](https://github.com/tempoxyz/tempo/pull/6283), [#6391](https://github.com/tempoxyz/tempo/pull/6391), [#6393](https://github.com/tempoxyz/tempo/pull/6393), [#6395](https://github.com/tempoxyz/tempo/pull/6395)). **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.9.1...v1.10.0 ## v1.9.1 — Release v1.9.1 (2026-06-19) This patch release updates the v1.9.x release line with follow-mode stability fixes, payload-builder latency fixes, Reth/Rust dependency updates, and transaction-pool/RPC improvements. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Operators running v1.9.0 should upgrade to v1.9.1. ### What's Changed * Fixed follower upstream event handling by making certified follow the default, keeping the deprecated certify flag accepted, and resubscribing after upstream event stream errors or termination ([#5845](https://github.com/tempoxyz/tempo/pull/5845), [#5964](https://github.com/tempoxyz/tempo/pull/5964), [#6051](https://github.com/tempoxyz/tempo/pull/6051)). * Improved payload builder and consensus latency by keeping executor-owned payload jobs running to completion and deferring block/state provider drops off the response path ([#5625](https://github.com/tempoxyz/tempo/pull/5625), [#6012](https://github.com/tempoxyz/tempo/pull/6012), [#6046](https://github.com/tempoxyz/tempo/pull/6046)). * Enabled builder cache sharing by default again, with `--engine.disable-execution-cache-sharing-with-builder` available as the opt-out flag ([#5529](https://github.com/tempoxyz/tempo/pull/5529)). * Fixed snapshot manifest/download logging by initializing tracing before running snapshot commands ([#5982](https://github.com/tempoxyz/tempo/pull/5982)). * Updated Reth/Alloy dependencies, the workspace MSRV and CI toolchains to Rust 1.96, and Rust Docker base images used by release builds ([#5564](https://github.com/tempoxyz/tempo/pull/5564), [#5822](https://github.com/tempoxyz/tempo/pull/5822), [#5876](https://github.com/tempoxyz/tempo/pull/5876), [#6035](https://github.com/tempoxyz/tempo/pull/6035), [#6025](https://github.com/tempoxyz/tempo/pull/6025)). * Improved transaction-pool validation and maintenance performance by sharing EVM/state reads, caching active hardfork state, reducing AA insertion and fee-payer lookups, narrowing TIP-20 transfer decoding, and batching maintenance work ([#5572](https://github.com/tempoxyz/tempo/pull/5572), [#5602](https://github.com/tempoxyz/tempo/pull/5602), [#5603](https://github.com/tempoxyz/tempo/pull/5603), [#5621](https://github.com/tempoxyz/tempo/pull/5621), [#5622](https://github.com/tempoxyz/tempo/pull/5622), [#5645](https://github.com/tempoxyz/tempo/pull/5645), [#5646](https://github.com/tempoxyz/tempo/pull/5646), [#5648](https://github.com/tempoxyz/tempo/pull/5648), [#5649](https://github.com/tempoxyz/tempo/pull/5649)). * Added developer/API improvements for programmatic node overrides, transaction-pool validation hooks, AA signer recovery, and consistent `tempo_simulateV1` metadata block selection ([#5910](https://github.com/tempoxyz/tempo/pull/5910), [#5628](https://github.com/tempoxyz/tempo/pull/5628), [#5664](https://github.com/tempoxyz/tempo/pull/5664), [#5658](https://github.com/tempoxyz/tempo/pull/5658)). * Added future-fork implementation work for dynamic base fees, the `StorageCredits` precompile, and TIP-20 rewards deprecation behind later hardfork activation ([#5153](https://github.com/tempoxyz/tempo/pull/5153), [#5228](https://github.com/tempoxyz/tempo/pull/5228), [#5433](https://github.com/tempoxyz/tempo/pull/5433)). * Bumped workspace and SDK crate versions to v1.9.1 ([#6040](https://github.com/tempoxyz/tempo/pull/6040), [#6089](https://github.com/tempoxyz/tempo/pull/6089)). **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.9.0...v1.9.1 ## v1.9.0 — Release v1.9.0 (2026-06-15) > \[!IMPORTANT] > **This release is required for the T6 network upgrade scheduled for testnet on June 18, 2026 16:00 CEST (`1781791200`) and mainnet on June 23, 2026 16:00 CEST (`1782223200`).** Node operators must update before activation or their nodes will fall out of sync. This release activates T6 support for address-level receive policies and admin access keys, along with performance, stability, and operator tooling improvements. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes that are not updated will fall out of sync at the T6 activation timestamp. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | June 18, 2026 16:00 CEST | `1781791200` | | Mainnet | June 23, 2026 16:00 CEST | `1782223200` | #### TIPs included with T6 The T6 network upgrade includes: 1. **[TIP-1028 Address-Level Receive Policies](https://tips.sh/1028)**: Lets receivers define which TIP-20 tokens and senders they accept, redirecting blocked transfers or mints to `ReceivePolicyGuard` for later recovery instead of reverting. 2. **[TIP-1049 Admin Access Keys](https://tips.sh/1049)**: Adds admin access keys for account key management and extends `SignatureVerifier` with stateful keychain/admin signature verification helpers. *** ### Operators #### What's Changed * **Certified Follow Mode** ([#2551](https://github.com/tempoxyz/tempo/pull/2551)): Followers run a lightweight consensus engine that validates consensus certificates as a prerequisite to progressing execution state. Mainnet & Testnet nodes behind the public RPC will be switched over by setting `--follow.experimental.certify` and serving `consensus_` rpcs. The flag is available and will become the default in the next release. * **Consensus finalized block storage pruning** ([#3870](https://github.com/tempoxyz/tempo/pull/3870), [#5234](https://github.com/tempoxyz/tempo/pull/5234)): The consensus layer now stores blocks as a persisted cache, pruning it as execution layer finalization watermark rises. This will be automatically pruned for new nodes, for operators running existing nodes may delete the legacy archive at `/engine-finalized_blocks-{freezer*,metadata,ordinal}`. Make sure to retain `/engine-finalized-blocks-prunable-{key,value}`! * **Embedded Network Identities** ([#4004](https://github.com/tempoxyz/tempo/pull/4004)): The network keys for Mainnet and Tesnet are embedded in the binaries. Certified followers can jump past several epochs when verifying a certificate against this key instead of progressing boundary to boundary. * **Improved snapshot bootstrapping** ([#4482](https://github.com/tempoxyz/tempo/pull/4482)): Tempo snapshots now include finalization data in the manifest and dump the finalization certificate for startup recovery when the finalizations archive is empty. * **Consensus execution actor no longer blocks on every EL interaction** ([#5312](https://github.com/tempoxyz/tempo/pull/5312)): Queues forkchoice/new-payload work through the executor actor to improve consensus responsiveness. * **Consensus signing key CLI fix** ([#5276](https://github.com/tempoxyz/tempo/pull/5276)): Adds `--secret` support for signing consensus smart contract arguments. *** ### Developers #### Compatible Tooling Versions | Package | Version | Notes | |---------|---------|-------| | `tempo-alloy` | `1.9.0` | Includes T6 admin access key support in SDK transaction builders. | | `tempo-primitives` | `1.9.0` | Includes T6 admin access key primitives and signature encoding improvements. | | `tempo-contracts` | `1.9.0` | Includes T6 AccountKeychain and SignatureVerifier ABI updates. | | `tempo-chainspec` | `1.9.0` | Includes T6 activation timestamps and hardfork configuration. | #### T6 Protocol Changes * **Address-level receive policies** ([#3800](https://github.com/tempoxyz/tempo/pull/3800), [#5476](https://github.com/tempoxyz/tempo/pull/5476)): TIP-1028 extends TIP-403/TIP-20 so receivers can configure accepted tokens and senders. Blocked TIP-20 transfers and mints succeed by redirecting funds to `ReceivePolicyGuard`, where they can later be claimed by the originator or configured recovery authority. Recovery addresses may not be precompile addresses. * **Admin access keys** ([#4265](https://github.com/tempoxyz/tempo/pull/4265)): TIP-1049 adds `authorizeAdminKey(...)`, `isAdminKey(...)`, admin key authorization events, and account-bound admin key permissions. Admin keys can manage other keys but must not carry spending limits, call scopes, or expiry. * **Keychain signature verification** ([#4302](https://github.com/tempoxyz/tempo/pull/4302)): `SignatureVerifier` now exposes stateful keychain verification helpers, including `verifyKeychain(account, hash, signature)` and `verifyKeychainAdmin(account, hash, signature)`, for contracts that need to validate active access-key or root/admin signatures against AccountKeychain state. #### EVM, Transaction, and Fee Semantics * **Per-transaction fee accounting fix** ([#5506](https://github.com/tempoxyz/tempo/pull/5506)): Resets collected-fee state for each transaction so stale fee collection cannot affect same-transaction key authorization/use paths. * **Transaction validation caching** ([#5522](https://github.com/tempoxyz/tempo/pull/5522), [#5518](https://github.com/tempoxyz/tempo/pull/5518), [#5513](https://github.com/tempoxyz/tempo/pull/5513)): Validation now reuses state reads across EVM validation, AMM liquidity planning, and ETH account/code checks, while avoiding unnecessary transaction/env clones and eager batch allocation. * **Signature encoding optimization** ([#5553](https://github.com/tempoxyz/tempo/pull/5553)): Signature RLP length calculation now avoids materializing an intermediate `Bytes` buffer. * **Dynamic base fee support in txpool ordering** ([#5243](https://github.com/tempoxyz/tempo/pull/5243)): Transaction priority is no longer cached under a fixed-base-fee assumption; ordering indices are recomputed or reindexed when the base fee changes. #### Performance * **Builder budget pacing** ([#5211](https://github.com/tempoxyz/tempo/pull/5211)): Payload building now reserves proposal-return time using recent local validation latency feedback instead of always mirroring projected builder work. * **Parallelize builder computations** ([#4473](https://github.com/tempoxyz/tempo/pull/4473)): Refactors block building to offload transaction cloning and computation of transaction/receipt roots to a separate spawned task * **Redundant builder precache disabled** ([#5419](https://github.com/tempoxyz/tempo/pull/5419)): Tempo nodes skip Reth’s basic parent-state pre-cache because the Tempo payload builder already uses the execution cache. * **Execution/storage hot-path work** (#3626, #4455, #5038, #5239, #4480): precomputes TIP-20 keccak slots, improves storage handler caching, removes useless TIP-403 SLOADs, and caches AA replay hash. * **Transaction pool iteration and eviction improvements** ([#5432](https://github.com/tempoxyz/tempo/pull/5432), [#5546](https://github.com/tempoxyz/tempo/pull/5546), [#5585](https://github.com/tempoxyz/tempo/pull/5585), [#5590](https://github.com/tempoxyz/tempo/pull/5590)): Adds best-transaction size hints, moves AA allocation out of write locks, removes eviction entries with lightweight order keys, and defers dropping removed transactions until after block update windows. **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.5.2...v1.9.0 ## v1.8.2 — Release v1.8.2 (2026-06-08) This patch release includes a fix for nodes running versions older than 1.8.2 in a `--minimal` configuration. The issue occurs when requesting historical blocks using the commonware marshal interface. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | ### What’s Changed * chore(1.8.2): avoid panic by mapping expired blocks to None in https://github.com/tempoxyz/tempo/pull/5214 **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.8.1...v1.8.2 ## v1.8.1 — Release v1.8.1 (2026-06-01) > \[!IMPORTANT] > **v1.8.1 is required for the T5 network upgrade scheduled for testnet and mainnet.** #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes running versions older than v1.8.0 will fall out of sync at the T5 activation timestamp. Nodes already running v1.8.0, must update to v1.8.1 for the latest operator defaults. ### Operators #### Performance With v1.8.0 we enabled state caching for block builder by default. Some nodes experienced validation errors caused by reading stale state in our cache sharing implementation. The issue could be resolved with a restart, affected nodes did not have their databases corrupted. This release reverts this performance improvement, we are currently testing a fix which will be published in a follow up release. * **Builder prewarming**: Reverted builder prewarming being enabled by default ([#4710](https://github.com/tempoxyz/tempo/pull/4710)). * **Cache sharing**: Reverted execution cache sharing between payload building and validation ([#4709](https://github.com/tempoxyz/tempo/pull/4709)). *** ### What’s Changed * Reverted builder prewarming being enabled by default ([#4710](https://github.com/tempoxyz/tempo/pull/4710)). * Reverted execution cache sharing for the payload builder ([#4709](https://github.com/tempoxyz/tempo/pull/4709)). * Bumped the workspace version to v1.8.1 ([6ade8f1](https://github.com/tempoxyz/tempo/commit/6ade8f1a5c4fdf963ac8b6fa23551417951ed310)). **Full Changelog**: [v1.8.0...v1.8.1](https://github.com/tempoxyz/tempo/compare/v1.8.0...v1.8.1) ## v1.8.0 — Release v1.8.0 (2026-05-28) > \[!IMPORTANT] > **This release is required for the T5 network upgrade scheduled for testnet and mainnet.** Node operators must update their nodes, otherwise your nodes will fall out of sync with the network. T5 activates enshrined channel reserve for native MPP support (cutting gas by up to 72% vs legacy contract), stricter payment-lane classification, DEX improvements, implicit approvals, and TIP-20 metadata updates. This release also includes performance improvements that combined push throughput to 18K TPS, with an average block time of 500ms. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes that are not updated will fall out of sync at the T5 activation timestamp. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Testnet | June 3rd 2026 16:00 CEST | 1780495200 | | Mainnet | June 9th 2026 16:00 CEST | 1781013600 | #### TIPs included with T5 The T5 network upgrade includes: 1. **[TIP-1034 TIP-20 Channel Reserve Precompile](https://tips.sh/1034)**: Enshrines channel reserve as a native precompile, cutting gas by up to 72% versus the legacy MPP contract. 2. **[TIP-1045 Payment Transaction Classification](https://tips.sh/1045)**: Formalizes payment-lane eligibility with an explicit consensus allow-list. 3. **[TIP-1030 Allow same-tick flip orders](https://tips.sh/1030)**: Allows flip orders to flip at the same tick for tighter two-sided markets. 4. **[TIP-1056 Keep the same order ID when flip orders flip](https://tips.sh/1056)**: Keeps `orderId` stable across flips. 5. **[TIP-1035 Implicit Approval List](https://tips.sh/1035)**: Lets approved protocol precompiles pull TIP-20 funds without prior allowances. 6. **[TIP-1033 Two-Hop FeeAMM Routing](https://tips.sh/1033)**: Adds a two-hop fallback route through a token's quote token. 7. **[TIP-1053 Witnesses in Key Authorizations](https://tips.sh/1053)**: Adds optional witnesses to key authorizations for challenge binding and revocation. 8. **[TIP-1026 Token Logo URI](https://tips.sh/1026)**: Adds optional onchain `logoURI` metadata for TIP-20 tokens. 9. **[TIP-1057 T5 Hardfork Meta TIP](https://tips.sh/1057)**: Bundles T5 storage correctness and hardfork-gated protocol hardening. ### Operators #### Consensus * **Consensus signing keys encrypted at rest** ([#4111](https://github.com/tempoxyz/tempo/pull/4111)): Adds encrypted signing-key support and CLI flows for generating, encrypting, and showing validator verification keys. See docs on signing keys [here](https://docs.tempo.xyz/guide/node/validator-keys#generating-a-signing-key). We recommend all operators to migrate their signing keys to be encrypted as un-encrypted signing keys will be phased out in a future release. #### Performance This release includes many improvements that combined push throughput to [18K TPS, with an average block time of 500ms](https://github.com/tempoxyz/tempo/actions/runs/26552492472): * **Builder prewarming**: Block builder now prewarms transactions ahead of execution (#4423, #3893) * **Elastic building budget**: Block time is now stable at ~500ms (#4277) * **Cache sharing**: State and trie caches are now shared between payload building and validation ([#4266](https://github.com/tempoxyz/tempo/pull/4266), [#4195](https://github.com/tempoxyz/tempo/pull/4195)) *** ### Developers #### Compatible tooling versions | Package | Version | Notes | |---------|---------|-------| | tempo-alloy (Rust) | [1.7.3](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.7.3) | T5-compatible transaction and provider helpers | | tempo-primitives (Rust) | [1.7.3](https://github.com/tempoxyz/tempo/releases/tag/tempo-primitives%401.7.3) | T5 transaction and hardfork support | | tempo-contracts (Rust) | [1.7.3](https://github.com/tempoxyz/tempo/releases/tag/tempo-contracts%401.7.3) | T5 precompile bindings | | foundry (Foundry) | nightly | T5 hardfork-aware verification | #### New TIPs * **[TIP-1034 TIP-20 Channel Reserve Precompile](https://tips.sh/1034)**: T5 enshrines the TIP-20 channel reserve as a native Tempo precompile, cutting gas by up to 72% versus the legacy stream channel contract. [MPP](https://mpp.dev/) migrates to the protocol-native channel reserve path for predictable gas behavior and payment-lane eligibility, see benchmarks: | Operation | Legacy contract | Enshrined Reserve precompile | Gas Reduction | |---|---:|---:|---:| | Open channel, existing reserve balance | 1,055,229 | 294,425 | 72% | | Open channel, first reserve balance | 1,302,429 | 791,625 | 39% | | Close existing channel | 85,118 | 62,913 | 26% | | Top up existing channel | 53,724 | 46,805 | 13% | | Top up and cancel close request | 58,785 | 48,680 | 17% | * **[TIP-1035 Implicit Approval List](https://tips.sh/1035)**: This removes an approval round trip for approved protocol flows such as DEX, FeeAMM, and MPP/channel reserve operations while keeping normal TIP-20 allowance semantics unchanged for other spenders. * **[TIP-1045 Payment Transaction Classification](https://tips.sh/1045)**: T5 replaces broad prefix-based classification with an explicit payment call allow-list, including TIP-20 operations and channel reserve methods. Developers should treat payment-lane eligibility as call-shape dependent rather than assuming any TIP-20-looking transaction qualifies. * **Stablecoin DEX improvements ([TIP-1030](https://tips.sh/1030), [TIP-1056](https://tips.sh/1056))**: Same-tick flip orders are now allowed, and flip orders keep the same `orderId` across flips. Indexers and market-making systems can treat `orderId` as a stable handle, and offchain systems can watch `FlipFailed` when an automatic flip cannot be placed. * **[TIP-1033 Two-Hop FeeAMM Routing](https://tips.sh/1033)**: Fee conversion gains a two-hop fallback route, `userToken -> quoteToken -> validatorToken`. Integrations that reason about fee-token conversion should account for the fallback path instead of assuming conversion is limited to a direct pool. * **[TIP-1053 Witnesses in Key Authorizations](https://tips.sh/1053)**: Key authorizations can include an optional witness for offchain challenge binding and revocation. Apps can bind login and key authorization into a single signed flow, and clients should handle burned witnesses invalidating previously signed but unsubmitted authorizations. * **[TIP-1026 Token Logo URI](https://tips.sh/1026)**: TIP-20 tokens can expose an optional onchain `logoURI`. Wallets, explorers, and token-list tooling can use this metadata directly instead of relying only on offchain registries. ### What’s Changed * **Alloy SDK relay support** ([#4010](https://github.com/tempoxyz/tempo/pull/4010)): Adds `RelayTransport` for sponsor and fee-payer flows. * **[TIP-1057 T5 Hardfork Meta TIP](https://tips.sh/1057)**: T5 fixes fixed-size array packing in precompile storage codegen and clears stale tail slots when dynamic storage values shrink. Developers maintaining precompile storage layouts or codegen should make sure their assumptions match the T5 storage behavior. * **Txpool reliability and performance**: Improves fee-token caching, payment classification caching, sponsored/keychain transaction handling, paused transaction handling, mined transaction eviction, and implicit-fee eviction. **Full Changelog**: [v1.7.1...v1.8.0](https://github.com/tempoxyz/tempo/compare/v1.7.1...v1.8.0) ## v1.7.1 — Release v1.7.1 (2026-05-21) Tempo v1.7.1 adds support for migrating validators to minimal nodes, the better default for validators: lower disk requirements, faster rebuilds from snapshots. Migrating from archive v1 to minimal v2 reduces validator node size approximately as follows: | Network | Before | After | Reduction | | --- | ---: | ---: | ---: | | Mainnet | 27 GB | 10 GB | 2.7x | | Moderato Testnet | 1 TB | 100 GB | 10x | This is not a network upgrade. ### New Features #### Validators * Supports validator migration to minimal snapshots. Validators should migrate to the minimal snapshot profile by default when they do not need archive-style historical RPC data. * `tempo download --minimal --force` downloads the minimal snapshot component set and replaces existing snapshot data in the datadir while preserving node identity/network files such as `discovery-secret` and `known-peers.json`. Note the expected downtime on mainnet: 10mins, testnet: 60mins. See the [running a validator docs](https://docs.tempo.xyz/guide/node/validator-setup#running-the-validator) for more information on how to see if you’re running a minimal node and how to migrate, as well as the [Snapshots UI](https://snapshots.tempo.xyz/) for reference. #### RPC nodes * Trustless RPC: Moderato Testnet now supports certificate checks when running RPC nodes. This increases security assumptions by reducing trust in the upstream RPC and will become the default in a follow-up release. Trustless RPC requires consensus finalization certificates from the upstream RPC, guaranteeing that the data they receive is backed by a validator quorum. Opt in by using `--follow.experimental.certify`; see the [`tempo rpc node` docs](https://docs.tempo.xyz/guide/node/rpc#trustless-rpc-nodes). Because trustless RPC nodes will have their certificate store prepopulated, this will allow better failover setups. ### What's Changed * Finalized block storage in the consensus layer is now automatically pruned. To make sure node software can roll back to a previous release, in this release blocks will be written to both the new prunable and the old legacy storage. To opt out of the legacy storage entirely, use `--consensus.no-legacy-archive`. This double-write will be removed in a future release. (#3870) **Full Changelog**: https://github.com/tempoxyz/tempo/compare/v1.7.0...v1.7.1 ## v1.7.0 — Release v1.7.0 — T4 Network Upgrade (2026-05-11) > \[!IMPORTANT] > **This release is required for the T4 network upgrade scheduled for testnet and mainnet.** Node operators must update their nodes, otherwise your nodes will fall out of sync with the network. T4 embeds consensus context into block headers and ships a coordinated bundle of audit-driven correctness fixes. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes that are not updated will fall out of sync at the T4 activation timestamp. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Moderato (testnet) | May 14th 2026 16:00 CEST | 1778767200 | | Presto (mainnet) | May 18th 2026 16:00 CEST | 1779112800 | #### TIPs included with T4 The T4 network upgrade includes: 1. **[TIP-1031: Embed Consensus Context in the Block Header](https://docs.tempo.xyz/protocol/tips/tip-1031)** — writes consensus metadata (notably the ed25519 proposer key) directly into the block header. This is a prerequisite for deferred verification (optimistic notarization with async verification). It also allows operators to easily identify when their node’s proposal landed on the chain and was finalized. 2. **[TIP-1046: T4 Hardfork Meta TIP](https://docs.tempo.xyz/protocol/tips/tip-1046)** — Bundle of audit-driven correctness and gas-pricing fixes coordinated under T4 (DEX, TIP-20, scoped key-auth, packed-struct stores, subblocks metadata, call scopes). This release contains breaking changes for node operators and developers — please read the release notes carefully and upgrade your SDK versions to the T4 compatible versions. *** ### Operators #### Breaking changes * **`--consensus.fee-recipient` removed** ([#3817](https://github.com/tempoxyz/tempo/pull/3817)): The CLI flag was removed. Fee recipients are configured exclusively via the V2 smart contract. Remove the flag from your run scripts before upgrading. #### What’s changed * **Default bootnodes endpoint per chain** ([#3664](https://github.com/tempoxyz/tempo/pull/3664)): Builds on the `--tempo.bootnodes-endpoint` flag and the curated peer lists [announced in v1.6.0](https://github.com/tempoxyz/tempo/releases/tag/v1.6.0) — the endpoint is now wired in by default per chain (mainnet → `https://peers.tempo.xyz`, testnet → `https://testnet-peers.tempo.xyz/`). Operators no longer need to set the flag manually to get faster, more reliable peer discovery. * **`--dry-run` for consensus CLI commands** ([#3710](https://github.com/tempoxyz/tempo/pull/3710)): Prints transaction details without sending. * **Sync stability**: drain CL→EL backfill before sending newer FCUs to prevent pipeline-sync regressions on restart with a CL/EL gap ([#3744](https://github.com/tempoxyz/tempo/pull/3744)); treat future timestamps as transient ([#3721](https://github.com/tempoxyz/tempo/pull/3721)); race-condition fix in `fast_sync_after_full_dkg` between epoch-manager hints and gap repair ([#3700](https://github.com/tempoxyz/tempo/pull/3700)). * **Supply-chain hardening for releases** ([#3804](https://github.com/tempoxyz/tempo/pull/3804), [#3536](https://github.com/tempoxyz/tempo/pull/3536)): SLSA build provenance, SBOM attestation, bare-binary checksums, `--locked` builds, and the foundation for byte-deterministic reproducible builds. `tempoup` now verifies the archive checksum, GPG signature, and SLSA provenance on every install — pass `--unsafe-skip-verify` to downgrade tool-availability failures to warnings (cryptographic failures still abort regardless of the flag). The README documents two independent verification paths (offline `sha256sum -c` + `gpg --verify`, or `gh attestation verify`). *** ### Developers #### Compatible tooling versions | Package | Version | Notes | |---------|---------|-------| | tempo-alloy (Rust) | [1.7.0](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.7.0) | Consensus-context block header support, nonce-key tx count helper | | tempo-primitives (Rust) | [1.7.0](https://github.com/tempoxyz/tempo/releases/tag/tempo-primitives%401.7.0) | TIP-1031 `Context` field on `TempoHeader`, T4 hardfork constants | | tempo-contracts (Rust) | [1.7.0](https://github.com/tempoxyz/tempo/releases/tag/tempo-contracts%401.7.0) | T4 precompile bindings | | foundry (Foundry) | nightly | T4 hardfork-aware decoding | #### What’s changed * **Consensus context in block headers**: Post-T4 block headers include `consensus_context` with `epoch`, `view`, `parent_view`, and Ed25519 `proposer`; indexers can index the new field for more proposer metadata. ([#3254](https://github.com/tempoxyz/tempo/pull/3254), [#3092](https://github.com/tempoxyz/tempo/pull/3092)) * **Reth updated** to the latest upstream main snapshot as of May 5th 2026. * **Commonware bumped** ([#3697](https://github.com/tempoxyz/tempo/pull/3697)) and application actor adapted accordingly. * **Alloy SDK helpers**: nonce-key transaction count helper ([#3726](https://github.com/tempoxyz/tempo/pull/3726)); `is_active_hardfork` on provider ([#3655](https://github.com/tempoxyz/tempo/pull/3655)); `TempoAddressExt` helpers ([#3637](https://github.com/tempoxyz/tempo/pull/3637)); nonce filler cache controls ([#3634](https://github.com/tempoxyz/tempo/pull/3634)). * **Precompile surface**: `Set` overflow protection ([#3574](https://github.com/tempoxyz/tempo/pull/3574)); new `U96` storage primitive ([#3734](https://github.com/tempoxyz/tempo/pull/3734)). **Full Changelog**: [v1.6.0...v1.7.0](https://github.com/tempoxyz/tempo/compare/v1.6.0...v1.7.0) ## v1.6.0 — Release v1.6.0 - T3 Network Upgrade (2026-04-16) ### Release v1.6.0 — T3 Network Upgrade > \[!IMPORTANT] > **This release is required for the T3 network upgrade scheduled for testnet and mainnet.** Node operators must update their nodes, otherwise your nodes will fall out of sync with the network. This release contains breaking changes for Developers, please read the release notes carefully and upgrade your SDK versions to the T3 compatible versions. #### Update Priority | User Class | Priority | |------------|----------| | Validators | High | | RPC Nodes | High | Nodes that are not updated will fall out of sync at the T3 activation timestamp. #### Activation Times | Network | Date | Timestamp | |---------|------|-----------| | Moderato (testnet) | Apr 21st 2026 16:00 CEST | 1776780000 | | Presto (mainnet) | Apr 27th 2026 16:00 CEST | 1777298400 | #### TIPs included with T3 The T3 network upgrade implements five TIPs: 1. **[TIP-1011: Enhanced Access Key Permissions](https://docs.tempo.xyz/protocol/tips/tip-1011)** — Extends AccountKeychain with periodic TIP-20 spending limits, per-target and per-selector call scopes, and recipient-constrained token calls for TIP 20 selectors. Enables fine-grained key restrictions for delegated signing use cases. 2. **[TIP-1020: Signature Verification Precompile](https://docs.tempo.xyz/protocol/tips/tip-1020)** — New precompile that allows contracts to verify Tempo signature types (secp256k1, P256, WebAuthn) onchain, reusing the audited verification logic from transaction processing. Same gas schedule as transaction signatures (3k secp256k1, 8k P256/WebAuthn). 3. **[TIP-1022: Virtual Forwarding Addresses](https://docs.tempo.xyz/protocol/tips/tip-1022)** — Introduces virtual TIP-20 deposit addresses that auto-forward to a registered master wallet. Eliminates sweep transactions and avoids per-deposit-address state creation and state bloat. 4. **[TIP-1031: Consensus Context in Block Header](https://docs.tempo.xyz/protocol/tips/tip-1031)** — Encodes consensus metadata directly into Tempo block headers, making consensus context available to all nodes verifying the chain. 5. **[TIP-1038: T3 Hardfork Improvements](https://docs.tempo.xyz/protocol/tips/tip-1038)** — Meta TIP collecting audit-driven bug fixes and gas correctness changes. #### Operators * **--consensus.enable-subblocks CLI flag removed**: The flag has been removed and subblocks are disabled. No action needed — subblocks will be reintroduced in a later release. ([#3510](https://github.com/tempoxyz/tempo/pull/3510)) * **ENR fork ID enforcement enabled by default**: --enforce-enr-fork-id is now true by default, filtering out discovered peers without a confirmed fork ID. ([#3594](https://github.com/tempoxyz/tempo/pull/3594)) * **Fetching bootnodes on startup**: --tempo.bootnodes-endpoint CLI flag was added, allowing to fetch bootnodes on startup from a predefined endpoint [#3582](https://github.com/tempoxyz/tempo/pull/3582) ##### Improved Peering & Discovery We now publish curated, regularly-updated peer lists for our networks. To take advantage of faster and more reliable peer discovery, add the following flag: Mainnet: `--tempo.bootnodes-endpoint https://peers.tempo. xyz` Testnet: `--tempo.bootnodes-endpoint https://testnet-peers.tempo. xyz` This will become the default in an upcoming release. #### Developers This release contains **breaking changes** for Developers, please upgrade your sdk versions to the T3 compatible versions: | Package | Version | Notes | |---------|---------|-------| | tempo-alloy (Rust) | [1.6.0](https://github.com/tempoxyz/tempo/releases/tag/tempo-alloy%401.6.0) | Call-scopes, nonzero AA validity bounds, alloy 2.0.0 | | tempo-primitives (Rust) | [1.6.0](https://github.com/tempoxyz/tempo/releases/tag/tempo-primitives%401.6.0) | NonZeroU64 validity bounds, stricter payment v2 criteria | | tempo-contracts (Rust) | [1.6.0](https://github.com/tempoxyz/tempo/releases/tag/tempo-contracts%401.6.0) | T3 precompile bindings | | tempo-go (Go) | [0.4.0](https://github.com/tempoxyz/tempo-go/releases/tag/v0.4.0) | CallScope/SelectorRule builders, T3 authorizeKey ABI, 192-bit nonce key support | | foundry (Foundry) | [nightly](https://github.com/foundry-rs/foundry/releases/tag/nightly-a8ef5bfc5f3fa3506151ae2d1d27af4ba7e40558) | SignatureVerifier, AddressRegistry, cast keychain commands, T3 authorizeKey ABI | **Breaking Changes**: * **Scoped access key validation moved to pre-execution**: T3 call-scope matching is enforced at pre-execution rather than transaction validation, changing when scope violations surface. ([#3537](https://github.com/tempoxyz/tempo/pull/3537)) * **Nonzero AA validity bounds enforced**: AA transactions with zero valid\_before/valid\_after or zero key expiry are now rejected. ([#3500](https://github.com/tempoxyz/tempo/pull/3500), [#3501](https://github.com/tempoxyz/tempo/pull/3501)) * **Spending limit clamping for T3 refunds**: AccountKeychain spending limits are now clamped in T3 refund paths. ([#3483](https://github.com/tempoxyz/tempo/pull/3483)) #### What's Changed * **Reth bumped to latest** (b3f5e62 → 2026-04-12): [#3245](https://github.com/tempoxyz/tempo/pull/3245), [#3332](https://github.com/tempoxyz/tempo/pull/3332), [#3464](https://github.com/tempoxyz/tempo/pull/3464), [#3515](https://github.com/tempoxyz/tempo/pull/3515), [#3549](https://github.com/tempoxyz/tempo/pull/3549) * **Performance**: Sparse trie state root in payload builder ([#3476](https://github.com/tempoxyz/tempo/pull/3476)), keccak cache enabled ([#3601](https://github.com/tempoxyz/tempo/pull/3601)), dual P256 signature verification backend ([#3339](https://github.com/tempoxyz/tempo/pull/3339)) * **RPC**: tempo\_simulateV1 with TIP-20 token metadata enrichment ([#3449](https://github.com/tempoxyz/tempo/pull/3449)), tempo\_forkSchedule endpoint ([#3434](https://github.com/tempoxyz/tempo/pull/3434)), operator\_peers API ([#3589](https://github.com/tempoxyz/tempo/pull/3589)) * **Networking**: --tempo.bootnodes-endpoint for dynamic bootnode fetching ([#3582](https://github.com/tempoxyz/tempo/pull/3582)), p2p-proxy subcommand integrated into main binary ([#2780](https://github.com/tempoxyz/tempo/pull/2780)) * **Node operations**: History pruning support for non-validator nodes ([#3511](https://github.com/tempoxyz/tempo/pull/3511)), reject history pruning on validators ([#3298](https://github.com/tempoxyz/tempo/pull/3298)), validator DKG role info in CLI ([#3538](https://github.com/tempoxyz/tempo/pull/3538)) * **Transaction pool**: Re-validate transactions on policy changes ([#3532](https://github.com/tempoxyz/tempo/pull/3532)), unified validation logic between pool and EVM ([#3463](https://github.com/tempoxyz/tempo/pull/3463)), improved AA transaction handling ([#3541](https://github.com/tempoxyz/tempo/pull/3541)) * **Alloy SDK**: Call-scopes support in keychain ([#3437](https://github.com/tempoxyz/tempo/pull/3437)), alloy bumped to 2.0.0 ([#3569](https://github.com/tempoxyz/tempo/pull/3569)) **Full Changelog**: [v1.5.0...v1.6.0](https://github.com/tempoxyz/tempo/compare/v1.5.0...v1.6.0) ## v1.5.3 — Release v1.5.3 (2026-04-09) This patch release fixes the validator telemetry regression introduced in v1.5.2 and resolves a consensus edge case during epoch transitions. We recommend validators to upgrade to this release. ### Update Priority This table provides priorities for which classes of users should update to this release. | User Class | Priority | | ------------- | ------------- | | Validators | High | | RPC Nodes | Medium | **It is recommended for all node operators to upgrade. Validators that skipped v1.5.2 should move directly to v1.5.3.** ### What's Changed * **Consensus epoch-transition fix**: Keeps scheme material for the previous two epochs so nodes can continue verifying late certificates from straggling peers during epoch transitions, avoiding incorrect Byzantine peer blocking. ([#3520](https://github.com/tempoxyz/tempo/pull/3520)) * **Telemetry / OTLP HTTPS fix**: Re-enables TLS for the OTLP HTTP exporter so HTTPS telemetry endpoints work again after the reth dependency update in v1.5.2. ([#3508](https://github.com/tempoxyz/tempo/pull/3508)) **Full Changelog**: [v1.5.2...v1.5.3](https://github.com/tempoxyz/tempo/compare/v1.5.2...v1.5.3) ## v1.5.2 — Release v1.5.2 (2026-04-08) This is a maintenance release that updates the reth dependency and deprecates the `--consensus.fee-recipient` flag. The flag will be deprecated ~2 weeks, we ask validators to migrate the recipient to the validator contract before the flag is removed. ### Update Priority This table provides priorities for which classes of users should update to this release. | User Class | Priority | | ------------- | ------------- | | Validators | Not Recommended \* | | RPC Nodes | Low | \*Validators have reported issues with the telemetry endpoint, we ask validators to skip this release and wait for a patch. ### Upcoming Breaking Changes #### Validators As part of the Validator Config v2 migration on Tempo testnet and mainnet, fee-recipient configuration will be migrated to the onchain validator configuration. This release includes the supporting node changes so proposers read the fee recipient from validator contract state. The `--consensus.fee-recipient` flag will be removed in about two weeks. After migration, fee-recipient updates should be made through the validator contract. See: https://docs.tempo.xyz/guide/node/validator-config-v2 ### What's Changed * **Reth dependency update**: Pulls in the latest upstream reth fixes and improvements across payload building, trie and state handling, RPC behavior, and overall execution-path stability and performance. * **Payload builder and consensus correctness**: Improves payload construction on non-canonical ancestors, aligns fee-recipient handling with the validator configuration changes, and tightens validation of end-of-block system transaction ordering. * **RPC and operator ergonomics**: Adds the `tempo_forkSchedule` RPC endpoint and improves telemetry labeling and CLI behavior around telemetry URL configuration. * **Validation and mempool hardening**: Strengthens transaction validation and pool rechecks with fixes for wildcard fee-token spending-limit handling, paused-token precedence, and P256 signature edge cases. **Full Changelog**: [v1.5.1...v1.5.2](https://github.com/tempoxyz/tempo/compare/v1.5.1...v1.5.2) ## v1.5.1 — Release v1.5.1 (2026-03-29) ### Security Patch Fixes a denial-of-service vulnerability in RPC endpoints that accept state overrides (e.g. `eth_call`, `debug_traceCall`). Crafted `stateOverride` payloads could cause unbounded memory allocation in storage decoding paths, crashing the node. #### Update Priority This table provides priorities for which classes of users should update to this release. | User Class | Priority | |------------|----------| | Validators | Low | | RPC Nodes | Critical | #### Changes * Bound dynamically-sized storage type lengths to prevent OOM on malicious input * Reject tampered short-string storage slots with invalid length encoding * Add clippy lint to prevent reintroduction of unbounded allocations in storage paths All RPC node operators should upgrade immediately. [Browse the full release history on GitHub](https://github.com/tempoxyz/tempo/releases). # Tempo developer documentation Connect to Tempo and learn how to build stablecoin payment products. Start with network setup, fund a wallet, send your first payment, then choose the product workflow, reference, or operational surface you need next. Tempo Mainnet has been live since March 18, 2026. Tempo Wallet and production assets such as pathUSD use mainnet. Moderato is the separate public testnet for development. If you are new to Tempo, start with **Connect to Tempo**, then use **Stablecoin Payments** as the first product guide. ## Start Here * [Connect to Tempo](https://tempo.xyz/developers/docs/quickstart/integrate-tempo) — Add Tempo chain configuration, RPC endpoints, explorer links, and wallet connection details. * [Get Funds](https://tempo.xyz/developers/docs/guide/getting-funds) — Add production funds to Tempo Wallet, bridge mainnet assets, or use the faucet for testnet development. * [Send Your First Payment](https://tempo.xyz/developers/docs/guide/payments/send-a-payment) — Make a stablecoin transfer, attach a memo, and see where fees and receive policies fit. ## Choose a Build Path * [Stablecoin Payments](https://tempo.xyz/developers/docs/guide/payments) — Send, accept, reconcile, and sponsor stablecoin payments on Tempo. * [Issue Stablecoins](https://tempo.xyz/developers/docs/guide/issuance) — Create, mint, and manage TIP-20 stablecoins. * [Exchange Stablecoins](https://tempo.xyz/developers/docs/guide/stablecoin-dex) — Use Tempo's stablecoin DEX for swaps, quote tokens, liquidity, and fee routing. * [Agentic Payments](https://tempo.xyz/developers/docs/guide/machine-payments) — Accept one-time, pay-as-you-go, and streamed payments through the Machine Payments Protocol. * [Private Zones](https://tempo.xyz/developers/docs/guide/private-zones) — Use private zones for isolated execution, deposits, transfers, swaps, bridges, and withdrawals. ## Reference and Operations * [Tools & SDKs](https://tempo.xyz/developers/docs/tools) — SDKs, CLI, Tempo API, wallet docs, JSON-RPC, and indexer tooling. * [Tempo API](https://tempo.xyz/developers/docs/api) — Read payment activity, use webhooks, quote asset routes and exchanges, sponsor fees, and query indexed data. * [Tempo Protocol](https://tempo.xyz/developers/docs/protocol) — Technical references for tokens, policies, fees, transactions, blockspace, exchange, zones, and TIPs. * [Run a Tempo Node](https://tempo.xyz/developers/docs/guide/node) — Operate Tempo infrastructure for direct RPC access, validation workflows, monitoring, and upgrades. ## Ecosystem Resources * [Use Tempo with AI](https://tempo.xyz/developers/docs/guide/using-tempo-with-ai) — Give coding agents Tempo docs, source context, MCP tools, and agent workflow plugins. * [Partners](https://tempo.xyz/developers/docs/partners) — Find issuers, wallets, ramps, compliance, custody, analytics, orchestration, and infrastructure partners. # Tempo API Version: `1.0.0` REST API for reading and interacting with Tempo ## Servers - `https://api.tempo.xyz`: Relative to the host serving this document. ## Endpoints ### Activities A readable feed of what an account did onchain. - [`GET /v1/addresses/{address}/activities`](https://tempo.xyz/developers/docs/api/activities#getaddressactivities): List address activities - [`GET /v1/transactions/{transactionHash}/activities`](https://tempo.xyz/developers/docs/api/activities#gettransactionactivities): List transaction activities ### Balances How much of each token an account holds. - [`GET /v1/addresses/{address}/balances`](https://tempo.xyz/developers/docs/api/balances#getaddressbalances): List address balances - [`GET /v1/addresses/{address}/valuation`](https://tempo.xyz/developers/docs/api/balances#getaddressvaluation): Get address valuation - [`GET /v1/addresses/{address}/balances/{token}`](https://tempo.xyz/developers/docs/api/balances#getaddressbalance): Get address balance ### Blocks The ordered batches of transactions making up the chain. - [`GET /v1/blocks`](https://tempo.xyz/developers/docs/api/blocks#getblocks): List blocks - [`GET /v1/blocks/{block}`](https://tempo.xyz/developers/docs/api/blocks#getblock): Get a block by selector ### Earn Vaults that earn yield from assets deposited on Tempo. - [`GET /v1/earn/vaults`](https://tempo.xyz/developers/docs/api/earn#getearnvaults): List vaults - [`GET /v1/earn/vaults/verified`](https://tempo.xyz/developers/docs/api/earn#getverifiedearnvaults): List verified vaults - [`GET /v1/earn/addresses/{address}/positions`](https://tempo.xyz/developers/docs/api/earn#getearnaddresspositions): List account positions - [`GET /v1/earn/vaults/{vaultId}/share-prices`](https://tempo.xyz/developers/docs/api/earn#getearnvaultshareprices): List share prices - [`GET /v1/earn/vaults/{vaultId}`](https://tempo.xyz/developers/docs/api/earn#getearnvault): Get vault - [`GET /v1/earn/vaults/{vaultId}/positions/{address}`](https://tempo.xyz/developers/docs/api/earn#getearnvaultposition): Get account position - [`GET /v1/earn/vaults/{vaultId}/earnings/{address}`](https://tempo.xyz/developers/docs/api/earn#getearnvaultearnings): Get vault earnings ### Exchange Tempo's built-in stablecoin exchange: pairs, swaps, orders, prices. - [`GET /v1/exchange/swaps`](https://tempo.xyz/developers/docs/api/exchange#getswaps): List swaps - [`GET /v1/exchange/pairs`](https://tempo.xyz/developers/docs/api/exchange#getpairs): List pairs - [`GET /v1/exchange/orders`](https://tempo.xyz/developers/docs/api/exchange#getorders): List orders - [`GET /v1/exchange/orders/{orderId}/fills`](https://tempo.xyz/developers/docs/api/exchange#getorderfills): List order fills - [`GET /v1/exchange/pairs/{base}`](https://tempo.xyz/developers/docs/api/exchange#getpair): Get pair - [`GET /v1/exchange/orders/{orderId}`](https://tempo.xyz/developers/docs/api/exchange#getorder): Get order - [`GET /v1/exchange/pairs/{base}/ohlc`](https://tempo.xyz/developers/docs/api/exchange#getpairohlc): Get pair OHLC - [`GET /v1/exchange/pairs/{base}/depth`](https://tempo.xyz/developers/docs/api/exchange#getpairdepth): Get pair depth - [`POST /v1/exchange/quotes`](https://tempo.xyz/developers/docs/api/exchange#createexchangequote): Create quote - [`POST /v1/exchange/quotes/execute`](https://tempo.xyz/developers/docs/api/exchange#finalizeexchangequote): Finalize quote ### Fee AMM Pools that convert stablecoins to pay fees. - [`GET /v1/fee-amm/pools`](https://tempo.xyz/developers/docs/api/fee-amm#getfeeammpools): List pools - [`GET /v1/fee-amm/mints`](https://tempo.xyz/developers/docs/api/fee-amm#getfeeammmints): List mints ### Tokens TIP-20 token details, supply, and holders. - [`GET /v1/tokens`](https://tempo.xyz/developers/docs/api/tokens#gettokens): List tokens - [`GET /v1/tokens/{token}/holders`](https://tempo.xyz/developers/docs/api/tokens#gettokenholders): List token holders - [`GET /v1/tokens/{symbol}`](https://tempo.xyz/developers/docs/api/tokens#gettokenbysymbol): Get token by symbol - [`GET /v1/tokens/{token}`](https://tempo.xyz/developers/docs/api/tokens#gettoken): Get token by address - [`GET /v1/tokens/{token}/logo`](https://tempo.xyz/developers/docs/api/tokens#gettokenlogo): Get token logo ### Transactions Transactions submitted to Tempo, and their receipts. - [`GET /v1/transactions`](https://tempo.xyz/developers/docs/api/transactions#gettransactions): List transactions - [`GET /v1/transactions/receipts`](https://tempo.xyz/developers/docs/api/transactions#getreceipts): List transaction receipts - [`GET /v1/tokens/{token}/transactions`](https://tempo.xyz/developers/docs/api/transactions#gettokentransactions): List token transactions - [`GET /v1/transactions/{transactionHash}`](https://tempo.xyz/developers/docs/api/transactions#gettransaction): Get a transaction by hash - [`GET /v1/transactions/{transactionHash}/receipt`](https://tempo.xyz/developers/docs/api/transactions#getreceipt): Get a transaction receipt ### Transfers Token movements from executing transactions. - [`GET /v1/transfers`](https://tempo.xyz/developers/docs/api/transfers#gettransfers): List transfers ### Verified Tokens A curated, trusted list of TIP-20 tokens. - [`GET /v1/verified-tokens`](https://tempo.xyz/developers/docs/api/verified-tokens#listverifiedtokens): List verified tokens - [`GET /v1/verified-tokens/currencies`](https://tempo.xyz/developers/docs/api/verified-tokens#getverifiedtokencurrencies): List verified currencies - [`GET /v1/tokenlist`](https://tempo.xyz/developers/docs/api/verified-tokens#gettokenlist): Get token list - [`GET /v1/verified-tokens/{address}`](https://tempo.xyz/developers/docs/api/verified-tokens#getverifiedtoken): Get verified token ### Webhooks Organization webhook subscriptions. - [`GET /v1/webhooks`](https://tempo.xyz/developers/docs/api/webhooks#listwebhooks): List webhooks - [`POST /v1/webhooks`](https://tempo.xyz/developers/docs/api/webhooks#createwebhook): Create webhook - [`GET /v1/orgs/{orgId}/webhooks`](https://tempo.xyz/developers/docs/api/webhooks#listorgwebhooks): List webhooks - [`POST /v1/orgs/{orgId}/webhooks`](https://tempo.xyz/developers/docs/api/webhooks#createorgwebhook): Create webhook - [`GET /v1/webhooks/{id}`](https://tempo.xyz/developers/docs/api/webhooks#getwebhook): Get webhook - [`PATCH /v1/webhooks/{id}`](https://tempo.xyz/developers/docs/api/webhooks#updatewebhook): Update webhook - [`DELETE /v1/webhooks/{id}`](https://tempo.xyz/developers/docs/api/webhooks#deletewebhook): Delete webhook - [`GET /v1/orgs/{orgId}/webhooks/{id}/deliveries/{deliveryId}`](https://tempo.xyz/developers/docs/api/webhooks#getorgwebhookdelivery): Get webhook delivery - [`GET /v1/webhooks/event-types`](https://tempo.xyz/developers/docs/api/webhooks#getwebhookeventtypes): List webhook event types - [`GET /v1/webhooks/{id}/deliveries`](https://tempo.xyz/developers/docs/api/webhooks#listwebhookdeliveries): List webhook deliveries - [`GET /v1/orgs/{orgId}/webhooks/{id}/deliveries`](https://tempo.xyz/developers/docs/api/webhooks#listorgwebhookdeliveries): List webhook deliveries - [`POST /v1/webhooks/{id}/ping`](https://tempo.xyz/developers/docs/api/webhooks#pingwebhook): Ping webhook - [`PATCH /v1/orgs/{orgId}/webhooks/{id}`](https://tempo.xyz/developers/docs/api/webhooks#updateorgwebhook): Update webhook - [`DELETE /v1/orgs/{orgId}/webhooks/{id}`](https://tempo.xyz/developers/docs/api/webhooks#deleteorgwebhook): Delete webhook - [`POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry`](https://tempo.xyz/developers/docs/api/webhooks#retrywebhookdelivery): Retry webhook delivery - [`POST /v1/orgs/{orgId}/webhooks/{id}/deliveries/{deliveryId}/retry`](https://tempo.xyz/developers/docs/api/webhooks#retryorgwebhookdelivery): Retry webhook delivery - [`POST event`](https://tempo.xyz/developers/docs/api/webhooks#receivewebhookevent): Webhook event delivery ### Zones Private chain operations anchored to Tempo. - [`GET /v1/zones/withdrawals/{senderTag}`](https://tempo.xyz/developers/docs/api/zones#listzonewithdrawals): List Zone withdrawals ### Indexer Run read-only SQL queries against Tempo's indexed data. - [`GET /v1/indexer/query`](https://tempo.xyz/developers/docs/api/indexer#indexerquery): Query indexed chain data ### RPC Direct access to the chain over Ethereum JSON-RPC. Not part of the stable API contract; best-effort support only. No compatibility, latency, availability, or data-freshness guarantees. Breaking changes may happen with limited notice. - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#admin_validatorkey): admin_validatorKey - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#consensus_getfinalization): consensus_getFinalization - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#consensus_getidentitytransitionproof): consensus_getIdentityTransitionProof - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#consensus_getlatest): consensus_getLatest - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#consensus_subscribe): consensus_subscribe - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#consensus_unsubscribe): consensus_unsubscribe - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_blocknumber): eth_blockNumber - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_call): eth_call - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_chainid): eth_chainId - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_createaccesslist): eth_createAccessList - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_estimategas): eth_estimateGas - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_feehistory): eth_feeHistory - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_filltransaction): eth_fillTransaction - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_gasprice): eth_gasPrice - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getblockaccesslist): eth_getBlockAccessList - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getblockbyhash): eth_getBlockByHash - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getblockbynumber): eth_getBlockByNumber - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getblockreceipts): eth_getBlockReceipts - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getblocktransactioncountbyhash): eth_getBlockTransactionCountByHash - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getblocktransactioncountbynumber): eth_getBlockTransactionCountByNumber - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getcode): eth_getCode - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getfilterchanges): eth_getFilterChanges - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getfilterlogs): eth_getFilterLogs - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getlogs): eth_getLogs - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getproof): eth_getProof - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getstorageat): eth_getStorageAt - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_getstoragevalues): eth_getStorageValues - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_gettransactionbyblockhashandindex): eth_getTransactionByBlockHashAndIndex - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_gettransactionbyblocknumberandindex): eth_getTransactionByBlockNumberAndIndex - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_gettransactionbyhash): eth_getTransactionByHash - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_gettransactioncount): eth_getTransactionCount - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_gettransactionreceipt): eth_getTransactionReceipt - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_maxpriorityfeepergas): eth_maxPriorityFeePerGas - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_newblockfilter): eth_newBlockFilter - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_newfilter): eth_newFilter - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_newpendingtransactionfilter): eth_newPendingTransactionFilter - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_sendrawtransaction): eth_sendRawTransaction - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_sendrawtransactionsync): eth_sendRawTransactionSync - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_simulatev1): eth_simulateV1 - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_syncing): eth_syncing - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#eth_uninstallfilter): eth_uninstallFilter - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#tempo_forkschedule): tempo_forkSchedule - [`POST /rpc`](https://tempo.xyz/developers/docs/api/rpc#tempo_fundaddress): tempo_fundAddress - [`POST /rpc/{chain}`](https://tempo.xyz/developers/docs/api/rpc#rpcrequestbychain): Call chain JSON-RPC ### CoinGecko Exchange data in CoinGecko's GeckoTerminal format. - [`GET /gecko/{chainId}/pairs`](https://tempo.xyz/developers/docs/api/coingecko#coingeckopairs): List trading pairs - [`GET /gecko/{chainId}/events`](https://tempo.xyz/developers/docs/api/coingecko#coingeckoevents): List swap events - [`GET /gecko/{chainId}/latest-block`](https://tempo.xyz/developers/docs/api/coingecko#coingeckolatestblock): Get latest indexed block - [`GET /gecko/{chainId}/assets/{address}`](https://tempo.xyz/developers/docs/api/coingecko#coingeckoasset): Get asset - [`GET /gecko/{chainId}/pairs/{pairId}`](https://tempo.xyz/developers/docs/api/coingecko#coingeckopair): Get trading pair ### Chains Source chains and stablecoins supported by Routes. - [`GET /v1/routes/chains`](https://tempo.xyz/developers/docs/api/routes/chains#getrouteschains): Get chains ### Deposit Addresses Reusable route addresses and deposits detected at those addresses. - [`GET /v1/routes/deposit-addresses`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#listroutesdepositaddresses): List deposit addresses - [`POST /v1/routes/deposit-addresses`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#createroutesdepositaddress): Create deposit address - [`POST /v1/routes/deposit-addresses/{id}/reconcile`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#reconcileroutesdepositaddress): Reconcile deposit address - [`GET /v1/routes/deposit-addresses/{id}`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#getroutesdepositaddress): Get deposit address - [`GET /v1/routes/deposits/{id}`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#getroutesdeposit): Get deposit - [`GET /v1/routes/deposits`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#listroutesdeposits): List deposits ### Providers Providers available through Routes. - [`GET /v1/routes/providers`](https://tempo.xyz/developers/docs/api/routes/providers#getroutesproviders): Get providers ### Quotes Live quotes for routing assets between supported chains. - [`GET /v1/routes/quotes`](https://tempo.xyz/developers/docs/api/routes/quotes#quoteroutes): Get quotes ### Transfers Transfers routed between supported chains. - [`POST /v1/routes/transfers/{id}/source-transactions`](https://tempo.xyz/developers/docs/api/routes/transfers#registerroutestransfersourcetransaction): Register source transaction - [`GET /v1/routes/transfers`](https://tempo.xyz/developers/docs/api/routes/transfers#listroutestransfers): List transfers - [`POST /v1/routes/transfers`](https://tempo.xyz/developers/docs/api/routes/transfers#createroutestransfer): Create transfer - [`POST /v1/routes/transfers/vault`](https://tempo.xyz/developers/docs/api/routes/transfers#createroutestransfervault): Create transfer into vault - [`POST /v1/routes/transfers/zone`](https://tempo.xyz/developers/docs/api/routes/transfers#createroutestransferzone): Create transfer into zone - [`GET /v1/routes/transfers/{id}`](https://tempo.xyz/developers/docs/api/routes/transfers#getroutestransfer): Get transfer ### API Keys Credentials for accessing the Tempo API. - [`GET /v1/scopes`](https://tempo.xyz/developers/docs/api/api-keys#getscopes): List scopes - [`GET /v1/orgs/{orgId}/api-keys`](https://tempo.xyz/developers/docs/api/api-keys#listorgapikeys): List organization API keys - [`GET /v1/orgs/{orgId}/projects/{projectId}/api-keys`](https://tempo.xyz/developers/docs/api/api-keys#listapikeys): List API keys - [`POST /v1/orgs/{orgId}/projects/{projectId}/api-keys`](https://tempo.xyz/developers/docs/api/api-keys#createapikey): Create API key - [`DELETE /v1/orgs/{orgId}/api-keys/{keyId}`](https://tempo.xyz/developers/docs/api/api-keys#revokeorgapikey): Revoke organization API key - [`POST /v1/orgs/{orgId}/api-keys/{keyId}/rotate`](https://tempo.xyz/developers/docs/api/api-keys#rotateorgapikey): Rotate organization API key - [`PATCH /v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}`](https://tempo.xyz/developers/docs/api/api-keys#updateapikey): Update API key - [`DELETE /v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}`](https://tempo.xyz/developers/docs/api/api-keys#revokeapikey): Revoke API key - [`POST /v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}/rotate`](https://tempo.xyz/developers/docs/api/api-keys#rotateapikey): Rotate API key ### Billing Organization billing backed by Stripe. - [`GET /v1/orgs/{orgId}/billing`](https://tempo.xyz/developers/docs/api/billing#getbilling): Get billing - [`PATCH /v1/orgs/{orgId}/billing`](https://tempo.xyz/developers/docs/api/billing#updatebilling): Update billing - [`GET /v1/orgs/{orgId}/billing/payment-methods`](https://tempo.xyz/developers/docs/api/billing#getbillingpaymentmethods): Get payment methods - [`POST /v1/orgs/{orgId}/billing/stripe/checkout`](https://tempo.xyz/developers/docs/api/billing#createstripecheckout): Create checkout session - [`POST /v1/orgs/{orgId}/billing/stripe/manage`](https://tempo.xyz/developers/docs/api/billing#createstripemanage): Create manage session - [`GET /v1/orgs/{orgId}/billing/invoice-preview`](https://tempo.xyz/developers/docs/api/billing#getbillinginvoicepreview): Preview billing invoice - [`DELETE /v1/orgs/{orgId}/billing/payment-methods/{methodId}`](https://tempo.xyz/developers/docs/api/billing#deletebillingpaymentmethod): Remove payment method ### Faucet Test token routes for Tempo testnet accounts. - [`POST /v1/orgs/{orgId}/faucet`](https://tempo.xyz/developers/docs/api/faucet#fundtestnetaccount): Fund testnet account ### Invitations Pending organization invitations. - [`GET /v1/invitations`](https://tempo.xyz/developers/docs/api/invitations#listmyinvitations): List my invitations - [`GET /v1/orgs/{orgId}/invitations`](https://tempo.xyz/developers/docs/api/invitations#listinvitations): List invitations - [`POST /v1/orgs/{orgId}/invitations`](https://tempo.xyz/developers/docs/api/invitations#createinvitation): Create invitation - [`POST /v1/invitations/{invitationId}/accept`](https://tempo.xyz/developers/docs/api/invitations#acceptinvitation): Accept invitation - [`POST /v1/invitations/{invitationId}/decline`](https://tempo.xyz/developers/docs/api/invitations#declineinvitation): Decline invitation - [`DELETE /v1/orgs/{orgId}/invitations/{invitationId}`](https://tempo.xyz/developers/docs/api/invitations#revokeinvitation): Revoke invitation ### Invite Links Reusable organization invite links. - [`GET /v1/orgs/{orgId}/invite-links/{inviteLinkId}/redemptions`](https://tempo.xyz/developers/docs/api/invite-links#listinvitelinkredemptions): List link redemptions - [`GET /v1/orgs/{orgId}/invite-links`](https://tempo.xyz/developers/docs/api/invite-links#listinvitelinks): List invite links - [`POST /v1/orgs/{orgId}/invite-links`](https://tempo.xyz/developers/docs/api/invite-links#createinvitelink): Create invite link - [`POST /v1/invite-links/resolve`](https://tempo.xyz/developers/docs/api/invite-links#resolveinvitelink): Resolve invite link - [`POST /v1/invite-links/accept`](https://tempo.xyz/developers/docs/api/invite-links#acceptinvitelink): Accept invite link - [`PATCH /v1/orgs/{orgId}/invite-links/{inviteLinkId}`](https://tempo.xyz/developers/docs/api/invite-links#updateinvitelink): Update invite link - [`DELETE /v1/orgs/{orgId}/invite-links/{inviteLinkId}`](https://tempo.xyz/developers/docs/api/invite-links#deleteinvitelink): Delete invite link ### Members Organization team membership. - [`GET /v1/orgs/{orgId}/members`](https://tempo.xyz/developers/docs/api/members#listmembers): List members - [`PATCH /v1/orgs/{orgId}/members/{userId}`](https://tempo.xyz/developers/docs/api/members#updatemember): Update member - [`DELETE /v1/orgs/{orgId}/members/{userId}`](https://tempo.xyz/developers/docs/api/members#removemember): Remove member ### Organizations Teams that own application workspaces and members. - [`GET /v1/orgs`](https://tempo.xyz/developers/docs/api/organizations#listorganizations): List organizations - [`POST /v1/orgs`](https://tempo.xyz/developers/docs/api/organizations#createorganization): Create organization - [`GET /v1/orgs/{orgId}`](https://tempo.xyz/developers/docs/api/organizations#getorganization): Get organization - [`PATCH /v1/orgs/{orgId}`](https://tempo.xyz/developers/docs/api/organizations#updateorganization): Update organization - [`DELETE /v1/orgs/{orgId}`](https://tempo.xyz/developers/docs/api/organizations#deleteorganization): Delete organization ### Projects Application workspaces within an organization. - [`GET /v1/orgs/{orgId}/projects`](https://tempo.xyz/developers/docs/api/projects#listprojects): List projects - [`POST /v1/orgs/{orgId}/projects`](https://tempo.xyz/developers/docs/api/projects#createproject): Create project - [`GET /v1/orgs/{orgId}/projects/{projectId}`](https://tempo.xyz/developers/docs/api/projects#getproject): Get project - [`PATCH /v1/orgs/{orgId}/projects/{projectId}`](https://tempo.xyz/developers/docs/api/projects#updateproject): Update project - [`DELETE /v1/orgs/{orgId}/projects/{projectId}`](https://tempo.xyz/developers/docs/api/projects#deleteproject): Delete project ### Usage Request and sponsorship usage for organizations. - [`GET /v1/orgs/{orgId}/usage/requests`](https://tempo.xyz/developers/docs/api/usage#getrequestusage): Get request usage - [`GET /v1/orgs/{orgId}/usage/sponsorships`](https://tempo.xyz/developers/docs/api/usage#getsponsorshipusage): Get sponsorship usage ### Users Users on the Tempo Platform. - [`GET /v1/me`](https://tempo.xyz/developers/docs/api/users#getme): Get current user ### Verified Token Requests Organization requests to add tokens to the curated verified list. - [`GET /v1/orgs/{orgId}/verified-token-requests`](https://tempo.xyz/developers/docs/api/verified-token-requests#listverifiedtokenrequests): List token requests - [`POST /v1/orgs/{orgId}/verified-token-requests`](https://tempo.xyz/developers/docs/api/verified-token-requests#createverifiedtokenrequest): Create token request - [`DELETE /v1/orgs/{orgId}/verified-token-requests/{requestId}`](https://tempo.xyz/developers/docs/api/verified-token-requests#cancelverifiedtokenrequest): Cancel token request ### MPP Submit completed MPP credentials to Tempo. Validate checks a credential without settling it; Broadcast validates, screens, and submits it. Both require `mpp:write`. - [`POST /v1/mpp/validate`](https://tempo.xyz/developers/docs/api/mpp#validatemppcredential): Validate MPP credential - [`POST /v1/mpp/broadcast`](https://tempo.xyz/developers/docs/api/mpp#broadcastmppcredential): Broadcast MPP credential ### MCP A hosted Model Context Protocol server: the data domain and Tempo docs exposed as tools. - [`POST /mcp`](https://tempo.xyz/developers/docs/api/mcp#mcprequest): Call MCP ### Authentication Authenticate into the Tempo Platform. - [`POST /v1/auth/siwe/challenge`](https://tempo.xyz/developers/docs/api/authentication#createsiwechallenge): Create challenge (SIWE) - [`POST /v1/auth/siwe`](https://tempo.xyz/developers/docs/api/authentication#verifysiwe): Authenticate (SIWE) - [`POST /v1/auth/identity`](https://tempo.xyz/developers/docs/api/authentication#verifyidentity): Authenticate identity - [`POST /v1/auth/logout`](https://tempo.xyz/developers/docs/api/authentication#logout): Sign out # Activities A readable feed of what an account did onchain. ## List address activities `GET /v1/addresses/{address}/activities` Get a human-readable feed of what an address has been doing onchain, including payments sent and received, swaps, approvals, and more. ### Path parameters - `address` `string` _(required)_: Account address whose activity you want to list. ### Query parameters - `amount.from` `string`: Only include activities with at least one token amount greater than or equal to this base-unit quantity. - `amount.to` `string`: Only include activities with at least one token amount less than or equal to this base-unit quantity. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `group` `boolean`: Set to `true` to fold consecutive items signed by the same access key on the same UTC day into `group` entries. Defaults to `false`, which returns every item separately. - `include` `string[]`: Comma-separated activity options. `zones` combines the selected parent chain with every readable Zone. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `logs` `boolean`: Set to `true` to attach each item’s full transaction log set (decoded when possible) as a top-level `events` array, so no co-located log is hidden. Defaults to `false`. `unknown` items always include it. - `recipient` `string`: Only include activities sent to this recipient address. - `sender` `string`: Only include activities sent from this sender address. - `token` `string`: Only include activities with a source, destination, or refund token for this TIP-20 contract. - `type` `string[]`: Only include these activity types, such as `transfer,swap`. Multiple values use OR semantics. - `valuation.amount.from` `string`: Only include activities with at least one valued amount greater than or equal to this amount. Requires `valuation.currency`. - `valuation.amount.to` `string`: Only include activities with at least one valued amount less than or equal to this amount. Requires `valuation.currency`. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Responses #### `200`: A page of address activity entries. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Activity feed entries for the address. - `meta` `object`: Page-level resources, such as valuation rate provenance. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Tempo could not read activity or token data from an upstream service. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/activities?amount.from=1000000&amount.to=1000000&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&group=true&include=token.logoUri,token.verified,zones&limit=10&logs=true&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&token=0x20c0000000000000000000000000000000000000&type=assets-deposited,shares-deposited,shares-redeemed,assets-withdrawn,shares-redemption-requested,shares-redemption-finalized,shares-redemption-cancelled,private-assets-deposited,private-shares-redeemed,transfer,mint,burn,swap,approval,session-closed,access-key-created,access-key-revoked,token-created,burn-blocked,channel-opened,channel-funded,channel-settled,channel-closed,channel-close-cancelled,order-placed,order-cancelled,fees-distributed,fee-rebalance-swap,reward-distributed,reward-recipient-set,spending-limit-updated,order-filled,order-flipped,pair-created,token-pause-set,token-supply-cap-set,token-transfer-policy-set,token-quote-token-set,token-next-quote-token-set,token-logo-set,role-membership-set,role-admin-set,fee-user-token-set,fee-validator-token-set,policy-created,policy-admin-set,whitelist-updated,blacklist-updated,compound-policy-created,master-registered,nonce-incremented,key-authorization-witness,key-authorization-witness-burned,validator-added,validator-deactivated,validator-rotated,validator-fee-recipient-set,validator-ip-set,validator-ownership-transferred,ownership-transferred,validator-migrated,validator-migration-skipped,network-identity-rotation-epoch-set,initialized,unknown&valuation.amount.from=1&valuation.amount.to=1&valuation.currency=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/activities?amount.from=1000000&amount.to=1000000&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&group=true&include=token.logoUri,token.verified,zones&limit=10&logs=true&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&token=0x20c0000000000000000000000000000000000000&type=assets-deposited,shares-deposited,shares-redeemed,assets-withdrawn,shares-redemption-requested,shares-redemption-finalized,shares-redemption-cancelled,private-assets-deposited,private-shares-redeemed,transfer,mint,burn,swap,approval,session-closed,access-key-created,access-key-revoked,token-created,burn-blocked,channel-opened,channel-funded,channel-settled,channel-closed,channel-close-cancelled,order-placed,order-cancelled,fees-distributed,fee-rebalance-swap,reward-distributed,reward-recipient-set,spending-limit-updated,order-filled,order-flipped,pair-created,token-pause-set,token-supply-cap-set,token-transfer-policy-set,token-quote-token-set,token-next-quote-token-set,token-logo-set,role-membership-set,role-admin-set,fee-user-token-set,fee-validator-token-set,policy-created,policy-admin-set,whitelist-updated,blacklist-updated,compound-policy-created,master-registered,nonce-incremented,key-authorization-witness,key-authorization-witness-burned,validator-added,validator-deactivated,validator-rotated,validator-fee-recipient-set,validator-ip-set,validator-ownership-transferred,ownership-transferred,validator-migrated,validator-migration-skipped,network-identity-rotation-epoch-set,initialized,unknown&valuation.amount.from=1&valuation.amount.to=1&valuation.currency=AUD') ``` ## List transaction activities `GET /v1/transactions/{transactionHash}/activities` Get a human-readable list of what happened on one transaction onchain, including payments, swaps, approvals, and more. ### Path parameters - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction whose activity you want to list. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated activity options. `zones` combines the selected parent chain with every readable Zone. - `logs` `boolean`: Set to `true` to attach each item’s full transaction log set (decoded when possible) as a top-level `events` array, so no co-located log is hidden. Defaults to `false`. `unknown` items always include it. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Responses #### `200`: The classified activity for the transaction. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `chainId` `string | number` _(required)_: Chain ID containing the transaction. - `data` `object[]` _(required)_: Activity items classified from the transaction. - `meta` `object`: Page-level resources, such as valuation rate provenance. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No mined transaction was found for that hash. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: More than one readable chain contains that transaction hash. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Tempo could not read activity or token data from an upstream service. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/transactions/0x4845ae2098724ab26a5d89370dce5124d044be6a5acc164afc348662de67d474/activities?chainId=4217&include=token.logoUri,token.verified,zones&logs=true&valuation.currency=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/transactions/0x4845ae2098724ab26a5d89370dce5124d044be6a5acc164afc348662de67d474/activities?chainId=4217&include=token.logoUri,token.verified,zones&logs=true&valuation.currency=AUD') ``` # API Keys Credentials for accessing the Tempo API. ## List scopes `GET /v1/scopes` See which scopes can be granted to API keys. ### Responses #### `200`: Issuable scope catalog. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Issuable API-key scopes. - `description` `string` _(required)_: Human-readable explanation of what the scope grants. - `scope` `string` _(required)_: Scope identifier used in API-key grants and route policies. Zone templates carry a concrete chain id and access level in grants. - `selfServe` `boolean` _(required)_: Whether an eligible session may self-mint this scope. #### `400`: Malformed API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/scopes ``` ```ts fetch('https://api.tempo.xyz/v1/scopes') ``` ## List organization API keys `GET /v1/orgs/{orgId}/api-keys` List every API key in the organization, across all of its projects. ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. - `include` `string[]`: Fields to enrich. Defaults to lastUsedAt; pass an empty value to load metadata without analytics. ### Responses #### `200`: The organization's keys across all projects, newest first (metadata only). Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The API keys, newest first. - `allowedIps` `string[]` _(required)_: Client IP addresses and CIDR ranges allowed to use this key. An empty list means unrestricted. - `createdAt` `string ` _(required)_: When the key was minted (ISO 8601). - `createdBy` `string`: Identity that minted the key (`usr_…`, or `super_admin`). - `environment` `string` _(required)_: Key environment. - `expiresAt` `string `: When the key expires (ISO 8601). Absent for a non-expiring key. - `id` `string` _(required)_: Opaque key id (`key_…`). - `lastUsedAt` `string `: When the key last authenticated a request (ISO 8601). Omitted with `include=` or when no usage is available. - `name` `string`: Human-readable key name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `projectId` `string`: Attributed project id (`prj_…`). - `scopes` `string[]` _(required)_: Granted scopes. '*' and non-self-serve scopes appear only on keys minted by the super admin. - `tokenLast4` `string` _(required)_: Last 4 characters of the plaintext token. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys?environment=sandbox&include=lastUsedAt' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys?environment=sandbox&include=lastUsedAt') ``` ## List API keys `GET /v1/orgs/{orgId}/projects/{projectId}/api-keys` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Query parameters - `environment` `string`: Resource environment. - `include` `string[]`: Fields to enrich. Defaults to lastUsedAt; pass an empty value to load metadata without analytics. ### Responses #### `200`: The project's keys, newest first (metadata only). Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The API keys, newest first. - `allowedIps` `string[]` _(required)_: Client IP addresses and CIDR ranges allowed to use this key. An empty list means unrestricted. - `createdAt` `string ` _(required)_: When the key was minted (ISO 8601). - `createdBy` `string`: Identity that minted the key (`usr_…`, or `super_admin`). - `environment` `string` _(required)_: Key environment. - `expiresAt` `string `: When the key expires (ISO 8601). Absent for a non-expiring key. - `id` `string` _(required)_: Opaque key id (`key_…`). - `lastUsedAt` `string `: When the key last authenticated a request (ISO 8601). Omitted with `include=` or when no usage is available. - `name` `string`: Human-readable key name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `projectId` `string`: Attributed project id (`prj_…`). - `scopes` `string[]` _(required)_: Granted scopes. '*' and non-self-serve scopes appear only on keys minted by the super admin. - `tokenLast4` `string` _(required)_: Last 4 characters of the plaintext token. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible project or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys?environment=sandbox&include=lastUsedAt' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys?environment=sandbox&include=lastUsedAt') ``` ## Create API key `POST /v1/orgs/{orgId}/projects/{projectId}/api-keys` Mint a project-attributed API key. The token appears once and is never shown again. ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Request body (required) (`application/json`) - `allowedIps` `string[]`: Client IP addresses and CIDR ranges allowed to use the key. Omit or use an empty list for unrestricted access. - `environment` `string`: Key environment. - `name` `string`: Human-readable key name. - `scopes` `string[]`: Unique scopes to grant. ### Responses #### `200`: The minted key, including its one-time token. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `allowedIps` `string[]` _(required)_: Client IP addresses and CIDR ranges allowed to use this key. An empty list means unrestricted. - `createdAt` `string ` _(required)_: When the key was minted (ISO 8601). - `createdBy` `string`: Identity that minted the key (`usr_…`, or `super_admin`). - `environment` `string` _(required)_: Key environment. - `expiresAt` `string `: When the key expires (ISO 8601). Absent for a non-expiring key. - `id` `string` _(required)_: Opaque key id (`key_…`). - `lastUsedAt` `string `: When the key last authenticated a request (ISO 8601). Omitted with `include=` or when no usage is available. - `name` `string`: Human-readable key name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `projectId` `string`: Attributed project id (`prj_…`). - `scopes` `string[]` _(required)_: Granted scopes. '*' and non-self-serve scopes appear only on keys minted by the super admin. - `tokenLast4` `string` _(required)_: Last 4 characters of the plaintext token. - `token` `string` _(required)_: The plaintext token — shown once, unrecoverable afterward. #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The requested scopes are not issuable. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible project or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: API-key limit reached. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys \ --request POST \ --header 'Content-Type: application/json' \ --data '{}' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }) ``` ## Revoke organization API key `DELETE /v1/orgs/{orgId}/api-keys/{keyId}` Revoke an organization-level API key. Requests presenting its token stop resolving. ### Path parameters - `keyId` `string` _(required)_: The key id (`key_…`). - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: Confirmation that the key was revoked. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: ID of the key that was revoked. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible API key or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` ## Rotate organization API key `POST /v1/orgs/{orgId}/api-keys/{keyId}/rotate` Create a replacement organization-level API key with the same access as an existing key. The existing key remains active until revoked. ### Path parameters - `keyId` `string` _(required)_: The key id (`key_…`). - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: The replacement key, including its one-time token. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `allowedIps` `string[]` _(required)_: Client IP addresses and CIDR ranges allowed to use this key. An empty list means unrestricted. - `createdAt` `string ` _(required)_: When the key was minted (ISO 8601). - `createdBy` `string`: Identity that minted the key (`usr_…`, or `super_admin`). - `environment` `string` _(required)_: Key environment. - `expiresAt` `string `: When the key expires (ISO 8601). Absent for a non-expiring key. - `id` `string` _(required)_: Opaque key id (`key_…`). - `lastUsedAt` `string `: When the key last authenticated a request (ISO 8601). Omitted with `include=` or when no usage is available. - `name` `string`: Human-readable key name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `projectId` `string`: Attributed project id (`prj_…`). - `scopes` `string[]` _(required)_: Granted scopes. '*' and non-self-serve scopes appear only on keys minted by the super admin. - `tokenLast4` `string` _(required)_: Last 4 characters of the plaintext token. - `token` `string` _(required)_: The plaintext token — shown once, unrecoverable afterward. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The source key contains scopes the caller cannot rotate. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible API key or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: API-key limit reached. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n/rotate \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n/rotate', { method: 'POST' }) ``` ## Update API key `PATCH /v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}` Replace an API key's client IP/CIDR allowlist. An empty list removes the restriction. ### Path parameters - `keyId` `string` _(required)_: The key id (`key_…`). - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Request body (required) (`application/json`) - `allowedIps` `string[]` _(required)_: Replacement client IP/CIDR allowlist. Use an empty list for unrestricted access. ### Responses #### `200`: The updated API key metadata. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `allowedIps` `string[]` _(required)_: Client IP addresses and CIDR ranges allowed to use this key. An empty list means unrestricted. - `createdAt` `string ` _(required)_: When the key was minted (ISO 8601). - `createdBy` `string`: Identity that minted the key (`usr_…`, or `super_admin`). - `environment` `string` _(required)_: Key environment. - `expiresAt` `string `: When the key expires (ISO 8601). Absent for a non-expiring key. - `id` `string` _(required)_: Opaque key id (`key_…`). - `lastUsedAt` `string `: When the key last authenticated a request (ISO 8601). Omitted with `include=` or when no usage is available. - `name` `string`: Human-readable key name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `projectId` `string`: Attributed project id (`prj_…`). - `scopes` `string[]` _(required)_: Granted scopes. '*' and non-self-serve scopes appear only on keys minted by the super admin. - `tokenLast4` `string` _(required)_: Last 4 characters of the plaintext token. #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible API key or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{ "allowedIps": [ "203.0.113.0/24" ] }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ allowedIps: ['203.0.113.0/24'] }) }) ``` ## Revoke API key `DELETE /v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}` Revoke an API key. Requests presenting its token stop resolving. ### Path parameters - `keyId` `string` _(required)_: The key id (`key_…`). - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Responses #### `200`: Confirmation that the key was revoked. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: ID of the key that was revoked. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible API key or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` ## Rotate API key `POST /v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId}/rotate` Create a replacement API key with the same access as an existing key. The existing key remains active until revoked. ### Path parameters - `keyId` `string` _(required)_: The key id (`key_…`). - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Responses #### `200`: The replacement key, including its one-time token. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `allowedIps` `string[]` _(required)_: Client IP addresses and CIDR ranges allowed to use this key. An empty list means unrestricted. - `createdAt` `string ` _(required)_: When the key was minted (ISO 8601). - `createdBy` `string`: Identity that minted the key (`usr_…`, or `super_admin`). - `environment` `string` _(required)_: Key environment. - `expiresAt` `string `: When the key expires (ISO 8601). Absent for a non-expiring key. - `id` `string` _(required)_: Opaque key id (`key_…`). - `lastUsedAt` `string `: When the key last authenticated a request (ISO 8601). Omitted with `include=` or when no usage is available. - `name` `string`: Human-readable key name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `projectId` `string`: Attributed project id (`prj_…`). - `scopes` `string[]` _(required)_: Granted scopes. '*' and non-self-serve scopes appear only on keys minted by the super admin. - `tokenLast4` `string` _(required)_: Last 4 characters of the plaintext token. - `token` `string` _(required)_: The plaintext token — shown once, unrecoverable afterward. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The source key contains scopes the caller cannot rotate. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible API key or API key surface was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: API-key limit reached. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n/rotate \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n/api-keys/key_1a2b3c4d5e6f7g8h9j0k1m2n/rotate', { method: 'POST' }) ``` # Authentication Authenticate into the Tempo Platform. ## Create challenge (SIWE) `POST /v1/auth/siwe/challenge` Issues a single-use SIWE challenge message to sign. The challenge expires after a short TTL. ### Request body (required) (`application/json`) ### Responses #### `200`: The challenge message to sign. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `message` `string` _(required)_: Single-use EIP-4361 (SIWE) message to sign and submit for verification. #### `400`: Malformed request body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/auth/siwe/challenge \ --request POST \ --header 'Content-Type: application/json' \ --data '{}' ``` ```ts fetch('https://api.tempo.xyz/v1/auth/siwe/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }) ``` ## Authenticate (SIWE) `POST /v1/auth/siwe` Verifies the signed challenge and establishes a session. Sets a session cookie by default; pass `returnToken` to receive a bearer token instead. ### Request body (required) (`application/json`) - `address` `string` _(required)_: Wallet address that signed the message; becomes the session subject. - `idToken` `string`: Wallet-minted OIDC identity token (JWT) asserting a verified email, folded onto the session. - `keyAuthorization` `string`: RLP-serialized signed key authorization (TIP-1053) whose witness binds this message; verification recovers over its digest. - `message` `string` _(required)_: The exact challenge message issued by `POST /v1/auth/siwe/challenge`. - `returnToken` `boolean`: Return the session token in the body instead of setting the session cookie. - `signature` `string` _(required)_: Signature over the challenge message (or the key-authorization digest). ### Responses #### `200`: Session established. Cookie mode sets `Set-Cookie`; token mode returns `token`. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Set-Cookie` `string`: Session cookie (`accounts_auth`); omitted when `returnToken` is true. Body (`application/json`): - `token` `string`: Bearer session token; present only when `returnToken` is true. #### `400`: Invalid, expired, or mismatched challenge message; or a required identity token is missing. Body (`application/json`): - `error` `string` _(required)_: Human-readable failure reason. - `issues` `unknown[]`: Field-level validation issues, present on request-schema failures. #### `401`: Signature or identity verification failed. Body (`application/json`): - `error` `string` _(required)_: Human-readable failure reason. - `issues` `unknown[]`: Field-level validation issues, present on request-schema failures. #### `409`: Challenge nonce already used or unknown. Body (`application/json`): - `error` `string` _(required)_: Human-readable failure reason. - `issues` `unknown[]`: Field-level validation issues, present on request-schema failures. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The Tempo RPC could not verify the signature. Body (`application/json`): - `error` `string` _(required)_: Human-readable failure reason. - `issues` `unknown[]`: Field-level validation issues, present on request-schema failures. ### Example request ```bash curl https://api.tempo.xyz/v1/auth/siwe \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "address": "0x0000000000000000000000000000000000000001", "message": "api.tempo.xyz wants you to sign in with your Ethereum account:\n0x0000000000000000000000000000000000000000\n\nURI: https://api.tempo.xyz\nVersion: 1\nChain ID: 0\nNonce: 3D0sZfnHqTBmc9tKR\nIssued At: 2026-01-01T00:00:00.000Z\nExpiration Time: 2026-01-01T00:10:00.000Z", "signature": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }' ``` ```ts fetch('https://api.tempo.xyz/v1/auth/siwe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: '0x0000000000000000000000000000000000000001', message: 'api.tempo.xyz wants you to sign in with your Ethereum account: 0x0000000000000000000000000000000000000000 URI: https://api.tempo.xyz Version: 1 Chain ID: 0 Nonce: 3D0sZfnHqTBmc9tKR Issued At: 2026-01-01T00:00:00.000Z Expiration Time: 2026-01-01T00:10:00.000Z', signature: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }) }) ``` ## Authenticate identity `POST /v1/auth/identity` Verifies a Wallet-issued OpenID Connect identity token and establishes a session. ### Request body (required) (`application/json`) - `idToken` `string` _(required)_: Wallet-issued OpenID Connect identity token. ### Responses #### `200`: Session established. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Set-Cookie` `string`: Session cookie (tempo_identity). #### `400`: Malformed request body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Identity verification failed. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/auth/identity \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "idToken": "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIweDAwMDAifQ.c2lnbmF0dXJl" }' ``` ```ts fetch('https://api.tempo.xyz/v1/auth/identity', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken: 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIweDAwMDAifQ.c2lnbmF0dXJl' }) }) ``` ## Sign out `POST /v1/auth/logout` Revokes auth sessions and clears their cookies. ### Responses #### `204`: Sessions revoked and cookies cleared. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/auth/logout \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/auth/logout', { method: 'POST' }) ``` # Balances How much of each token an account holds. ## List address balances `GET /v1/addresses/{address}/balances` Lists how much of each TIP-20 token an account holds, ordered from largest to smallest balance. Amounts are returned in raw base units and human-readable form. Pass `valuation.currency` to include nominal values. ### Path parameters - `address` `string` _(required)_: The account address whose token balances you want to list. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `currency` `string`: Only include verified tokens denominated in this currency (e.g. `USD`). Case-insensitive. Implies `verified=true` because the balances snapshot does not store currency for unverified tokens. Unknown currency strings are accepted but match no rows. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `feeEligible` `boolean`: When `true`, include only tokens eligible to pay transaction fees on Tempo: verified tokens preferred by an active validator or backed by a liquid pool against one, plus the default fee token (`pathUSD`). - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `valuation.currency` `string`: When present, include each holding’s nominal value in this denomination. Case-insensitive and must be priced by the configured FX oracle. - `verified` `boolean`: When `true`, include only tokens from Tempo’s curated verified token list. ### Responses #### `200`: A page of token balances for this account. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The balances in this page, ordered from largest to smallest amount. - `amount` `string` _(required)_: A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token. - `currency` `string` _(required)_: The currency label for this balance, such as `USD` for a USD-denominated token. - `decimals` `integer` _(required)_: The number of decimal places used to convert this balance between base units and human-readable form. - `feeEligible` `boolean`: Whether this token can be used to pay transaction fees on Tempo: `true` when the token is verified and preferred by an active validator or backed by a liquid pool against one, or is the default fee token (`pathUSD`). Omitted when the lookup is unavailable. - `formatted` `string` _(required)_: The same balance in human-readable decimal form, using this balance’s `decimals`. - `id` `string` _(required)_: A stable resource ID for this balance, equal to the token contract address. - `token` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `valuation` `object`: The holding’s nominal value when `valuation.currency` is requested, or `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `meta` `object`: Page-level resources: opt-in counts and valuation rate provenance. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `totalCount` `integer`: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `totalCountCapped` `boolean`: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read balance or token data from an upstream service. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/balances?chainId=4217¤cy=EUR&cursor=WzIzNDU2Nzg5LDBd&feeEligible=true&include=totalCount&limit=10&page=1&valuation.currency=AUD&verified=true' ``` ```ts fetch('https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/balances?chainId=4217¤cy=EUR&cursor=WzIzNDU2Nzg5LDBd&feeEligible=true&include=totalCount&limit=10&page=1&valuation.currency=AUD&verified=true') ``` ## Get address valuation `GET /v1/addresses/{address}/valuation` Values every verified TIP-20 token an account holds as one unit of its display currency, converts each currency into the requested denomination using the configured FX rates, and returns the total. ### Path parameters - `address` `string` _(required)_: The account address whose holdings you want to value. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `currency` `string`: Currency to denominate the account value in. Defaults to `USD` and must be priced by the configured FX oracle. ### Responses #### `200`: The valuation of this account’s verified holdings. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string` _(required)_: The account address this valuation covers. - `amount` `string` _(required)_: The total value of the account’s verified holdings, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of `amount`. - `id` `string` _(required)_: A stable resource ID for this valuation, equal to the account address. - `pricing` `object` _(required)_: Rate provenance, or `null` when every valued holding was already denominated in `currency`. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `unpriced` `string[]` _(required)_: Display currencies excluded from `amount` because the FX oracle has no rate for them. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read balances or exchange rates from an upstream service. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/valuation?chainId=4217¤cy=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/valuation?chainId=4217¤cy=AUD') ``` ## Get address balance `GET /v1/addresses/{address}/balances/{token}` Returns the live balance of one TIP-20 token held by an account, including token metadata and a human-readable amount. ### Path parameters - `address` `string` _(required)_: The account address whose token balance you want to read. - `token` `string` _(required)_: The TIP-20 token contract address. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `valuation.currency` `string`: When present, include the holding’s nominal value in this denomination. Case-insensitive and must be priced by the configured FX oracle. ### Responses #### `200`: The requested token balance for this account. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `amount` `string` _(required)_: A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token. - `currency` `string` _(required)_: The currency label for this balance, such as `USD` for a USD-denominated token. - `decimals` `integer` _(required)_: The number of decimal places used to convert this balance between base units and human-readable form. - `feeEligible` `boolean`: Whether this token can be used to pay transaction fees on Tempo: `true` when the token is verified and preferred by an active validator or backed by a liquid pool against one, or is the default fee token (`pathUSD`). Omitted when the lookup is unavailable. - `formatted` `string` _(required)_: The same balance in human-readable decimal form, using this balance’s `decimals`. - `id` `string` _(required)_: A stable resource ID for this balance, equal to the token contract address. - `token` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `valuation` `object`: The holding’s nominal value when `valuation.currency` is requested, or `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: The token is not a registered TIP-20 token. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read balance or token data from an upstream service. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/balances/0x20c0000000000000000000000000000000000000?chainId=4217&valuation.currency=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/balances/0x20c0000000000000000000000000000000000000?chainId=4217&valuation.currency=AUD') ``` # Billing Organization billing backed by Stripe. ## Get billing `GET /v1/orgs/{orgId}/billing` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. ### Responses #### `200`: The organization's billing state. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `enabledSources` `string[]` _(required)_: Billing sources this organization may set up. - `spend` `object`: Billable production spend committed in the current window. - `amount` `string` _(required)_: Committed spend as a decimal string: finalized fees plus in-flight fee caps. - `currency` `string` _(required)_: Currency of the spend figure. - `period` `string` _(required)_: Window the figure covers. - `spendLimit` `object`: Configured spend limit; absent when the organization has none. - `amount` `string` _(required)_: Limit as a positive decimal string in `currency` units. - `currency` `string`: Currency the limit is denominated in; `usd` only today. - `period` `string`: Window the limit applies over; `month` (UTC calendar) only today. - `status` `string` _(required)_: Billing status derived from the organization's billing source; `active` opens production fee sponsorship. - `txFeeLimit` `object`: Configured per-transaction fee cap; absent when the platform default applies. - `amount` `string` _(required)_: Cap as a positive decimal string in `currency` units. - `currency` `string`: Currency the cap is denominated in; `usd` only today. - `updatedAt` `string `: When the billing status last changed (ISO 8601); absent before first checkout. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing?environment=sandbox' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing?environment=sandbox') ``` ## Update billing `PATCH /v1/orgs/{orgId}/billing` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. ### Request body (required) (`application/json`) - `spendLimit` `object`: Spend limit per period; absent leaves it unchanged, null removes it. - `amount` `string` _(required)_: Limit as a positive decimal string in `currency` units. - `currency` `string`: Currency the limit is denominated in; `usd` only today. - `period` `string`: Window the limit applies over; `month` (UTC calendar) only today. - `txFeeLimit` `object`: Per-transaction fee cap; absent leaves it unchanged, null restores the platform default. - `amount` `string` _(required)_: Cap as a positive decimal string in `currency` units. - `currency` `string`: Currency the cap is denominated in; `usd` only today. ### Responses #### `200`: The organization's updated billing state. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `enabledSources` `string[]` _(required)_: Billing sources this organization may set up. - `spend` `object`: Billable production spend committed in the current window. - `amount` `string` _(required)_: Committed spend as a decimal string: finalized fees plus in-flight fee caps. - `currency` `string` _(required)_: Currency of the spend figure. - `period` `string` _(required)_: Window the figure covers. - `spendLimit` `object`: Configured spend limit; absent when the organization has none. - `amount` `string` _(required)_: Limit as a positive decimal string in `currency` units. - `currency` `string`: Currency the limit is denominated in; `usd` only today. - `period` `string`: Window the limit applies over; `month` (UTC calendar) only today. - `status` `string` _(required)_: Billing status derived from the organization's billing source; `active` opens production fee sponsorship. - `txFeeLimit` `object`: Configured per-transaction fee cap; absent when the platform default applies. - `amount` `string` _(required)_: Cap as a positive decimal string in `currency` units. - `currency` `string`: Currency the cap is denominated in; `usd` only today. - `updatedAt` `string `: When the billing status last changed (ISO 8601); absent before first checkout. #### `400`: Malformed API key, invalid path, invalid query, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing?environment=sandbox' \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{}' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing?environment=sandbox', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }) ``` ## Get payment methods `GET /v1/orgs/{orgId}/billing/payment-methods` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. ### Responses #### `200`: The organization's payment methods. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Payment methods on file, newest first. - `card` `object`: Card details; present when `type` is `card`. - `brand` `string` _(required)_: Card brand (e.g. `visa`). - `expMonth` `number` _(required)_: Expiry month (1-12). - `expYear` `number` _(required)_: Expiry year. - `last4` `string` _(required)_: Last four digits. - `createdAt` `string ` _(required)_: When the method was attached (ISO 8601). - `default` `boolean` _(required)_: Whether invoices charge this method. - `id` `string` _(required)_: The payment method id (`pm_…`). - `provider` `string` _(required)_: Payment provider; `stripe` only today. - `type` `string` _(required)_: Method type (e.g. `card`, `us_bank_account`). #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Billing is not configured on this deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/payment-methods?environment=sandbox' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/payment-methods?environment=sandbox') ``` ## Create checkout session `POST /v1/orgs/{orgId}/billing/stripe/checkout` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. ### Responses #### `200`: A Stripe Checkout (setup mode) session for attaching a payment method. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `url` `string ` _(required)_: Hosted Stripe session URL; navigate the browser here. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Requires the owner role and Stripe billing enabled. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Billing is not configured on this deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/stripe/checkout?environment=sandbox' \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/stripe/checkout?environment=sandbox', { method: 'POST' }) ``` ## Create manage session `POST /v1/orgs/{orgId}/billing/stripe/manage` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. ### Responses #### `200`: A Stripe billing-portal session for managing payment methods and invoices. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `url` `string ` _(required)_: Hosted Stripe session URL; navigate the browser here. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or billing account was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Billing is not configured on this deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/stripe/manage?environment=sandbox' \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/stripe/manage?environment=sandbox', { method: 'POST' }) ``` ## Preview billing invoice `GET /v1/orgs/{orgId}/billing/invoice-preview` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. ### Responses #### `200`: The next managed Stripe invoice estimate. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: Invalid path or query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Project credentials cannot read organization financial information. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Stripe is not configured for this environment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/invoice-preview?environment=sandbox' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/invoice-preview?environment=sandbox') ``` ## Remove payment method `DELETE /v1/orgs/{orgId}/billing/payment-methods/{methodId}` ### Path parameters - `methodId` `string` _(required)_: The payment method id (`pm_…`). - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Resource environment. ### Responses #### `200`: The organization's updated billing state. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `enabledSources` `string[]` _(required)_: Billing sources this organization may set up. - `spend` `object`: Billable production spend committed in the current window. - `amount` `string` _(required)_: Committed spend as a decimal string: finalized fees plus in-flight fee caps. - `currency` `string` _(required)_: Currency of the spend figure. - `period` `string` _(required)_: Window the figure covers. - `spendLimit` `object`: Configured spend limit; absent when the organization has none. - `amount` `string` _(required)_: Limit as a positive decimal string in `currency` units. - `currency` `string`: Currency the limit is denominated in; `usd` only today. - `period` `string`: Window the limit applies over; `month` (UTC calendar) only today. - `status` `string` _(required)_: Billing status derived from the organization's billing source; `active` opens production fee sponsorship. - `txFeeLimit` `object`: Configured per-transaction fee cap; absent when the platform default applies. - `amount` `string` _(required)_: Cap as a positive decimal string in `currency` units. - `currency` `string`: Currency the cap is denominated in; `usd` only today. - `updatedAt` `string `: When the billing status last changed (ISO 8601); absent before first checkout. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization, billing account, or payment method was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Billing is not configured on this deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/payment-methods/pm_1NVChw2eZvKYlo2CHxiM5E2N?environment=sandbox' \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/billing/payment-methods/pm_1NVChw2eZvKYlo2CHxiM5E2N?environment=sandbox', { method: 'DELETE' }) ``` # Blocks The ordered batches of transactions making up the chain. ## List blocks `GET /v1/blocks` List the ordered batches of transactions that make up Tempo. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. ### Responses #### `200`: A page of block summaries. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Blocks on this page. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this block. - `id` `string` _(required)_: Stable resource ID for this API response; it is the block hash. - `number` `integer` _(required)_: The block height, starting from genesis block 0. - `timestamp` `string ` _(required)_: Block timestamp formatted as ISO 8601. - `transactionCount` `integer` _(required)_: Number of transactions included in this block. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read indexed block data from TIDX. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/blocks?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&limit=10&order=desc&page=1' ``` ```ts fetch('https://api.tempo.xyz/v1/blocks?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&limit=10&order=desc&page=1') ``` ## Get a block by selector `GET /v1/blocks/{block}` Get one block by tag, number, hash, or timestamp selector. ### Path parameters - `block` `string | string ` _(required)_: Block selector: `latest`, `finalized`, a decimal or hex block number, a 32-byte block hash, or an ISO 8601 timestamp. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. ### Responses #### `200`: A single block with number, timestamp, gas usage, producer, and original JSON-RPC data. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `baseFeePerGas` `string` _(required)_: Base fee per gas for EIP-1559-style fee markets, or `null` when unsupported. - `gasLimit` `integer` _(required)_: Maximum gas available for all transactions in this block. - `gasUsed` `integer` _(required)_: Total gas used by transactions in this block. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this block. - `id` `string` _(required)_: Stable resource ID for this API response; it is the block hash. - `meta` `object` _(required)_: The original JSON-RPC payload plus any resources requested with `include`. - `rpc` `object` _(required)_: The original JSON-RPC block payload. - `baseFeePerGas` `string`: Base fee per gas for EIP-1559-style fee markets, if present. - `difficulty` `string`: Block difficulty value from legacy proof-of-work fields, when present. - `extraData` `string`: Extra data bytes included by the block producer. - `gasLimit` `string` _(required)_: Maximum gas available for all transactions in this block. - `gasUsed` `string` _(required)_: Total gas used by every transaction in this block. - `hash` `string` _(required)_: The block hash, or `null` for a pending block. - `logsBloom` `string`: Bloom filter summarizing logs in the block, or `null` while pending. - `miner` `string` _(required)_: The address of the block producer, also called the proposer or miner. - `mixHash` `string`: Consensus mix hash carried in the JSON-RPC block payload. - `nonce` `string`: Legacy block nonce, or `null` while pending. - `number` `string` _(required)_: The block number, or `null` for a pending block. - `parentHash` `string` _(required)_: The hash of the previous block in the chain. - `receiptsRoot` `string` _(required)_: Root hash of the receipts trie for this block. - `sha3Uncles` `string`: Keccak-256 hash of the uncle blocks list. - `size` `string` _(required)_: Size of the block in bytes. - `stateRoot` `string` _(required)_: Root hash of the world-state trie after this block. - `timestamp` `string` _(required)_: Block timestamp as Unix seconds. - `totalDifficulty` `string`: Total cumulative difficulty through this block, when present. - `transactions` `string[]` _(required)_: Transaction hashes included in this ordered batch. - `transactionsRoot` `string` _(required)_: Root hash of the transactions trie for this block. - `uncles` `string[]`: Hashes of uncle blocks included in this block, when present. - `withdrawals` `object[]`: EIP-4895 withdrawals included in this block, when present. - `withdrawalsRoot` `string`: Root hash of the withdrawals trie, when present. - `miner` `string` _(required)_: The address of the block producer, also called the proposer or miner. - `number` `integer` _(required)_: The block height, starting from genesis block 0. - `parentHash` `string` _(required)_: The hash of the previous block in the chain. - `size` `integer` _(required)_: Size of the block in bytes. - `timestamp` `string ` _(required)_: Block timestamp formatted as ISO 8601. - `transactionCount` `integer` _(required)_: Number of transactions included in this block. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No block matched the selector, or no indexed block exists at or after the timestamp. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read block data from the upstream JSON-RPC node or TIDX. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/blocks/1000000?chainId=4217' ``` ```ts fetch('https://api.tempo.xyz/v1/blocks/1000000?chainId=4217') ``` # CoinGecko Exchange data in CoinGecko's GeckoTerminal format. ## List trading pairs `GET /gecko/{chainId}/pairs` Lists Tempo stablecoin DEX trading pairs in GeckoTerminal format. ### Path parameters - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). ### Query parameters - `limit` `integer`: Maximum number of pairs to return. Defaults to 50 and is capped at 500. ### Responses #### `200`: Trading pairs in GeckoTerminal format. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `pairs` `object[]` _(required)_: Trading pairs ordered by creation block. - `id` `string` _(required)_: 32-byte onchain order-book key for this trading pair. - `dexKey` `string` _(required)_: Stable identifier for Tempo’s DEX venue. - `asset0Id` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `asset1Id` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `token0` `object` _(required)_: One token side of a trading pair. - `address` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `name` `string` _(required)_: Human-readable token name. - `symbol` `string` _(required)_: Short token ticker symbol. - `decimals` `integer` _(required)_: Number of decimal places the token uses. - `totalSupply` `string` _(required)_: Total token supply as a decimal string, already adjusted for token decimals. - `token1` `object` _(required)_: One token side of a trading pair. - `address` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `name` `string` _(required)_: Human-readable token name. - `symbol` `string` _(required)_: Short token ticker symbol. - `decimals` `integer` _(required)_: Number of decimal places the token uses. - `totalSupply` `string` _(required)_: Total token supply as a decimal string, already adjusted for token decimals. - `reserve0` `string` _(required)_: Best-effort current base-side reserve as a decimal string. - `reserve1` `string` _(required)_: Best-effort current quote-side reserve as a decimal string. - `createdAtBlockNumber` `integer`: Block number where the pair was created, when indexed. - `createdAtBlockTimestamp` `integer`: Unix timestamp for when the pair was created, when indexed. - `createdAtTxnId` `string`: Transaction hash that created the pair, when indexed. #### `400`: The chain id or `limit` query parameter is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Tempo RPC or the upstream indexer could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/gecko/4217/pairs?limit=50' ``` ```ts fetch('https://api.tempo.xyz/gecko/4217/pairs?limit=50') ``` ## List swap events `GET /gecko/{chainId}/events` Returns Tempo DEX swap events over a block range in GeckoTerminal format. ### Path parameters - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). ### Query parameters - `fromBlock` `integer`: First block to include. Defaults to `toBlock` minus 1000. - `toBlock` `integer`: Last block to include. Defaults to the latest indexed block. ### Responses #### `200`: Swap events in the requested block range. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `events` `object[]` _(required)_: Swap events in the requested block range, ordered by block and log index. - `block` `object` _(required)_: A Tempo block reference with its number and timestamp. - `blockNumber` `integer` _(required)_: Block number on Tempo. - `blockTimestamp` `integer` _(required)_: Block time as Unix seconds. - `eventType` `string` _(required)_: Event type; always `swap` for this feed. - `txnId` `string` _(required)_: Hash of the transaction that emitted the swap event. - `txnIndex` `integer` _(required)_: Zero-based position of the transaction within its block. - `eventIndex` `integer` _(required)_: Zero-based position of this swap within its transaction. - `maker` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `pairId` `string` _(required)_: On-chain order-book key for the trading pair. - `asset0In` `string`: Base token (`asset0`) amount sent into the swap, when applicable. - `asset1In` `string`: Quote token (`asset1`) amount sent into the swap, when applicable. - `asset0Out` `string`: Base token (`asset0`) amount received from the swap, when applicable. - `asset1Out` `string`: Quote token (`asset1`) amount received from the swap, when applicable. - `priceNative` `string` _(required)_: Quote-per-base price as a 36-decimal-place string. - `reserves` `object` _(required)_: Best-effort current reserves for both sides of the pair. - `asset0` `string` _(required)_: Best-effort current base-token reserve. - `asset1` `string` _(required)_: Best-effort current quote-token reserve. #### `400`: The chain id or requested block range is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No indexed block was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The upstream Tempo indexer could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/gecko/4217/events?fromBlock=23456789&toBlock=23456999' ``` ```ts fetch('https://api.tempo.xyz/gecko/4217/events?fromBlock=23456789&toBlock=23456999') ``` ## Get latest indexed block `GET /gecko/{chainId}/latest-block` Returns the latest indexed Tempo block in GeckoTerminal format so DEX data tools know where to start polling. ### Path parameters - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). ### Responses #### `200`: Latest indexed Tempo block. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `block` `object` _(required)_: A Tempo block reference with its number and timestamp. - `blockNumber` `integer` _(required)_: Block number on Tempo. - `blockTimestamp` `integer` _(required)_: Block time as Unix seconds. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No indexed block was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The upstream Tempo indexer could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/gecko/4217/latest-block ``` ```ts fetch('https://api.tempo.xyz/gecko/4217/latest-block') ``` ## Get asset `GET /gecko/{chainId}/assets/{address}` Returns one TIP-20 asset by contract address in GeckoTerminal format. ### Path parameters - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). - `address` `string` _(required)_: TIP-20 token contract address for the asset. ### Responses #### `200`: A TIP-20 token in CoinGecko’s GeckoTerminal asset format. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `asset` `object` _(required)_: A TIP-20 token in CoinGecko’s GeckoTerminal asset format. - `id` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `name` `string` _(required)_: Human-readable token name. - `symbol` `string` _(required)_: Short token ticker symbol. - `decimals` `integer` _(required)_: Number of decimal places the token uses. - `totalSupply` `string` _(required)_: Total token supply as a decimal string, already adjusted for token decimals. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No TIP-20 asset was found for that address. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Tempo RPC could not resolve the requested asset. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/gecko/4217/assets/0x20c0000000000000000000000000000000000000 ``` ```ts fetch('https://api.tempo.xyz/gecko/4217/assets/0x20c0000000000000000000000000000000000000') ``` ## Get trading pair `GET /gecko/{chainId}/pairs/{pairId}` Returns one Tempo stablecoin DEX trading pair in GeckoTerminal format. ### Path parameters - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). - `pairId` `string` _(required)_: 32-byte onchain order-book key for this trading pair. ### Responses #### `200`: One trading pair in GeckoTerminal format. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `pair` `object` _(required)_: A Tempo stablecoin DEX trading pair in GeckoTerminal format. - `id` `string` _(required)_: 32-byte onchain order-book key for this trading pair. - `dexKey` `string` _(required)_: Stable identifier for Tempo’s DEX venue. - `asset0Id` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `asset1Id` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `token0` `object` _(required)_: One token side of a trading pair. - `address` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `name` `string` _(required)_: Human-readable token name. - `symbol` `string` _(required)_: Short token ticker symbol. - `decimals` `integer` _(required)_: Number of decimal places the token uses. - `totalSupply` `string` _(required)_: Total token supply as a decimal string, already adjusted for token decimals. - `token1` `object` _(required)_: One token side of a trading pair. - `address` `string` _(required)_: Checksummed address of the taker, usually the transaction sender. - `name` `string` _(required)_: Human-readable token name. - `symbol` `string` _(required)_: Short token ticker symbol. - `decimals` `integer` _(required)_: Number of decimal places the token uses. - `totalSupply` `string` _(required)_: Total token supply as a decimal string, already adjusted for token decimals. - `reserve0` `string` _(required)_: Best-effort current base-side reserve as a decimal string. - `reserve1` `string` _(required)_: Best-effort current quote-side reserve as a decimal string. - `createdAtBlockNumber` `integer`: Block number where the pair was created, when indexed. - `createdAtBlockTimestamp` `integer`: Unix timestamp for when the pair was created, when indexed. - `createdAtTxnId` `string`: Transaction hash that created the pair, when indexed. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No trading pair was found for that id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Tempo RPC or the upstream indexer could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/gecko/4217/pairs/0x44f7b8011db3e3647a530b4ff635726de5fafc8fa8ad10f0f31c0eb9dd52fc65 ``` ```ts fetch('https://api.tempo.xyz/gecko/4217/pairs/0x44f7b8011db3e3647a530b4ff635726de5fafc8fa8ad10f0f31c0eb9dd52fc65') ``` # Using the Tempo API Console [Tempo API Console](https://console.tempo.xyz) is the control UI for your Tempo API account. Use it to organize integrations, issue credentials, monitor API usage and sponsored transactions, configure billing, and manage access to your organization. :::info Access to Tempo API Console is currently invite-only. [Contact us](https://tempo.xyz/contact/) to request access. ::: ## How the console is organized | Resource | What it controls | | --- | --- | | **Organization** | The team, projects, billing settings, and aggregate usage for a company or group. | | **Project** | An app or integration and the API keys attributed to it. | | **Environment** | Separate production and sandbox credentials, usage, and billing state. | | **API key** | A project-scoped credential with selected permissions for calling Tempo APIs. | Most console pages show information across the organization. Use the project filter where available to narrow a page to one integration. ## Set up your first project :::steps ### Sign in to Tempo API Console Open [console.tempo.xyz](https://console.tempo.xyz/?to=/\:org/projects) and sign in. If you have access to more than one organization, choose the organization you want to configure from the organization menu. ### Create a project Select [**Projects**](https://console.tempo.xyz/?to=/\:org/projects), then [**New project**](https://console.tempo.xyz/?to=/\:org/new). Enter a name that identifies the app or integration using the API, then select **Create project**. Projects hold API keys and provide an attribution boundary for usage. Create separate projects when you need to track or revoke integrations independently. ### Start in sandbox Open the organization menu and select [**Switch to sandbox**](https://console.tempo.xyz/?to=/\:org/projects%3Fenv%3Dsandbox). The amber banner confirms that the console is showing sandbox state. Sandbox API keys are restricted to non-mainnet chains and default to testnet. Production and sandbox credentials, usage, and billing state are separate. ### Create an API key Select [**API Keys**](https://console.tempo.xyz/?to=/\:org/api-keys%3Fenv%3Dsandbox), then [**New key**](https://console.tempo.xyz/?to=/\:org/api-keys/new%3Fenv%3Dsandbox). Choose the project, give the key a descriptive name, and review its scopes before selecting **Create key**. The console selects the available self-service scopes by default. If you clear every scope, the key is created without access to any scoped API. ### Copy the API key Copy the plaintext token when the console reveals it and store it in your secret manager. The token is shown only once and cannot be recovered later. Never commit an API key to source control or place it in browser-delivered code. If you lose a token, create a replacement and revoke the old key. ### Make a test request Replace the placeholder below with the sandbox key you just created: ```bash curl 'https://api.tempo.xyz/v1/blocks' \ --header 'Authorization: Bearer tempo_sandbox:sk:...' ``` A sandbox key automatically targets testnet when the request does not specify `chainId`. See [API authentication](https://tempo.xyz/developers/docs/api/authentication) for other headers and chain-selection behavior. ### Review usage Select [**Usage**](https://console.tempo.xyz/?to=/\:org/usage%3Fenv%3Dsandbox) and choose your project from the filter. The page reports request volume, errors, sponsored transactions, high-volume routes, and usage by API key for the selected environment. ::: ## Continue configuring the console * [Projects and environments](https://tempo.xyz/developers/docs/api/console/projects-and-environments) — Organize integrations and keep sandbox activity separate from production. * [API keys](https://tempo.xyz/developers/docs/api/console/api-keys) — Create, store, rotate, and revoke project credentials. * [Usage and billing](https://tempo.xyz/developers/docs/api/console/usage-and-billing) — Monitor requests and sponsorship, then configure payment methods and limits. * [Teams and access](https://tempo.xyz/developers/docs/api/console/team) — Invite members and manage organization roles. # Tempo API conventions and base URL The Tempo API follows consistent formatting rules for identifiers, amounts, timestamps, and shared query parameters. This page is a quick reference for the patterns you'll encounter on every endpoint. For authentication and pagination, see the dedicated [Authentication](https://tempo.xyz/developers/docs/api/authentication) and [Pagination](https://tempo.xyz/developers/docs/api/pagination) pages. ## Tempo API base URL The Tempo API is served from a single base URL, with endpoints versioned under the `/v1` prefix: ``` https://api.tempo.xyz/v1 ``` For example: `https://api.tempo.xyz/v1/tokens`. See the [Versioning Policy](https://tempo.xyz/developers/docs/api/versioning-policy) for how versions, breaking changes, and deprecations work. ## Chain identifiers Tempo runs multiple chains. Select one per request with the `chainId` query parameter, which accepts a chain alias or a numeric chain id: | Chain | Alias | Chain id | | --- | --- | --- | | Mainnet | `mainnet` | `4217` | | Testnet | `testnet` | `42431` | ```bash curl 'https://api.tempo.xyz/v1/blocks?chainId=testnet' \ --header 'Authorization: Bearer tempo:sk:...' ``` `chainId` defaults to `mainnet` when omitted. A sandbox API key steers an omitted `chainId` to `testnet` — see [Authentication](https://tempo.xyz/developers/docs/api/authentication) for how key environments interact with chain selection. ## Resource identifiers On-chain identifiers are `0x`-prefixed lowercase hex strings. They are returned in lowercase, and are accepted case-insensitively on input. | Identifier | Format | Example | | --- | --- | --- | | Account address | `0x`-prefixed, 20 bytes (40 hex chars) | `0xbe058e1c4df8a4366a387bf595b284246a93039e` | | Token address | `0x`-prefixed, 20 bytes; TIP-20 tokens start with `0x20c` | `0x20c0000000000000000000008f5425160ebe5525` | | Transaction / block hash | `0x`-prefixed, 32 bytes (64 hex chars) | `0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665` | :::info Treat identifiers as opaque strings. Always compare them in lowercase, since the API normalizes them to lowercase on output. ::: ## Token amount formats Token amounts are returned as **decimal strings in the token's smallest unit**, never as numbers. This preserves full precision for large values that exceed the range of a JSON number. ```json { "amount": "1000000" } ``` The value above is `1000000` smallest units — for example, `1.00` of a 6-decimal stablecoin. Convert to a human-readable amount by dividing by `10` raised to the token's `decimals`. :::warning Never parse amounts into floating-point numbers. JSON floats lose precision for many values (for example, `0.1 + 0.2 !== 0.3`) and silently overflow for large token balances. Use a big-integer or arbitrary-precision decimal type. ::: Raw EVM quantities (such as gas values) use a `0x`-prefixed hexadecimal integer instead, for example `0x1a` (which is `26`). ## API timestamp formats All datetime fields are returned in UTC, formatted as ISO 8601 with a `Z` suffix: ``` YYYY-MM-DDTHH:MM:SSZ ``` For example: `2024-01-01T00:00:00Z`. Date-range query parameters accept the same ISO 8601 format. ## API query parameters A few conventions are shared by query parameters across endpoints: * **Booleans** are the literal strings `true` or `false` (for example, `?verified=true`). * **Opt-in fields** are requested with a comma-separated `include` parameter (for example, `?include=token,totalCount`). Expensive fields are computed only when you ask for them. The available values differ per endpoint and are documented on each operation. ## API request IDs Every response carries a `tempo-request-id` header that uniquely identifies the request. Error responses also echo it as `requestId` in the body. Include this value when contacting support so a request can be traced. ```http tempo-request-id: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9 ``` ## API error conventions Errors use a single, consistent envelope with a conventional HTTP status code and a stable, machine-readable `error.code` you branch on. See [Errors](https://tempo.xyz/developers/docs/api/errors) for the envelope and the full code catalog. ## API rate-limit conventions Every response carries standard `RateLimit-*` headers, and requests that exceed quota receive `429 Too Many Requests` with a `Retry-After` header. See [Rate Limits](https://tempo.xyz/developers/docs/api/rate-limits) for the full model and best practices. # Earn Vaults that earn yield from assets deposited on Tempo. ## List vaults `GET /v1/earn/vaults` Lists compatible earn vaults discovered from deployment events and resolved from current chain state. ### Query parameters - `asset` `string`: Only include vaults accepting this asset address. - `capability` `string[]`: Comma-separated capabilities that every returned vault must support. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `engine.type` `string`: Only include vaults with this inferred engine type. - `include` `string[]`: Comma-separated vault fields to include, such as `apy,tvl`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `apy.window` `string`: Rate measurement window. Selecting one includes `apy`; omission prefers `7d` and falls back to `1h` when unavailable. ### Responses #### `200`: A page of compatible deployed earn vaults. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Compatible deployed earn vaults in deployment order. - `access` `object`: Vault share-token access, when requested. - `status` `string` _(required)_: Vault share-token access status. - `apy` `object`: Annualized vault yield over the selected window, plus every available current Merkle reward APR. Null when no component is measurable. - `asOf` `string ` _(required)_: UTC instant the rate was measured to, rounded to a 15-minute bucket. - `methodology` `string` _(required)_: Identifier of the calculation that produced this rate. - `net` `string` _(required)_: Higher of observed vault APY and configured reward campaign targets. - `rewards` `object[]`: Enabled, registered Merkle rewards. Omitted when reward enrichment is unavailable. - `apr` `string` _(required)_: Current reward APR, or null before its first calculation. - `asset` `object` _(required)_: EarnShare token distributed by the reward campaign. - `address` `string` _(required)_: An account address — the `0x`-prefixed 20-byte identifier for a wallet or contract, returned in lowercase. - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). - `decimals` `integer` _(required)_: Reward asset decimal places. - `calculatedThrough` `integer`: Unix timestamp through which a calculated reward APR applies. - `campaignId` `string` _(required)_: EarnVault address that identifies the reward campaign. - `endsAt` `integer` _(required)_: Exclusive campaign end as a Unix timestamp. - `startsAt` `integer` _(required)_: Inclusive campaign start as a Unix timestamp. - `targetTotalApr` `string` _(required)_: Configured target for combined observed vault yield and Merkle rewards. - `vault` `string` _(required)_: Observed fee-aware vault APY before reward APR. - `window` `string` _(required)_: Window this rate covers. - `assetToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `capabilities` `object`: Vault capabilities, when requested. - `asyncRedeem` `boolean` _(required)_: Whether queued redemptions are supported. - `boundedRedeem` `boolean` _(required)_: Whether redemption accepts a minimum asset bound. - `deposit` `boolean` _(required)_: Whether asset deposits are supported. - `exactWithdraw` `boolean` _(required)_: Whether exact asset withdrawals are supported. - `inKindDeposit` `boolean` _(required)_: Whether venue-share deposits are supported. - `privateRouting` `boolean` _(required)_: Whether private routing is supported. - `redeem` `boolean` _(required)_: Whether immediate redemptions are supported. - `routerSwaps` `boolean` _(required)_: Whether router token swaps are supported. - `description` `string` _(required)_: Curated vault description, or null when the vault is not verified. - `engine` `object` _(required)_: Current vault engine. - `address` `string` _(required)_: Current vault engine contract address. - `type` `string` _(required)_: Inferred engine type, or null when it is unknown. - `venue` `string` _(required)_: Yield venue, or null when the engine does not expose one. - `id` `string` _(required)_: Stable vault id, equal to the lowercase vault address. - `instantLiquidity` `string` _(required)_: Assets the venue would release right now, capped at the vault backing, in asset base units. Null when the engine interface cannot report a reliable number. - `instantLiquidityValue` `object`: Instant liquidity valued in USD, when requested via `include=tvl`, or null when liquidity is unavailable or the asset has no priced display currency. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `label` `string` _(required)_: Vault display label. - `sharePrice` `object` _(required)_: Assets returned for one whole earn share, in the asset display currency, or null when the engine cannot quote an exit. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `shareToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `slug` `string` _(required)_: Curated URL slug, or null when the vault is not verified. - `state` `object` _(required)_: Live vault accounting state. - `depositsPaused` `boolean` _(required)_: Whether new deposits are paused. - `engineShares` `string` _(required)_: Venue shares held for the vault. - `feesActive` `boolean` _(required)_: Whether fees are configured and not emergency-disabled. - `isAccountingAligned` `boolean` _(required)_: Whether Earn share supply matches its asset backing. - `openRedeemRequestCount` `integer` _(required)_: Queued redemptions still awaiting settlement. - `totalAssets` `string` _(required)_: Asset value of the active backing. - `totalEarnShares` `string` _(required)_: Active Earn share supply. - `tvl` `object`: Total assets valued in USD, when requested via `include=tvl`, or null when the asset has no priced display currency. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `vaultAddress` `string` _(required)_: Earn vault contract address. - `verified` `boolean` _(required)_: Whether this vault is in the curated earn registry. - `zone` `object`: Curated private Zone route, when requested, or null when absent. - `chainId` `string | number` _(required)_: Private Zone chain id. - `inputTokens` `string[]` _(required)_: Tokens accepted by private deposits. - `name` `string` _(required)_: Private Zone name. - `outputTokens` `string[]` _(required)_: Tokens supported by private redemptions. - `zones` `object[]`: Curated private Zone routes, when requested. - `chainId` `string | number` _(required)_: Private Zone chain id. - `deploymentBlock` `integer` _(required)_: Parent-chain block that deployed the Earn router. - `earnRouter` `string` _(required)_: Parent-chain router for this Zone and Earn vault. - `inputTokens` `string[]` _(required)_: Tokens accepted by private deposits. - `name` `string` _(required)_: Private Zone name. - `outputTokens` `string[]` _(required)_: Tokens returned by private redemptions. - `meta` `object`: Rate provenance for valued fields. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read earn deployment or vault data. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/earn/vaults?asset=0xbe058e1c4df8a4366a387bf595b284246a93039e&capability=asyncRedeem,boundedRedeem,deposit,exactWithdraw,inKindDeposit,privateRouting,redeem,routerSwaps&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&engine.type=erc4626&include=access,apy,capabilities,token.logoUri,tvl,zone,zones&limit=10&apy.window=7d' ``` ```ts fetch('https://api.tempo.xyz/v1/earn/vaults?asset=0xbe058e1c4df8a4366a387bf595b284246a93039e&capability=asyncRedeem,boundedRedeem,deposit,exactWithdraw,inKindDeposit,privateRouting,redeem,routerSwaps&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&engine.type=erc4626&include=access,apy,capabilities,token.logoUri,tvl,zone,zones&limit=10&apy.window=7d') ``` ## List verified vaults `GET /v1/earn/vaults/verified` Lists registry-curated earn vaults after resolving their current onchain configuration. ### Query parameters - `asset` `string`: Only include vaults accepting this asset address. - `capability` `string[]`: Comma-separated capabilities that every returned vault must support. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `engine.type` `string`: Only include vaults with this inferred engine type. - `include` `string[]`: Comma-separated vault fields to include, such as `apy,tvl`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `apy.window` `string`: Rate measurement window. Selecting one includes `apy`; omission prefers `7d` and falls back to `1h` when unavailable. ### Responses #### `200`: A page of registry-curated earn vaults. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Registry-curated earn vaults ordered by vault address. - `access` `object`: Vault share-token access, when requested. - `status` `string` _(required)_: Vault share-token access status. - `apy` `object`: Annualized vault yield over the selected window, plus every available current Merkle reward APR. Null when no component is measurable. - `asOf` `string ` _(required)_: UTC instant the rate was measured to, rounded to a 15-minute bucket. - `methodology` `string` _(required)_: Identifier of the calculation that produced this rate. - `net` `string` _(required)_: Higher of observed vault APY and configured reward campaign targets. - `rewards` `object[]`: Enabled, registered Merkle rewards. Omitted when reward enrichment is unavailable. - `apr` `string` _(required)_: Current reward APR, or null before its first calculation. - `asset` `object` _(required)_: EarnShare token distributed by the reward campaign. - `address` `string` _(required)_: An account address — the `0x`-prefixed 20-byte identifier for a wallet or contract, returned in lowercase. - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). - `decimals` `integer` _(required)_: Reward asset decimal places. - `calculatedThrough` `integer`: Unix timestamp through which a calculated reward APR applies. - `campaignId` `string` _(required)_: EarnVault address that identifies the reward campaign. - `endsAt` `integer` _(required)_: Exclusive campaign end as a Unix timestamp. - `startsAt` `integer` _(required)_: Inclusive campaign start as a Unix timestamp. - `targetTotalApr` `string` _(required)_: Configured target for combined observed vault yield and Merkle rewards. - `vault` `string` _(required)_: Observed fee-aware vault APY before reward APR. - `window` `string` _(required)_: Window this rate covers. - `assetToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `capabilities` `object`: Vault capabilities, when requested. - `asyncRedeem` `boolean` _(required)_: Whether queued redemptions are supported. - `boundedRedeem` `boolean` _(required)_: Whether redemption accepts a minimum asset bound. - `deposit` `boolean` _(required)_: Whether asset deposits are supported. - `exactWithdraw` `boolean` _(required)_: Whether exact asset withdrawals are supported. - `inKindDeposit` `boolean` _(required)_: Whether venue-share deposits are supported. - `privateRouting` `boolean` _(required)_: Whether private routing is supported. - `redeem` `boolean` _(required)_: Whether immediate redemptions are supported. - `routerSwaps` `boolean` _(required)_: Whether router token swaps are supported. - `description` `string` _(required)_: Curated vault description, or null when the vault is not verified. - `engine` `object` _(required)_ - `address` `string` _(required)_: Current vault engine contract address. - `type` `string` _(required)_: Inferred vault engine type. - `venue` `string` _(required)_: Yield venue, or null when the engine does not expose one. - `id` `string` _(required)_: Stable vault id, equal to the lowercase vault address. - `instantLiquidity` `string` _(required)_: Assets the venue would release right now, capped at the vault backing, in asset base units. Null when the engine interface cannot report a reliable number. - `instantLiquidityValue` `object`: Instant liquidity valued in USD, when requested via `include=tvl`, or null when liquidity is unavailable or the asset has no priced display currency. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `label` `string` _(required)_: Vault display label. - `sharePrice` `object` _(required)_: Assets returned for one whole earn share, in the asset display currency, or null when the engine cannot quote an exit. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `shareToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `slug` `string` _(required)_: Stable curated vault slug. - `state` `object` _(required)_: Live vault accounting state. - `depositsPaused` `boolean` _(required)_: Whether new deposits are paused. - `engineShares` `string` _(required)_: Venue shares held for the vault. - `feesActive` `boolean` _(required)_: Whether fees are configured and not emergency-disabled. - `isAccountingAligned` `boolean` _(required)_: Whether Earn share supply matches its asset backing. - `openRedeemRequestCount` `integer` _(required)_: Queued redemptions still awaiting settlement. - `totalAssets` `string` _(required)_: Asset value of the active backing. - `totalEarnShares` `string` _(required)_: Active Earn share supply. - `tvl` `object`: Total assets valued in USD, when requested via `include=tvl`, or null when the asset has no priced display currency. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `vaultAddress` `string` _(required)_: Earn vault contract address. - `verified` `boolean` _(required)_: This vault is in the curated earn registry. - `zone` `object`: Curated private Zone route, when requested, or null when absent. - `chainId` `string | number` _(required)_: Private Zone chain id. - `inputTokens` `string[]` _(required)_: Tokens accepted by private deposits. - `name` `string` _(required)_: Private Zone name. - `outputTokens` `string[]` _(required)_: Tokens supported by private redemptions. - `zones` `object[]`: Curated private Zone routes, when requested. - `chainId` `string | number` _(required)_: Private Zone chain id. - `deploymentBlock` `integer` _(required)_: Parent-chain block that deployed the Earn router. - `earnRouter` `string` _(required)_: Parent-chain router for this Zone and Earn vault. - `inputTokens` `string[]` _(required)_: Tokens accepted by private deposits. - `name` `string` _(required)_: Private Zone name. - `outputTokens` `string[]` _(required)_: Tokens returned by private redemptions. - `meta` `object`: Rate provenance for valued fields. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read curated earn registry or vault data. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/earn/vaults/verified?asset=0xbe058e1c4df8a4366a387bf595b284246a93039e&capability=asyncRedeem,boundedRedeem,deposit,exactWithdraw,inKindDeposit,privateRouting,redeem,routerSwaps&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&engine.type=erc4626&include=access,apy,capabilities,token.logoUri,tvl,zone,zones&limit=10&apy.window=7d' ``` ```ts fetch('https://api.tempo.xyz/v1/earn/vaults/verified?asset=0xbe058e1c4df8a4366a387bf595b284246a93039e&capability=asyncRedeem,boundedRedeem,deposit,exactWithdraw,inKindDeposit,privateRouting,redeem,routerSwaps&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&engine.type=erc4626&include=access,apy,capabilities,token.logoUri,tvl,zone,zones&limit=10&apy.window=7d') ``` ## List account positions `GET /v1/earn/addresses/{address}/positions` Lists every earn vault where an account currently holds shares on the selected Tempo chain or private Zone. Lifetime cash flows are available on Tempo chains. ### Path parameters - `address` `string` _(required)_: Account whose earn positions to list. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated position fields to include, such as `earnings`. Earnings are unavailable when selecting a private Zone. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `valuation.currency` `string`: When present, include each position’s nominal value in this denomination. Case-insensitive and must be priced by the configured FX oracle. - `verified` `boolean`: When `true`, include only positions in registry-curated (verified) earn vaults. ### Responses #### `200`: A page of the account’s earn vault positions. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Vaults where the account holds earn shares. - `assetAmount` `object` _(required)_: Current asset value of the held shares, including fees, in `valueToken` units. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `assetToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `chainId` `string | number` _(required)_: Chain containing the held shares. - `id` `string` _(required)_: Stable resource ID for this position, equal to the vault address. - `lifetimeCashFlows` `object`: Lifetime deposits and completed withdrawals, when requested. - `shareAmount` `object` _(required)_: Earn share balance this account holds, in `shareToken` units. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `shareToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `valuation` `object`: The position’s nominal value when `valuation.currency` is requested, or `null` when the asset token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `valueToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `vaultAddress` `string` _(required)_: Earn vault contract address holding this position. - `verified` `boolean` _(required)_: Whether the vault is in the curated earn registry. - `meta` `object`: Page-level valuation rate provenance. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read earn deployment or position data. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/earn/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/positions?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=earnings&limit=10&valuation.currency=AUD&verified=true' ``` ```ts fetch('https://api.tempo.xyz/v1/earn/addresses/0xbe058e1c4df8a4366a387bf595b284246a93039e/positions?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=earnings&limit=10&valuation.currency=AUD&verified=true') ``` ## List share prices `GET /v1/earn/vaults/{vaultId}/share-prices` Lists daily historical share prices for one earn vault. ### Path parameters - `vaultId` `string` _(required)_: Earn vault contract address. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `from` `string ` _(required)_: First daily share-price observation, as an ISO 8601 timestamp. - `interval` `string`: Time between share-price observations. - `to` `string ` _(required)_: Last possible daily share-price observation, as an ISO 8601 timestamp. ### Responses #### `200`: Daily historical share prices for one earn vault. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Daily Earn vault share prices. - `sharePrice` `object` _(required)_: Assets returned for one whole Earn share at the observation block. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `timestamp` `string ` _(required)_: Requested daily observation timestamp. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No compatible earn vault was found at the supplied vault address. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read historical earn vault share prices. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/earn/vaults/0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a/share-prices?chainId=4217&from=2026-07-01T00:00:00.000Z&interval=day&to=2026-07-31T00:00:00.000Z' ``` ```ts fetch('https://api.tempo.xyz/v1/earn/vaults/0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a/share-prices?chainId=4217&from=2026-07-01T00:00:00.000Z&interval=day&to=2026-07-31T00:00:00.000Z') ``` ## Get vault `GET /v1/earn/vaults/{vaultId}` Resolves any compatible earn vault directly from current chain state and enriches it when curated. ### Path parameters - `vaultId` `string` _(required)_: Earn vault contract address. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated vault fields to include, such as `apy,tvl`. - `apy.window` `string`: Rate measurement window. Selecting one includes `apy`; omission prefers `7d` and falls back to `1h` when unavailable. ### Responses #### `200`: One compatible earn vault. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `access` `object`: Vault share-token access, when requested. - `status` `string` _(required)_: Vault share-token access status. - `apy` `object`: Annualized vault yield over the selected window, plus every available current Merkle reward APR. Null when no component is measurable. - `asOf` `string ` _(required)_: UTC instant the rate was measured to, rounded to a 15-minute bucket. - `methodology` `string` _(required)_: Identifier of the calculation that produced this rate. - `net` `string` _(required)_: Higher of observed vault APY and configured reward campaign targets. - `rewards` `object[]`: Enabled, registered Merkle rewards. Omitted when reward enrichment is unavailable. - `apr` `string` _(required)_: Current reward APR, or null before its first calculation. - `asset` `object` _(required)_: EarnShare token distributed by the reward campaign. - `address` `string` _(required)_: An account address — the `0x`-prefixed 20-byte identifier for a wallet or contract, returned in lowercase. - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). - `decimals` `integer` _(required)_: Reward asset decimal places. - `calculatedThrough` `integer`: Unix timestamp through which a calculated reward APR applies. - `campaignId` `string` _(required)_: EarnVault address that identifies the reward campaign. - `endsAt` `integer` _(required)_: Exclusive campaign end as a Unix timestamp. - `startsAt` `integer` _(required)_: Inclusive campaign start as a Unix timestamp. - `targetTotalApr` `string` _(required)_: Configured target for combined observed vault yield and Merkle rewards. - `vault` `string` _(required)_: Observed fee-aware vault APY before reward APR. - `window` `string` _(required)_: Window this rate covers. - `assetToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `capabilities` `object`: Vault capabilities, when requested. - `asyncRedeem` `boolean` _(required)_: Whether queued redemptions are supported. - `boundedRedeem` `boolean` _(required)_: Whether redemption accepts a minimum asset bound. - `deposit` `boolean` _(required)_: Whether asset deposits are supported. - `exactWithdraw` `boolean` _(required)_: Whether exact asset withdrawals are supported. - `inKindDeposit` `boolean` _(required)_: Whether venue-share deposits are supported. - `privateRouting` `boolean` _(required)_: Whether private routing is supported. - `redeem` `boolean` _(required)_: Whether immediate redemptions are supported. - `routerSwaps` `boolean` _(required)_: Whether router token swaps are supported. - `description` `string` _(required)_: Curated vault description, or null when the vault is not verified. - `engine` `object` _(required)_: Current vault engine. - `address` `string` _(required)_: Current vault engine contract address. - `type` `string` _(required)_: Inferred engine type, or null when it is unknown. - `venue` `string` _(required)_: Yield venue, or null when the engine does not expose one. - `id` `string` _(required)_: Stable vault id, equal to the lowercase vault address. - `instantLiquidity` `string` _(required)_: Assets the venue would release right now, capped at the vault backing, in asset base units. Null when the engine interface cannot report a reliable number. - `instantLiquidityValue` `object`: Instant liquidity valued in USD, when requested via `include=tvl`, or null when liquidity is unavailable or the asset has no priced display currency. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `label` `string` _(required)_: Vault display label. - `sharePrice` `object` _(required)_: Assets returned for one whole earn share, in the asset display currency, or null when the engine cannot quote an exit. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `shareToken` `object` _(required)_: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `slug` `string` _(required)_: Curated URL slug, or null when the vault is not verified. - `state` `object` _(required)_: Live vault accounting state. - `depositsPaused` `boolean` _(required)_: Whether new deposits are paused. - `engineShares` `string` _(required)_: Venue shares held for the vault. - `feesActive` `boolean` _(required)_: Whether fees are configured and not emergency-disabled. - `isAccountingAligned` `boolean` _(required)_: Whether Earn share supply matches its asset backing. - `openRedeemRequestCount` `integer` _(required)_: Queued redemptions still awaiting settlement. - `totalAssets` `string` _(required)_: Asset value of the active backing. - `totalEarnShares` `string` _(required)_: Active Earn share supply. - `tvl` `object`: Total assets valued in USD, when requested via `include=tvl`, or null when the asset has no priced display currency. - `amount` `string` _(required)_: Value in base units at `decimals` precision. - `currency` `string` _(required)_: Display currency of this value. - `decimals` `integer` _(required)_: Decimal places used to convert `amount` into `formatted`. - `formatted` `string` _(required)_: Value rendered in whole units. - `vaultAddress` `string` _(required)_: Earn vault contract address. - `verified` `boolean` _(required)_: Whether this vault is in the curated earn registry. - `zone` `object`: Curated private Zone route, when requested, or null when absent. - `chainId` `string | number` _(required)_: Private Zone chain id. - `inputTokens` `string[]` _(required)_: Tokens accepted by private deposits. - `name` `string` _(required)_: Private Zone name. - `outputTokens` `string[]` _(required)_: Tokens supported by private redemptions. - `zones` `object[]`: Curated private Zone routes, when requested. - `chainId` `string | number` _(required)_: Private Zone chain id. - `deploymentBlock` `integer` _(required)_: Parent-chain block that deployed the Earn router. - `earnRouter` `string` _(required)_: Parent-chain router for this Zone and Earn vault. - `inputTokens` `string[]` _(required)_: Tokens accepted by private deposits. - `name` `string` _(required)_: Private Zone name. - `outputTokens` `string[]` _(required)_: Tokens returned by private redemptions. - `meta` `object`: Rate provenance for valued fields. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No compatible earn vault was found at this address. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read earn vault data. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/earn/vaults/0xbe058e1c4df8a4366a387bf595b284246a93039e?chainId=4217&include=access,apy,capabilities,token.logoUri,tvl,zone,zones&apy.window=7d' ``` ```ts fetch('https://api.tempo.xyz/v1/earn/vaults/0xbe058e1c4df8a4366a387bf595b284246a93039e?chainId=4217&include=access,apy,capabilities,token.logoUri,tvl,zone,zones&apy.window=7d') ``` ## Get account position `GET /v1/earn/vaults/{vaultId}/positions/{address}` Gets an account’s direct or Zone-routed balances and asset value for one earn vault, optionally at an ISO 8601 timestamp. ### Path parameters - `address` `string` _(required)_: Account whose earn position to read. - `vaultId` `string` _(required)_: Earn vault contract address. ### Query parameters - `asOf` `string `: ISO 8601 timestamp at which to read the position. Resolves the latest indexed block at or before this time. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Include wallet-specific APY or current reward state, optionally with the latest recovery proof. ### Responses #### `200`: Current or historical earn position for one account in one vault. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `account` `string` _(required)_: Account whose earn position was read. - `assetAllowance` `string` _(required)_: Assets the vault may spend from a direct account, in asset base units. Always zero for Zone-routed positions, which use Zone withdrawals instead of approvals. - `assetBalance` `string` _(required)_: Assets held by this account at the observation block, in asset base units. - `assetToken` `string` _(required)_: TIP-20 asset token accepted by the vault or its selected Zone route. - `chainId` `string | number` _(required)_: Chain containing the account position. - `id` `string` _(required)_: Stable resource ID for this vault position, equal to the account address. - `shareAllowance` `string` _(required)_: Earn shares the vault may spend from a direct account, in share base units. Always zero for Zone-routed positions, which use Zone withdrawals instead of approvals. - `shareBalance` `string` _(required)_: Earn shares held by this account at the observation block, in share base units. - `shareToken` `string` _(required)_: TIP-20 token representing earn shares. - `value` `string` _(required)_: Asset value of this account’s earn shares at the observation block, including fees, in `valueToken` base units. - `valueToken` `string` _(required)_: TIP-20 asset denominating `value` on the vault’s parent chain. - `apy` `object`: Wallet-specific effective APY when requested, or null when unavailable. Qualification comes from the latest delivered reward snapshot and may lag recent deposits or transfers. - `breakdown` `object[]` _(required)_: Position amounts grouped by the annual rate they receive. - `assetAmount` `object` _(required)_: Vault asset amount receiving this annual rate. - `baseUnits` `string` _(required)_: Position amount in vault asset base units. - `decimals` `integer` _(required)_: Vault asset decimal places. - `formatted` `string` _(required)_: Position amount rendered in whole vault asset units. - `net` `string` _(required)_: Annual rate applied to this position amount. - `type` `string` _(required)_: Base vault yield or qualified depositor boost. - `net` `string` _(required)_: Balance-weighted annual rate for this wallet position. - `asOf` `string `: Requested historical observation time, when supplied. - `block` `object`: Indexed block used for a historical Earn position observation, when requested. - `number` `integer` _(required)_: Indexed observation block. - `timestamp` `string ` _(required)_: Timestamp of the indexed observation block. - `rewards` `object`: Current reward state when requested through `include`. - `cumulativeEntitlement` `string` _(required)_: Cumulative EarnShare entitlement. - `cumulativePaid` `string` _(required)_: Cumulative EarnShare paid. - `deferral` `string`: Current payout delivery exception. - `pending` `string` _(required)_: EarnShare still owed. - `proof` `object`: Permissionless recovery proof against the confirmed root. - `cumulativeAmount` `string` _(required)_: A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token (e.g. `1000000` is 1.00 of a 6-decimal stablecoin). - `distributorAddress` `string` _(required)_: An account address — the `0x`-prefixed 20-byte identifier for a wallet or contract, returned in lowercase. - `proof` `string[]` _(required)_ - `rootVersion` `string` _(required)_: A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token (e.g. `1000000` is 1.00 of a 6-decimal stablecoin). - `statementHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase — for example a transaction or block hash. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No compatible earn vault was found at the supplied vault address. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read the earn vault position. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/earn/vaults/0x4f94590b636f5878bce585e82379de81e1ec174f/positions/0xbe058e1c4df8a4366a387bf595b284246a93039e?asOf=2026-07-21T09:00:00.000Z&chainId=4217&include=apy,rewards,rewards.proof' ``` ```ts fetch('https://api.tempo.xyz/v1/earn/vaults/0x4f94590b636f5878bce585e82379de81e1ec174f/positions/0xbe058e1c4df8a4366a387bf595b284246a93039e?asOf=2026-07-21T09:00:00.000Z&chainId=4217&include=apy,rewards,rewards.proof') ``` ## Get vault earnings `GET /v1/earn/vaults/{vaultId}/earnings/{address}` Gets an account’s indexed asset value and earnings for lifetime history, the trailing 30 days, or shares still held. ### Path parameters - `address` `string` _(required)_: Account whose vault earnings to read. - `vaultId` `string` _(required)_: Earn vault contract address. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `period` `string`: Earnings period to calculate: trailing 30 days, shares still held, or lifetime history. ### Responses #### `200`: Cost-basis-aware earnings for one account in one vault. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No compatible earn vault was found at this address. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read the earn vault earnings. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/earn/vaults/0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a/earnings/0xbe058e1c4df8a4366a387bf595b284246a93039e?chainId=4217&period=lifetime' ``` ```ts fetch('https://api.tempo.xyz/v1/earn/vaults/0xf4ae63687d6753a78e7f551d2eda1d0d31a5ea3a/earnings/0xbe058e1c4df8a4366a387bf595b284246a93039e?chainId=4217&period=lifetime') ``` # Tempo API errors and response format The Tempo API uses conventional HTTP status codes to indicate the result of a request: * **`2xx`**: the request succeeded. * **`4xx`**: the request failed given the information provided (for example, a missing parameter or an unknown resource). * **`5xx`**: something went wrong on Tempo's side. ## API error response format Every error returns the same JSON envelope: ```json { "error": { "code": "token_not_found", "message": "No token exists at the given address.", "details": [ { "message": "Check the address and try again.", "path": ["param", "token"] } ] }, "requestId": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" } ``` | Field | Type | Description | | --- | --- | --- | | `error.code` | string | A short, stable, machine-readable code. Branch on this. | | `error.message` | string | A human-readable explanation. May change without notice. | | `error.details` | array | Present on validation errors. Each entry has a `message` and a `path` into your request, such as `["query", "limit"]`. | | `requestId` | string | The request's `tempo-request-id`. Include it when contacting support. | :::info Branch on `error.code`, never on `error.message`. Codes are stable; messages are human-readable text and may change at any time. New codes may be added within an existing category. See the [Versioning Policy](https://tempo.xyz/developers/docs/api/versioning-policy). ::: ## 400: Bad Request The request was malformed or contained invalid data. Validation errors include a `details` array pointing at the offending fields. | Code | Meaning | | --- | --- | | `query_invalid` | A query parameter is missing or invalid. | | `param_invalid` | A path parameter is invalid. | | `body_invalid` | The request body is missing fields or malformed. | | `address_invalid` | An account address is not a valid `0x` 20-byte address. | | `token_invalid` | A token address is not a valid TIP-20 token address. | | `symbol_invalid` | A token symbol is invalid. | | `pair_invalid` | A trading pair is invalid. | | `pair_id_invalid` | A pair identifier is invalid. | | `transaction_invalid` | A transaction hash or parameter is invalid. | | `order_invalid` | An order identifier or parameter is invalid. | | `block_invalid` | A block number or hash is invalid. | | `chain_id_invalid` | `chainId` is not a known alias or numeric chain id. | | `chain_id_unsupported` | `chainId` is valid but not served by this deployment. | | `url_invalid` | A supplied URL (such as a webhook target) is invalid. | | `api_key_malformed` | The API key token is malformed. | ## 401: Unauthorized Authentication failed or is required. See [Authentication](https://tempo.xyz/developers/docs/api/authentication). | Code | Meaning | | --- | --- | | `api_key_missing` | The endpoint requires a key and none was supplied. | | `api_key_invalid` | The API key is not recognized, or has been revoked. | | `unauthorized` | The request is not properly authenticated. | ## 402: Payment Required The request exceeded the anonymous quota and carried no valid payment. This response is **protocol-native**, not the JSON error envelope: read the `WWW-Authenticate` header for the challenge, settle it, and retry with an `Authorization: Payment` credential. See [paying per request with MPP](https://tempo.xyz/developers/docs/api/authentication). ## 403: Forbidden The credential is valid but not permitted to perform the request. | Code | Meaning | | --- | --- | | `api_key_forbidden` | The API key is missing a [scope](https://tempo.xyz/developers/docs/api/authentication) required by this endpoint. | | `api_key_ip_forbidden` | The API key has an IP allowlist, but the trusted client IP is missing or does not match it. | | `limit_exceeded` | A resource limit was reached (for example, the maximum number of webhooks). | ## 404: Not Found The requested resource does not exist, or is not available on the selected chain. | Code | Meaning | | --- | --- | | `not_found` | The route or resource does not exist. | | `token_not_found` | No token exists at the given address. | | `token_logo_not_found` | No logo is available for the token. | | `block_not_found` | No block matches the request. | | `transaction_not_found` | No transaction matches the hash. | | `receipt_not_found` | No receipt is available for the transaction. | | `order_not_found` | No order matches the request. | | `pair_not_found` | No trading pair matches the request. | | `verified_token_not_found` | The token is not on the verified list. | | `webhook_not_found` | No webhook matches the id. | | `delivery_not_found` | No webhook delivery matches the id. | | `webhooks_not_enabled` | Webhooks are not enabled on this deployment. | ## 429: Too Many Requests You exceeded the rate limit. The response carries a `Retry-After` header. See [Rate Limits](https://tempo.xyz/developers/docs/api/rate-limits). | Code | Meaning | | --- | --- | | `rate_limit_exceeded` | The request exceeded the quota for its window. | | `payment_required` | The request was over quota on an endpoint where payment is accepted but none was supplied. | ## 5xx: Tempo server errors Something went wrong on Tempo's side. These are transient. Retry with [exponential backoff](https://tempo.xyz/developers/docs/api/rate-limits#retry-rate-limited-requests-with-backoff). | Status | Code | Meaning | | --- | --- | --- | | `500` | `internal_error` | An unexpected error occurred. | | `502` | `upstream_error` | An upstream node or indexer failed to respond. | :::warning For non-idempotent requests (such as `POST`), a `5xx` response does not guarantee the operation failed; it may have succeeded on the backend. Treat the outcome as **unknown** and reconcile state before blindly retrying. Read requests (`GET`) are always safe to retry. ::: # Exchange Tempo's built-in stablecoin exchange: pairs, swaps, orders, prices. ## List swaps `GET /v1/exchange/swaps` List recent swaps on Tempo’s built-in stablecoin exchange. A swap trades one token for another and may contain multiple maker-order fills. ### Query parameters - `blockNumber.from` `integer`: Only include swaps at or after this block number. - `blockNumber.to` `integer`: Only include swaps at or before this block number. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated token fields to include, such as `token.logoUri,token.verified`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `maker` `string`: Only include swaps that filled against an order owned by this maker address. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `participant` `string`: Only include swaps where this address participated as the taker or as a maker whose order was filled. - `taker` `string`: Only include swaps initiated by this taker address. - `timestamp.from` `string `: Only include swaps at or after this ISO 8601 timestamp. - `timestamp.to` `string `: Only include swaps at or before this ISO 8601 timestamp. - `transactionHash` `string`: Only include swaps included in this transaction hash. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Responses #### `200`: A page of swaps from Tempo’s built-in stablecoin exchange. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Swaps returned on this page. - `blockNumber` `integer` _(required)_: Block number where the swap was included. - `destinationAmount` `object` _(required)_: A token amount carrying its nominal value in the requested denomination. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The amount’s nominal value in the requested `valuation.currency`. `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `destinationToken` `object` _(required)_: A token referenced by one side of a swap. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `filledAt` `string ` _(required)_: Block timestamp when the swap was filled onchain. - `fills` `object[]` _(required)_: Maker-order fills that make up this swap, ordered by execution `logIndex`. Exact-destination swaps can execute hops in reverse, so use `route` for the source-to-destination path. - `destinationAmount` `object` _(required)_: A token amount carrying its nominal value in the requested denomination. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The amount’s nominal value in the requested `valuation.currency`. `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `destinationToken` `object` _(required)_: A token referenced by one side of a swap. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `logIndex` `integer` _(required)_: Log index of this fill within its block. - `maker` `string` _(required)_: Maker address whose resting order was filled. - `orderId` `string` _(required)_: On-chain maker order id, returned as a decimal string. - `partialFill` `boolean` _(required)_: Whether this fill used only part of the maker order. - `price` `string` _(required)_: Quote-per-base price at fill time, returned as a fixed-decimal string with 5 decimal places. This value is direction-independent and works well for charts. - `sourceAmount` `object` _(required)_: A token amount carrying its nominal value in the requested denomination. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The amount’s nominal value in the requested `valuation.currency`. `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `sourceToken` `object` _(required)_: A token referenced by one side of a swap. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `id` `string` _(required)_: Stable API id built from the lowercase transaction hash and the first fill log index. - `logIndex` `integer` _(required)_: Block-wide log index for the swap, taken from its first fill. - `mode` `string` _(required)_: The amount type fixed by the swap call: `exactSource` for `swapExactAmountIn` or `exactDestination` for `swapExactAmountOut`. This is recovered from transaction calldata and is `null` when the swap did not come from a decodable swap call. - `rate` `string` _(required)_: Swap-level effective rate, calculated as destination amount divided by source amount from the taker’s perspective and returned with 5 decimal places. Compare it with `1` to see distance from peg. - `route` `string[]` _(required)_: Token path for the swap, from source through any intermediate tokens to destination. `route.length - 1` is the hop count; two entries means a direct swap. - `sourceAmount` `object` _(required)_: A token amount carrying its nominal value in the requested denomination. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The amount’s nominal value in the requested `valuation.currency`. `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `sourceToken` `object` _(required)_: A token referenced by one side of a swap. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `taker` `string` _(required)_: Taker address that initiated the swap. - `transactionHash` `string` _(required)_: Transaction hash for the transaction containing the swap. - `meta` `object`: Page-level resources, such as valuation rate provenance. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer or Tempo RPC could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/swaps?blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=token.logoUri,token.verified&limit=10&maker=0xbe058e1c4df8a4366a387bf595b284246a93039e&order=desc&participant=0xbe058e1c4df8a4366a387bf595b284246a93039e&taker=0xbe058e1c4df8a4366a387bf595b284246a93039e×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z&transactionHash=0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665&valuation.currency=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/swaps?blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=token.logoUri,token.verified&limit=10&maker=0xbe058e1c4df8a4366a387bf595b284246a93039e&order=desc&participant=0xbe058e1c4df8a4366a387bf595b284246a93039e&taker=0xbe058e1c4df8a4366a387bf595b284246a93039e×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z&transactionHash=0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665&valuation.currency=AUD') ``` ## List pairs `GET /v1/exchange/pairs` List trading pairs available on Tempo’s built-in stablecoin exchange. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `tokens,totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `sort` `string`: Sort key: `created` orders by when the pair was created, while `liquidity` ranks by the DEX-held base-token balance as a practical liquidity signal. ### Responses #### `200`: A page of exchange trading pairs. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Trading pairs returned on this page. - `base` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `blockNumber` `integer` _(required)_: Block number where the pair was created. - `id` `string` _(required)_: Stable API id for this pair; it is the same value as the onchain pair key. - `key` `string` _(required)_: Stable onchain pair identifier returned by the DEX precompile as `pairKey`. - `liquidity` `string`: Liquidity signal for the pair, present only when you request `sort=liquidity`. - `quote` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `timestamp` `string ` _(required)_: Block timestamp when the pair was created. - `transactionHash` `string` _(required)_: Transaction hash for the transaction that created the pair. - `meta` `object`: Response-level resources requested with `include`, such as `totalCount`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/pairs?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=tokens,totalCount&limit=10&order=desc&page=1&sort=created' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/pairs?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=tokens,totalCount&limit=10&order=desc&page=1&sort=created') ``` ## List orders `GET /v1/exchange/orders` List resting maker orders on Tempo’s built-in stablecoin exchange. These are open orders waiting in the orderbook. ### Query parameters - `base` `string`: Only include orders for the pair with this base token. The quote token is determined onchain from the base token, so `base` alone selects the pair; returns 404 if no pair exists. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `tokens,totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `maker` `string`: Only include orders placed by this maker address. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `side` `string`: Limit results to one side of the orderbook: `bid` orders pay quote for base, while `ask` orders sell base for quote. - `sort` `string`: Sort key: `tick` orders by price in standard book order, while `time` orders by block number and log index. ### Responses #### `200`: A page of resting maker orders. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Resting orders returned on this page. - `amount` `string` _(required)_: Original placed amount, returned as a decimal integer string in base-token smallest units. - `blockNumber` `integer` _(required)_: Block number where the order was placed. - `id` `string` _(required)_: Stable API id for this order; it is the same value as the onchain order id. - `logIndex` `integer` _(required)_: Log index of the order placement within its block. - `maker` `string` _(required)_: Maker address that owns the order. - `orderId` `string` _(required)_: On-chain order id, returned as a decimal string. - `placedAt` `string ` _(required)_: Block timestamp when the order was placed. - `price` `string` _(required)_: Quote-per-base limit price implied by `tick`, returned with 5 decimal places. This value is direction-independent; use `rate` for the taker-perspective ratio. - `rate` `string` _(required)_: Order limit price as a destination/source ratio from the taker’s perspective, returned with 5 decimal places. Use `price` for orderbook displays. - `remaining` `string` _(required)_: Unfilled remainder of `amount`, returned as a decimal integer string in base-token smallest units. This is always positive for rows in this response. - `side` `string` _(required)_: Orderbook side: `bid` pays quote for base, and `ask` sells base for quote. - `tick` `integer` _(required)_: Order tick as a signed offset from peg; one tick equals `1/priceScale`. - `transactionHash` `string` _(required)_: Transaction hash for the transaction that placed the order. - `pair` `object` _(required)_: The trading pair the order belongs to. - `base` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `key` `string` _(required)_: On-chain key for this `(base, quote)` trading pair. - `quote` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `meta` `object`: Response-level resources requested with `include`, such as `totalCount`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. - `truncated` `boolean` _(required)_: `true` when the `OrderPlaced` scan hit its hard cap, meaning the resting set was computed from only the most recent placements. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No exchange pair was found for the requested `base` filter. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/orders?base=0x20c000000000000000000000b9537d11c60e8b50&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=tokens,totalCount&limit=10&maker=0xbe058e1c4df8a4366a387bf595b284246a93039e&order=desc&page=1&side=bid&sort=tick' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/orders?base=0x20c000000000000000000000b9537d11c60e8b50&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=tokens,totalCount&limit=10&maker=0xbe058e1c4df8a4366a387bf595b284246a93039e&order=desc&page=1&side=bid&sort=tick') ``` ## List order fills `GET /v1/exchange/orders/{orderId}/fills` List fills for one order. A fill records an execution against the maker order. ### Path parameters - `orderId` `string` _(required)_: On-chain order id, returned as a decimal string. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. ### Responses #### `200`: A page of fills for one order. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Fills returned on this page. - `amountFilled` `string` _(required)_: Base-side amount filled by this event, returned as a decimal integer string in base-token smallest units. - `blockNumber` `integer` _(required)_: Block number where the fill was included. - `filledAt` `string ` _(required)_: Block timestamp when the fill landed onchain. - `id` `string` _(required)_: Stable API id built from the lowercase transaction hash and log index. - `logIndex` `integer` _(required)_: Log index of this fill within its block. - `orderId` `string` _(required)_: On-chain order id, returned as a decimal string. - `partialFill` `boolean` _(required)_: Whether this fill used only part of the maker order. - `taker` `string` _(required)_: Taker address that submitted the incoming order. - `transactionHash` `string` _(required)_: Transaction hash for the transaction containing the fill. - `meta` `object`: Response-level resources requested with `include`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/orders/1/fills?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&order=desc&page=1' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/orders/1/fills?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&order=desc&page=1') ``` ## Get pair `GET /v1/exchange/pairs/{base}` Get one trading pair by its base token address. On Tempo, the quote token for a pair is determined from the base token onchain. ### Path parameters - `base` `string` _(required)_: Base token address for the trading pair. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `tokens`. ### Responses #### `200`: Details for one exchange trading pair. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `base` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `blockNumber` `integer` _(required)_: Block number where the pair was created. - `id` `string` _(required)_: Stable API id for this pair; it is the same value as the onchain pair key. - `key` `string` _(required)_: Stable onchain pair identifier returned by the DEX precompile as `pairKey`. - `liquidity` `string`: Liquidity signal for the pair, present only when you request `sort=liquidity`. - `quote` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `timestamp` `string ` _(required)_: Block timestamp when the pair was created. - `transactionHash` `string` _(required)_: Transaction hash for the transaction that created the pair. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No exchange pair was found for that base token. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/pairs/0x20c000000000000000000000b9537d11c60e8b50?chainId=4217&include=tokens' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/pairs/0x20c000000000000000000000b9537d11c60e8b50?chainId=4217&include=tokens') ``` ## Get order `GET /v1/exchange/orders/{orderId}` Get one order by id, using live onchain state for fields such as the remaining amount. ### Path parameters - `orderId` `string` _(required)_: On-chain order id, returned as a decimal string. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `tokens`. ### Responses #### `200`: Details for one exchange order. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `amount` `string` _(required)_: Initial order size, returned as a decimal integer string in base-token smallest units. - `flipTick` `integer` _(required)_: Replacement `tick` used if this order auto-flips into a counter-order after filling. - `id` `string` _(required)_: Stable API id for this order; it is the same value as the onchain order id. - `isBid` `boolean` _(required)_: Order side as a boolean: `true` means the maker buys base, and `false` means the maker sells base. - `isFlipOrder` `boolean` _(required)_: Whether this order automatically becomes a counter-order after it fills. - `maker` `string` _(required)_: Maker address that owns the order. - `mode` `string` _(required)_: Taker-perspective mode for the order. `exactSource` applies to maker bids where the taker sells base; `exactDestination` applies to maker asks where the taker buys base. - `orderId` `string` _(required)_: On-chain order id, returned as a decimal string. - `pair` `object` _(required)_: The trading pair the order belongs to. - `base` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `key` `string` _(required)_: On-chain key for this `(base, quote)` trading pair. - `quote` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `price` `string` _(required)_: Quote-per-base price implied by `tick`, returned as a fixed-decimal string with 5 decimal places. - `rate` `string` _(required)_: Destination-to-source price ratio, returned as a fixed-decimal string with 5 decimal places. - `remaining` `string` _(required)_: Unfilled base-side amount still resting on the orderbook, returned as a decimal integer string. - `tick` `integer` _(required)_: On-chain signed tick, scaled by `1/priceScale`; use `price` for the decoded ratio. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No exchange order was found for that id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The Tempo RPC node could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/orders/1?chainId=4217&include=tokens' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/orders/1?chainId=4217&include=tokens') ``` ## Get pair OHLC `GET /v1/exchange/pairs/{base}/ohlc` Get OHLC price candles for one trading pair. OHLC means open, high, low, and close over each time bucket for charting. ### Path parameters - `base` `string` _(required)_: Base token address for the trading pair. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `tokens`. - `interval` `string`: Time size for each OHLC candle bucket. - `window` `string`: Rolling time window covered by the OHLC candles. ### Responses #### `200`: OHLC candle data for the pair. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `base` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `data` `object[]` _(required)_: OHLC candle buckets ordered oldest to newest; empty buckets are omitted. - `close` `string` _(required)_: Close price for the bucket: the latest fill rate, returned with 5 decimal places. - `fillCount` `integer` _(required)_: Number of fills included in this bucket. - `high` `string` _(required)_: High price for the bucket: the highest fill rate, returned with 5 decimal places. - `id` `string` _(required)_: Stable API id built from the pair key, candle interval, and bucket start time in Unix seconds. - `low` `string` _(required)_: Low price for the bucket: the lowest fill rate, returned with 5 decimal places. - `open` `string` _(required)_: Open price for the bucket: the earliest fill rate, returned with 5 decimal places. - `timestamp` `string ` _(required)_: ISO 8601 timestamp for the start of this candle bucket. - `volume` `object` _(required)_: Total filled volume in this bucket, split by pair side. - `base` `string` _(required)_: Total base-side amount filled in this bucket, in the token’s smallest units. - `quote` `string` _(required)_: Total quote-side amount filled in this bucket, reconstructed from each fill’s tick and returned in the token’s smallest units. - `interval` `string` _(required)_: Time size for each OHLC candle bucket. - `quote` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `truncated` `boolean` _(required)_: `true` when the fill scan hit its hard cap, meaning OHLC values were computed from only the most recent fills. - `window` `string` _(required)_: Rolling time window covered by the OHLC candles. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No exchange pair was found for that base token. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/pairs/0x20c000000000000000000000b9537d11c60e8b50/ohlc?chainId=4217&include=tokens&interval=1h&window=24h' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/pairs/0x20c000000000000000000000b9537d11c60e8b50/ohlc?chainId=4217&include=tokens&interval=1h&window=24h') ``` ## Get pair depth `GET /v1/exchange/pairs/{base}/depth` Get orderbook depth for one trading pair. Depth shows resting liquidity at price ticks on both sides of the book. ### Path parameters - `base` `string` _(required)_: Base token address for the trading pair. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `tokens`. - `levels` `integer`: Maximum number of non-empty price levels to return per side (1-200). ### Responses #### `200`: Orderbook depth data for the pair. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `asks` `object[]` _(required)_: Ask-side levels, where makers sell base and takers buy it, ordered from best lowest ask outward. - `cumulativeSize` `string` _(required)_: Running sum of `size` from the best populated tick outward to this level, in base-token smallest units. - `id` `string` _(required)_: Stable API id built from the pair key, orderbook side, and tick. - `price` `string` _(required)_: Tick converted to a price ratio with 5 decimal places. - `size` `string` _(required)_: Resting liquidity at this tick level, in base-token smallest units. - `tick` `integer` _(required)_: Signed price tick (`int16`), always a multiple of the DEX tick spacing. - `base` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. - `bids` `object[]` _(required)_: Bid-side levels, where makers buy base and takers sell it, ordered from best highest bid outward. - `cumulativeSize` `string` _(required)_: Running sum of `size` from the best populated tick outward to this level, in base-token smallest units. - `id` `string` _(required)_: Stable API id built from the pair key, orderbook side, and tick. - `price` `string` _(required)_: Tick converted to a price ratio with 5 decimal places. - `size` `string` _(required)_: Resting liquidity at this tick level, in base-token smallest units. - `tick` `integer` _(required)_: Signed price tick (`int16`), always a multiple of the DEX tick spacing. - `quote` `object` _(required)_: One side of a trading pair (with metadata when requested via `include=tokens`). - `address` `string` _(required)_: TIP-20 token contract address — the `0x`-prefixed 20-byte identifier for this token. - `currency` `string`: Human-readable currency code for the token, when known. - `decimals` `integer`: Number of decimal places the token uses; stablecoins on Tempo typically use 6. - `logoUri` `string`: URL for the token logo image, when one is available. - `name` `string`: Human-readable token name, such as `USD Coin`. - `symbol` `string`: Short token ticker symbol, such as `USDC`. - `verified` `boolean`: Whether Tempo has verified this token metadata. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No exchange pair was found for that base token. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The Tempo RPC node could not serve the exchange data right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/pairs/0x20c000000000000000000000b9537d11c60e8b50/depth?chainId=4217&include=tokens&levels=50' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/pairs/0x20c000000000000000000000b9537d11c60e8b50/depth?chainId=4217&include=tokens&levels=50') ``` ## Create quote `POST /v1/exchange/quotes` Attempts exchange providers in priority order and returns the first executable quote. This endpoint does not submit a transaction. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated token fields to include, such as `token.logoUri,token.verified`. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Request body (required) (`application/json`) - `account` `string`: Wallet that will sign and execute the swap. - `amount` `string` _(required)_: Pinned amount in base units: source amount for `exactSource`, destination amount for `exactDestination`. - `destinationToken` `string` _(required)_: TIP-20 token the swap receives. - `mode` `string` _(required)_: Which side of the swap keeps the requested amount exact. - `slippageBps` `integer` _(required)_: Allowed execution slippage in basis points, where 100 is 1%. - `sourceToken` `string` _(required)_: TIP-20 token the swap spends. ### Responses #### `200`: A provider-neutral quote and its next execution action. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No configured provider can price an executable exchange for these terms. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The Tempo RPC could not quote the swap or resolve token metadata. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/quotes?chainId=4217&include=token.logoUri,token.verified&valuation.currency=AUD' \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "amount": "1000000", "destinationToken": "0x20c000000000000000000000b9537d11c60e8b50", "mode": "exactSource", "slippageBps": 50, "sourceToken": "0x20c0000000000000000000008f5425160ebe5525" }' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/quotes?chainId=4217&include=token.logoUri,token.verified&valuation.currency=AUD', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: '1000000', destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', mode: 'exactSource', slippageBps: 50, sourceToken: '0x20c0000000000000000000008f5425160ebe5525' }) }) ``` ## Finalize quote `POST /v1/exchange/quotes/execute` Finishes a provider quote after the wallet supplies the requested typed-data signature. This endpoint does not submit a transaction. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated token fields to include, such as `token.logoUri,token.verified`. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Request body (required) (`application/json`) - `account` `string` _(required)_: Wallet that signed and will execute the swap. - `continuation` `string` _(required)_: Opaque state returned by the quote request. - `provider` `string` _(required)_: Provider that issued the continuation. - `signature` `string` _(required)_: 65-byte EIP-712 signature requested by the quote. ### Responses #### `200`: The final unsigned calls needed to execute the swap. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `provider` `string` _(required)_: Provider that built the transaction. - `transaction` `object` _(required)_: Unsigned Tempo transaction plan. - `calls` `object[]` _(required)_: Calls to execute in order. - `data` `string` _(required)_: ABI-encoded call data. - `to` `string` _(required)_: Contract or precompile that receives the call. - `value` `string` _(required)_: Native token value sent with the call. - `chainId` `string | number` _(required)_: Tempo chain where these calls execute. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The selected provider could not build the swap right now. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/exchange/quotes/execute?chainId=4217&include=token.logoUri,token.verified&valuation.currency=AUD' \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "account": "0xbe058e1c4df8a4366a387bf595b284246a93039e", "continuation": "eyJwcm92aWRlciI6InVuaXN3YXAifQ", "provider": "uniswap", "signature": "0x1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" }' ``` ```ts fetch('https://api.tempo.xyz/v1/exchange/quotes/execute?chainId=4217&include=token.logoUri,token.verified&valuation.currency=AUD', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ account: '0xbe058e1c4df8a4366a387bf595b284246a93039e', continuation: 'eyJwcm92aWRlciI6InVuaXN3YXAifQ', provider: 'uniswap', signature: '0x1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' }) }) ``` # Tempo API FAQ Use these answers to choose the right Tempo API surface and understand what each hosted service does. For parameters, responses, and errors, use the [Tempo REST API reference](https://tempo.xyz/developers/docs/api#tempo-rest-api-endpoints). ## What is the Tempo API? The Tempo API is Tempo's official hosted integration surface for stablecoin products. It provides indexed data, signed webhooks, asset route and exchange quotes, fee sponsorship, read-only SQL, and JSON-RPC access without requiring you to operate the underlying infrastructure. ## How is the Tempo API different from Tempo RPC? The Tempo API combines higher-level hosted services for indexed data, webhooks, quotes, sponsorship, and SQL. Tempo RPC is the lower-level [JSON-RPC interface](https://tempo.xyz/developers/docs/api/json-rpc) for querying chain state, calling contracts and precompiles, and submitting signed transactions. Use RPC when an Ethereum-compatible client needs direct chain access. ## Do I need a Tempo API key? Most read endpoints work without credentials at the public rate limit. Use [MPP or an API key](https://tempo.xyz/developers/docs/api/authentication) for more throughput; webhook management requires an API key. ## How do I monitor a stablecoin payment? Read the [transaction and receipt](https://tempo.xyz/developers/docs/api/transactions) to follow execution, then inspect its [transaction activities](https://tempo.xyz/developers/docs/api/activities#list-transaction-activities) to identify the stablecoin movement. Configure [webhooks](https://tempo.xyz/developers/docs/api/webhooks) for signed callbacks instead of polling, and review delivery history when you need to audit an attempt. ## Does the Tempo Routes API execute a transfer? No. The [Routes API](https://tempo.xyz/developers/docs/api/routes/quotes) compares live quotes across supported chains and providers, but it does not execute a transfer. Execute the chosen transfer separately. ## Does the Tempo Exchange API submit a transaction? No. The [Exchange API](https://tempo.xyz/developers/docs/api/exchange) creates a priced preview on Tempo's native stablecoin DEX and returns unsigned approval and swap calls. Sign and submit the returned calls to complete the exchange. ## Where do I manage Tempo API projects and keys? Use [Tempo API Console](https://tempo.xyz/developers/docs/api/console) to create projects, switch between production and sandbox, issue API keys, monitor usage, and configure billing. Console access is currently invite-only. # Faucet Test token routes for Tempo testnet accounts. ## Fund testnet account `POST /v1/orgs/{orgId}/faucet` Funds an account with the test tokens configured on Tempo testnet. This operation is available to signed-in organization members and is rate limited. ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Request body (required) (`application/json`) - `address` `string` _(required)_: An account address — the `0x`-prefixed 20-byte identifier for a wallet or contract, returned in lowercase. ### Responses #### `200`: The account was funded. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `tokens` `object[]` _(required)_: The configured test tokens minted to the account. - `amount` `object` _(required)_: Amount minted by the faucet. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `token` `object` _(required)_: A test token minted by the faucet. - `address` `string` _(required)_: The test token contract that minted funds. - `currency` `string` _(required)_: The token’s currency label. - `decimals` `integer` _(required)_: The decimal places used by the token. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The token’s symbol. - `transactionHash` `string` _(required)_: The transaction that minted this token. - `transactionHashes` `string[]` _(required)_: Transactions that minted the configured test tokens. #### `400`: The path or account address is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: The user or account faucet limit was exceeded. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/faucet \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "address": "0xbe058e1c4df8a4366a387bf595b284246a93039e" }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/faucet', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: '0xbe058e1c4df8a4366a387bf595b284246a93039e' }) }) ``` # Fee AMM Pools that convert stablecoins to pay fees. ## List pools `GET /v1/fee-amm/pools` Lists Fee AMM pools by mint activity, showing which stablecoin fee conversions have the most liquidity activity. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated token fields to include, such as `token.logoUri,token.verified`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. ### Responses #### `200`: List of Fee AMM pools. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Fee AMM pools ordered by mint count, most active first. - `createdAt` `string ` _(required)_: Timestamp when liquidity was first minted into this pool. - `id` `string` _(required)_: Stable resource id for the pool; this is the same value as `poolId`. - `lastMintAt` `string ` _(required)_: Timestamp when liquidity was most recently minted into this pool. - `mintCount` `integer` _(required)_: Number of liquidity mints into this pool. - `poolId` `string` _(required)_: Pool id computed as `keccak256(abi.encode(userToken, validatorToken))`, matching `FeeManager.getPoolId`. - `userAmount` `object`: Current user-token reserve. Omitted when the onchain read fails. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `userToken` `object` _(required)_: A token referenced by one side of a Fee AMM pool. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `validatorAmount` `object`: Current validator-token reserve. Omitted when the onchain read fails. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `validatorToken` `object` _(required)_: A token referenced by one side of a Fee AMM pool. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Tempo RPC or the upstream indexer could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/fee-amm/pools?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=token.logoUri,token.verified&limit=10&page=1' ``` ```ts fetch('https://api.tempo.xyz/v1/fee-amm/pools?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=token.logoUri,token.verified&limit=10&page=1') ``` ## List mints `GET /v1/fee-amm/mints` Lists liquidity mints into Fee AMM pools. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `userToken` `string`: Only include mints for pools where users paid fees with this token. - `validatorToken` `string`: Only include mints for pools where validators receive this token. ### Responses #### `200`: Page of Fee AMM liquidity mints. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Liquidity mints returned on this page. - `amountUserToken` `string`: Amount of the user-side token deposited, in base units. Only present on legacy-signature mints. - `amountValidatorToken` `string` _(required)_: Amount of the validator-side token deposited, in base units. - `blockNumber` `integer` _(required)_: Block number where the mint occurred. - `id` `string` _(required)_: Stable mint id built from the transaction hash and log index (`${transactionHash}-${logIndex}`). - `liquidity` `string` _(required)_: Amount of pool liquidity minted, as an integer string. - `logIndex` `integer` _(required)_: Log index of this mint within the block. - `minter` `string` _(required)_: Address that deposited tokens into the pool. - `recipient` `string`: Address that received the minted liquidity. Only present on current-signature mints. - `timestamp` `string ` _(required)_: Block timestamp when the mint occurred. - `transactionHash` `string` _(required)_: Hash of the transaction that emitted this mint. - `userToken` `object` _(required)_: A TIP-20 token used by a Fee AMM pool, with metadata when available. - `address` `string` _(required)_: TIP-20 token contract address used by this fee pool. - `currency` `string`: Display currency for the token, when available. - `decimals` `integer`: Number of decimal places the token uses. - `logoUri` `string`: URL for the token logo image, when available. - `name` `string`: Human-readable token name. - `symbol` `string`: Short token ticker symbol. - `verified` `boolean`: Whether Tempo has verified this token’s metadata. - `validatorToken` `object` _(required)_: A TIP-20 token used by a Fee AMM pool, with metadata when available. - `address` `string` _(required)_: TIP-20 token contract address used by this fee pool. - `currency` `string`: Display currency for the token, when available. - `decimals` `integer`: Number of decimal places the token uses. - `logoUri` `string`: URL for the token logo image, when available. - `name` `string`: Human-readable token name. - `symbol` `string`: Short token ticker symbol. - `verified` `boolean`: Whether Tempo has verified this token’s metadata. - `meta` `object`: Response-wide metadata requested with `include`, such as `totalCount`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The upstream Tempo indexer could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/fee-amm/mints?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&order=desc&page=1&userToken=0x20c0000000000000000000000000000000000000&validatorToken=0x20c0000000000000000000000000000000000000' ``` ```ts fetch('https://api.tempo.xyz/v1/fee-amm/mints?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&order=desc&page=1&userToken=0x20c0000000000000000000000000000000000000&validatorToken=0x20c0000000000000000000000000000000000000') ``` # Tempo Fee Payer API Use the Fee Payer API to cover transaction fees for your users. ## How the Fee Payer API works A fee payer pays the fee for a transaction sent by another account. Use one to cover transaction costs for your users. The sender authorizes the transaction, and the fee payer separately authorizes the fee. When you prepare a transaction, request sponsorship from a fee payer endpoint. The service applies its policy, fills the required fields, and reports whether the request was sponsored. Tempo supports this flow natively, so you do not need a paymaster contract, bundler, or EntryPoint contract. The Tempo API provides a hosted JSON-RPC relay that fills, sponsors, and broadcasts Tempo Transactions. Authenticate with a [Tempo API key](https://tempo.xyz/developers/docs/api/authentication), then use the Fee Payer API as your sponsorship RPC URL: ```text https://api.tempo.xyz/rpc/sponsor ``` ## Get Started ::::steps ### Sign into the Tempo Console Open the [Tempo Console](https://console.tempo.xyz/?to=/\:org/projects) and select **Sign in with Tempo**. :::info Tempo Console signups are currently limited. [Contact us](https://tempo.xyz/contact/) to request access. ::: ### Set up Organization Create an organization and project. ### Set up Billing For this testnet example, switch to [**Sandbox mode**](https://console.tempo.xyz/?to=/\:org/projects%3Fenv%3Dsandbox) (located in the top-left organization picker), then open [**Billing**](https://console.tempo.xyz/?to=/\:org/billing%3Fenv%3Dsandbox) and add a payment method. :::info For production, keep the Console in the production environment and do not switch to **Sandbox mode**. Production fee sponsorship requires active billing. ::: ### Get an API Key Open [**API Keys**](https://console.tempo.xyz/?to=/\:org/api-keys%3Fenv%3Dsandbox), select [**New key**](https://console.tempo.xyz/?to=/\:org/api-keys/new%3Fenv%3Dsandbox), choose your project, and create the key. Copy it when prompted; it is shown only once. ### Sponsor Transaction Fees Configure Viem's [`withRelay`](https://viem.sh/tempo/transports/withRelay) transport with a default RPC transport and the Tempo API sponsorship endpoint. Add your Tempo API key through the endpoint's `key` search parameter, then set `feePayer: true` when sending a transaction to request sponsorship. :::info For production, do not switch to **Sandbox mode**. Remove `testnet: true` and replace the sandbox API key with a production API key (`tempo:sk:...`). ::: ```ts twoslash [example.ts] import { Account, createClient, http, withRelay } from 'viem/tempo' const client = createClient({ account: Account.fromSecp256k1('0x...'), testnet: true, transport: withRelay( // [!code focus] http(), // default transport // [!code focus] http('https://api.tempo.xyz/rpc/sponsor?key=tempo_sandbox:sk:...'), // relay transport // [!code focus] ), // [!code focus] }) const receipt = await client.sendTransactionSync({ data: '0xdeadbeef', feePayer: true, // [!code focus] to: '0xcafebabecafebabecafebabecafebabecafebabe', }) ``` :::: ## Public testnet fee payer For demos and testnet development without an API key, use Tempo's public fee payer endpoint: ```text https://sponsor.moderato.tempo.xyz ``` Use the Fee Payer API above for authenticated sandbox and production integrations. ## Learn more * [Viem Fee Sponsorship](https://viem.sh/tempo/guides/sponsor-fees) — Use a local account or fee payer endpoint to sponsor transaction fees with Viem. * [Tempo API Authentication](https://tempo.xyz/developers/docs/api/authentication) — Authenticate fee payer requests with a production or sandbox API key. * [Sponsor User Fees](https://tempo.xyz/developers/docs/guide/payments/sponsor-user-fees) — Learn how Tempo fee sponsorship works across supported SDKs. * [Tempo Transaction Spec](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#fee-payer-signature-details) — Read the fee payer signature details in the Tempo Transaction specification. # Indexer Run read-only SQL queries against Tempo's indexed data. ## Query indexed chain data `GET /v1/indexer/query` Runs a read-only SQL-style query against Tempo’s indexed chain data, with optional live streaming as new blocks arrive. ### Query parameters - `sql` `string` _(required)_: Read-only SQL query to run against Tempo’s indexed chain data. Use `SELECT` statements only. - `chainId` `string | integer`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). - `signature` `string[]`: ABI event signature to expose as a named SQL CTE, so you can query decoded event fields directly. Repeat this parameter to add multiple signatures. - `engine` `string`: Choose the indexer engine yourself. By default the indexer routes automatically; `clickhouse` cannot be used with `live=true`. - `live` `boolean`: Stream results as Server-Sent Events and re-run the query on every new block. This cannot be combined with `engine=clickhouse`. - `limit` `integer`: Maximum number of rows to return. The server clamps this to the range 1 through 10,000. - `timeout_ms` `integer`: Per-query timeout in milliseconds. The server clamps this to the range 100 through 30,000. ### Responses #### `200`: Structured result from a Tempo indexer SQL query. When `live=true` the response is a `text/event-stream` (SSE) of `result` events with this same shape, interleaved with `error` and `lagged` events, re-run on every new block. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `ok` `boolean` _(required)_: Whether the SQL query completed successfully. - `columns` `string[]` _(required)_: Column names returned by the query, in row-value order. - `rows` `unknown[][]` _(required)_: Query results. Each row is an array of values in the same order as `columns`. - `row_count` `integer` _(required)_: Number of rows returned in this response. - `engine` `string`: Indexer engine that ran the query. - `query_time_ms` `number`: Server-side query execution time in milliseconds. #### `400`: The query is invalid, the API key is malformed, or the selected chain is invalid or unsupported. Query errors use the indexer envelope; Tempo errors use the standard API envelope. #### `401`: The API key is missing or invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: The API key cannot query the selected indexer. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `422`: The SQL failed validation or execution; this uses the upstream indexer’s error shape. Body (`application/json`): - `ok` `boolean` _(required)_: Always `false` when the indexer returns an error. - `error` `string` _(required)_: Human-readable error message from the indexer. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer could not complete the request. Indexer HTTP errors retain the indexer envelope; network failures use the Tempo envelope. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/indexer/query?sql=string&chainId=mainnet&signature=string&engine=postgres&live=false&limit=10000&timeout_ms=5000' ``` ```ts fetch('https://api.tempo.xyz/v1/indexer/query?sql=string&chainId=mainnet&signature=string&engine=postgres&live=false&limit=10000&timeout_ms=5000') ``` # Tempo Indexer API Use the Indexer API to run read-only SQL against Tempo chain data without operating your own indexing pipeline. The Tempo API exposes [`GET /v1/indexer/query`](https://tempo.xyz/developers/docs/api/indexer), backed by [`tidx`](https://github.com/tempoxyz/tidx). The indexer continuously follows Tempo, stores blocks, transactions, logs, and receipts, and maintains analytics tables for common reads. ## Querying the Indexer API Send a `GET` request to the Tempo API with a network and SQL query: ```bash curl --get "https://api.tempo.xyz/v1/indexer/query" \ --data-urlencode "sql=SELECT num, hash, timestamp FROM blocks ORDER BY num DESC LIMIT 5" ``` Requests use the same [authentication](https://tempo.xyz/developers/docs/api/authentication), [rate limits](https://tempo.xyz/developers/docs/api/rate-limits), and `RateLimit-*` headers as the rest of the Tempo API. Use public access for low-volume queries, MPP for pay-per-request access, or an API key for a higher dedicated quota. See the [indexer query reference](https://tempo.xyz/developers/docs/api/indexer) for the complete request and response contract, live streaming, and error behavior. ## Indexer API architecture `tidx` writes chain data to two stores and routes each query to the appropriate one: * **PostgreSQL** handles low-latency point lookups and recent data. It is also the source for live streaming. * **ClickHouse** handles full-history analytics, large scans, and pre-computed tables for tokens, holders, transfers, and DEX data. When tiered retention is enabled, PostgreSQL keeps a recent hot window and ClickHouse holds the full archive. Direct `tidx` queries use the tiered route by default, while `engine=clickhouse` selects native ClickHouse for analytical queries. Omit `engine` to let the Tempo API choose, or set `engine=clickhouse` for analytical queries. The sync engine runs a realtime loop that follows the chain head and a backfill loop that fills historical gaps, prioritizing recent gaps so new data becomes queryable quickly. ```text ╭───────────────╮ Tempo JSON-RPC ─▶│ tidx sync │ realtime + backfill │ engine │ ╰───────┬───────╯ │ writes in parallel ╭───────────┴───────────╮ ▼ ▼ ╭───────────────╮ ╭───────────────╮ │ PostgreSQL │ │ ClickHouse │ │ hot + point │ │ archive + │ │ reads + SSE │ │ analytics │ ╰───────┬───────╯ ╰───────┬───────╯ ╰───────────┬───────────╯ ▼ query router ``` ## Tempo data the indexer API covers Every Tempo block is decoded into base tables available through both stores: | Table | Contents | | --- | --- | | `blocks` | Block headers, including number, hash, parent hash, timestamp, gas, and proposer. | | `txs` | Transactions, including block, index, hash, type, sender, recipient, value, calldata, gas, nonce, and fee token. | | `logs` | EVM logs, including block, log index, transaction hash, contract, topics, and data. | | `receipts` | Receipts, including status, gas used, effective gas price, contract address, and fee payer. | ## Indexer API analytics tables With `engine=clickhouse`, the indexer also exposes pre-computed tables for common analytical reads: * **Tokens**: `token_balances`, `token_balances_snapshot`, `token_holder_counts`, `token_holder_deltas`, `token_metadata`, `token_supply`, `token_transfers`, `token_approvals`, `token_approvals_current`, and `token_transfer_stats`. * **Addresses**: `address_balances`, `address_balances_snapshot`, `address_transfers`, `address_txs`, and `address_holder_deltas`. * **DEX**: `dex_pairs`, `dex_orders`, `dex_fills`, `dex_ohlc_1m`, and `dex_pair_liquidity`. * **Contracts**: `contract_creations`. :::info The typed REST endpoints for tokens, balances, activity, and exchanges are the stable interface for common reads. Query the analytics tables directly when you need custom SQL that the REST endpoints do not cover. ::: ## Indexer API decoded events Read decoded event data in two ways: * **Pre-decoded tables**: common events are decoded into ClickHouse tables such as `token_transfers`, `token_approvals`, and the DEX tables. Prefer these when available. * **Query-time decoding**: pass a `signature` parameter to expose matching logs as a virtual table named after the event. This works with either engine. For example, query decoded `Transfer` events through the Tempo API: ```bash curl --get "https://api.tempo.xyz/v1/indexer/query" \ --data-urlencode "signature=Transfer(address indexed from, address indexed to, uint256 value)" \ --data-urlencode 'sql=SELECT "from", "to", value, block_num, tx_hash FROM Transfer ORDER BY block_num DESC LIMIT 5' ``` ## Interactive Example Run live SQL against the public hosted indexer. Use the interactive web page to run SQL against the public Tempo indexer. ## Run your own Tempo indexer The [`tidx` repository](https://github.com/tempoxyz/tidx) includes Docker, source build, configuration, CLI, schema, and materialized view documentation. ```bash git clone https://github.com/tempoxyz/tidx cd tidx docker pull ghcr.io/tempoxyz/tidx:latest docker run -v $(pwd)/config.toml:/config.toml ghcr.io/tempoxyz/tidx up ``` See the [`tidx` README](https://github.com/tempoxyz/tidx) for the full setup guide and CLI reference. ## Learn more * [Indexer Query Reference](https://tempo.xyz/developers/docs/api/indexer) — Review the Tempo API request parameters, response shapes, streaming, and errors. * [Tempo Explorer](https://explore.tempo.xyz) — Inspect blocks, transactions, accounts, and token activity. * [Connection Details](https://tempo.xyz/developers/docs/quickstart/connection-details) — Find RPC URLs, chain IDs, explorers, and network metadata. * [tidx Repository](https://github.com/tempoxyz/tidx) — Run and configure your own Tempo indexer. # Invitations Pending organization invitations. ## List my invitations `GET /v1/invitations` Pending invitations addressed to the session's verified email. ### Responses #### `200`: Pending invitations, newest first. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Pending invitations, newest first. - `createdAt` `string ` _(required)_: When the invitation was created (ISO 8601). - `email` `string ` _(required)_: Invitee email (lowercase). - `expiresAt` `string ` _(required)_: When the invitation expires (ISO 8601). - `id` `string` _(required)_: Opaque invitation id (`inv_…`). - `invitedBy` `string` _(required)_: User or API key id that created the invitation. - `orgId` `string` _(required)_: Organization id (`org_…`) the invitation joins. - `role` `string` _(required)_: Role granted when the invitation is accepted. - `orgName` `string` _(required)_: Human-readable name of the inviting organization. #### `400`: Malformed API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/invitations ``` ```ts fetch('https://api.tempo.xyz/v1/invitations') ``` ## List invitations `GET /v1/orgs/{orgId}/invitations` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: Pending invitations, newest first. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Pending invitations, newest first. - `createdAt` `string ` _(required)_: When the invitation was created (ISO 8601). - `email` `string ` _(required)_: Invitee email (lowercase). - `expiresAt` `string ` _(required)_: When the invitation expires (ISO 8601). - `id` `string` _(required)_: Opaque invitation id (`inv_…`). - `invitedBy` `string` _(required)_: User or API key id that created the invitation. - `orgId` `string` _(required)_: Organization id (`org_…`) the invitation joins. - `role` `string` _(required)_: Role granted when the invitation is accepted. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invitations ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invitations') ``` ## Create invitation `POST /v1/orgs/{orgId}/invitations` Invite an email to the organization with a role. The invitee accepts after signing in with that verified email. ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Request body (required) (`application/json`) - `email` `string ` _(required)_: Invitee email. - `role` `string` _(required)_: Role granted when the invitation is accepted. ### Responses #### `200`: The created invitation. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the invitation was created (ISO 8601). - `email` `string ` _(required)_: Invitee email (lowercase). - `expiresAt` `string ` _(required)_: When the invitation expires (ISO 8601). - `id` `string` _(required)_: Opaque invitation id (`inv_…`). - `invitedBy` `string` _(required)_: User or API key id that created the invitation. - `orgId` `string` _(required)_: Organization id (`org_…`) the invitation joins. - `role` `string` _(required)_: Role granted when the invitation is accepted. #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: The invitation email rate limit was exceeded. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invitations \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "email": "dev@example.com", "role": "member" }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invitations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'dev@example.com', role: 'member' }) }) ``` ## Accept invitation `POST /v1/invitations/{invitationId}/accept` Accept an invitation addressed to the session's verified email, joining the organization with the invited role. ### Path parameters - `invitationId` `string` _(required)_: The invitation id (`inv_…`). ### Responses #### `200`: The membership granted by accepting. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `orgId` `string` _(required)_: Organization id (`org_…`) joined. - `role` `string` _(required)_: Granted role. - `userId` `string` _(required)_: Member user id (`usr_…`). #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible pending invitation was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/invitations/inv_1a2b3c4d5e6f7g8h9j0k1m2n/accept \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/invitations/inv_1a2b3c4d5e6f7g8h9j0k1m2n/accept', { method: 'POST' }) ``` ## Decline invitation `POST /v1/invitations/{invitationId}/decline` Decline an invitation addressed to the session's verified email. Declining settles the invitation; it can no longer be accepted. ### Path parameters - `invitationId` `string` _(required)_: The invitation id (`inv_…`). ### Responses #### `200`: Confirmation that the invitation was declined. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: ID of the invitation that was declined. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible pending invitation was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/invitations/inv_1a2b3c4d5e6f7g8h9j0k1m2n/decline \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/invitations/inv_1a2b3c4d5e6f7g8h9j0k1m2n/decline', { method: 'POST' }) ``` ## Revoke invitation `DELETE /v1/orgs/{orgId}/invitations/{invitationId}` ### Path parameters - `invitationId` `string` _(required)_: The invitation id (`inv_…`). - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: Confirmation that the invitation was revoked. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: ID of the invitation that was revoked. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or pending invitation was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invitations/inv_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invitations/inv_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` # Invite Links Reusable organization invite links. ## List link redemptions `GET /v1/orgs/{orgId}/invite-links/{inviteLinkId}/redemptions` ### Path parameters - `inviteLinkId` `string` _(required)_: Invite-link resource id (`iln_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: Redemptions. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Redemptions, newest first. - `createdAt` `string ` _(required)_: When the membership was created (ISO 8601). - `email` `string ` _(required)_: Verified email used for redemption. - `id` `string` _(required)_: Opaque redemption id (`ilr_…`). - `inviteLinkId` `string` _(required)_: Invite-link resource id used for redemption. - `inviteLinkName` `string` _(required)_: Link name at redemption time. - `orgId` `string` _(required)_: Organization id joined through the link. - `userId` `string` _(required)_: User id that redeemed the link. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or invite link was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links/iln_1a2b3c4d5e6f7g8h9j0k1m2n/redemptions ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links/iln_1a2b3c4d5e6f7g8h9j0k1m2n/redemptions') ``` ## List invite links `GET /v1/orgs/{orgId}/invite-links` ### Path parameters - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: Invite links. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Invite links, newest first. - `allowedEmailDomains` `string[]` _(required)_: Email domains allowed to redeem, or null for unrestricted. - `createdAt` `string ` _(required)_: When the link was created (ISO 8601). - `createdBy` `string` _(required)_: User id that created the link. - `enabled` `boolean` _(required)_: Whether the link is enabled. - `expiresAt` `string ` _(required)_: When the link expires (ISO 8601), or null for no expiry. - `id` `string` _(required)_: Opaque invite-link resource id (`iln_…`). - `lastUsedAt` `string ` _(required)_: When the link last created a membership, or null. - `maxUses` `integer` _(required)_: Maximum memberships the link may create, or null for unlimited. - `name` `string` _(required)_: Human-readable link name. - `orgId` `string` _(required)_: Organization id (`org_…`) the link joins. - `role` `string` _(required)_: Role granted by the link. - `status` `string` _(required)_: Current link availability. - `token` `string` _(required)_: Opaque bearer token embedded in the recipient URL (`lnk_…`). - `updatedAt` `string ` _(required)_: When the link was last changed (ISO 8601). - `useCount` `integer` _(required)_: Memberships created through the link. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links') ``` ## Create invite link `POST /v1/orgs/{orgId}/invite-links` ### Path parameters - `orgId` `string` _(required)_: Organization id (`org_…`). ### Request body (required) (`application/json`) - `allowedEmailDomains` `string[]`: Email domains allowed to redeem, or null for unrestricted. - `expiresAt` `string `: When the link expires (ISO 8601), or null for no expiry. - `maxUses` `integer`: Maximum memberships the link may create, or null for unlimited. - `name` `string`: Human-readable link name. - `role` `string`: Role granted by the link. ### Responses #### `200`: Created link. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `allowedEmailDomains` `string[]` _(required)_: Email domains allowed to redeem, or null for unrestricted. - `createdAt` `string ` _(required)_: When the link was created (ISO 8601). - `createdBy` `string` _(required)_: User id that created the link. - `enabled` `boolean` _(required)_: Whether the link is enabled. - `expiresAt` `string ` _(required)_: When the link expires (ISO 8601), or null for no expiry. - `id` `string` _(required)_: Opaque invite-link resource id (`iln_…`). - `lastUsedAt` `string ` _(required)_: When the link last created a membership, or null. - `maxUses` `integer` _(required)_: Maximum memberships the link may create, or null for unlimited. - `name` `string` _(required)_: Human-readable link name. - `orgId` `string` _(required)_: Organization id (`org_…`) the link joins. - `role` `string` _(required)_: Role granted by the link. - `status` `string` _(required)_: Current link availability. - `token` `string` _(required)_: Opaque bearer token embedded in the recipient URL (`lnk_…`). - `updatedAt` `string ` _(required)_: When the link was last changed (ISO 8601). - `useCount` `integer` _(required)_: Memberships created through the link. #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links \ --request POST \ --header 'Content-Type: application/json' \ --data '{}' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }) ``` ## Resolve invite link `POST /v1/invite-links/resolve` ### Request body (required) (`application/json`) - `token` `string` _(required)_: Opaque invite-link bearer token. ### Responses #### `200`: Active link summary. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `orgName` `string` _(required)_: Human-readable name of the organization. - `role` `string` _(required)_: Role granted by the link. #### `400`: Malformed API key or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No active invite link was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/invite-links/resolve \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "token": "lnk_1a2b3c4d5e6f7g8h9j0k1m2n" }' ``` ```ts fetch('https://api.tempo.xyz/v1/invite-links/resolve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: 'lnk_1a2b3c4d5e6f7g8h9j0k1m2n' }) }) ``` ## Accept invite link `POST /v1/invite-links/accept` ### Request body (required) (`application/json`) - `token` `string` _(required)_: Opaque invite-link bearer token. ### Responses #### `200`: Membership. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `orgId` `string` _(required)_: Organization id joined through the link. - `role` `string` _(required)_: Role granted by the link. - `userId` `string` _(required)_: Member user id. #### `400`: Malformed API key or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: A verified session email from an allowed domain is required. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No active invite link was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/invite-links/accept \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "token": "lnk_1a2b3c4d5e6f7g8h9j0k1m2n" }' ``` ```ts fetch('https://api.tempo.xyz/v1/invite-links/accept', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: 'lnk_1a2b3c4d5e6f7g8h9j0k1m2n' }) }) ``` ## Update invite link `PATCH /v1/orgs/{orgId}/invite-links/{inviteLinkId}` ### Path parameters - `inviteLinkId` `string` _(required)_: Invite-link resource id (`iln_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Request body (required) (`application/json`) - `enabled` `boolean` _(required)_: Whether the link is enabled. ### Responses #### `200`: Updated link. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `allowedEmailDomains` `string[]` _(required)_: Email domains allowed to redeem, or null for unrestricted. - `createdAt` `string ` _(required)_: When the link was created (ISO 8601). - `createdBy` `string` _(required)_: User id that created the link. - `enabled` `boolean` _(required)_: Whether the link is enabled. - `expiresAt` `string ` _(required)_: When the link expires (ISO 8601), or null for no expiry. - `id` `string` _(required)_: Opaque invite-link resource id (`iln_…`). - `lastUsedAt` `string ` _(required)_: When the link last created a membership, or null. - `maxUses` `integer` _(required)_: Maximum memberships the link may create, or null for unlimited. - `name` `string` _(required)_: Human-readable link name. - `orgId` `string` _(required)_: Organization id (`org_…`) the link joins. - `role` `string` _(required)_: Role granted by the link. - `status` `string` _(required)_: Current link availability. - `token` `string` _(required)_: Opaque bearer token embedded in the recipient URL (`lnk_…`). - `updatedAt` `string ` _(required)_: When the link was last changed (ISO 8601). - `useCount` `integer` _(required)_: Memberships created through the link. #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or invite link was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links/iln_1a2b3c4d5e6f7g8h9j0k1m2n \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{ "enabled": false }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links/iln_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: false }) }) ``` ## Delete invite link `DELETE /v1/orgs/{orgId}/invite-links/{inviteLinkId}` ### Path parameters - `inviteLinkId` `string` _(required)_: Invite-link resource id (`iln_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: Deleted link. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: Deleted invite-link resource id. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or invite link was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links/iln_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/invite-links/iln_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` # Tempo JSON-RPC API The Tempo API exposes a **JSON-RPC entrypoint** for raw, node-level chain access alongside its REST and Indexer surfaces. Through it, Tempo nodes serve all standard [Ethereum JSON-RPC methods](https://ethereum.org/developers/docs/apis/json-rpc/) (`eth_`, `net_`, `web3_`, `txpool_`, `trace_`, `debug_`) plus Tempo-specific namespaces for fork scheduling, consensus data, and node administration. Point any Ethereum-compatible client at the hosted entrypoint, or [run your own node](https://tempo.xyz/developers/docs/guide/node/rpc): | Chain | RPC URL | |---------|---------| | Mainnet | `https://api.tempo.xyz/rpc` | | Testnet | `https://api.tempo.xyz/rpc/testnet` | ## Connect to Tempo JSON-RPC with tools Use the Tempo JSON-RPC API with command-line tools, TypeScript clients, React hooks, or Rust providers. ### Cast [Cast](https://www.getfoundry.sh/reference/cast/cast) is Foundry's command-line tool that includes tools to call JSON-RPC methods and inspect chain state from a terminal. ```bash [Terminal] cast block-number --rpc-url https://api.tempo.xyz/rpc # [!code focus] ``` ### Viem [Viem](https://viem.sh/docs/clients/public) is a TypeScript client for Ethereum-compatible JSON-RPC APIs, useful for server-side scripts and app logic. ```ts [example.ts] import { createClient, http } from 'viem/tempo' const client = createClient({ transport: http('https://api.tempo.xyz/rpc'), // [!code focus] }) const blockNumber = await client.getBlockNumber() // [!code focus] ``` ### Typed Client The [Tempo API Typed Client](https://tempo.xyz/developers/docs/api/typed-client) is Tempo's typed API client for TypeScript. Use it when you want one client for Tempo REST endpoints and the JSON-RPC passthrough, with typed route parameters, status narrowing, and Tempo API error envelopes. ```ts [example.ts] import { Client } from 'tapimo' const client = Client.create({ apiKey: process.env.TEMPO_API_KEY }) const response = await client.rpc[':chain{mainnet|testnet|[0-9]+}?'].$post({ param: { chain: 'testnet' }, // [!code focus] json: { jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] }, // [!code focus] }) const body = await response.json() // [!code focus] ``` ### Wagmi [Wagmi](https://wagmi.sh/react/api/createConfig) provides React hooks and configuration helpers on top of Viem clients. :::code-group ```ts [wagmi.config.ts] import { createConfig, http } from 'wagmi' import { tempo } from 'viem/chains' export const config = createConfig({ chains: [tempo], transports: { [tempo.id]: http('https://api.tempo.xyz/rpc'), // [!code focus] }, }) ``` ```tsx [BlockNumber.tsx] import { useBlockNumber } from 'wagmi' export function BlockNumber() { const blockNumber = useBlockNumber() // [!code focus] return {blockNumber.data?.toString()} // [!code focus] } ``` ::: ### Rust [Alloy](https://docs.rs/alloy-provider) is a Rust toolkit for Ethereum-compatible chains, including providers for JSON-RPC calls. ```rs [example.rs] use alloy::providers::{Provider, ProviderBuilder}; use tempo_alloy::TempoNetwork; #[tokio::main] async fn main() -> Result<(), Box> { let provider = ProviderBuilder::new_with_network::() .connect("https://api.tempo.xyz/rpc") // [!code focus] .await?; let block_number = provider.get_block_number().await?; // [!code focus] println!("Latest block: {block_number}"); // [!code focus] Ok(()) } ``` ## Tempo JSON-RPC endpoints Tempo JSON-RPC endpoints are defined in the [Tempo OpenAPI specification](https://api.tempo.xyz/openapi.json). # MCP A hosted Model Context Protocol server: the data domain and Tempo docs exposed as tools. ## Call MCP `POST /mcp` A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests allow 100 requests per minute; send `Authorization: Bearer ` for protected tools. ### Request body (required) (`application/json`) ### Responses #### `200`: JSON-RPC response returned by the MCP server. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `error` `object`: JSON-RPC error object returned when a message fails. - `code` `integer` _(required)_: Numeric JSON-RPC error code returned by the MCP server. - `data` `unknown`: Optional extra error details returned by the MCP server. - `message` `string` _(required)_: Human-readable JSON-RPC error message. - `id` `string | number | null`: Client-supplied JSON-RPC request id used to match responses to requests. - `jsonrpc` `string` _(required)_: JSON-RPC protocol version; MCP uses `2.0`. - `result` `unknown`: Result returned by the MCP method. #### `202`: The message was a notification or client response; there is nothing to return. #### `400`: Malformed or invalid MCP message, returned as a JSON-RPC error response rather than the standard error envelope. A malformed API key returns the standard `400` envelope instead. Body (`application/json`): - `error` `object`: JSON-RPC error object returned when a message fails. - `code` `integer` _(required)_: Numeric JSON-RPC error code returned by the MCP server. - `data` `unknown`: Optional extra error details returned by the MCP server. - `message` `string` _(required)_: Human-readable JSON-RPC error message. - `id` `string | number | null`: Client-supplied JSON-RPC request id used to match responses to requests. - `jsonrpc` `string` _(required)_: JSON-RPC protocol version; MCP uses `2.0`. - `result` `unknown`: Result returned by the MCP method. #### `401`: The presented API key is invalid. Anonymous requests are allowed under the public quota. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `406`: The `Accept` header must include both `application/json` and `text/event-stream`. Returned as a JSON-RPC error response. Body (`application/json`): - `error` `object`: JSON-RPC error object returned when a message fails. - `code` `integer` _(required)_: Numeric JSON-RPC error code returned by the MCP server. - `data` `unknown`: Optional extra error details returned by the MCP server. - `message` `string` _(required)_: Human-readable JSON-RPC error message. - `id` `string | number | null`: Client-supplied JSON-RPC request id used to match responses to requests. - `jsonrpc` `string` _(required)_: JSON-RPC protocol version; MCP uses `2.0`. - `result` `unknown`: Result returned by the MCP method. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/mcp \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }' ``` ```ts fetch('https://api.tempo.xyz/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }) }) ``` # Members Organization team membership. ## List members `GET /v1/orgs/{orgId}/members` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: The members, oldest first. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The members, oldest first. - `address` `string`: The member's wallet address, when wallet sign-in established it. - `createdAt` `string ` _(required)_: When the member joined (ISO 8601). - `email` `string `: The member's verified email, when known. - `role` `string` _(required)_: Membership role. - `userId` `string` _(required)_: Member user id (`usr_…`). #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/members ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/members') ``` ## Update member `PATCH /v1/orgs/{orgId}/members/{userId}` Change a member's role. The last owner cannot be demoted. ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). - `userId` `string` _(required)_: The member user id (`usr_…`). ### Request body (required) (`application/json`) - `role` `string` _(required)_: Membership role. ### Responses #### `200`: The updated member. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string`: The member's wallet address, when wallet sign-in established it. - `createdAt` `string ` _(required)_: When the member joined (ISO 8601). - `email` `string `: The member's verified email, when known. - `role` `string` _(required)_: Membership role. - `userId` `string` _(required)_: Member user id (`usr_…`). #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or member was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: The organization would be left without an owner. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/members/usr_1a2b3c4d5e6f7g8h9j0k1m2n \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{ "role": "admin" }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/members/usr_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role: 'admin' }) }) ``` ## Remove member `DELETE /v1/orgs/{orgId}/members/{userId}` Remove a member. Owners and management-write keys remove anyone; members remove themselves. The last owner stays. ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). - `userId` `string` _(required)_: The member user id (`usr_…`). ### Responses #### `200`: Confirmation that the member was removed. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `userId` `string` _(required)_: User id (`usr_…`) of the removed member. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or member was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: The organization would be left without an owner. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/members/usr_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/members/usr_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` # MPP Submit completed MPP credentials to Tempo. Validate checks a credential without settling it; Broadcast validates, screens, and submits it. Both require `mpp:write`. ## Validate MPP credential `POST /v1/mpp/validate` Checks whether a completed MPP credential can be processed without submitting payment. Tempo validates the challenge and credential, confirms the chain is supported, and screens the payment parties. It does not settle, broadcast, reserve funds, or consume the credential. Requires `mpp:write` and, for Zones, `zone::read` or `zone::write`. Well-formed requests return HTTP `200`; inspect `success`. Invalid bodies return `400`; missing or unauthorized API keys return `401` or `403`. ### Request body (required) (`application/json`) - `challenge` `object` _(required)_: An MPP payment challenge. - `description` `string`: A human-readable description of the payment. - `expires` `string`: When the payment challenge expires, as an ISO-8601 timestamp. - `id` `string` _(required)_: The payment challenge identifier. - `intent` `string` _(required)_: The MPP payment intent. - `method` `string` _(required)_: The MPP payment method. - `opaque` `string`: Opaque data bound to the challenge by its issuer. - `realm` `string` _(required)_: The MPP realm that issued the challenge. - `request` `object` _(required)_: The payment request bound to the challenge. - `payload` `unknown` _(required)_: The method-specific MPP credential payload. - `source` `string`: An optional payer identifier supplied with the credential. ### Responses #### `200`: The MPP credential validation result. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The API key does not grant MPP relay access. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/mpp/validate \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "challenge": { "id": "ch_01j3j1k2l3m4n5p6q7r8s9t0u", "intent": "charge", "method": "tempo", "realm": "merchant.example", "request": { "amount": "1", "currency": "USD" } }, "payload": { "signature": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "type": "transaction" } }' ``` ```ts fetch('https://api.tempo.xyz/v1/mpp/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ challenge: { id: 'ch_01j3j1k2l3m4n5p6q7r8s9t0u', intent: 'charge', method: 'tempo', realm: 'merchant.example', request: { amount: '1', currency: 'USD' } }, payload: { signature: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', type: 'transaction' } }) }) ``` ## Broadcast MPP credential `POST /v1/mpp/broadcast` Validates, screens, and submits a completed MPP credential to Tempo. Requires `mpp:write` and, for Zones, `zone::write`. Well-formed requests return HTTP `200`; inspect `success`. Use `Idempotency-Key` on retries to replay a successful receipt. Reusing a key for a different credential returns `invalid_payment`, and in-flight duplicates return `temporarily_unavailable`. ### Header parameters - `Idempotency-Key` `string`: Optional opaque retry key, scoped to the API key and credential. Reusing it after a successful broadcast returns the original receipt without another submission. Reusing it with a different credential returns `invalid_payment`. While the first request is running, duplicates return `temporarily_unavailable`. Failed attempts are not retained and may be retried with the same key. ### Request body (required) (`application/json`) - `challenge` `object` _(required)_: An MPP payment challenge. - `description` `string`: A human-readable description of the payment. - `expires` `string`: When the payment challenge expires, as an ISO-8601 timestamp. - `id` `string` _(required)_: The payment challenge identifier. - `intent` `string` _(required)_: The MPP payment intent. - `method` `string` _(required)_: The MPP payment method. - `opaque` `string`: Opaque data bound to the challenge by its issuer. - `realm` `string` _(required)_: The MPP realm that issued the challenge. - `request` `object` _(required)_: The payment request bound to the challenge. - `payload` `unknown` _(required)_: The method-specific MPP credential payload. - `source` `string`: An optional payer identifier supplied with the credential. ### Responses #### `200`: The MPP credential broadcast result. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The API key does not grant MPP relay access. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/mpp/broadcast \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "challenge": { "id": "ch_01j3j1k2l3m4n5p6q7r8s9t0u", "intent": "charge", "method": "tempo", "realm": "merchant.example", "request": { "amount": "1", "currency": "USD" } }, "payload": { "signature": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "type": "transaction" } }' ``` ```ts fetch('https://api.tempo.xyz/v1/mpp/broadcast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ challenge: { id: 'ch_01j3j1k2l3m4n5p6q7r8s9t0u', intent: 'charge', method: 'tempo', realm: 'merchant.example', request: { amount: '1', currency: 'USD' } }, payload: { signature: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', type: 'transaction' } }) }) ``` # Organizations Teams that own application workspaces and members. ## List organizations `GET /v1/orgs` ### Responses #### `200`: The organizations, newest first. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The organizations, newest first. - `createdAt` `string ` _(required)_: When the organization was created (ISO 8601). - `id` `string` _(required)_: Opaque organization id (`org_…`). - `name` `string` _(required)_: Human-readable organization name. - `role` `string`: The caller's role; absent for API keys and the super admin. - `updatedAt` `string ` _(required)_: When the organization was last updated (ISO 8601). #### `400`: Malformed API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs ``` ```ts fetch('https://api.tempo.xyz/v1/orgs') ``` ## Create organization `POST /v1/orgs` ### Request body (required) (`application/json`) - `name` `string` _(required)_: Human-readable name. ### Responses #### `200`: The created organization. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the organization was created (ISO 8601). - `id` `string` _(required)_: Opaque organization id (`org_…`). - `name` `string` _(required)_: Human-readable organization name. - `role` `string`: The caller's role; absent for API keys and the super admin. - `updatedAt` `string ` _(required)_: When the organization was last updated (ISO 8601). #### `400`: Malformed API key or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "name": "Acme, Inc." }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Acme, Inc.' }) }) ``` ## Get organization `GET /v1/orgs/{orgId}` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: One organization. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the organization was created (ISO 8601). - `id` `string` _(required)_: Opaque organization id (`org_…`). - `name` `string` _(required)_: Human-readable organization name. - `role` `string`: The caller's role; absent for API keys and the super admin. - `updatedAt` `string ` _(required)_: When the organization was last updated (ISO 8601). #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n') ``` ## Update organization `PATCH /v1/orgs/{orgId}` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Request body (required) (`application/json`) - `name` `string` _(required)_: Human-readable name. ### Responses #### `200`: The updated organization. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the organization was created (ISO 8601). - `id` `string` _(required)_: Opaque organization id (`org_…`). - `name` `string` _(required)_: Human-readable organization name. - `role` `string`: The caller's role; absent for API keys and the super admin. - `updatedAt` `string ` _(required)_: When the organization was last updated (ISO 8601). #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{ "name": "Acme, Inc." }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Acme, Inc.' }) }) ``` ## Delete organization `DELETE /v1/orgs/{orgId}` Delete an organization and all of its projects. ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: Confirmation that the organization was deleted. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: ID of the organization that was deleted. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: Billable usage is awaiting settlement, address provisioning is active, or an irrevocable subsidized address remains at its provider. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` # Tempo API pagination: two modes explained Tempo list endpoints share one pagination contract. Use **cursor** pagination for stable traversal and deep reads. Use **page** pagination only when an endpoint supports it and you need a shallow, page-numbered UI. ## Tempo API pagination modes | Mode | Parameters | Use for | Notes | | --- | --- | --- | --- | | Cursor | `limit`, `cursor` | Deep traversal, bulk reads, live feeds | Canonical. Omit `cursor` on the first request, then pass each `nextCursor` back verbatim. | | Page | `limit`, `page` | Shallow, page-numbered UIs | Optional. `page` is 1-indexed and `page × limit` must be at most `10,000`. | Cursor pagination anchors traversal to a row position, so the next cursor continues from exactly where the previous page ended, even when new rows arrive at the head of a feed between requests. Page pagination is positional. If new rows arrive at the head of a feed, items can shift across page boundaries, which can duplicate or skip results. Some endpoints support cursor pagination only. :::warning Do not send `cursor` and `page` together. The request fails with a `422` validation error. ::: ## Pagination request parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `limit` | integer | `10` | Items per page. Must be between `5` and `200`. | | `cursor` | string | none | Opaque cursor from the previous response's `nextCursor`. Omit on the first request. Mutually exclusive with `page`. | | `page` | integer | none | 1-indexed positional page number. `page=1` is the head page. Mutually exclusive with `cursor`. `page × limit` must be at most `10,000`. | | `include` | string | none | Comma-separated include flags. Send `include=totalCount` to add `meta.totalCount` and `meta.totalCountCapped` to the response. | ## Pagination response envelope List endpoints return a common envelope: ```json { "data": [ /* items for this page */ ], "nextCursor": "WzIzNDU2Nzg5LDBd", "meta": { "totalCount": 42, "totalCountCapped": false } } ``` | Field | Type | Description | | --- | --- | --- | | `data` | array | Items for this page. | | `nextCursor` | string or `null` | Cursor for the next page. Pass it back as the `cursor` query parameter. `null` means you have reached the end of the list. | | `meta.totalCount` | integer | Present only when you request `include=totalCount`. Exact when `meta.totalCountCapped` is `false`; a lower bound when `true`. | | `meta.totalCountCapped` | boolean | Present only when you request `include=totalCount`. `true` means the count hit the `10,000` cap. | :::warning `nextCursor === null` is the only end-of-list signal. Do not stop because `data` is shorter than `limit`, because `data` is empty, or because of `totalCount`. ::: ## Cursor pagination for Tempo API lists Fetch the first page without a cursor: ```bash curl 'https://api.tempo.xyz/v1/tokens?limit=100' \ --header 'Authorization: Bearer tempo:sk:...' ``` The response includes `nextCursor` when another page is available: ```json { "data": [ /* items for this page */ ], "nextCursor": "WzIzNDU2Nzg5LDBd", "meta": {} } ``` Fetch the next page by passing that value back as `cursor`: ```bash curl --get 'https://api.tempo.xyz/v1/tokens' \ --data-urlencode 'limit=100' \ --data-urlencode 'cursor=WzIzNDU2Nzg5LDBd' \ --header 'Authorization: Bearer tempo:sk:...' ``` Keep going until `nextCursor` is `null`: ```json { "data": [ /* final page items */ ], "nextCursor": null, "meta": {} } ``` :::info Cursors are opaque. Store and pass them back exactly as received; do not decode, parse, shorten, or construct them. Their format and length may change without notice. If a cursor is malformed or forged, the request starts again from the head instead of returning an error. ::: ## Page pagination for shallow Tempo API lists Use `page` only for shallow, page-numbered interfaces: ```bash curl 'https://api.tempo.xyz/v1/tokens?limit=25&page=2' \ --header 'Authorization: Bearer tempo:sk:...' ``` `page` is 1-indexed: * `page=1` returns the head page. * `page=2` returns the next positional page. * `page × limit` must be at most `10,000`. For anything deeper than `10,000` rows, use cursor pagination. Some endpoints support cursor pagination only — this is common when each returned item is derived from a variable number of underlying rows, which makes positional page boundaries unstable. ## Count results in paginated responses Counts are optional. Request them with `include=totalCount`: ```bash curl 'https://api.tempo.xyz/v1/tokens?limit=50&include=totalCount' \ --header 'Authorization: Bearer tempo:sk:...' ``` The count is computed by a capped subquery up to `10,000` matched rows: ```json { "data": [ /* items for this page */ ], "nextCursor": "WzIzNDU2Nzg5LDBd", "meta": { "totalCount": 10000, "totalCountCapped": true } } ``` * When `meta.totalCountCapped` is `false`, `meta.totalCount` is exact. * When `meta.totalCountCapped` is `true`, `meta.totalCount` is a lower bound: there are at least that many matches. * Counts are independent of pagination. Use `nextCursor`, not `totalCount`, to decide whether to request another page. ## Pagination best practices for Tempo API clients * Prefer cursor pagination for integrations, sync jobs, exports, and live feeds. * Use the largest `limit` your workload can handle, up to `200`, to reduce round trips. * Persist `nextCursor` if you need to resume traversal later. * Pass cursors back verbatim, and URL-encode them when placing them in a query string. * Stop only when `nextCursor` is `null`. * Use page pagination only for shallow UI navigation where duplicate or skipped rows on a live feed are acceptable. # Projects Application workspaces within an organization. ## List projects `GET /v1/orgs/{orgId}/projects` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Responses #### `200`: The organization's projects, newest first. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The projects, newest first. - `createdAt` `string ` _(required)_: When the project was created (ISO 8601). - `environments` `string[]` _(required)_: Environments available to this project: sandbox (testnet) and production (mainnet). - `id` `string` _(required)_: Opaque project id (`prj_…`). - `name` `string` _(required)_: Human-readable project name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `updatedAt` `string ` _(required)_: When the project was last updated (ISO 8601). #### `400`: Malformed API key or invalid path parameters. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects') ``` ## Create project `POST /v1/orgs/{orgId}/projects` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Request body (required) (`application/json`) - `name` `string` _(required)_: Human-readable name. ### Responses #### `200`: The created project. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the project was created (ISO 8601). - `environments` `string[]` _(required)_: Environments available to this project: sandbox (testnet) and production (mainnet). - `id` `string` _(required)_: Opaque project id (`prj_…`). - `name` `string` _(required)_: Human-readable project name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `updatedAt` `string ` _(required)_: When the project was last updated (ISO 8601). #### `400`: Malformed API key, invalid path parameters, or invalid request body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "name": "Checkout" }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Checkout' }) }) ``` ## Get project `GET /v1/orgs/{orgId}/projects/{projectId}` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Responses #### `200`: One project. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the project was created (ISO 8601). - `environments` `string[]` _(required)_: Environments available to this project: sandbox (testnet) and production (mainnet). - `id` `string` _(required)_: Opaque project id (`prj_…`). - `name` `string` _(required)_: Human-readable project name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `updatedAt` `string ` _(required)_: When the project was last updated (ISO 8601). #### `400`: Malformed API key or invalid path parameters. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible project was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n') ``` ## Update project `PATCH /v1/orgs/{orgId}/projects/{projectId}` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Request body (required) (`application/json`) - `name` `string` _(required)_: Human-readable name. ### Responses #### `200`: The updated project. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the project was created (ISO 8601). - `environments` `string[]` _(required)_: Environments available to this project: sandbox (testnet) and production (mainnet). - `id` `string` _(required)_: Opaque project id (`prj_…`). - `name` `string` _(required)_: Human-readable project name. - `orgId` `string` _(required)_: Owning organization id (`org_…`). - `updatedAt` `string ` _(required)_: When the project was last updated (ISO 8601). #### `400`: Malformed API key, invalid path parameters, or invalid request body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible project was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{ "name": "Checkout" }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Checkout' }) }) ``` ## Delete project `DELETE /v1/orgs/{orgId}/projects/{projectId}` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). - `projectId` `string` _(required)_: The project id (`prj_…`). ### Responses #### `200`: Confirmation that the project was deleted. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: ID of the project that was deleted. #### `400`: Malformed API key or invalid path parameters. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible project was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/projects/prj_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` # Tempo API rate limits and quota structure The Tempo API enforces rate limits to ensure fair usage and system stability. Limits are counted in **fixed one-minute windows**, and every response reports your current quota in `RateLimit-*` headers. ## Tempo API quota structure Each request consumes from one quota bucket, chosen by how the request is authenticated: | Caller | Bucket | Default limit | | --- | --- | --- | | API key | Per key, per [scope](https://tempo.xyz/developers/docs/api/authentication) | 100 requests / minute | | Anonymous | Per client IP | 20 requests / minute | * **API key requests** are counted per key *and* per scope. Each endpoint draws from the bucket for the scope it requires (such as `data:read`), so traffic to one scope does not exhaust another. * **Anonymous requests** — endpoints that allow access without a key — are counted per client IP at a lower limit. :::info Default limits are a starting point and may be raised for a given key. The authoritative limit for any request is always the value in its `RateLimit-Limit` response header — read the headers rather than hardcoding a number. ::: ## Rate-limit response headers Every rate-limited response carries your current quota: ```http RateLimit-Limit: 100 RateLimit-Remaining: 97 RateLimit-Reset: 1735689600 RateLimit-Scope: data:read ``` | Header | Description | | --- | --- | | `RateLimit-Limit` | Requests allowed in the current window. | | `RateLimit-Remaining` | Requests remaining in the current window. | | `RateLimit-Reset` | Unix timestamp (seconds) when the window resets and the count returns to the full limit. | | `RateLimit-Scope` | The scope bucket the request was counted against. Present on API-key requests. | ## Exceeding Tempo API rate limits When you exceed the limit, the API responds with `429 Too Many Requests` and a `Retry-After` header giving the number of seconds to wait before retrying: ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 ``` ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded" }, "requestId": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" } ``` :::info On endpoints that allow anonymous access, an over-quota request can instead be answered with `402 Payment Required`, letting you pay per request with MPP rather than wait. See [Authentication](https://tempo.xyz/developers/docs/api/authentication) for details. ::: ## Rate-limit best practices for Tempo API clients ### Respect rate-limit headers Read `RateLimit-Remaining` and `RateLimit-Reset` to pace your requests, and pause until `RateLimit-Reset` when `RateLimit-Remaining` reaches `0` rather than retrying blindly. ### Retry rate-limited requests with backoff When you receive a `429`, wait for the duration in `Retry-After`, then retry with exponential backoff and jitter for repeated failures: ```typescript async function withRetry(fn: () => Promise): Promise { let attempt = 0 while (true) { const response = await fn() if (response.status !== 429) return response const retryAfter = Number(response.headers.get('Retry-After') ?? 1) const backoff = Math.min(retryAfter, 2 ** attempt) * 1000 await new Promise((resolve) => setTimeout(resolve, backoff)) attempt++ } } ``` ### Cache and paginate API reads * Cache responses for read-heavy data that does not need to be real-time. * Use [pagination](https://tempo.xyz/developers/docs/api/pagination) with a large `limit` to fetch more per request instead of issuing many small calls. * Request only the fields you need with `include`, so expensive computations run only when required. ### Authenticate for higher API quotas Anonymous traffic is held to the lower IP-based limit. Send an [API key](https://tempo.xyz/developers/docs/api/authentication) to get a per-key, per-scope quota, and request a higher limit if your workload needs it. # Tempo API reference Browse every Tempo API group and endpoint. Choose a group to see its operations, then open an endpoint for request parameters, response schemas, examples, and the interactive playground. ## Tempo API endpoints Tempo REST API endpoints are defined in the [Tempo OpenAPI specification](https://api.tempo.xyz/openapi.json). # RPC Direct access to the chain over Ethereum JSON-RPC. Not part of the stable API contract; best-effort support only. No compatibility, latency, availability, or data-freshness guarantees. Breaking changes may happen with limited notice. ## admin_validatorKey `POST /rpc` Returns the validator public key configured on the node, or null for non-validator nodes. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "admin_validatorKey", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'admin_validatorKey', params: [] }) }) ``` ## consensus_getFinalization `POST /rpc` Returns a finalized consensus block by height or the latest finalized block. ### Parameters - `Query` `string | object` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `block` `object` _(required)_: The Tempo block. - `baseFeePerGas` `string` - `blobGasUsed` `string` - `blockAccessListHash` `string` - `difficulty` `string` - `excessBlobGas` `string` - `extraData` `string` _(required)_ - `gasLimit` `string` _(required)_ - `gasUsed` `string` _(required)_ - `hash` `string` _(required)_ - `logsBloom` `string` _(required)_ - `miner` `string` _(required)_ - `mixHash` `string` _(required)_ - `nonce` `string` _(required)_ - `number` `string` _(required)_ - `parentBeaconBlockRoot` `string` - `parentHash` `string` _(required)_ - `receiptsRoot` `string` _(required)_ - `requestsHash` `string` - `sha3Uncles` `string` _(required)_ - `size` `string` _(required)_ - `stateRoot` `string` _(required)_ - `timestamp` `string` _(required)_ - `transactions` `string[] | object & object & object | object[]` _(required)_ - `transactionsRoot` `string` _(required)_ - `uncles` `string[]` _(required)_ - `withdrawals` `object[]` - `address` `string` _(required)_ - `amount` `string` _(required)_ - `index` `string` _(required)_ - `validatorIndex` `string` _(required)_ - `withdrawalsRoot` `string` - `consensusContext` `object`: Tempo consensus context. - `epoch` `string` _(required)_ - `parentView` `string` _(required)_ - `proposer` `string` _(required)_: Proposer consensus public key. - `view` `string` _(required)_ - `mainBlockGeneralGasLimit` `string` _(required)_ - `sharedGasLimit` `string` _(required)_ - `timestampMillis` `string` _(required)_ - `timestampMillisPart` `string` _(required)_ - `certificate` `string` _(required)_: Hex-encoded notarization or finalization. - `digest` `string` _(required)_: Consensus block digest. - `epoch` `integer` _(required)_ - `view` `integer` _(required)_ ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "consensus_getFinalization", "params": [ "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'consensus_getFinalization', params: ['latest'] }) }) ``` ## consensus_getIdentityTransitionProof `POST /rpc` Returns DKG identity transition proofs for the network identity at a requested epoch. ### Parameters - `from_epoch` `integer` - `full` `boolean` ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `identity` `string` _(required)_: Network identity for the requested epoch. - `transitions` `object[]` _(required)_ - `newIdentity` `string` _(required)_: Hex-encoded BLS public key after the transition. - `oldIdentity` `string` _(required)_: Hex-encoded BLS public key before the transition. - `proof` `object` - `finalizationCertificate` `string` _(required)_: Hex-encoded finalization certificate. - `header` `object` _(required)_ - `baseFeePerGas` `string` - `consensusContext` `object` - `difficulty` `string` - `extraData` `string` _(required)_ - `gasLimit` `string` _(required)_ - `gasUsed` `string` _(required)_ - `hash` `string` _(required)_ - `logsBloom` `string` _(required)_ - `mainBlockGeneralGasLimit` `string` _(required)_ - `miner` `string` _(required)_ - `mixHash` `string` - `nonce` `string` - `number` `string` _(required)_ - `parentHash` `string` _(required)_ - `receiptsRoot` `string` _(required)_ - `sha3Uncles` `string` _(required)_ - `sharedGasLimit` `string` _(required)_ - `stateRoot` `string` _(required)_ - `timestamp` `string` _(required)_ - `timestampMillis` `string` _(required)_ - `timestampMillisPart` `string` _(required)_ - `transactionsRoot` `string` _(required)_ - `withdrawalsRoot` `string` - `transitionEpoch` `integer` _(required)_ ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "consensus_getIdentityTransitionProof", "params": [ null, false ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'consensus_getIdentityTransitionProof', params: [null, false] }) }) ``` ## consensus_getLatest `POST /rpc` Returns the current consensus state snapshot, including the latest finalized block and latest notarized block. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `finalized` `object` _(required)_ - `block` `object` _(required)_: The Tempo block. - `baseFeePerGas` `string` - `blobGasUsed` `string` - `blockAccessListHash` `string` - `difficulty` `string` - `excessBlobGas` `string` - `extraData` `string` _(required)_ - `gasLimit` `string` _(required)_ - `gasUsed` `string` _(required)_ - `hash` `string` _(required)_ - `logsBloom` `string` _(required)_ - `miner` `string` _(required)_ - `mixHash` `string` _(required)_ - `nonce` `string` _(required)_ - `number` `string` _(required)_ - `parentBeaconBlockRoot` `string` - `parentHash` `string` _(required)_ - `receiptsRoot` `string` _(required)_ - `requestsHash` `string` - `sha3Uncles` `string` _(required)_ - `size` `string` _(required)_ - `stateRoot` `string` _(required)_ - `timestamp` `string` _(required)_ - `transactions` `string[] | object & object & object | object[]` _(required)_ - `transactionsRoot` `string` _(required)_ - `uncles` `string[]` _(required)_ - `withdrawals` `object[]` - `address` `string` _(required)_ - `amount` `string` _(required)_ - `index` `string` _(required)_ - `validatorIndex` `string` _(required)_ - `withdrawalsRoot` `string` - `consensusContext` `object`: Tempo consensus context. - `epoch` `string` _(required)_ - `parentView` `string` _(required)_ - `proposer` `string` _(required)_: Proposer consensus public key. - `view` `string` _(required)_ - `mainBlockGeneralGasLimit` `string` _(required)_ - `sharedGasLimit` `string` _(required)_ - `timestampMillis` `string` _(required)_ - `timestampMillisPart` `string` _(required)_ - `certificate` `string` _(required)_: Hex-encoded notarization or finalization. - `digest` `string` _(required)_: Consensus block digest. - `epoch` `integer` _(required)_ - `view` `integer` _(required)_ - `notarized` `object` _(required)_ - `block` `object` _(required)_: The Tempo block. - `baseFeePerGas` `string` - `blobGasUsed` `string` - `blockAccessListHash` `string` - `difficulty` `string` - `excessBlobGas` `string` - `extraData` `string` _(required)_ - `gasLimit` `string` _(required)_ - `gasUsed` `string` _(required)_ - `hash` `string` _(required)_ - `logsBloom` `string` _(required)_ - `miner` `string` _(required)_ - `mixHash` `string` _(required)_ - `nonce` `string` _(required)_ - `number` `string` _(required)_ - `parentBeaconBlockRoot` `string` - `parentHash` `string` _(required)_ - `receiptsRoot` `string` _(required)_ - `requestsHash` `string` - `sha3Uncles` `string` _(required)_ - `size` `string` _(required)_ - `stateRoot` `string` _(required)_ - `timestamp` `string` _(required)_ - `transactions` `string[] | object & object & object | object[]` _(required)_ - `transactionsRoot` `string` _(required)_ - `uncles` `string[]` _(required)_ - `withdrawals` `object[]` - `address` `string` _(required)_ - `amount` `string` _(required)_ - `index` `string` _(required)_ - `validatorIndex` `string` _(required)_ - `withdrawalsRoot` `string` - `consensusContext` `object`: Tempo consensus context. - `epoch` `string` _(required)_ - `parentView` `string` _(required)_ - `proposer` `string` _(required)_: Proposer consensus public key. - `view` `string` _(required)_ - `mainBlockGeneralGasLimit` `string` _(required)_ - `sharedGasLimit` `string` _(required)_ - `timestampMillis` `string` _(required)_ - `timestampMillisPart` `string` _(required)_ - `certificate` `string` _(required)_: Hex-encoded notarization or finalization. - `digest` `string` _(required)_: Consensus block digest. - `epoch` `integer` _(required)_ - `view` `integer` _(required)_ ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "consensus_getLatest", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'consensus_getLatest', params: [] }) }) ``` ## consensus_subscribe `POST /rpc` Subscribes to consensus events over WebSocket. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "consensus_subscribe", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'consensus_subscribe', params: [] }) }) ``` ## consensus_unsubscribe `POST /rpc` Unsubscribes from a consensus event subscription. ### Parameters - `Subscription ID` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `boolean` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "consensus_unsubscribe", "params": [ "string" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'consensus_unsubscribe', params: ['string'] }) }) ``` ## eth_blockNumber `POST /rpc` Returns the number of most recent block. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] }) }) ``` ## eth_call `POST /rpc` Executes a new message call immediately without creating a transaction on the block chain. ### Parameters - `Transaction` `object` _(required)_ - `accessList` `object[]`: EIP-2930 access list - `address` `string` _(required)_ - `storageKeys` `string[]` _(required)_ - `authorizationList` `object[]`: EIP-7702 authorization list - `address` `string` _(required)_ - `chainId` `string` _(required)_: Chain ID on which this transaction is valid - `nonce` `string` _(required)_ - `r` `string` _(required)_ - `s` `string` _(required)_ - `yParity` `string` _(required)_: The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature - `blobVersionedHashes` `string[]`: List of versioned blob hashes associated with the transaction's EIP-4844 data blobs. - `blobs` `string[]`: Raw blob data. - `chainId` `string`: Chain ID that this transaction is valid on. - `from` `string` - `gas` `string` - `gasPrice` `string`: The gas price willing to be paid by the sender in wei - `input` `string` - `maxFeePerBlobGas` `string`: The maximum total fee per gas the sender is willing to pay for blob gas in wei - `maxFeePerGas` `string`: The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei - `maxPriorityFeePerGas` `string`: Maximum fee per gas the sender is willing to pay to miners in wei - `nonce` `string` - `to` `string` - `type` `string` - `value` `string` - `aaAuthorizationList` `object[]`: EIP-7702-style authorizations (Tempo AA). - `address` `string` _(required)_ - `chainId` `string` _(required)_ - `nonce` `string` _(required)_ - `signature` `object` _(required)_ - `calls` `object[]`: Tempo AA batched calls. - `input` `string` _(required)_: Call input data (accepts `data` on input). - `to` `string` _(required)_ - `value` `string` _(required)_ - `feePayerSignature` `object`: Gas-sponsor signature, when fee-payer sponsored. - `r` `string` _(required)_ - `s` `string` _(required)_ - `v` `string` _(required)_: Recovery id (`0` or `1`); mirrors `yParity`. - `yParity` `string` _(required)_ - `feeToken` `string`: TIP-20 token used to pay fees. - `keyAuthorization` `object`: Access-key provisioning authorization. - `account` `string`: Target account binding. - `allowedCalls` `object[]`: Allowed call scopes; `null` means unrestricted. - `selectorRules` `object[]` - `recipients` `string[]` - `selector` `string` _(required)_ - `target` `string` _(required)_ - `chainId` `string` _(required)_ - `expiry` `string`: Expiry timestamp; `null` means no expiry. - `isAdmin` `boolean` _(required)_ - `keyId` `string` _(required)_ - `keyType` `string` _(required)_ - `limits` `object[]`: Spending limits; `null` means unlimited. - `limit` `string` _(required)_ - `period` `string` _(required)_ - `token` `string` _(required)_ - `signature` `object` _(required)_ - `witness` `string` - `keyData` `string`: Access-key public key data. - `keyId` `string`: Access-key id (address). - `keyType` `string`: Access-key signature scheme. - `nonceKey` `string`: Tempo 2D nonce key. - `validAfter` `string`: Earliest valid timestamp. - `validBefore` `string`: Latest valid timestamp. - `Block` `string` ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_call", "params": [ { "to": "0x69498dd54bd25aa0c886cf1f8b8ae0856d55ff13", "value": "0x1" }, "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [ { to: '0x69498dd54bd25aa0c886cf1f8b8ae0856d55ff13', value: '0x1' }, 'latest' ] }) }) ``` ## eth_chainId `POST /rpc` Returns the chain ID of the current network. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_chainId", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }) }) ``` ## eth_createAccessList `POST /rpc` Generates an access list for a transaction. ### Parameters - `Transaction` `object` _(required)_ - `accessList` `object[]`: EIP-2930 access list - `address` `string` _(required)_ - `storageKeys` `string[]` _(required)_ - `authorizationList` `object[]`: EIP-7702 authorization list - `address` `string` _(required)_ - `chainId` `string` _(required)_: Chain ID on which this transaction is valid - `nonce` `string` _(required)_ - `r` `string` _(required)_ - `s` `string` _(required)_ - `yParity` `string` _(required)_: The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature - `blobVersionedHashes` `string[]`: List of versioned blob hashes associated with the transaction's EIP-4844 data blobs. - `blobs` `string[]`: Raw blob data. - `chainId` `string`: Chain ID that this transaction is valid on. - `from` `string` - `gas` `string` - `gasPrice` `string`: The gas price willing to be paid by the sender in wei - `input` `string` - `maxFeePerBlobGas` `string`: The maximum total fee per gas the sender is willing to pay for blob gas in wei - `maxFeePerGas` `string`: The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei - `maxPriorityFeePerGas` `string`: Maximum fee per gas the sender is willing to pay to miners in wei - `nonce` `string` - `to` `string` - `type` `string` - `value` `string` - `aaAuthorizationList` `object[]`: EIP-7702-style authorizations (Tempo AA). - `address` `string` _(required)_ - `chainId` `string` _(required)_ - `nonce` `string` _(required)_ - `signature` `object` _(required)_ - `calls` `object[]`: Tempo AA batched calls. - `input` `string` _(required)_: Call input data (accepts `data` on input). - `to` `string` _(required)_ - `value` `string` _(required)_ - `feePayerSignature` `object`: Gas-sponsor signature, when fee-payer sponsored. - `r` `string` _(required)_ - `s` `string` _(required)_ - `v` `string` _(required)_: Recovery id (`0` or `1`); mirrors `yParity`. - `yParity` `string` _(required)_ - `feeToken` `string`: TIP-20 token used to pay fees. - `keyAuthorization` `object`: Access-key provisioning authorization. - `account` `string`: Target account binding. - `allowedCalls` `object[]`: Allowed call scopes; `null` means unrestricted. - `selectorRules` `object[]` - `recipients` `string[]` - `selector` `string` _(required)_ - `target` `string` _(required)_ - `chainId` `string` _(required)_ - `expiry` `string`: Expiry timestamp; `null` means no expiry. - `isAdmin` `boolean` _(required)_ - `keyId` `string` _(required)_ - `keyType` `string` _(required)_ - `limits` `object[]`: Spending limits; `null` means unlimited. - `limit` `string` _(required)_ - `period` `string` _(required)_ - `token` `string` _(required)_ - `signature` `object` _(required)_ - `witness` `string` - `keyData` `string`: Access-key public key data. - `keyId` `string`: Access-key id (address). - `keyType` `string`: Access-key signature scheme. - `nonceKey` `string`: Tempo 2D nonce key. - `validAfter` `string`: Earliest valid timestamp. - `validBefore` `string`: Latest valid timestamp. - `Block` `string` ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `accessList` `object[]` - `address` `string` _(required)_ - `storageKeys` `string[]` _(required)_ - `error` `string` - `gasUsed` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_createAccessList", "params": [ { "data": "0x608060806080608155", "from": "0xaea8f8f781326bfe6a7683c2bd48dd6aa4d3ba63" }, "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_createAccessList', params: [ { data: '0x608060806080608155', from: '0xaea8f8f781326bfe6a7683c2bd48dd6aa4d3ba63' }, 'latest' ] }) }) ``` ## eth_estimateGas `POST /rpc` Generates and returns an estimate of how much gas is necessary to allow the transaction to complete. ### Parameters - `Transaction` `object` _(required)_ - `accessList` `object[]`: EIP-2930 access list - `address` `string` _(required)_ - `storageKeys` `string[]` _(required)_ - `authorizationList` `object[]`: EIP-7702 authorization list - `address` `string` _(required)_ - `chainId` `string` _(required)_: Chain ID on which this transaction is valid - `nonce` `string` _(required)_ - `r` `string` _(required)_ - `s` `string` _(required)_ - `yParity` `string` _(required)_: The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature - `blobVersionedHashes` `string[]`: List of versioned blob hashes associated with the transaction's EIP-4844 data blobs. - `blobs` `string[]`: Raw blob data. - `chainId` `string`: Chain ID that this transaction is valid on. - `from` `string` - `gas` `string` - `gasPrice` `string`: The gas price willing to be paid by the sender in wei - `input` `string` - `maxFeePerBlobGas` `string`: The maximum total fee per gas the sender is willing to pay for blob gas in wei - `maxFeePerGas` `string`: The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei - `maxPriorityFeePerGas` `string`: Maximum fee per gas the sender is willing to pay to miners in wei - `nonce` `string` - `to` `string` - `type` `string` - `value` `string` - `aaAuthorizationList` `object[]`: EIP-7702-style authorizations (Tempo AA). - `address` `string` _(required)_ - `chainId` `string` _(required)_ - `nonce` `string` _(required)_ - `signature` `object` _(required)_ - `calls` `object[]`: Tempo AA batched calls. - `input` `string` _(required)_: Call input data (accepts `data` on input). - `to` `string` _(required)_ - `value` `string` _(required)_ - `feePayerSignature` `object`: Gas-sponsor signature, when fee-payer sponsored. - `r` `string` _(required)_ - `s` `string` _(required)_ - `v` `string` _(required)_: Recovery id (`0` or `1`); mirrors `yParity`. - `yParity` `string` _(required)_ - `feeToken` `string`: TIP-20 token used to pay fees. - `keyAuthorization` `object`: Access-key provisioning authorization. - `account` `string`: Target account binding. - `allowedCalls` `object[]`: Allowed call scopes; `null` means unrestricted. - `selectorRules` `object[]` - `recipients` `string[]` - `selector` `string` _(required)_ - `target` `string` _(required)_ - `chainId` `string` _(required)_ - `expiry` `string`: Expiry timestamp; `null` means no expiry. - `isAdmin` `boolean` _(required)_ - `keyId` `string` _(required)_ - `keyType` `string` _(required)_ - `limits` `object[]`: Spending limits; `null` means unlimited. - `limit` `string` _(required)_ - `period` `string` _(required)_ - `token` `string` _(required)_ - `signature` `object` _(required)_ - `witness` `string` - `keyData` `string`: Access-key public key data. - `keyId` `string`: Access-key id (address). - `keyType` `string`: Access-key signature scheme. - `nonceKey` `string`: Tempo 2D nonce key. - `validAfter` `string`: Earliest valid timestamp. - `validBefore` `string`: Latest valid timestamp. - `Block` `string` ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_estimateGas", "params": [ { "from": "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73", "to": "0x44aa93095d6749a706051658b970b941c72c1d53", "value": "0x1" }, null ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_estimateGas', params: [ { from: '0xfe3b557e8fb62b89f4916b721be55ceb828dbd73', to: '0x44aa93095d6749a706051658b970b941c72c1d53', value: '0x1' }, null ] }) }) ``` ## eth_feeHistory `POST /rpc` Transaction fee history Returns transaction base fee per gas and effective priority fee per gas for the requested/supported block range. ### Parameters - `blockCount` `string` _(required)_: Requested range of blocks. Clients will return less than the requested range if not all blocks are available. - `newestBlock` `string` _(required)_: Highest block of the requested range. - `rewardPercentiles` `number[]` _(required)_: A monotonically increasing list of percentile values. For each block in the requested range, the transactions will be sorted in ascending order by effective tip per gas and the corresponding effective tip for the percentile will be determined, accounting for gas consumed. ### Responses #### `200`: Fee history for the returned block range. This can be a subsection of the requested range if not all blocks are available. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object`: Fee history results. - `baseFeePerBlobGas` `string[]`: An array of block base fees per blob gas. This includes the next block after the newest of the returned range, because this value can be derived from the newest block. Zeroes are returned for pre-EIP-4844 blocks. - `baseFeePerGas` `string[]` _(required)_: An array of block base fees per gas. This includes the next block after the newest of the returned range, because this value can be derived from the newest block. Zeroes are returned for pre-EIP-1559 blocks. - `blobGasUsedRatio` `number[]`: An array of block blob gas used ratios. These are calculated as the ratio of blobGasUsed and the max blob gas per block. - `gasUsedRatio` `number[]` _(required)_: An array of block gas used ratios. These are calculated as the ratio of gasUsed and gasLimit. - `oldestBlock` `string` _(required)_: Lowest number block of returned range. - `reward` `string[][]`: A two-dimensional array of effective priority fees per gas at the requested block percentiles. ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_feeHistory", "params": [ "0x5", "latest", [ 20, 30 ] ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_feeHistory', params: [ '0x5', 'latest', { '0': 20, '1': 30 } ] }) }) ``` ## eth_fillTransaction `POST /rpc` Fills the defaults (nonce, gas, fees, chainId, …) on a given unsigned transaction, returning the filled transaction object and its raw EIP-2718 encoding. ### Parameters - `Transaction` `object` _(required)_ - `accessList` `object[]`: EIP-2930 access list - `address` `string` _(required)_ - `storageKeys` `string[]` _(required)_ - `authorizationList` `object[]`: EIP-7702 authorization list - `address` `string` _(required)_ - `chainId` `string` _(required)_: Chain ID on which this transaction is valid - `nonce` `string` _(required)_ - `r` `string` _(required)_ - `s` `string` _(required)_ - `yParity` `string` _(required)_: The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature - `blobVersionedHashes` `string[]`: List of versioned blob hashes associated with the transaction's EIP-4844 data blobs. - `blobs` `string[]`: Raw blob data. - `chainId` `string`: Chain ID that this transaction is valid on. - `from` `string` - `gas` `string` - `gasPrice` `string`: The gas price willing to be paid by the sender in wei - `input` `string` - `maxFeePerBlobGas` `string`: The maximum total fee per gas the sender is willing to pay for blob gas in wei - `maxFeePerGas` `string`: The maximum total fee per gas the sender is willing to pay (includes the network / base fee and miner / priority fee) in wei - `maxPriorityFeePerGas` `string`: Maximum fee per gas the sender is willing to pay to miners in wei - `nonce` `string` - `to` `string` - `type` `string` - `value` `string` - `aaAuthorizationList` `object[]`: EIP-7702-style authorizations (Tempo AA). - `address` `string` _(required)_ - `chainId` `string` _(required)_ - `nonce` `string` _(required)_ - `signature` `object` _(required)_ - `calls` `object[]`: Tempo AA batched calls. - `input` `string` _(required)_: Call input data (accepts `data` on input). - `to` `string` _(required)_ - `value` `string` _(required)_ - `feePayerSignature` `object`: Gas-sponsor signature, when fee-payer sponsored. - `r` `string` _(required)_ - `s` `string` _(required)_ - `v` `string` _(required)_: Recovery id (`0` or `1`); mirrors `yParity`. - `yParity` `string` _(required)_ - `feeToken` `string`: TIP-20 token used to pay fees. - `keyAuthorization` `object`: Access-key provisioning authorization. - `account` `string`: Target account binding. - `allowedCalls` `object[]`: Allowed call scopes; `null` means unrestricted. - `selectorRules` `object[]` - `recipients` `string[]` - `selector` `string` _(required)_ - `target` `string` _(required)_ - `chainId` `string` _(required)_ - `expiry` `string`: Expiry timestamp; `null` means no expiry. - `isAdmin` `boolean` _(required)_ - `keyId` `string` _(required)_ - `keyType` `string` _(required)_ - `limits` `object[]`: Spending limits; `null` means unlimited. - `limit` `string` _(required)_ - `period` `string` _(required)_ - `token` `string` _(required)_ - `signature` `object` _(required)_ - `witness` `string` - `keyData` `string`: Access-key public key data. - `keyId` `string`: Access-key id (address). - `keyType` `string`: Access-key signature scheme. - `nonceKey` `string`: Tempo 2D nonce key. - `validAfter` `string`: Earliest valid timestamp. - `validBefore` `string`: Latest valid timestamp. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `raw` `string` _(required)_: EIP-2718 typed-envelope encoding of the filled transaction. - `tx` `object & object | object` _(required)_: The defaults-filled transaction object. ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_fillTransaction", "params": [ { "to": "0x44aa93095d6749a706051658b970b941c72c1d53", "input": "0xa9059cbb000000000000000000000000627306090abab3a6e1400e9345bc60c78a8bef570000000000000000000000000000000000000000000000000de0b6b3a7640000" } ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_fillTransaction', params: [ { to: '0x44aa93095d6749a706051658b970b941c72c1d53', input: '0xa9059cbb000000000000000000000000627306090abab3a6e1400e9345bc60c78a8bef570000000000000000000000000000000000000000000000000de0b6b3a7640000' } ] }) }) ``` ## eth_gasPrice `POST /rpc` Returns the current price per gas in wei. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_gasPrice", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_gasPrice', params: [] }) }) ``` ## eth_getBlockAccessList `POST /rpc` Returns the block access list for a given block. ### Parameters - `Block` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object[]` - `address` `string` _(required)_ - `balanceChanges` `object[]` - `index` `string` _(required)_ - `value` `string` _(required)_ - `codeChanges` `object[]` - `code` `string` _(required)_ - `index` `string` _(required)_ - `nonceChanges` `object[]` - `index` `string` _(required)_ - `value` `string` _(required)_ - `storageChanges` `object[]` - `changes` `object[]` _(required)_ - `index` `string` _(required)_ - `value` `string` _(required)_ - `key` `string` _(required)_ - `storageReads` `string[]` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getBlockAccessList", "params": [ "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockAccessList', params: ['latest'] }) }) ``` ## eth_getBlockByHash `POST /rpc` Returns information about a block by hash. ### Parameters - `Block hash` `string` _(required)_ - `Hydrated transactions` `boolean` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `baseFeePerGas` `string` - `blobGasUsed` `string` - `blockAccessListHash` `string` - `difficulty` `string` - `excessBlobGas` `string` - `extraData` `string` _(required)_ - `gasLimit` `string` _(required)_ - `gasUsed` `string` _(required)_ - `hash` `string` _(required)_ - `logsBloom` `string` _(required)_ - `miner` `string` _(required)_ - `mixHash` `string` _(required)_ - `nonce` `string` _(required)_ - `number` `string` _(required)_ - `parentBeaconBlockRoot` `string` - `parentHash` `string` _(required)_ - `receiptsRoot` `string` _(required)_ - `requestsHash` `string` - `sha3Uncles` `string` _(required)_ - `size` `string` _(required)_ - `stateRoot` `string` _(required)_ - `timestamp` `string` _(required)_ - `transactions` `string[] | object & object & object | object[]` _(required)_ - `transactionsRoot` `string` _(required)_ - `uncles` `string[]` _(required)_ - `withdrawals` `object[]` - `address` `string` _(required)_ - `amount` `string` _(required)_ - `index` `string` _(required)_ - `validatorIndex` `string` _(required)_ - `withdrawalsRoot` `string` - `consensusContext` `object`: Tempo consensus context. - `epoch` `string` _(required)_ - `parentView` `string` _(required)_ - `proposer` `string` _(required)_: Proposer consensus public key. - `view` `string` _(required)_ - `mainBlockGeneralGasLimit` `string` _(required)_ - `sharedGasLimit` `string` _(required)_ - `timestampMillis` `string` _(required)_ - `timestampMillisPart` `string` _(required)_ ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByHash", "params": [ "0xd5f1812548be429cbdc6376b29611fc49e06f1359758c4ceaaa3b393e2239f9c", false ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByHash', params: ['0xd5f1812548be429cbdc6376b29611fc49e06f1359758c4ceaaa3b393e2239f9c', false] }) }) ``` ## eth_getBlockByNumber `POST /rpc` Returns information about a block by number. ### Parameters - `Block` `string` _(required)_ - `Hydrated transactions` `boolean` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `baseFeePerGas` `string` - `blobGasUsed` `string` - `blockAccessListHash` `string` - `difficulty` `string` - `excessBlobGas` `string` - `extraData` `string` _(required)_ - `gasLimit` `string` _(required)_ - `gasUsed` `string` _(required)_ - `hash` `string` _(required)_ - `logsBloom` `string` _(required)_ - `miner` `string` _(required)_ - `mixHash` `string` _(required)_ - `nonce` `string` _(required)_ - `number` `string` _(required)_ - `parentBeaconBlockRoot` `string` - `parentHash` `string` _(required)_ - `receiptsRoot` `string` _(required)_ - `requestsHash` `string` - `sha3Uncles` `string` _(required)_ - `size` `string` _(required)_ - `stateRoot` `string` _(required)_ - `timestamp` `string` _(required)_ - `transactions` `string[] | object & object & object | object[]` _(required)_ - `transactionsRoot` `string` _(required)_ - `uncles` `string[]` _(required)_ - `withdrawals` `object[]` - `address` `string` _(required)_ - `amount` `string` _(required)_ - `index` `string` _(required)_ - `validatorIndex` `string` _(required)_ - `withdrawalsRoot` `string` - `consensusContext` `object`: Tempo consensus context. - `epoch` `string` _(required)_ - `parentView` `string` _(required)_ - `proposer` `string` _(required)_: Proposer consensus public key. - `view` `string` _(required)_ - `mainBlockGeneralGasLimit` `string` _(required)_ - `sharedGasLimit` `string` _(required)_ - `timestampMillis` `string` _(required)_ - `timestampMillisPart` `string` _(required)_ ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getBlockByNumber", "params": [ "0x68b3", false ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockByNumber', params: ['0x68b3', false] }) }) ``` ## eth_getBlockReceipts `POST /rpc` Returns the receipts of a block by number or hash. ### Parameters - `Block` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object[]` - `blobGasPrice` `string`: The actual value per gas deducted from the sender's account for blob gas. Only specified for blob transactions as defined by EIP-4844. - `blobGasUsed` `string`: The amount of blob gas used for this specific transaction. Only specified for blob transactions as defined by EIP-4844. - `blockHash` `string` _(required)_ - `blockNumber` `string` _(required)_ - `contractAddress` `string`: The contract address created, if the transaction was a contract creation, otherwise null. - `cumulativeGasUsed` `string` _(required)_: The sum of gas used by this transaction and all preceding transactions in the same block. - `effectiveGasPrice` `string` _(required)_: The actual value per gas deducted from the sender's account. Before EIP-1559, this is equal to the transaction's gas price. After, it is equal to baseFeePerGas + min(maxFeePerGas - baseFeePerGas, maxPriorityFeePerGas). - `from` `string` _(required)_ - `gasUsed` `string` _(required)_: The amount of gas used for this specific transaction alone. - `logs` `object[]` _(required)_ - `address` `string` - `blockHash` `string` - `blockNumber` `string` - `blockTimestamp` `string` - `data` `string` - `logIndex` `string` - `removed` `boolean` - `topics` `string[]` - `transactionHash` `string` _(required)_ - `transactionIndex` `string` - `logsBloom` `string` _(required)_ - `root` `string`: The post-transaction state root. Only specified for transactions included before the Byzantium upgrade. - `status` `string`: Either 1 (success) or 0 (failure). Only specified for transactions included after the Byzantium upgrade. - `to` `string`: Address of the receiver or null in a contract creation transaction. - `transactionHash` `string` _(required)_ - `transactionIndex` `string` _(required)_ - `type` `string` - `feePayer` `string` _(required)_: Address that paid the transaction fee. - `feeToken` `string`: TIP-20 token used to pay fees. ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getBlockReceipts", "params": [ "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockReceipts', params: ['latest'] }) }) ``` ## eth_getBlockTransactionCountByHash `POST /rpc` Returns the number of transactions in a block from a block matching the given block hash. ### Parameters - `Block hash` `string` ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getBlockTransactionCountByHash", "params": [ "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockTransactionCountByHash', params: ['0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238'] }) }) ``` ## eth_getBlockTransactionCountByNumber `POST /rpc` Returns the number of transactions in a block matching the given block number. ### Parameters - `Block` `string` ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getBlockTransactionCountByNumber", "params": [ "0xe8" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getBlockTransactionCountByNumber', params: ['0xe8'] }) }) ``` ## eth_getCode `POST /rpc` Returns code at a given address. ### Parameters - `Address` `string` _(required)_ - `Block` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getCode", "params": [ "0xa50a51c09a5c451c52bb714527e1974b686d8e77", "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getCode', params: ['0xa50a51c09a5c451c52bb714527e1974b686d8e77', 'latest'] }) }) ``` ## eth_getFilterChanges `POST /rpc` Polling method for the filter with the given ID (created using `eth_newFilter`). Returns an array of logs, block hashes, or transaction hashes since last poll, depending on the installed filter. ### Parameters - `Filter identifier` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string[] | object[]` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getFilterChanges", "params": [ "0x01" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getFilterChanges', params: ['0x01'] }) }) ``` ## eth_getFilterLogs `POST /rpc` Returns an array of all logs matching the filter with the given ID (created using `eth_newFilter`). ### Parameters - `Filter identifier` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string[] | object[]` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getFilterLogs", "params": [ "0x01" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getFilterLogs', params: ['0x01'] }) }) ``` ## eth_getLogs `POST /rpc` Returns an array of all logs matching the specified filter. ### Parameters - `Filter` `object` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string[] | object[]` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getLogs", "params": [ { "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "fromBlock": "0x137d3c2", "toBlock": "0x137d3c3", "topics": [] } ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [ { address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', fromBlock: '0x137d3c2', toBlock: '0x137d3c3', topics: [] } ] }) }) ``` ## eth_getProof `POST /rpc` Returns the merkle proof for a given account and optionally some storage keys. ### Parameters - `Address` `string` _(required)_ - `StorageKeys` `string[]` _(required)_ - `Block` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `accountProof` `string[]` _(required)_ - `address` `string` _(required)_ - `balance` `string` _(required)_ - `codeHash` `string` _(required)_ - `nonce` `string` _(required)_ - `storageHash` `string` _(required)_ - `storageProof` `object[]` _(required)_ - `key` `string` _(required)_ - `proof` `string[]` _(required)_ - `value` `string` _(required)_ ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getProof", "params": [ "0xe5cB067E90D5Cd1F8052B83562Ae670bA4A211a8", [ "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" ], "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getProof', params: [ '0xe5cB067E90D5Cd1F8052B83562Ae670bA4A211a8', { '0': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421' }, 'latest' ] }) }) ``` ## eth_getStorageAt `POST /rpc` Returns the value from a storage position at a given address. ### Parameters - `Address` `string` _(required)_ - `Storage slot` `string` _(required)_ - `Block` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getStorageAt", "params": [ "0xfe3b557e8fb62b89f4916b721be55ceb828dbd73", "0x0", "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getStorageAt', params: ['0xfe3b557e8fb62b89f4916b721be55ceb828dbd73', '0x0', 'latest'] }) }) ``` ## eth_getStorageValues `POST /rpc` Returns the values of multiple storage slots for multiple accounts in a single request. ### Parameters - `Requests` `object` _(required)_ - `Block` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getStorageValues", "params": [ { "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": [ "0x0000000000000000000000000000000000000000000000000000000000000003" ], "0xdAC17F958D2ee523a2206206994597C13D831ec7": [ "0x0000000000000000000000000000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000000000000000000000000000006" ] }, "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getStorageValues', params: [ { '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': ['0x0000000000000000000000000000000000000000000000000000000000000003'], '0xdAC17F958D2ee523a2206206994597C13D831ec7': ['0x0000000000000000000000000000000000000000000000000000000000000002', '0x0000000000000000000000000000000000000000000000000000000000000006'] }, 'latest' ] }) }) ``` ## eth_getTransactionByBlockHashAndIndex `POST /rpc` Returns information about a transaction by block hash and transaction index position. ### Parameters - `Block hash` `string` _(required)_ - `Transaction index` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object & object & object | object` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionByBlockHashAndIndex", "params": [ "0xbf137c3a7a1ebdfac21252765e5d7f40d115c2757e4a4abee929be88c624fdb7", "0x2" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionByBlockHashAndIndex', params: ['0xbf137c3a7a1ebdfac21252765e5d7f40d115c2757e4a4abee929be88c624fdb7', '0x2'] }) }) ``` ## eth_getTransactionByBlockNumberAndIndex `POST /rpc` Returns information about a transaction by block number and transaction index position. ### Parameters - `Block` `string` _(required)_ - `Transaction index` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object & object & object | object` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionByBlockNumberAndIndex", "params": [ "0x1442e", "0x2" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionByBlockNumberAndIndex', params: ['0x1442e', '0x2'] }) }) ``` ## eth_getTransactionByHash `POST /rpc` Returns the information about a transaction requested by transaction hash. ### Parameters - `Transaction hash` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object & object & object | object` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionByHash", "params": [ "0xa52be92809541220ee0aaaede6047d9a6c5d0cd96a517c854d944ee70a0ebb44" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionByHash', params: ['0xa52be92809541220ee0aaaede6047d9a6c5d0cd96a517c854d944ee70a0ebb44'] }) }) ``` ## eth_getTransactionCount `POST /rpc` Returns the nonce of an account in the state. NOTE: The name eth_getTransactionCount reflects the historical fact that an account's nonce and sent transaction count were the same. After the Pectra fork, with the inclusion of EIP-7702, this is no longer true. ### Parameters - `Address` `string` _(required)_ - `Block` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionCount", "params": [ "0xc94770007dda54cF92009BFF0dE90c06F603a09f", "latest" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionCount', params: ['0xc94770007dda54cF92009BFF0dE90c06F603a09f', 'latest'] }) }) ``` ## eth_getTransactionReceipt `POST /rpc` Returns the receipt of a transaction by transaction hash. ### Parameters - `Transaction hash` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `blobGasPrice` `string`: The actual value per gas deducted from the sender's account for blob gas. Only specified for blob transactions as defined by EIP-4844. - `blobGasUsed` `string`: The amount of blob gas used for this specific transaction. Only specified for blob transactions as defined by EIP-4844. - `blockHash` `string` _(required)_ - `blockNumber` `string` _(required)_ - `contractAddress` `string`: The contract address created, if the transaction was a contract creation, otherwise null. - `cumulativeGasUsed` `string` _(required)_: The sum of gas used by this transaction and all preceding transactions in the same block. - `effectiveGasPrice` `string` _(required)_: The actual value per gas deducted from the sender's account. Before EIP-1559, this is equal to the transaction's gas price. After, it is equal to baseFeePerGas + min(maxFeePerGas - baseFeePerGas, maxPriorityFeePerGas). - `from` `string` _(required)_ - `gasUsed` `string` _(required)_: The amount of gas used for this specific transaction alone. - `logs` `object[]` _(required)_ - `address` `string` - `blockHash` `string` - `blockNumber` `string` - `blockTimestamp` `string` - `data` `string` - `logIndex` `string` - `removed` `boolean` - `topics` `string[]` - `transactionHash` `string` _(required)_ - `transactionIndex` `string` - `logsBloom` `string` _(required)_ - `root` `string`: The post-transaction state root. Only specified for transactions included before the Byzantium upgrade. - `status` `string`: Either 1 (success) or 0 (failure). Only specified for transactions included after the Byzantium upgrade. - `to` `string`: Address of the receiver or null in a contract creation transaction. - `transactionHash` `string` _(required)_ - `transactionIndex` `string` _(required)_ - `type` `string` - `feePayer` `string` _(required)_: Address that paid the transaction fee. - `feeToken` `string`: TIP-20 token used to pay fees. ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionReceipt", "params": [ "0x504ce587a65bdbdb6414a0c6c16d86a04dd79bfcc4f2950eec9634b30ce5370f" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionReceipt', params: ['0x504ce587a65bdbdb6414a0c6c16d86a04dd79bfcc4f2950eec9634b30ce5370f'] }) }) ``` ## eth_maxPriorityFeePerGas `POST /rpc` Returns the current maxPriorityFeePerGas per gas in wei. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_maxPriorityFeePerGas", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_maxPriorityFeePerGas', params: [] }) }) ``` ## eth_newBlockFilter `POST /rpc` Creates a filter in the node, allowing for later polling. Registers client interest in new blocks, and returns an identifier. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_newBlockFilter", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_newBlockFilter', params: [] }) }) ``` ## eth_newFilter `POST /rpc` Install a log filter in the server, allowing for later polling. Registers client interest in logs matching the filter, and returns an identifier. ### Parameters - `Filter` `object` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_newFilter", "params": [ { "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "fromBlock": "0x137d3c2", "toBlock": "0x137d3c3", "topics": [] } ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_newFilter', params: [ { address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', fromBlock: '0x137d3c2', toBlock: '0x137d3c3', topics: [] } ] }) }) ``` ## eth_newPendingTransactionFilter `POST /rpc` Creates a filter in the node, allowing for later polling. Registers client interest in new transactions, and returns an identifier. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_newPendingTransactionFilter", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_newPendingTransactionFilter', params: [] }) }) ``` ## eth_sendRawTransaction `POST /rpc` Submits a raw transaction. You can create and sign a transaction externally using a library such as [web3.js](https://web3js.readthedocs.io/) or [ethers.js](https://docs.ethers.org/). For [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) transactions, the raw form must be the network form. This means it includes the blobs, KZG commitments, and KZG proofs. For [EIP-7594](https://eips.ethereum.org/EIPS/eip-7594) transactions, the raw format must be the network form. This means it includes the blobs, KZG commitments, and cell proofs. The logic for handling the new transaction during fork boundaries are 1. When receiving an encoded transaction with cell proofs before the PeerDAS fork activates, we reject it. Only blob proofs are accepted into the pool. 2. At the time of fork activation, the implementer could (not mandatory) - Drop all old-format transactions - Convert old proofs to new format (computationally expensive) - Convert only when including in a locally produced block 3. After the fork has activated, only txs with cell proofs are accepted via p2p relay. 4. On RPC (eth_sendRawTransaction), txs with blob proofs may still be accepted and will be auto-converted by the node. At implementer discretion, this facility can be deprecated later when users have switched to new client libraries that can create cell proofs. ### Parameters - `Transaction` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_sendRawTransaction", "params": [ "0xf869018203e882520894f17f52151ebef6c7334fad080c5704d77216b732881bc16d674ec80000801ba02da1c48b670996dcb1f447ef9ef00b33033c48a4fe938f420bec3e56bfd24071a062e0aa78a81bf0290afbc3a9d8e9a068e6d74caa66c5e0fa8a46deaae96b0833" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_sendRawTransaction', params: ['0xf869018203e882520894f17f52151ebef6c7334fad080c5704d77216b732881bc16d674ec80000801ba02da1c48b670996dcb1f447ef9ef00b33033c48a4fe938f420bec3e56bfd24071a062e0aa78a81bf0290afbc3a9d8e9a068e6d74caa66c5e0fa8a46deaae96b0833'] }) }) ``` ## eth_sendRawTransactionSync `POST /rpc` Submits a raw (signed) transaction and waits for it to be included, returning its receipt. Returns a timeout error if the transaction is not included within the node-configured window. ### Parameters - `Transaction` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `blobGasPrice` `string`: The actual value per gas deducted from the sender's account for blob gas. Only specified for blob transactions as defined by EIP-4844. - `blobGasUsed` `string`: The amount of blob gas used for this specific transaction. Only specified for blob transactions as defined by EIP-4844. - `blockHash` `string` _(required)_ - `blockNumber` `string` _(required)_ - `contractAddress` `string`: The contract address created, if the transaction was a contract creation, otherwise null. - `cumulativeGasUsed` `string` _(required)_: The sum of gas used by this transaction and all preceding transactions in the same block. - `effectiveGasPrice` `string` _(required)_: The actual value per gas deducted from the sender's account. Before EIP-1559, this is equal to the transaction's gas price. After, it is equal to baseFeePerGas + min(maxFeePerGas - baseFeePerGas, maxPriorityFeePerGas). - `from` `string` _(required)_ - `gasUsed` `string` _(required)_: The amount of gas used for this specific transaction alone. - `logs` `object[]` _(required)_ - `address` `string` - `blockHash` `string` - `blockNumber` `string` - `blockTimestamp` `string` - `data` `string` - `logIndex` `string` - `removed` `boolean` - `topics` `string[]` - `transactionHash` `string` _(required)_ - `transactionIndex` `string` - `logsBloom` `string` _(required)_ - `root` `string`: The post-transaction state root. Only specified for transactions included before the Byzantium upgrade. - `status` `string`: Either 1 (success) or 0 (failure). Only specified for transactions included after the Byzantium upgrade. - `to` `string`: Address of the receiver or null in a contract creation transaction. - `transactionHash` `string` _(required)_ - `transactionIndex` `string` _(required)_ - `type` `string` - `feePayer` `string` _(required)_: Address that paid the transaction fee. - `feeToken` `string`: TIP-20 token used to pay fees. ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_sendRawTransactionSync", "params": [ "0xf869018203e882520894f17f52151ebef6c7334fad080c5704d77216b732881bc16d674ec80000801ba02da1c48b670996dcb1f447ef9ef00b33033c48a4fe938f420bec3e56bfd24071a062e0aa78a81bf0290afbc3a9d8e9a068e6d74caa66c5e0fa8a46deaae96b0833" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_sendRawTransactionSync', params: ['0xf869018203e882520894f17f52151ebef6c7334fad080c5704d77216b732881bc16d674ec80000801ba02da1c48b670996dcb1f447ef9ef00b33033c48a4fe938f420bec3e56bfd24071a062e0aa78a81bf0290afbc3a9d8e9a068e6d74caa66c5e0fa8a46deaae96b0833'] }) }) ``` ## eth_simulateV1 `POST /rpc` Executes a sequence of message calls building on each other's state without creating transactions on the block chain, optionally overriding block and state data ### Parameters - `Payload` `object` _(required)_ - `blockStateCalls` `unknown[]` _(required)_: Definition of blocks that can contain calls and overrides - `returnFullTransactions` `boolean`: When true, the method returns full transaction objects, otherwise, just hashes are returned. - `traceTransfers` `boolean`: Adds ETH transfers as ERC20 transfer events to the logs. These transfers have emitter contract parameter set as address(0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee). Default: false. - `validation` `boolean`: When true, the eth_simulateV1 does all validations that a normal EVM would do, except contract sender and signature checks. When false, eth_simulateV1 behaves like eth_call. Default: false. - `Block tag` `string`: default: 'latest' ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object & object[]` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_simulateV1", "params": [ null, null ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_simulateV1', params: [null, null] }) }) ``` ## eth_syncing `POST /rpc` Returns an object with data about the sync status or false. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object | boolean` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_syncing", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_syncing', params: [] }) }) ``` ## eth_uninstallFilter `POST /rpc` Uninstalls a filter with given id. ### Parameters - `Filter identifier` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `boolean` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "eth_uninstallFilter", "params": [ "0x01" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_uninstallFilter', params: ['0x01'] }) }) ``` ## tempo_forkSchedule `POST /rpc` Returns the Tempo fork schedule and the currently active fork at the chain head. ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `object` - `active` `string` _(required)_ - `schedule` `object[]` _(required)_ - `activationTime` `integer` _(required)_ - `active` `boolean` _(required)_ - `forkId` `string`: EIP-2124 fork hash at this fork activation point. Omitted until active. - `name` `string` _(required)_ ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "tempo_forkSchedule", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tempo_forkSchedule', params: [] }) }) ``` ## tempo_fundAddress `POST /rpc` Mints test stablecoins to an address on faucet-enabled testnet endpoints and returns the transaction hashes. ### Parameters - `Address` `string` _(required)_ ### Responses #### `200`: JSON-RPC response. Body (`application/json`): - `jsonrpc` `string` - `id` `integer` - `result` `string[]` ### Example request ```bash curl https://api.tempo.xyz/rpc \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "jsonrpc": "2.0", "id": 1, "method": "tempo_fundAddress", "params": [ "0x627306090abab3a6e1400e9345bc60c78a8bef57" ] }' ``` ```ts fetch('https://api.tempo.xyz/rpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tempo_fundAddress', params: ['0x627306090abab3a6e1400e9345bc60c78a8bef57'] }) }) ``` ## Call chain JSON-RPC `POST /rpc/{chain}` Proxies single or batch Ethereum JSON-RPC calls to a selected Tempo chain. Zone API keys are method restricted; upstream responses pass through unchanged. ### Path parameters - `chain` `string` _(required)_: Chain selector. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). ### Header parameters - `X-Authorization-Token` `string`: Optional signed Zone credential forwarded only to the selected Zone RPC. ### Request body (required) (`application/json`) ### Responses #### `200`: JSON-RPC response returned by the upstream endpoint. Empty 200 responses are normalized to null; unexpected empty responses are reported as provider failures. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `204`: Notification accepted with no response body. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: The chain id is invalid or this API deployment does not support it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `413`: Zone and anonymous RPC requests must not exceed 64 KiB. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The Tempo API could not reach the upstream RPC endpoint. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `default`: An upstream HTTP response passed through unchanged, including its status, headers, content type, and body. ### Example request ```bash curl https://api.tempo.xyz/rpc/string \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "id": 1, "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [] }' ``` ```ts fetch('https://api.tempo.xyz/rpc/string', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_blockNumber', params: [] }) }) ``` # Tokens TIP-20 token details, supply, and holders. ## List tokens `GET /v1/tokens` Lists TIP-20 tokens on Tempo. TIP-20 is Tempo’s payments-focused token standard and a superset of ERC-20. ### Query parameters - `addresses` `string[]`: Comma-separated token contract addresses to fetch (max 50). Returns a single page in input order (unresolvable addresses omitted); `cursor`, `page`, and `order` are inapplicable. Combines with `include`; `currency` and `verified` further filter the resolved set. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `currency` `string`: Only include tokens denominated in this currency (e.g. `USD`). Case-insensitive. Matched against the `currency` field of the onchain `TokenCreated` event, so any string a token deployer wrote is acceptable; the listed examples are the well-known curated currencies. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated extra token details to include, such as `admin,createdAt,holderCount,quoteToken,transferStats`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `verified` `boolean`: When `true`, only return tokens in the curated verified list. `currency`, `include`, and the pagination parameters (`limit`, `page`, `cursor`, `order`) all apply; the list is paginated positionally over its canonical order (`order=asc` reverses it). ### Responses #### `200`: A page of TIP-20 tokens. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The tokens in this page. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `admin` `string`: Token admin from the onchain `TokenCreated` event, present when requested via `include=admin` and indexed data is available. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `createdAt` `string `: Token creation timestamp, present when requested via `include=createdAt` and indexed data is available. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `holderCount` `integer`: The number of accounts that currently hold a positive balance of this token, when indexed holder data is available. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `quoteToken` `string`: Quote token from the onchain `TokenCreated` event, present when requested via `include=quoteToken` and indexed data is available. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `totalSupply` `string`: The token’s total supply as a decimal string in the smallest unit, so large values keep full precision. - `transferStats` `object`: Lifetime `Transfer` event statistics, present when requested via `include=transferStats` and indexed data is available. - `count` `integer` _(required)_: The total number of `Transfer` events emitted by this token. - `firstAt` `string ` _(required)_: The time of this token’s first `Transfer` event, or `null` if no transfers exist. - `lastAt` `string ` _(required)_: The time of this token’s most recent `Transfer` event, or `null` if no transfers exist. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/tokens?addresses=0x20c0000000000000000000008f5425160ebe5525&chainId=4217¤cy=EUR&cursor=WzIzNDU2Nzg5LDBd&include=admin,createdAt,holderCount,quoteToken,transferStats&limit=10&order=desc&page=1&verified=true' ``` ```ts fetch('https://api.tempo.xyz/v1/tokens?addresses=0x20c0000000000000000000008f5425160ebe5525&chainId=4217¤cy=EUR&cursor=WzIzNDU2Nzg5LDBd&include=admin,createdAt,holderCount,quoteToken,transferStats&limit=10&order=desc&page=1&verified=true') ``` ## List token holders `GET /v1/tokens/{token}/holders` Lists the accounts that hold a TIP-20 token, ordered from largest to smallest balance. ### Path parameters - `token` `string` _(required)_: The TIP-20 token contract address on Tempo. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated related resources to include, such as `token,totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. ### Responses #### `200`: A page of token holders. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The holders in this page, ordered by balance from highest to lowest. - `address` `string` _(required)_: The account address that holds this token. - `balance` `string` _(required)_: A non-negative whole number, given as a decimal string so very large token amounts keep full precision. Expressed in the smallest unit of the token. - `id` `string` _(required)_: A stable resource ID for this holder, equal to the holder address. - `meta` `object`: Extra resources you requested with `include`. - `totalCountCapped` `boolean`: Whether `totalCount` reached the API count cap; holder counts are exact here. - `token` `object`: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `totalCount` `integer`: The total number of token holders, returned when you request `include=totalCount`. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The upstream indexer could not serve this request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/tokens/0x20c0000000000000000000000000000000000000/holders?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=token,totalCount&limit=10&page=1' ``` ```ts fetch('https://api.tempo.xyz/v1/tokens/0x20c0000000000000000000000000000000000000/holders?chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=token,totalCount&limit=10&page=1') ``` ## Get token by symbol `GET /v1/tokens/{symbol}` Returns token metadata for a verified TIP-20 token using its symbol, such as `USDC`. ### Path parameters - `symbol` `string` _(required)_: The symbol of a token in Tempo’s verified token list. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated extra token details to include, such as `admin,createdAt,holderCount,quoteToken,transferStats`. ### Responses #### `200`: Metadata for one TIP-20 token. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `admin` `string`: Token admin from the onchain `TokenCreated` event, present when requested via `include=admin` and indexed data is available. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `createdAt` `string `: Token creation timestamp, present when requested via `include=createdAt` and indexed data is available. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `holderCount` `integer`: The number of accounts that currently hold a positive balance of this token, when indexed holder data is available. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `quoteToken` `string`: Quote token from the onchain `TokenCreated` event, present when requested via `include=quoteToken` and indexed data is available. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `totalSupply` `string`: The token’s total supply as a decimal string in the smallest unit, so large values keep full precision. - `transferStats` `object`: Lifetime `Transfer` event statistics, present when requested via `include=transferStats` and indexed data is available. - `count` `integer` _(required)_: The total number of `Transfer` events emitted by this token. - `firstAt` `string ` _(required)_: The time of this token’s first `Transfer` event, or `null` if no transfers exist. - `lastAt` `string ` _(required)_: The time of this token’s most recent `Transfer` event, or `null` if no transfers exist. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No token was found for this address or symbol. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: An upstream RPC or indexer request failed while resolving this token. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/tokens/USDC.e?chainId=4217&include=admin,createdAt,holderCount,quoteToken,transferStats' ``` ```ts fetch('https://api.tempo.xyz/v1/tokens/USDC.e?chainId=4217&include=admin,createdAt,holderCount,quoteToken,transferStats') ``` ## Get token by address `GET /v1/tokens/{token}` Returns token metadata for a TIP-20 contract address, whether or not the token is in the verified list. ### Path parameters - `token` `string` _(required)_: The TIP-20 token contract address on Tempo. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated extra token details to include, such as `admin,createdAt,holderCount,quoteToken,transferStats`. ### Responses #### `200`: Metadata for one TIP-20 token. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `admin` `string`: Token admin from the onchain `TokenCreated` event, present when requested via `include=admin` and indexed data is available. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `createdAt` `string `: Token creation timestamp, present when requested via `include=createdAt` and indexed data is available. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `holderCount` `integer`: The number of accounts that currently hold a positive balance of this token, when indexed holder data is available. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `quoteToken` `string`: Quote token from the onchain `TokenCreated` event, present when requested via `include=quoteToken` and indexed data is available. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `totalSupply` `string`: The token’s total supply as a decimal string in the smallest unit, so large values keep full precision. - `transferStats` `object`: Lifetime `Transfer` event statistics, present when requested via `include=transferStats` and indexed data is available. - `count` `integer` _(required)_: The total number of `Transfer` events emitted by this token. - `firstAt` `string ` _(required)_: The time of this token’s first `Transfer` event, or `null` if no transfers exist. - `lastAt` `string ` _(required)_: The time of this token’s most recent `Transfer` event, or `null` if no transfers exist. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No token was found for this address or symbol. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: An upstream RPC or indexer request failed while resolving this token. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/tokens/0x20c000000000000000000000b9537d11c60e8b50?chainId=4217&include=admin,createdAt,holderCount,quoteToken,transferStats' ``` ```ts fetch('https://api.tempo.xyz/v1/tokens/0x20c000000000000000000000b9537d11c60e8b50?chainId=4217&include=admin,createdAt,holderCount,quoteToken,transferStats') ``` ## Get token logo `GET /v1/tokens/{token}/logo` Returns the logo image for a TIP-20 token, using Tempo’s curated asset when available. ### Path parameters - `token` `string` _(required)_: The TIP-20 token contract address on Tempo. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. ### Responses #### `200`: The token logo image. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: The request parameters were invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: The API key is missing or invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: The API key cannot access this resource. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No logo image was found for this token. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: An upstream RPC request failed while fetching the logo. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/tokens/0x20c0000000000000000000000000000000000000/logo?chainId=4217' ``` ```ts fetch('https://api.tempo.xyz/v1/tokens/0x20c0000000000000000000000000000000000000/logo?chainId=4217') ``` # Transactions Transactions submitted to Tempo, and their receipts. ## List transactions `GET /v1/transactions` List transactions across Tempo, with filters for addresses, blocks, timestamps, fees, and status. ### Query parameters - `address` `string`: Filter to transactions where this address is either the sender or recipient. - `blockNumber.from` `integer`: Only include transactions at or after this block number. - `blockNumber.to` `integer`: Only include transactions at or before this block number. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `feePayer` `string`: Filter to transactions whose fee payer is this address. - `feeToken` `string`: Filter to transactions whose fee was paid in this token address. - `include` `string[]`: Comma-separated related resources to include, such as `feeToken.logoUri,feeToken.verified,receipt,totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `recipient` `string`: Filter to transactions sent to this recipient address. - `sender` `string`: Filter to transactions sent by this sender address. - `status` `string`: Filter to transactions whose receipt shows this execution status. - `timestamp.from` `string `: Only include transactions at or after this ISO 8601 timestamp. - `timestamp.to` `string `: Only include transactions at or before this ISO 8601 timestamp. ### Responses #### `200`: A page of transactions. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Transactions on this page. - `blockHash` `string` _(required)_: The block hash once included, or `null` while the transaction is pending. - `blockNumber` `integer` _(required)_: The block number once included, or `null` while the transaction is pending. - `calls` `object[]`: Decoded calls for Tempo account-abstraction transactions. - `data` `string`: Call data sent to the contract, as `0x`-prefixed bytes. - `to` `string` _(required)_: The address this call targets, or `null` when it creates a contract. - `chainId` `integer`: The chain ID that this transaction belongs to. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gas` `integer` _(required)_: Maximum gas the transaction is allowed to use. - `gasPrice` `string`: Gas price for legacy-style transactions, expressed as a decimal string when humanized. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `input` `string`: Call data for non-Tempo transaction formats, as `0x`-prefixed bytes. - `maxFeePerGas` `string`: Maximum total fee per gas the sender is willing to pay. - `maxPriorityFeePerGas` `string`: Maximum priority fee per gas for EIP-1559-style transactions. - `meta` `object` _(required)_: The original JSON-RPC payload plus any resources requested with `include`. - `receipt` `object`: The transaction outcome included only when you request `include=receipt`. - `blockHash` `string` _(required)_: The block hash, or `null` when the receipt was reconstructed from indexed data. - `blockNumber` `integer` _(required)_: The block number that included this receipt. - `contractAddress` `string` _(required)_: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `integer` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction, in attodollars per gas. - `feeAmount` `object` _(required)_: Fee charged to the fee payer, denominated in USD. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The fee’s nominal value in the requested `valuation.currency`. `null` when rates were unavailable. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gasUsed` `integer` _(required)_: Gas actually used to execute the transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `logs` `object[]` _(required)_: Event logs emitted by this transaction, returned in JSON-RPC log format. - `address` `string` _(required)_: The contract address that emitted this event log. - `blockHash` `string` _(required)_: The block hash once included, or `null` while pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while pending. - `blockTimestamp` `string`: Tempo-provided block timestamp for this log. - `data` `string` _(required)_: ABI-encoded data for the event parameters that are not indexed. - `logIndex` `string` _(required)_: The log position within the block, or `null` while pending. - `removed` `boolean` _(required)_: Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg. - `topics` `string[]` _(required)_: Indexed event topics, including the event signature hash as the first topic. - `transactionHash` `string` _(required)_: The transaction hash for this log, or `null` while pending. - `transactionIndex` `string` _(required)_: The transaction position within the block, or `null` while pending. - `meta` `object` _(required)_: Receipt metadata containing valuation rate provenance and the JSON-RPC payload. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `rpc` `object` _(required)_: The original JSON-RPC receipt payload. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `sender` `string` _(required)_: The address that sent or authorized the transaction. - `status` `string` _(required)_: Whether the transaction succeeded or reverted. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` when it is unavailable. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `integer` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. - `rpc` `object` _(required)_: The original JSON-RPC transaction payload. - `aaAuthorizationList` `object[]`: Tempo account-abstraction authorizations attached to the transaction. - `accessList` `object[]`: EIP-2930 access list that predeclares accounts and storage slots for the transaction. - `address` `string` _(required)_: The account whose storage slots are listed in this access-list entry. - `storageKeys` `string[]` _(required)_: The storage slot keys the transaction plans to access. - `authorizationList` `object[]`: EIP-7702 authorizations attached to the transaction. - `blobVersionedHashes` `string[]`: Versioned blob hashes for EIP-4844 blob data. - `blockHash` `string` _(required)_: The block hash once included, or `null` while the transaction is pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while the transaction is pending. - `blockTimestamp` `string`: The block timestamp for the block that included this transaction. - `calls` `object[]`: Decoded calls for Tempo account-abstraction transactions. - `data` `string`: Call data for this call; this is the same bytes as `input` when both are present. - `input` `string`: Call data sent to the contract, as `0x`-prefixed bytes. - `to` `string`: The address this call targets, or `null` when the call creates a contract. - `value` `string`: Native value sent with this call, as a hex quantity. - `chainId` `string`: The chain ID that the transaction is valid on; legacy transactions may omit it. - `feePayer` `string`: The address that paid the transaction fee. - `feePayerSignature` `object`: A signature envelope exactly as returned by JSON-RPC. - `type` `string`: The signature scheme used by this envelope, such as `secp256k1`, `p256`, or `keychain` when the RPC includes it. - `feeToken` `string`: The fee token requested by the transaction; Tempo fees are paid in USD stablecoins such as `pathUSD`. - `from` `string` _(required)_: The address that submitted or authorized the transaction. - `gas` `string` _(required)_: Maximum gas the transaction is allowed to use. - `gasPrice` `string`: Gas price for legacy-style transactions, expressed as a decimal string when humanized. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction. - `input` `string`: Call data for non-Tempo transaction formats, as `0x`-prefixed bytes. - `keyAuthorization` `object`: Key authorization data attached to this Tempo transaction. - `maxFeePerBlobGas` `string`: Maximum fee per blob gas for EIP-4844 blob data. - `maxFeePerGas` `string`: Maximum total fee per gas the sender is willing to pay. - `maxPriorityFeePerGas` `string`: Maximum priority fee per gas for EIP-1559-style transactions. - `nonce` `string` _(required)_: Nonce from the sender account that orders and de-duplicates transactions. - `nonceKey` `string`: Tempo two-dimensional nonce key used to group nonce sequences. - `r` `string`: The `r` value from the transaction ECDSA signature. - `s` `string`: The `s` value from the transaction ECDSA signature. - `signature` `object`: A signature envelope exactly as returned by JSON-RPC. - `type` `string`: The signature scheme used by this envelope, such as `secp256k1`, `p256`, or `keychain` when the RPC includes it. - `to` `string`: The recipient address, or `null` when the transaction creates a contract. - `transactionIndex` `string` _(required)_: The transaction position within its block, or `null` while pending. - `type` `string` _(required)_: Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo. - `v` `string`: The `v` recovery value from the transaction ECDSA signature. - `validAfter` `string`: Earliest Unix timestamp when this Tempo transaction may be included. - `validBefore` `string`: Latest Unix timestamp when this Tempo transaction may be included. - `value` `string`: Native value sent by non-Tempo transaction formats. - `yParity` `string`: The y-parity value from the transaction signature. - `nonce` `integer` _(required)_: Nonce from the sender account that orders and de-duplicates transactions. - `nonceKey` `string`: Tempo two-dimensional nonce key used by Tempo transactions. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction creates a contract. - `sender` `string` _(required)_: The address that submitted or authorized the transaction. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` while the transaction is pending. - `transactionIndex` `integer` _(required)_: The transaction position within its block, or `null` while pending. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. - `validAfter` `string `: Earliest ISO 8601 time when this Tempo transaction may be included. - `validBefore` `string `: Latest ISO 8601 time when this Tempo transaction may be included. - `value` `string` _(required)_: Native value transferred by this transaction, as a decimal string. - `meta` `object`: Response-level metadata requested with `include`, such as `totalCount`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read transaction or token data from an upstream service. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/transactions?address=0xbe058e1c4df8a4366a387bf595b284246a93039e&blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&feePayer=0xbe058e1c4df8a4366a387bf595b284246a93039e&feeToken=0x20c0000000000000000000000000000000000000&include=feeToken.logoUri,feeToken.verified,receipt,totalCount&limit=10&order=desc&page=1&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&status=success×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z' ``` ```ts fetch('https://api.tempo.xyz/v1/transactions?address=0xbe058e1c4df8a4366a387bf595b284246a93039e&blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&feePayer=0xbe058e1c4df8a4366a387bf595b284246a93039e&feeToken=0x20c0000000000000000000000000000000000000&include=feeToken.logoUri,feeToken.verified,receipt,totalCount&limit=10&order=desc&page=1&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&status=success×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z') ``` ## List transaction receipts `GET /v1/transactions/receipts` List transaction receipts across Tempo, with filters for addresses, blocks, timestamps, fees, and status. ### Query parameters - `address` `string`: Filter to receipts whose transaction has this address as sender or recipient. - `blockNumber.from` `integer`: Only include receipts at or after this block number. - `blockNumber.to` `integer`: Only include receipts at or before this block number. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `feePayer` `string`: Filter to receipts whose transaction fee was paid by this address. - `feeToken` `string`: Filter to receipts whose transaction fee was paid in this token address. - `include` `string[]`: Comma-separated resources to include, such as `feeToken.logoUri,feeToken.verified,totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `recipient` `string`: Filter to receipts for transactions sent to this recipient address. - `sender` `string`: Filter to receipts for transactions sent by this sender address. - `status` `string`: Filter to receipts with this execution status. - `timestamp.from` `string `: Only include receipts at or after this ISO 8601 timestamp. - `timestamp.to` `string `: Only include receipts at or before this ISO 8601 timestamp. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Responses #### `200`: A page of transaction receipts. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Receipts on this page. - `blockHash` `string` _(required)_: The block hash, or `null` when the receipt was reconstructed from indexed data. - `blockNumber` `integer` _(required)_: The block number that included this receipt. - `contractAddress` `string` _(required)_: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `integer` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction, in attodollars per gas. - `feeAmount` `object` _(required)_: Fee charged to the fee payer, denominated in USD. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The fee’s nominal value in the requested `valuation.currency`. `null` when rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gasUsed` `integer` _(required)_: Gas actually used to execute the transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `logs` `object[]` _(required)_: Event logs emitted by this transaction, returned in JSON-RPC log format. - `address` `string` _(required)_: The contract address that emitted this event log. - `blockHash` `string` _(required)_: The block hash once included, or `null` while pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while pending. - `blockTimestamp` `string`: Tempo-provided block timestamp for this log. - `data` `string` _(required)_: ABI-encoded data for the event parameters that are not indexed. - `logIndex` `string` _(required)_: The log position within the block, or `null` while pending. - `removed` `boolean` _(required)_: Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg. - `topics` `string[]` _(required)_: Indexed event topics, including the event signature hash as the first topic. - `transactionHash` `string` _(required)_: The transaction hash for this log, or `null` while pending. - `transactionIndex` `string` _(required)_: The transaction position within the block, or `null` while pending. - `meta` `object` _(required)_: Receipt metadata containing valuation rate provenance and the JSON-RPC payload. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `rpc` `object` _(required)_: The original JSON-RPC receipt payload. - `blobGasPrice` `string`: Blob gas price for EIP-4844 blob data. - `blobGasUsed` `string`: Blob gas used by EIP-4844 blob data. - `blockHash` `string` _(required)_: The block hash, or `null` for receipts reconstructed from the index. - `blockNumber` `string` _(required)_: The block number that included this receipt. - `blockTimestamp` `string`: Block timestamp added when this receipt is reconstructed from indexed data. - `contractAddress` `string`: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `string` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `string`: The token used to pay the fee; on Tempo this is a USD stablecoin such as `pathUSD`. - `from` `string` _(required)_: The address that sent or authorized the transaction. - `gasUsed` `string` _(required)_: Gas actually used to execute the transaction. - `logs` `object[]` _(required)_: Event logs emitted by contracts while this transaction executed. - `address` `string` _(required)_: The contract address that emitted this event log. - `blockHash` `string` _(required)_: The block hash once included, or `null` while pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while pending. - `blockTimestamp` `string`: Tempo-provided block timestamp for this log. - `data` `string` _(required)_: ABI-encoded data for the event parameters that are not indexed. - `logIndex` `string` _(required)_: The log position within the block, or `null` while pending. - `removed` `boolean` _(required)_: Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg. - `topics` `string[]` _(required)_: Indexed event topics, including the event signature hash as the first topic. - `transactionHash` `string` _(required)_: The transaction hash for this log, or `null` while pending. - `transactionIndex` `string` _(required)_: The transaction position within the block, or `null` while pending. - `logsBloom` `string`: Bloom filter summarizing the logs in this receipt. - `root` `string`: Post-transaction state root used by pre-Byzantium Ethereum receipts. - `status` `string` _(required)_: Execution status as a hex quantity: `0x1` means success and `0x0` means reverted. - `to` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `string` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `sender` `string` _(required)_: The address that sent or authorized the transaction. - `status` `string` _(required)_: Whether the transaction succeeded or reverted. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` when it is unavailable. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `integer` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. - `meta` `object`: Page-level resources: opt-in counts and valuation rate provenance. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `totalCount` `integer`: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `totalCountCapped` `boolean`: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read receipt or token data from an upstream service. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/transactions/receipts?address=0xbe058e1c4df8a4366a387bf595b284246a93039e&blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&feePayer=0xbe058e1c4df8a4366a387bf595b284246a93039e&feeToken=0x20c0000000000000000000000000000000000000&include=feeToken.logoUri,feeToken.verified,totalCount&limit=10&order=desc&page=1&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&status=success×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z&valuation.currency=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/transactions/receipts?address=0xbe058e1c4df8a4366a387bf595b284246a93039e&blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&feePayer=0xbe058e1c4df8a4366a387bf595b284246a93039e&feeToken=0x20c0000000000000000000000000000000000000&include=feeToken.logoUri,feeToken.verified,totalCount&limit=10&order=desc&page=1&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&status=success×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z&valuation.currency=AUD') ``` ## List token transactions `GET /v1/tokens/{token}/transactions` Lists transactions that interacted with this token contract. ### Path parameters - `token` `string` _(required)_: The TIP-20 token contract address on Tempo. ### Query parameters - `blockNumber.from` `integer`: Only include transactions at or after this block number. - `blockNumber.to` `integer`: Only include transactions at or before this block number. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `feePayer` `string`: Only include transactions where this account paid the fee. - `feeToken` `string`: Only include transactions whose fee was paid in this token. On Tempo, fees are paid in USD stablecoins instead of a separate volatile gas token. - `include` `string[]`: Comma-separated related resources to include, such as `feeToken.logoUri,feeToken.verified,token,totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `timestamp.from` `string `: Only include transactions at or after this ISO 8601 timestamp. - `timestamp.to` `string `: Only include transactions at or before this ISO 8601 timestamp. ### Responses #### `200`: A page of transactions involving this token contract. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The transactions in this page. - `blockHash` `string` _(required)_: The block hash once included, or `null` while the transaction is pending. - `blockNumber` `integer` _(required)_: The block number once included, or `null` while the transaction is pending. - `calls` `object[]`: Decoded calls for Tempo account-abstraction transactions. - `data` `string`: Call data sent to the contract, as `0x`-prefixed bytes. - `to` `string` _(required)_: The address this call targets, or `null` when it creates a contract. - `chainId` `integer`: The chain ID that this transaction belongs to. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gas` `integer` _(required)_: Maximum gas the transaction is allowed to use. - `gasPrice` `string`: Gas price for legacy-style transactions, expressed as a decimal string when humanized. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `input` `string`: Call data for non-Tempo transaction formats, as `0x`-prefixed bytes. - `maxFeePerGas` `string`: Maximum total fee per gas the sender is willing to pay. - `maxPriorityFeePerGas` `string`: Maximum priority fee per gas for EIP-1559-style transactions. - `meta` `object` _(required)_: The original JSON-RPC payload plus any resources requested with `include`. - `receipt` `object`: The transaction outcome included only when you request `include=receipt`. - `blockHash` `string` _(required)_: The block hash, or `null` when the receipt was reconstructed from indexed data. - `blockNumber` `integer` _(required)_: The block number that included this receipt. - `contractAddress` `string` _(required)_: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `integer` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction, in attodollars per gas. - `feeAmount` `object` _(required)_: Fee charged to the fee payer, denominated in USD. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The fee’s nominal value in the requested `valuation.currency`. `null` when rates were unavailable. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gasUsed` `integer` _(required)_: Gas actually used to execute the transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `logs` `object[]` _(required)_: Event logs emitted by this transaction, returned in JSON-RPC log format. - `address` `string` _(required)_: The contract address that emitted this event log. - `blockHash` `string` _(required)_: The block hash once included, or `null` while pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while pending. - `blockTimestamp` `string`: Tempo-provided block timestamp for this log. - `data` `string` _(required)_: ABI-encoded data for the event parameters that are not indexed. - `logIndex` `string` _(required)_: The log position within the block, or `null` while pending. - `removed` `boolean` _(required)_: Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg. - `topics` `string[]` _(required)_: Indexed event topics, including the event signature hash as the first topic. - `transactionHash` `string` _(required)_: The transaction hash for this log, or `null` while pending. - `transactionIndex` `string` _(required)_: The transaction position within the block, or `null` while pending. - `meta` `object` _(required)_: Receipt metadata containing valuation rate provenance and the JSON-RPC payload. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `rpc` `object` _(required)_: The original JSON-RPC receipt payload. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `sender` `string` _(required)_: The address that sent or authorized the transaction. - `status` `string` _(required)_: Whether the transaction succeeded or reverted. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` when it is unavailable. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `integer` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. - `rpc` `object` _(required)_: The original JSON-RPC transaction payload. - `aaAuthorizationList` `object[]`: Tempo account-abstraction authorizations attached to the transaction. - `accessList` `object[]`: EIP-2930 access list that predeclares accounts and storage slots for the transaction. - `address` `string` _(required)_: The account whose storage slots are listed in this access-list entry. - `storageKeys` `string[]` _(required)_: The storage slot keys the transaction plans to access. - `authorizationList` `object[]`: EIP-7702 authorizations attached to the transaction. - `blobVersionedHashes` `string[]`: Versioned blob hashes for EIP-4844 blob data. - `blockHash` `string` _(required)_: The block hash once included, or `null` while the transaction is pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while the transaction is pending. - `blockTimestamp` `string`: The block timestamp for the block that included this transaction. - `calls` `object[]`: Decoded calls for Tempo account-abstraction transactions. - `data` `string`: Call data for this call; this is the same bytes as `input` when both are present. - `input` `string`: Call data sent to the contract, as `0x`-prefixed bytes. - `to` `string`: The address this call targets, or `null` when the call creates a contract. - `value` `string`: Native value sent with this call, as a hex quantity. - `chainId` `string`: The chain ID that the transaction is valid on; legacy transactions may omit it. - `feePayer` `string`: The address that paid the transaction fee. - `feePayerSignature` `object`: A signature envelope exactly as returned by JSON-RPC. - `type` `string`: The signature scheme used by this envelope, such as `secp256k1`, `p256`, or `keychain` when the RPC includes it. - `feeToken` `string`: The fee token requested by the transaction; Tempo fees are paid in USD stablecoins such as `pathUSD`. - `from` `string` _(required)_: The address that submitted or authorized the transaction. - `gas` `string` _(required)_: Maximum gas the transaction is allowed to use. - `gasPrice` `string`: Gas price for legacy-style transactions, expressed as a decimal string when humanized. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction. - `input` `string`: Call data for non-Tempo transaction formats, as `0x`-prefixed bytes. - `keyAuthorization` `object`: Key authorization data attached to this Tempo transaction. - `maxFeePerBlobGas` `string`: Maximum fee per blob gas for EIP-4844 blob data. - `maxFeePerGas` `string`: Maximum total fee per gas the sender is willing to pay. - `maxPriorityFeePerGas` `string`: Maximum priority fee per gas for EIP-1559-style transactions. - `nonce` `string` _(required)_: Nonce from the sender account that orders and de-duplicates transactions. - `nonceKey` `string`: Tempo two-dimensional nonce key used to group nonce sequences. - `r` `string`: The `r` value from the transaction ECDSA signature. - `s` `string`: The `s` value from the transaction ECDSA signature. - `signature` `object`: A signature envelope exactly as returned by JSON-RPC. - `type` `string`: The signature scheme used by this envelope, such as `secp256k1`, `p256`, or `keychain` when the RPC includes it. - `to` `string`: The recipient address, or `null` when the transaction creates a contract. - `transactionIndex` `string` _(required)_: The transaction position within its block, or `null` while pending. - `type` `string` _(required)_: Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo. - `v` `string`: The `v` recovery value from the transaction ECDSA signature. - `validAfter` `string`: Earliest Unix timestamp when this Tempo transaction may be included. - `validBefore` `string`: Latest Unix timestamp when this Tempo transaction may be included. - `value` `string`: Native value sent by non-Tempo transaction formats. - `yParity` `string`: The y-parity value from the transaction signature. - `nonce` `integer` _(required)_: Nonce from the sender account that orders and de-duplicates transactions. - `nonceKey` `string`: Tempo two-dimensional nonce key used by Tempo transactions. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction creates a contract. - `sender` `string` _(required)_: The address that submitted or authorized the transaction. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` while the transaction is pending. - `transactionIndex` `integer` _(required)_: The transaction position within its block, or `null` while pending. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. - `validAfter` `string `: Earliest ISO 8601 time when this Tempo transaction may be included. - `validBefore` `string `: Latest ISO 8601 time when this Tempo transaction may be included. - `value` `string` _(required)_: Native value transferred by this transaction, as a decimal string. - `meta` `object`: Extra resources you requested with `include`. - `totalCountCapped` `boolean`: Whether `totalCount` reached the API count cap; when `true`, treat the count as a lower bound. - `token` `object`: A compact token reference, returned when you request `include=token`. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean` _(required)_: Whether this token is in Tempo’s curated verified token list. - `totalCount` `integer`: The capped total number of matching transactions, returned when you request `include=totalCount`. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The upstream indexer could not serve this request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/tokens/0x20c0000000000000000000000000000000000000/transactions?blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&feePayer=0xbe058e1c4df8a4366a387bf595b284246a93039e&feeToken=0x20c0000000000000000000000000000000000000&include=feeToken.logoUri,feeToken.verified,token,totalCount&limit=10&order=desc&page=1×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z' ``` ```ts fetch('https://api.tempo.xyz/v1/tokens/0x20c0000000000000000000000000000000000000/transactions?blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&feePayer=0xbe058e1c4df8a4366a387bf595b284246a93039e&feeToken=0x20c0000000000000000000000000000000000000&include=feeToken.logoUri,feeToken.verified,token,totalCount&limit=10&order=desc&page=1×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z') ``` ## Get a transaction by hash `GET /v1/transactions/{transactionHash}` Get one transaction by its 32-byte transaction hash. ### Path parameters - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated related resources to include, such as `feeToken.logoUri,feeToken.verified,receipt`. ### Responses #### `200`: A single transaction with decoded fields and original JSON-RPC data. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `blockHash` `string` _(required)_: The block hash once included, or `null` while the transaction is pending. - `blockNumber` `integer` _(required)_: The block number once included, or `null` while the transaction is pending. - `calls` `object[]`: Decoded calls for Tempo account-abstraction transactions. - `data` `string`: Call data sent to the contract, as `0x`-prefixed bytes. - `to` `string` _(required)_: The address this call targets, or `null` when it creates a contract. - `chainId` `integer`: The chain ID that this transaction belongs to. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gas` `integer` _(required)_: Maximum gas the transaction is allowed to use. - `gasPrice` `string`: Gas price for legacy-style transactions, expressed as a decimal string when humanized. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `input` `string`: Call data for non-Tempo transaction formats, as `0x`-prefixed bytes. - `maxFeePerGas` `string`: Maximum total fee per gas the sender is willing to pay. - `maxPriorityFeePerGas` `string`: Maximum priority fee per gas for EIP-1559-style transactions. - `meta` `object` _(required)_: The original JSON-RPC payload plus any resources requested with `include`. - `receipt` `object`: The transaction outcome included only when you request `include=receipt`. - `blockHash` `string` _(required)_: The block hash, or `null` when the receipt was reconstructed from indexed data. - `blockNumber` `integer` _(required)_: The block number that included this receipt. - `contractAddress` `string` _(required)_: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `integer` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction, in attodollars per gas. - `feeAmount` `object` _(required)_: Fee charged to the fee payer, denominated in USD. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The fee’s nominal value in the requested `valuation.currency`. `null` when rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gasUsed` `integer` _(required)_: Gas actually used to execute the transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `logs` `object[]` _(required)_: Event logs emitted by this transaction, returned in JSON-RPC log format. - `address` `string` _(required)_: The contract address that emitted this event log. - `blockHash` `string` _(required)_: The block hash once included, or `null` while pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while pending. - `blockTimestamp` `string`: Tempo-provided block timestamp for this log. - `data` `string` _(required)_: ABI-encoded data for the event parameters that are not indexed. - `logIndex` `string` _(required)_: The log position within the block, or `null` while pending. - `removed` `boolean` _(required)_: Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg. - `topics` `string[]` _(required)_: Indexed event topics, including the event signature hash as the first topic. - `transactionHash` `string` _(required)_: The transaction hash for this log, or `null` while pending. - `transactionIndex` `string` _(required)_: The transaction position within the block, or `null` while pending. - `meta` `object` _(required)_: Receipt metadata containing valuation rate provenance and the JSON-RPC payload. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `rpc` `object` _(required)_: The original JSON-RPC receipt payload. - `blobGasPrice` `string`: Blob gas price for EIP-4844 blob data. - `blobGasUsed` `string`: Blob gas used by EIP-4844 blob data. - `blockHash` `string` _(required)_: The block hash, or `null` for receipts reconstructed from the index. - `blockNumber` `string` _(required)_: The block number that included this receipt. - `blockTimestamp` `string`: Block timestamp added when this receipt is reconstructed from indexed data. - `contractAddress` `string`: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `string` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `string`: The token used to pay the fee; on Tempo this is a USD stablecoin such as `pathUSD`. - `from` `string` _(required)_: The address that sent or authorized the transaction. - `gasUsed` `string` _(required)_: Gas actually used to execute the transaction. - `logs` `object[]` _(required)_: Event logs emitted by contracts while this transaction executed. - `logsBloom` `string`: Bloom filter summarizing the logs in this receipt. - `root` `string`: Post-transaction state root used by pre-Byzantium Ethereum receipts. - `status` `string` _(required)_: Execution status as a hex quantity: `0x1` means success and `0x0` means reverted. - `to` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `string` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `sender` `string` _(required)_: The address that sent or authorized the transaction. - `status` `string` _(required)_: Whether the transaction succeeded or reverted. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` when it is unavailable. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `integer` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. - `rpc` `object` _(required)_: The original JSON-RPC transaction payload. - `aaAuthorizationList` `object[]`: Tempo account-abstraction authorizations attached to the transaction. - `accessList` `object[]`: EIP-2930 access list that predeclares accounts and storage slots for the transaction. - `address` `string` _(required)_: The account whose storage slots are listed in this access-list entry. - `storageKeys` `string[]` _(required)_: The storage slot keys the transaction plans to access. - `authorizationList` `object[]`: EIP-7702 authorizations attached to the transaction. - `blobVersionedHashes` `string[]`: Versioned blob hashes for EIP-4844 blob data. - `blockHash` `string` _(required)_: The block hash once included, or `null` while the transaction is pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while the transaction is pending. - `blockTimestamp` `string`: The block timestamp for the block that included this transaction. - `calls` `object[]`: Decoded calls for Tempo account-abstraction transactions. - `data` `string`: Call data for this call; this is the same bytes as `input` when both are present. - `input` `string`: Call data sent to the contract, as `0x`-prefixed bytes. - `to` `string`: The address this call targets, or `null` when the call creates a contract. - `value` `string`: Native value sent with this call, as a hex quantity. - `chainId` `string`: The chain ID that the transaction is valid on; legacy transactions may omit it. - `feePayer` `string`: The address that paid the transaction fee. - `feePayerSignature` `object`: A signature envelope exactly as returned by JSON-RPC. - `type` `string`: The signature scheme used by this envelope, such as `secp256k1`, `p256`, or `keychain` when the RPC includes it. - `feeToken` `string`: The fee token requested by the transaction; Tempo fees are paid in USD stablecoins such as `pathUSD`. - `from` `string` _(required)_: The address that submitted or authorized the transaction. - `gas` `string` _(required)_: Maximum gas the transaction is allowed to use. - `gasPrice` `string`: Gas price for legacy-style transactions, expressed as a decimal string when humanized. - `hash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies this transaction. - `input` `string`: Call data for non-Tempo transaction formats, as `0x`-prefixed bytes. - `keyAuthorization` `object`: Key authorization data attached to this Tempo transaction. - `maxFeePerBlobGas` `string`: Maximum fee per blob gas for EIP-4844 blob data. - `maxFeePerGas` `string`: Maximum total fee per gas the sender is willing to pay. - `maxPriorityFeePerGas` `string`: Maximum priority fee per gas for EIP-1559-style transactions. - `nonce` `string` _(required)_: Nonce from the sender account that orders and de-duplicates transactions. - `nonceKey` `string`: Tempo two-dimensional nonce key used to group nonce sequences. - `r` `string`: The `r` value from the transaction ECDSA signature. - `s` `string`: The `s` value from the transaction ECDSA signature. - `signature` `object`: A signature envelope exactly as returned by JSON-RPC. - `type` `string`: The signature scheme used by this envelope, such as `secp256k1`, `p256`, or `keychain` when the RPC includes it. - `to` `string`: The recipient address, or `null` when the transaction creates a contract. - `transactionIndex` `string` _(required)_: The transaction position within its block, or `null` while pending. - `type` `string` _(required)_: Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo. - `v` `string`: The `v` recovery value from the transaction ECDSA signature. - `validAfter` `string`: Earliest Unix timestamp when this Tempo transaction may be included. - `validBefore` `string`: Latest Unix timestamp when this Tempo transaction may be included. - `value` `string`: Native value sent by non-Tempo transaction formats. - `yParity` `string`: The y-parity value from the transaction signature. - `nonce` `integer` _(required)_: Nonce from the sender account that orders and de-duplicates transactions. - `nonceKey` `string`: Tempo two-dimensional nonce key used by Tempo transactions. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction creates a contract. - `sender` `string` _(required)_: The address that submitted or authorized the transaction. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` while the transaction is pending. - `transactionIndex` `integer` _(required)_: The transaction position within its block, or `null` while pending. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. - `validAfter` `string `: Earliest ISO 8601 time when this Tempo transaction may be included. - `validBefore` `string `: Latest ISO 8601 time when this Tempo transaction may be included. - `value` `string` _(required)_: Native value transferred by this transaction, as a decimal string. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No transaction was found for that hash. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read transaction or token data from upstream JSON-RPC. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/transactions/0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665?chainId=4217&include=feeToken.logoUri,feeToken.verified,receipt' ``` ```ts fetch('https://api.tempo.xyz/v1/transactions/0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665?chainId=4217&include=feeToken.logoUri,feeToken.verified,receipt') ``` ## Get a transaction receipt `GET /v1/transactions/{transactionHash}/receipt` Get the outcome of one transaction by its transaction hash. ### Path parameters - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `include` `string[]`: Comma-separated fee-token fields to include, such as `feeToken.logoUri,feeToken.verified`. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Responses #### `200`: A single transaction receipt with status, gas, fees, and logs. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `blockHash` `string` _(required)_: The block hash, or `null` when the receipt was reconstructed from indexed data. - `blockNumber` `integer` _(required)_: The block number that included this receipt. - `contractAddress` `string` _(required)_: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `integer` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction, in attodollars per gas. - `feeAmount` `object` _(required)_: Fee charged to the fee payer, denominated in USD. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The fee’s nominal value in the requested `valuation.currency`. `null` when rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `object`: The token requested for transaction fees, with RPC metadata and optional curated fields. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, when one is available. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified token list. - `gasUsed` `integer` _(required)_: Gas actually used to execute the transaction. - `id` `string` _(required)_: Stable resource ID for this API response; it is the transaction hash. - `logs` `object[]` _(required)_: Event logs emitted by this transaction, returned in JSON-RPC log format. - `address` `string` _(required)_: The contract address that emitted this event log. - `blockHash` `string` _(required)_: The block hash once included, or `null` while pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while pending. - `blockTimestamp` `string`: Tempo-provided block timestamp for this log. - `data` `string` _(required)_: ABI-encoded data for the event parameters that are not indexed. - `logIndex` `string` _(required)_: The log position within the block, or `null` while pending. - `removed` `boolean` _(required)_: Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg. - `topics` `string[]` _(required)_: Indexed event topics, including the event signature hash as the first topic. - `transactionHash` `string` _(required)_: The transaction hash for this log, or `null` while pending. - `transactionIndex` `string` _(required)_: The transaction position within the block, or `null` while pending. - `meta` `object` _(required)_: Receipt metadata containing valuation rate provenance and the JSON-RPC payload. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `rpc` `object` _(required)_: The original JSON-RPC receipt payload. - `blobGasPrice` `string`: Blob gas price for EIP-4844 blob data. - `blobGasUsed` `string`: Blob gas used by EIP-4844 blob data. - `blockHash` `string` _(required)_: The block hash, or `null` for receipts reconstructed from the index. - `blockNumber` `string` _(required)_: The block number that included this receipt. - `blockTimestamp` `string`: Block timestamp added when this receipt is reconstructed from indexed data. - `contractAddress` `string`: The created contract address, or `null` if the transaction did not create a contract. - `cumulativeGasUsed` `string` _(required)_: Total gas used in the block up to and including this transaction. - `effectiveGasPrice` `string` _(required)_: Effective gas price paid for this transaction. - `feePayer` `string`: The address that paid the transaction fee. - `feeToken` `string`: The token used to pay the fee; on Tempo this is a USD stablecoin such as `pathUSD`. - `from` `string` _(required)_: The address that sent or authorized the transaction. - `gasUsed` `string` _(required)_: Gas actually used to execute the transaction. - `logs` `object[]` _(required)_: Event logs emitted by contracts while this transaction executed. - `address` `string` _(required)_: The contract address that emitted this event log. - `blockHash` `string` _(required)_: The block hash once included, or `null` while pending. - `blockNumber` `string` _(required)_: The block number once included, or `null` while pending. - `blockTimestamp` `string`: Tempo-provided block timestamp for this log. - `data` `string` _(required)_: ABI-encoded data for the event parameters that are not indexed. - `logIndex` `string` _(required)_: The log position within the block, or `null` while pending. - `removed` `boolean` _(required)_: Whether this log was removed by a chain reorganization; Tempo finality means finalized logs do not reorg. - `topics` `string[]` _(required)_: Indexed event topics, including the event signature hash as the first topic. - `transactionHash` `string` _(required)_: The transaction hash for this log, or `null` while pending. - `transactionIndex` `string` _(required)_: The transaction position within the block, or `null` while pending. - `logsBloom` `string`: Bloom filter summarizing the logs in this receipt. - `root` `string`: Post-transaction state root used by pre-Byzantium Ethereum receipts. - `status` `string` _(required)_: Execution status as a hex quantity: `0x1` means success and `0x0` means reverted. - `to` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `string` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Raw transaction type byte, such as `0x2` for EIP-1559 or `0x76` for Tempo. - `recipient` `string` _(required)_: The recipient address, or `null` when the transaction created a contract. - `sender` `string` _(required)_: The address that sent or authorized the transaction. - `status` `string` _(required)_: Whether the transaction succeeded or reverted. - `timestamp` `string ` _(required)_: The block timestamp as ISO 8601, or `null` when it is unavailable. - `transactionHash` `string` _(required)_: A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction. - `transactionIndex` `integer` _(required)_: The transaction position within its block. - `type` `string` _(required)_: Human-readable transaction type decoded from the raw JSON-RPC `type` byte. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No receipt was found for that transaction hash. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Could not read receipt or token data from upstream JSON-RPC. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/transactions/0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665/receipt?chainId=4217&include=feeToken.logoUri,feeToken.verified&valuation.currency=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/transactions/0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665/receipt?chainId=4217&include=feeToken.logoUri,feeToken.verified&valuation.currency=AUD') ``` # The data model for transactions and transfers Transactions and transfers are the two most common resources in the Tempo API, and they are easy to confuse. They describe different things: a **transaction** is what a user submits to the chain, while a **transfer** is a token movement that results from executing one. :::info **Transactions vs. transfers** — a transaction is what a user *submits*; transfers are what *happens* when it executes. One transaction can produce zero, one, or many token transfers. Use [`/v1/transactions`](https://tempo.xyz/developers/docs/api/transactions) to inspect submitted transactions and their outcomes, and [`/v1/transfers`](https://tempo.xyz/developers/docs/api/transfers) to list the token movements that resulted. ::: ## Tempo transaction and transfer model * A **transaction** is what a user submits to Tempo: a signed envelope that moves value or calls a contract. It may produce zero, one, or many token transfers. Query [`/v1/transfers`](https://tempo.xyz/developers/docs/api/transfers) to list the token movements it caused. * A **transfer** is an effect of transaction execution: a TIP-20 token moved from one account to another. Multiple transfers can come from a single transaction, and some transactions produce no transfers at all. A transaction that calls a contract might emit several transfers (for example, a swap that moves tokens in and out), exactly one (a simple payment), or none (a contract call that updates state without moving tokens). Each transfer carries the `transactionHash` of the transaction that produced it, so you can always trace a movement back to the transaction that caused it. ```text ╭───────────────────────╮ produces 0..N ╭───────────────────────╮ │ Transaction │ ───────────────────────▶ │ Transfer │ │ (what a user submits) │ │ (observed token move) │ ╰───────────────────────╯ ╰───────────────────────╯ │ │ transactionHash ▼ resolves back to its transaction ``` ## When to use transactions or transfers | | Transaction | Transfer | | --- | --- | --- | | **What it is** | Something a user submits to the chain | A token movement caused by execution | | **Cardinality** | One per submission | Zero, one, or many per transaction | | **Endpoint** | [`/v1/transactions`](https://tempo.xyz/developers/docs/api/transactions) | [`/v1/transfers`](https://tempo.xyz/developers/docs/api/transfers) | | **Use it for** | Inspecting submitted transactions, fees, and status | Tracking who received which tokens, and reconciling balances | | **Key link** | Its hash appears on every transfer it produced | `transactionHash` points back to its transaction | # Transfers Token movements from executing transactions. ## List transfers `GET /v1/transfers` List token transfer events across Tempo. ### Query parameters - `address` `string`: Only include transfers where this account is the sender or recipient. Use this shortcut by itself, not with `sender` or `recipient`. - `recipient` `string`: Match transfers sent to this recipient address (`to`). - `sender` `string`: Match transfers sent from this sender address (`from`). - `token` `string`: Match transfers for this TIP-20 token contract address. - `blockNumber.from` `integer`: Only include transfers at or after this block number. - `blockNumber.to` `integer`: Only include transfers at or before this block number. - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated related resources to include, such as `token.logoUri,token.verified,memo,crossToken,totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `order` `string`: Sort order: `desc` for descending (the default), or `asc` for ascending. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. - `timestamp.from` `string `: Only include transfers at or after this ISO 8601 timestamp. - `timestamp.to` `string `: Only include transfers at or before this ISO 8601 timestamp. - `valuation.currency` `string`: Currency to denominate values in (case-insensitive, e.g. `AUD`). Must be priced by the configured FX oracle. ### Responses #### `200`: A page of token transfer events. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The transfers in this page. - `attribution` `string`: The resolved MPP service name, when the transfer memo matches a known service fingerprint. Returned when you request `include=memo`. - `blockNumber` `integer` _(required)_: The block number where this transfer was recorded. - `destinationAmount` `object`: A token amount carrying its nominal value in the requested denomination. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The amount’s nominal value in the requested `valuation.currency`. `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `destinationToken` `object`: The token that moved. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, returned with `include=token.logoUri`. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified list, returned with `include=token.verified`. - `id` `string` _(required)_: A stable resource ID for this transfer, built from `${transactionHash}-${logIndex}`. - `logIndex` `integer` _(required)_: The transfer event’s log index within the block. - `memo` `string`: The decoded text memo from the matching `TransferWithMemo` event, when one is present. Returned when you request `include=memo`. - `recipient` `string` _(required)_: The account address that received the tokens. - `sender` `string` _(required)_: The account address that sent the tokens. - `sourceAmount` `object` _(required)_: A token amount carrying its nominal value in the requested denomination. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `valuation` `object`: The amount’s nominal value in the requested `valuation.currency`. `null` when the token is unverified, its display currency has no rate, or rates were unavailable. - `amount` `string` _(required)_: The holding’s nominal value, as a decimal string in `currency`. - `currency` `string` _(required)_: The denomination currency of this value. - `sourceToken` `object` _(required)_: The token that moved. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string`: A URL for the token’s logo image, returned with `include=token.logoUri`. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `verified` `boolean`: Whether this token is in Tempo’s curated verified list, returned with `include=token.verified`. - `timestamp` `string ` _(required)_: The block timestamp when this transfer was recorded. - `transactionHash` `string` _(required)_: The hash of the transaction that emitted this transfer event. - `meta` `object`: Page-level resources: opt-in counts and valuation rate provenance. - `valuation` `object`: Rate provenance for leg valuations. Present when conversion rates were consulted; absent when every value was identity-valued or rates were unavailable. - `asOf` `string ` _(required)_: Publication date of the rate set as an ISO 8601 timestamp. - `basis` `string` _(required)_: Valuation basis: one token unit counts as one unit of its display currency. - `source` `string` _(required)_: FX rate oracle that supplied the conversion rates. - `totalCount` `integer`: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `totalCountCapped` `boolean`: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The indexer or Tempo RPC could not serve this request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/transfers?address=0xbe058e1c4df8a4366a387bf595b284246a93039e&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&token=0x20c0000000000000000000000000000000000000&blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=crossToken,memo,token.logoUri,token.verified,totalCount&limit=10&order=desc&page=1×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z&valuation.currency=AUD' ``` ```ts fetch('https://api.tempo.xyz/v1/transfers?address=0xbe058e1c4df8a4366a387bf595b284246a93039e&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sender=0xbe058e1c4df8a4366a387bf595b284246a93039e&token=0x20c0000000000000000000000000000000000000&blockNumber.from=23456789&blockNumber.to=23456999&chainId=4217&cursor=WzIzNDU2Nzg5LDBd&include=crossToken,memo,token.logoUri,token.verified,totalCount&limit=10&order=desc&page=1×tamp.from=2024-01-01T00:00:00Z×tamp.to=2024-12-31T23:59:59Z&valuation.currency=AUD') ``` # Tempo API typed client for TypeScript Use the Tempo API Typed Client for endpoint autocomplete and full end-to-end type-safety across API parameters, response bodies, statuses, and errors. ## Install Tempo API package Install the [`tapimo`](https://www.npmjs.com/package/tapimo) package with your package manager. :::code-group ```bash [npm] npm install tapimo ``` ```bash [pnpm] pnpm add tapimo ``` ```bash [bun] bun add tapimo ``` ::: ## Create typed Tempo API client Create a Tempo API client with `Client.create`. ```ts twoslash [client.ts] import { Client } from 'tapimo' const client = Client.create({ // API key sugar for the canonical `tempo-api-key` header. apiKey: 'tempo_api_key', // Override the API URL. Defaults to `Client.defaultUrl`. url: 'https://api.tempo.xyz', // Or pass custom headers directly. headers: { authorization: 'Bearer tempo_api_key' }, }) ``` `Client.create()` also accepts Hono client options such as a custom `fetch` implementation. If you pass `apiKey`, the client merges it into the request headers as `tempo-api-key`. ## Make type-safe Tempo API requests Requests are end-to-end type-safe across parameters, response bodies, statuses, and errors. For example, `GET /v1/tokens/:token` narrows the response body from the HTTP status: ```ts twoslash [tokens.ts] import { Client } from 'tapimo' const client = Client.create() const response = await client.v1.tokens[':token'].$get({ param: { token: '0x20c0000000000000000000000000000000000000' }, }) // Narrow responses by status. if (response.status !== 200) { response.status // status is now typed to non-200 series // ^? if (response.status === 404) { // Handle 404 status specifically const json = await response.json() // ^? json.error.code // which narrows to "token_not_found" // ^? } else if (response.status === 402) { // Handle the payment challenge from `WWW-Authenticate` const challenge = await response.text() // ^? } else { // Handle validation, auth, rate-limit, or upstream errors const json = await response.json() // ^? } throw new Error('Request failed') } // Request is successfull response.status // Response is narrowed to 200 // ^? // Get typed response body const token = await response.json() // ^? token.symbol // ^? ``` ## Tempo API endpoint autocomplete Routes autocomplete directly on the client, so you can discover endpoints without leaving your editor: ```ts twoslash [endpoints.ts] // @noErrors import { Client } from 'tapimo' const client = Client.create() const response1 = await client.v1. // ^| const response2 = await client.v1.transactions. // ^| ``` ## Call Tempo JSON-RPC through Typed Client The Typed Client exposes the raw JSON-RPC passthrough as a typed route under `client.rpc`. The route key includes Hono's path pattern because the chain selector is optional: ```ts twoslash [rpc.ts] // @noErrors import { Client } from 'tapimo' const client = Client.create() const response = await client.rpc[':chain{mainnet|testnet|[0-9]+}?'].$post({ // Use `undefined` for `/rpc`, or pass `mainnet`, `testnet`, or a numeric chain id. param: { chain: 'testnet' }, json: { jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [], }, }) if (response.status !== 200) { const error = await response.json() throw new Error(error.error.message) } const body = await response.json() if (!Array.isArray(body) && body.error) throw new Error(body.error.message) const blockNumber = Array.isArray(body) ? body[0]?.result : body.result ``` Chain selectors map to these hosted endpoints: | `chain` value | Hosted RPC path | | --- | --- | | `undefined` | `https://api.tempo.xyz/rpc` | | `'mainnet'` | `https://api.tempo.xyz/rpc/mainnet` | | `'testnet'` | `https://api.tempo.xyz/rpc/testnet` | | `'4217'` | `https://api.tempo.xyz/rpc/4217` | ### Send batch JSON-RPC requests The JSON-RPC body type accepts one request or an array of requests. The client narrows the success body to the same single-or-batch response union: ```ts twoslash [batch-rpc.ts] // @noErrors import { Client } from 'tapimo' const client = Client.create() const response = await client.rpc[':chain{mainnet|testnet|[0-9]+}?'].$post({ param: { chain: undefined }, json: [ { jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }, { jsonrpc: '2.0', id: 2, method: 'eth_blockNumber', params: [] }, ], }) if (response.status === 200) { const results = await response.json() // results is typed as one JSON-RPC response or an array of JSON-RPC responses. } ``` # Usage Request and sponsorship usage for organizations. ## Get request usage `GET /v1/orgs/{orgId}/usage/requests` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Only include requests made with this API-key environment. - `from` `string `: Only include requests at or after this ISO 8601 timestamp. - `interval` `string`: Time bucket size for the usage series. - `projectId` `string`: Only include requests attributed to this project (`prj_…`). Omit for organization-wide usage. - `to` `string `: Only include requests at or before this ISO 8601 timestamp. ### Responses #### `200`: The request usage. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `byKey` `object[]` _(required)_: Usage grouped by API key. - `apiKeyId` `string` _(required)_: API key id (`key_…`). - `averageDurationMs` `number` _(required)_: Average request duration in milliseconds. - `errors` `integer` _(required)_: Requests whose status was at least 400. - `requests` `integer` _(required)_: Total requests. - `byRoute` `object[]` _(required)_: Usage grouped by route. - `averageDurationMs` `number` _(required)_: Average request duration in milliseconds. - `errors` `integer` _(required)_: Requests whose status was at least 400. - `requests` `integer` _(required)_: Total requests. - `route` `string` _(required)_: Matched route pattern. - `byStatus` `object[]` _(required)_: Usage grouped by status code. - `requests` `integer` _(required)_: Total requests. - `status` `integer` _(required)_: HTTP status code. - `from` `string ` _(required)_: Inclusive lower timestamp bound used for the read (ISO 8601). - `interval` `string` _(required)_: Bucket size used for the time series. - `series` `object[]` _(required)_: Usage grouped into time buckets. - `errors` `integer` _(required)_: Requests whose status was at least 400. - `requests` `integer` _(required)_: Total requests. - `time` `string ` _(required)_: Bucket start timestamp (ISO 8601). - `to` `string ` _(required)_: Inclusive upper timestamp bound used for the read (ISO 8601). - `totals` `object` _(required)_: Overall usage totals. - `averageDurationMs` `number` _(required)_: Average request duration in milliseconds. - `errors` `integer` _(required)_: Requests whose status was at least 400. - `requests` `integer` _(required)_: Total requests. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Usage analytics is not configured on this deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/usage/requests?environment=sandbox&from=2026-01-01T00:00:00Z&interval=day&projectId=prj_1a2b3c4d5e6f7g8h9j0k1m2n&to=2026-01-31T23:59:59Z' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/usage/requests?environment=sandbox&from=2026-01-01T00:00:00Z&interval=day&projectId=prj_1a2b3c4d5e6f7g8h9j0k1m2n&to=2026-01-31T23:59:59Z') ``` ## Get sponsorship usage `GET /v1/orgs/{orgId}/usage/sponsorships` ### Path parameters - `orgId` `string` _(required)_: The organization id (`org_…`). ### Query parameters - `environment` `string`: Only include sponsorships requested under this key environment. - `from` `string `: Window start (ISO 8601), inclusive. Defaults to 30 days before `to`. - `interval` `string`: Bucket width, aligned to UTC calendar boundaries. - `projectId` `string`: Only include sponsorships attributed to this project (`prj_…`). - `to` `string `: Window end (ISO 8601), exclusive. Defaults to now. ### Responses #### `200`: The organization's sponsorship usage series. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Time-ordered usage buckets; buckets with no sponsorships are omitted. - `count` `number` _(required)_: Sponsored transactions recorded in the bucket, any status. - `failed` `number` _(required)_: Sponsored transactions in the bucket whose status is `failed`. - `feeTotal` `object` _(required)_: Committed fees for the bucket. - `amount` `string` _(required)_: Committed fees as a decimal string: finalized fees plus in-flight fee caps; failed rows contribute zero. - `currency` `string` _(required)_: Currency of the fee figure. - `timestamp` `string ` _(required)_: Bucket start (ISO 8601), aligned to UTC calendar boundaries. #### `400`: Malformed API key, invalid path, or invalid query. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found for this id. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/usage/sponsorships?environment=production&from=2026-01-01T00:00:00Z&interval=day&projectId=prj_1a2b3c4d5e6f7g8h9j0k1m2n&to=2026-01-31T00:00:00Z' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/usage/sponsorships?environment=production&from=2026-01-01T00:00:00Z&interval=day&projectId=prj_1a2b3c4d5e6f7g8h9j0k1m2n&to=2026-01-31T00:00:00Z') ``` # Users Users on the Tempo Platform. ## Get current user `GET /v1/me` ### Responses #### `200`: The authenticated user. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string`: Wallet address bound to this user, when wallet sign-in established it. - `createdAt` `string ` _(required)_: When the user was created (ISO 8601). - `email` `string `: Verified email bound to this user, when present. - `id` `string` _(required)_: Opaque user id (`usr_…`). - `updatedAt` `string ` _(required)_: When the user was last updated (ISO 8601). #### `400`: Malformed API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The caller is not a signed-in user. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No user exists for this session. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/me ``` ```ts fetch('https://api.tempo.xyz/v1/me') ``` # Verified Token Requests Organization requests to add tokens to the curated verified list. ## List token requests `GET /v1/orgs/{orgId}/verified-token-requests` ### Path parameters - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: Verification requests. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The organization's verification requests, newest first. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string `: The curated HTTPS URL for this token’s logo image, when one is set. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `chainId` `integer` _(required)_: The Tempo chain id the token belongs to. - `createdAt` `string ` _(required)_: When the request was created (ISO 8601). - `id` `string` _(required)_: Opaque request resource id (`vtr_…`). - `logo` `string`: Inline SVG logo markup uploaded with the request, when present. - `note` `string` _(required)_: Requester note to the reviewers, or null. - `orgId` `string` _(required)_: Requesting organization id (`org_…`). - `requestedBy` `string` _(required)_: User id that submitted the request. - `reviewNote` `string` _(required)_: Reviewer note shown when the request was denied, or null. - `reviewedAt` `string ` _(required)_: When the request was reviewed (ISO 8601), or null while pending. - `status` `string` _(required)_: Review status of the request. - `updatedAt` `string ` _(required)_: When the request was last changed (ISO 8601). #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/verified-token-requests ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/verified-token-requests') ``` ## Create token request `POST /v1/orgs/{orgId}/verified-token-requests` ### Path parameters - `orgId` `string` _(required)_: Organization id (`org_…`). ### Request body (required) (`application/json`) - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD`. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string `: The HTTPS URL for this token’s logo image, when one is set. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `chainId` `string | number` _(required)_: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id (mainnet is `4217`). - `logo` `string`: Inline SVG logo markup, published to the asset store on approval. - `note` `string`: Optional note to the reviewers. ### Responses #### `200`: Created request. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string `: The curated HTTPS URL for this token’s logo image, when one is set. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `chainId` `integer` _(required)_: The Tempo chain id the token belongs to. - `createdAt` `string ` _(required)_: When the request was created (ISO 8601). - `id` `string` _(required)_: Opaque request resource id (`vtr_…`). - `logo` `string`: Inline SVG logo markup uploaded with the request, when present. - `note` `string` _(required)_: Requester note to the reviewers, or null. - `orgId` `string` _(required)_: Requesting organization id (`org_…`). - `requestedBy` `string` _(required)_: User id that submitted the request. - `reviewNote` `string` _(required)_: Reviewer note shown when the request was denied, or null. - `reviewedAt` `string ` _(required)_: When the request was reviewed (ISO 8601), or null while pending. - `status` `string` _(required)_: Review status of the request. - `updatedAt` `string ` _(required)_: When the request was last changed (ISO 8601). #### `400`: Malformed API key, invalid path, or invalid body. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: The token is already verified or requested, or the requester is at the pending-request limit. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/verified-token-requests \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "address": "0xbe058e1c4df8a4366a387bf595b284246a93039e", "currency": "USD", "decimals": 6, "name": "USD Coin", "symbol": "USDC", "chainId": 4217 }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/verified-token-requests', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: '0xbe058e1c4df8a4366a387bf595b284246a93039e', currency: 'USD', decimals: 6, name: 'USD Coin', symbol: 'USDC', chainId: 4217 }) }) ``` ## Cancel token request `DELETE /v1/orgs/{orgId}/verified-token-requests/{requestId}` ### Path parameters - `orgId` `string` _(required)_: Organization id (`org_…`). - `requestId` `string` _(required)_: Request resource id (`vtr_…`). ### Responses #### `200`: Canceled request. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: Canceled request resource id. #### `400`: Malformed API key or invalid path. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or verification request was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: Only pending requests can be canceled. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/verified-token-requests/vtr_1a2b3c4d5e6f7g8h9j0k1m2n \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/verified-token-requests/vtr_1a2b3c4d5e6f7g8h9j0k1m2n', { method: 'DELETE' }) ``` # Verified Tokens A curated, trusted list of TIP-20 tokens. ## List verified tokens `GET /v1/verified-tokens` Returns Tempo’s curated verified TIP-20 tokens. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. - `currency` `string`: Only include verified tokens denominated in this currency, such as `USD`. Matching is case-insensitive. ### Responses #### `200`: The curated verified TIP-20 tokens. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The curated verified TIP-20 tokens. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string `: The curated HTTPS URL for this token’s logo image, when one is set. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The verified-token store could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/verified-tokens?chainId=4217¤cy=EUR' ``` ```ts fetch('https://api.tempo.xyz/v1/verified-tokens?chainId=4217¤cy=EUR') ``` ## List verified currencies `GET /v1/verified-tokens/currencies` Returns the distinct display currencies represented by the curated verified-token list. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. ### Responses #### `200`: The distinct currencies used by verified tokens. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `string[]` _(required)_: The sorted, unique currency labels used by tokens in the verified list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The verified-token store could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/verified-tokens/currencies?chainId=4217' ``` ```ts fetch('https://api.tempo.xyz/v1/verified-tokens/currencies?chainId=4217') ``` ## Get token list `GET /v1/tokenlist` Returns every verified token on a Tempo chain in the standard Uniswap Token Lists format, so wallets and apps can show trusted names, symbols, and logos. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. ### Responses #### `200`: A standard token list of Tempo’s verified TIP-20 tokens. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `name` `string` _(required)_: The human-readable name of this token list. - `timestamp` `string ` _(required)_: The ISO 8601 timestamp when this token list was last updated. - `version` `object` _(required)_: The token list’s semantic version. - `major` `integer` _(required)_: The major number in the token list’s semantic version. - `minor` `integer` _(required)_: The minor number in the token list’s semantic version. - `patch` `integer` _(required)_: The patch number in the token list’s semantic version. - `tokens` `object[]` _(required)_: The verified TIP-20 tokens on this chain. - `chainId` `integer` _(required)_: The Tempo chain ID where this token lives. - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoURI` `string `: The curated URL for this token’s logo image, when one is set. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The verified-token store could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/tokenlist?chainId=4217' ``` ```ts fetch('https://api.tempo.xyz/v1/tokenlist?chainId=4217') ``` ## Get verified token `GET /v1/verified-tokens/{address}` Returns one curated verified TIP-20 token by contract address. ### Path parameters - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. ### Query parameters - `chainId` `string | number`: Which Tempo network to query. Use the alias `mainnet` or `testnet`, or a numeric chain id. Defaults to `mainnet` when omitted. ### Responses #### `200`: One verified TIP-20 token from Tempo’s curated list. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string` _(required)_: The TIP-20 token contract address on Tempo. - `currency` `string` _(required)_: The currency label for this token, such as `USD` for USD-denominated stablecoins. - `decimals` `integer` _(required)_: The number of decimal places the token uses; Tempo stablecoins typically use 6. - `logoUri` `string `: The curated HTTPS URL for this token’s logo image, when one is set. - `name` `string` _(required)_: The token’s human-readable name. - `symbol` `string` _(required)_: The short ticker symbol wallets and apps show for this token. - `id` `string` _(required)_: A stable resource ID for this token, equal to its contract address. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No verified token was found for this address. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The verified-token store could not complete the request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/verified-tokens/0x20c0000000000000000000000000000000000000?chainId=4217' ``` ```ts fetch('https://api.tempo.xyz/v1/verified-tokens/0x20c0000000000000000000000000000000000000?chainId=4217') ``` # Tempo API versioning policy The Tempo API evolves without breaking existing integrations whenever practical. This page explains how the API is versioned, what we consider a breaking change, and how changes are communicated. :::warning The Tempo API is under active development. Endpoints are not yet stable and may change without notice. The policy below will apply after stabilization. ::: ## Where This Policy Does Not Apply This policy covers the versioned REST API under `/v1`. It does **not** apply to the following endpoint, which follows an external specification and is versioned independently of the Tempo API: | Endpoint | Description | | --- | --- | | `/rpc` | The Ethereum JSON-RPC interface, which tracks the upstream JSON-RPC and node specifications. | ## Backward-Compatible Changes These changes are released continuously and **do not** require you to change your integration or select a new version: * Adding a new endpoint, resource, or response. * Adding new optional request query parameters, headers, or body fields. * Adding new fields, objects, or headers to an existing response. * Adding new values to an open-ended enum or `status` field. * Adding new error `code`s within an existing documented category. * Changing human-readable text, such as descriptions or error `message`s, without changing the machine-readable `code` or its meaning. * Fixing behavior to match the documented API. * Changing the format or length of opaque identifiers, hashes, and cursors. Treat them as opaque strings. * Adding or adjusting `RateLimit-*` headers. ## Breaking Changes A breaking change is any change that could require a correctly implemented client to change code to keep working as documented. Breaking changes are introduced behind an explicit version with a migration path. Examples: * Removing or renaming an endpoint, request parameter, or response field. * Changing the type, format, nullability, or meaning of an existing field. * Making a previously optional request parameter required. * Changing authentication, pagination, or the documented status code or error shape for an existing condition. * Rejecting requests that were previously valid under the documented API. :::warning We may make immediate changes without the usual notice when required to address security, abuse, data-integrity, legal, or chain-safety issues. We document these as soon as practical. ::: ## Deprecation and Sunset We will not deprecate a stable endpoint or version until a replacement is available and documented with a migration path. Deprecated endpoints are marked `deprecated: true` in this OpenAPI document and return standard signaling headers: ```http Deprecation: true Sunset: Wed, 15 Jan 2027 00:00:00 GMT Link: ; rel="deprecation" ``` ## Communicating Changes * All breaking changes and meaningful additions are recorded in the changelog with affected endpoints, migration notes, and any sunset date. * Deprecated operations and fields are flagged in this OpenAPI document. # Webhooks Organization webhook subscriptions. ## List webhooks `GET /v1/webhooks` List your webhook subscriptions. ### Query parameters - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. ### Responses #### `200`: A page of webhook subscriptions. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Webhook subscriptions on this page. - `chainId` `integer` _(required)_: Tempo chain ID for this subscription. - `context` `object`: Human context describing what this webhook subscription is for. - `description` `string`: Longer description of what this subscription is for. - `metadata` `object`: Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries. - `title` `string`: Short label for this subscription. - `createdAt` `string ` _(required)_: When the subscription was created, as an ISO 8601 timestamp. - `destination` `object` _(required)_: Delivery destination with bearer credentials redacted. - `environment` `string`: API-key environment for private resource events, when applicable. - `eventType` `string` _(required)_: Event type you can subscribe to. - `expiresAt` `string `: When the subscription expires, as an ISO 8601 timestamp. Only MPP-paid subscriptions expire. - `failureCount` `integer` _(required)_: Number of delivery failures in a row for this subscription, capped at the auto-disable threshold. - `filters` `object` _(required)_: Filters applied to this event type. - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). - `lastDeliveryAt` `string `: When Tempo last delivered an event successfully, as an ISO 8601 timestamp. Refreshed at most once per minute during sustained delivery. - `status` `string` _(required)_: Current state of the webhook subscription. - `updatedAt` `string ` _(required)_: When the subscription was last changed, as an ISO 8601 timestamp. - `meta` `object`: Extra response metadata requested with `include`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: The query parameters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: Webhooks are not enabled for this API deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/webhooks?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&page=1' ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&page=1') ``` ## Create webhook `POST /v1/webhooks` Create a webhook subscription so Tempo can POST signed onchain events to your URL. The signing secret is shown only once, and each call creates a separate subscription. ### Request body (required) (`application/json`) ### Responses #### `200`: The created webhook subscription, including the one-time signing secret. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `chainId` `integer` _(required)_: Tempo chain ID for this subscription. - `context` `object`: Human context describing what this webhook subscription is for. - `description` `string`: Longer description of what this subscription is for. - `metadata` `object`: Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries. - `title` `string`: Short label for this subscription. - `createdAt` `string ` _(required)_: When the subscription was created, as an ISO 8601 timestamp. - `destination` `object` _(required)_: Delivery destination: an HTTPS URL, a Slack incoming-webhook URL, or a Better Stack source. - `environment` `string`: API-key environment for private resource events, when applicable. - `eventType` `string` _(required)_: Event type you can subscribe to. - `expiresAt` `string `: When the subscription expires, as an ISO 8601 timestamp. Only MPP-paid subscriptions expire. - `failureCount` `integer` _(required)_: Number of delivery failures in a row for this subscription, capped at the auto-disable threshold. - `filters` `object` _(required)_: Filters applied to this event type. - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). - `lastDeliveryAt` `string `: When Tempo last delivered an event successfully, as an ISO 8601 timestamp. Refreshed at most once per minute during sustained delivery. - `status` `string` _(required)_: Current state of the webhook subscription. - `updatedAt` `string ` _(required)_: When the subscription was last changed, as an ISO 8601 timestamp. - `secret` `string` _(required)_: HMAC signing secret (`whsec_…`) used to verify Tempo webhook signatures. It is shown once when you create the webhook, so store it now. #### `400`: The request, destination, filters, or chain are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The API key lacks access or its creator-shared subscription limit was reached. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: Webhooks are not enabled for this API deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/webhooks \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "destination": { "type": "url", "url": "https://example.com/webhooks" }, "eventType": "token:transfer" }' ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ destination: { type: 'url', url: 'https://example.com/webhooks' }, eventType: 'token:transfer' }) }) ``` ## List webhooks `GET /v1/orgs/{orgId}/webhooks` ### Path parameters - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: Organization webhooks. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: The organization's webhooks, newest first. - `chainId` `integer` _(required)_: Tempo chain ID for this subscription. - `context` `object`: Human context describing what this webhook subscription is for. - `description` `string`: Longer description of what this subscription is for. - `metadata` `object`: Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries. - `title` `string`: Short label for this subscription. - `createdAt` `string ` _(required)_: When the subscription was created, as an ISO 8601 timestamp. - `destination` `object` _(required)_: Delivery destination with bearer credentials redacted. - `environment` `string`: API-key environment for private resource events, when applicable. - `eventType` `string` _(required)_: Event type you can subscribe to. - `expiresAt` `string `: When the subscription expires, as an ISO 8601 timestamp. Only MPP-paid subscriptions expire. - `failureCount` `integer` _(required)_: Number of delivery failures in a row for this subscription, capped at the auto-disable threshold. - `filters` `object` _(required)_: Filters applied to this event type. - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). - `lastDeliveryAt` `string `: When Tempo last delivered an event successfully, as an ISO 8601 timestamp. Refreshed at most once per minute during sustained delivery. - `status` `string` _(required)_: Current state of the webhook subscription. - `updatedAt` `string ` _(required)_: When the subscription was last changed, as an ISO 8601 timestamp. #### `400`: The path parameters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or webhook capability was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks') ``` ## Create webhook `POST /v1/orgs/{orgId}/webhooks` ### Path parameters - `orgId` `string` _(required)_: Organization id (`org_…`). ### Request body (required) (`application/json`) ### Responses #### `200`: Created webhook. HTTPS endpoints include a one-time signing secret. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: The request or destination is invalid, or the chain is not polled. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Admin access is required; API-key organizations share their creator subscription limit. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization or webhook capability was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "chainId": 4217, "destination": { "type": "url", "url": "https://example.com/webhooks" }, "eventType": "token:transfer" }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chainId: 4217, destination: { type: 'url', url: 'https://example.com/webhooks' }, eventType: 'token:transfer' }) }) ``` ## Get webhook `GET /v1/webhooks/{id}` Get one webhook subscription by its ID. ### Path parameters - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). ### Responses #### `200`: The requested webhook subscription. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `chainId` `integer` _(required)_: Tempo chain ID for this subscription. - `context` `object`: Human context describing what this webhook subscription is for. - `description` `string`: Longer description of what this subscription is for. - `metadata` `object`: Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries. - `title` `string`: Short label for this subscription. - `createdAt` `string ` _(required)_: When the subscription was created, as an ISO 8601 timestamp. - `destination` `object` _(required)_: Delivery destination with bearer credentials redacted. - `environment` `string`: API-key environment for private resource events, when applicable. - `eventType` `string` _(required)_: Event type you can subscribe to. - `expiresAt` `string `: When the subscription expires, as an ISO 8601 timestamp. Only MPP-paid subscriptions expire. - `failureCount` `integer` _(required)_: Number of delivery failures in a row for this subscription, capped at the auto-disable threshold. - `filters` `object` _(required)_: Filters applied to this event type. - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). - `lastDeliveryAt` `string `: When Tempo last delivered an event successfully, as an ISO 8601 timestamp. Refreshed at most once per minute during sustained delivery. - `status` `string` _(required)_: Current state of the webhook subscription. - `updatedAt` `string ` _(required)_: When the subscription was last changed, as an ISO 8601 timestamp. #### `400`: The webhook ID is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No webhook subscription was found for that ID. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx') ``` ## Update webhook `PATCH /v1/webhooks/{id}` Update a webhook subscription URL, filters, or delivery status. ### Path parameters - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). ### Request body (required) (`application/json`) - `context` `object`: New human context, or `null` to clear it. - `description` `string`: Longer description of what this subscription is for. - `metadata` `object`: Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries. - `title` `string`: Short label for this subscription. - `destination` `object`: Delivery destination: an HTTPS URL, a Slack incoming-webhook URL, or a Better Stack source. - `filters` `object`: New filters to apply to future webhook events. - `status` `string`: New lifecycle status, such as pausing or resuming delivery. ### Responses #### `200`: The updated webhook subscription. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `chainId` `integer` _(required)_: Tempo chain ID for this subscription. - `context` `object`: Human context describing what this webhook subscription is for. - `description` `string`: Longer description of what this subscription is for. - `metadata` `object`: Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries. - `title` `string`: Short label for this subscription. - `createdAt` `string ` _(required)_: When the subscription was created, as an ISO 8601 timestamp. - `destination` `object` _(required)_: Delivery destination with bearer credentials redacted. - `environment` `string`: API-key environment for private resource events, when applicable. - `eventType` `string` _(required)_: Event type you can subscribe to. - `expiresAt` `string `: When the subscription expires, as an ISO 8601 timestamp. Only MPP-paid subscriptions expire. - `failureCount` `integer` _(required)_: Number of delivery failures in a row for this subscription, capped at the auto-disable threshold. - `filters` `object` _(required)_: Filters applied to this event type. - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). - `lastDeliveryAt` `string `: When Tempo last delivered an event successfully, as an ISO 8601 timestamp. Refreshed at most once per minute during sustained delivery. - `status` `string` _(required)_: Current state of the webhook subscription. - `updatedAt` `string ` _(required)_: When the subscription was last changed, as an ISO 8601 timestamp. #### `400`: The update, destination, or filters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No webhook subscription was found for that ID. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{}' ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }) ``` ## Delete webhook `DELETE /v1/webhooks/{id}` Delete a webhook subscription and stop future deliveries immediately. ### Path parameters - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). ### Responses #### `200`: Confirmation that the webhook subscription was deleted. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: ID of the webhook subscription that was deleted. #### `400`: The webhook ID is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No webhook subscription was found for that ID. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx', { method: 'DELETE' }) ``` ## Get webhook delivery `GET /v1/orgs/{orgId}/webhooks/{id}/deliveries/{deliveryId}` ### Path parameters - `deliveryId` `string` _(required)_: Delivery ID (`whd_…`) you want Tempo to replay. - `id` `string` _(required)_: Webhook subscription id (`wh_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: The delivery attempt and exact JSON payload Tempo sent. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `attempt` `integer` _(required)_: Retry attempt number for this delivery, starting at 1. - `createdAt` `string ` _(required)_: When this delivery attempt was created, as an ISO 8601 timestamp. - `error` `string`: Why delivery failed, present when `status` is `failed`. - `eventId` `string` _(required)_: Stable event ID (`evt_…`) you can use to dedupe webhook deliveries. - `id` `string` _(required)_: Webhook delivery ID (`whd_…`). - `requestUrl` `string ` _(required)_: Your callback URL that Tempo attempted to deliver to. - `responseMs` `integer`: How long the delivery attempt took in milliseconds, when Tempo made a request. - `responseStatus` `integer`: HTTP status your endpoint returned, when Tempo received a response. - `status` `string` _(required)_: Outcome of a webhook delivery attempt. - `subscriptionId` `string` _(required)_: Subscription ID (`wh_…`) this delivery belongs to. - `envelope` `object` _(required)_: The signed JSON payload Tempo POSTs to a webhook destination. #### `400`: The path parameters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization, webhook, delivery, or webhook capability was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries/whd_001718668800000_C1yJkk7CuML98HFthEDk9PsY ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries/whd_001718668800000_C1yJkk7CuML98HFthEDk9PsY') ``` ## List webhook event types `GET /v1/webhooks/event-types` See which onchain event types Tempo can POST to your webhook URL. ### Responses #### `200`: Webhook event types available for subscription. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Webhook event types available for subscription. - `description` `string` _(required)_: Plain-English description of the event type. - `type` `string` _(required)_: Event type you can subscribe to. #### `400`: The API key credential is malformed. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: Webhooks are not enabled for this API deployment. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/webhooks/event-types ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks/event-types') ``` ## List webhook deliveries `GET /v1/webhooks/{id}/deliveries` List delivery attempts for a webhook subscription. ### Path parameters - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). ### Query parameters - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. ### Responses #### `200`: A newest-first page of webhook delivery attempts. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Webhook delivery attempts on this page. - `attempt` `integer` _(required)_: Retry attempt number for this delivery, starting at 1. - `createdAt` `string ` _(required)_: When this delivery attempt was created, as an ISO 8601 timestamp. - `error` `string`: Why delivery failed, present when `status` is `failed`. - `eventId` `string` _(required)_: Stable event ID (`evt_…`) you can use to dedupe webhook deliveries. - `id` `string` _(required)_: Webhook delivery ID (`whd_…`). - `requestUrl` `string ` _(required)_: Your callback URL that Tempo attempted to deliver to. - `responseMs` `integer`: How long the delivery attempt took in milliseconds, when Tempo made a request. - `responseStatus` `integer`: HTTP status your endpoint returned, when Tempo received a response. - `status` `string` _(required)_: Outcome of a webhook delivery attempt. - `subscriptionId` `string` _(required)_: Subscription ID (`wh_…`) this delivery belongs to. - `meta` `object`: Extra response metadata requested with `include`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: The webhook ID or query parameters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No webhook subscription was found for that ID. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&page=1' ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&page=1') ``` ## List webhook deliveries `GET /v1/orgs/{orgId}/webhooks/{id}/deliveries` ### Path parameters - `id` `string` _(required)_: Webhook subscription id (`wh_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Query parameters - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `page` `integer`: Page number, 1-indexed (positional pagination for page-numbered access; `page=1` is the head page). Mutually exclusive with `cursor`, and `page × limit` must be at most 500 rows — use cursor pagination for deeper traversal. Pages are positional, so rows arriving at the head of a live feed can shift page contents. ### Responses #### `200`: A newest-first page of webhook delivery attempts. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Webhook delivery attempts on this page. - `attempt` `integer` _(required)_: Retry attempt number for this delivery, starting at 1. - `createdAt` `string ` _(required)_: When this delivery attempt was created, as an ISO 8601 timestamp. - `error` `string`: Why delivery failed, present when `status` is `failed`. - `eventId` `string` _(required)_: Stable event ID (`evt_…`) you can use to dedupe webhook deliveries. - `id` `string` _(required)_: Webhook delivery ID (`whd_…`). - `requestUrl` `string ` _(required)_: Your callback URL that Tempo attempted to deliver to. - `responseMs` `integer`: How long the delivery attempt took in milliseconds, when Tempo made a request. - `responseStatus` `integer`: HTTP status your endpoint returned, when Tempo received a response. - `status` `string` _(required)_: Outcome of a webhook delivery attempt. - `subscriptionId` `string` _(required)_: Subscription ID (`wh_…`) this delivery belongs to. - `meta` `object`: Extra response metadata requested with `include`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: The path or query parameters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization, webhook, or webhook capability was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&page=1' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&page=1') ``` ## Ping webhook `POST /v1/webhooks/{id}/ping` Send a signed `ping` event to your webhook URL to test delivery and signature verification. ### Path parameters - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). ### Responses #### `200`: Result of the synthetic ping delivery. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `delivered` `boolean` _(required)_: Whether your endpoint returned a 2xx response. - `error` `string`: Why the delivery failed, present when `delivered` is `false`. - `eventId` `string` _(required)_: Synthetic event ID (`evt_…`) Tempo sent for this ping. - `responseMs` `integer`: Round-trip delivery time in milliseconds, when the request completed. - `responseStatus` `integer`: HTTP status your endpoint returned, when Tempo received a response. #### `400`: The webhook ID is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No webhook subscription was found for that ID. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/ping \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/ping', { method: 'POST' }) ``` ## Update webhook `PATCH /v1/orgs/{orgId}/webhooks/{id}` ### Path parameters - `id` `string` _(required)_: Webhook subscription id (`wh_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Request body (required) (`application/json`) - `status` `string` _(required)_: Current state of the webhook subscription. ### Responses #### `200`: Updated webhook. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `chainId` `integer` _(required)_: Tempo chain ID for this subscription. - `context` `object`: Human context describing what this webhook subscription is for. - `description` `string`: Longer description of what this subscription is for. - `metadata` `object`: Arbitrary key/value labels echoed on every delivered event and rendered by destinations (Slack fields, Better Stack log fields). Keys are 1–64 chars, values ≤500 chars, ≤20 entries. - `title` `string`: Short label for this subscription. - `createdAt` `string ` _(required)_: When the subscription was created, as an ISO 8601 timestamp. - `destination` `object` _(required)_: Delivery destination with bearer credentials redacted. - `environment` `string`: API-key environment for private resource events, when applicable. - `eventType` `string` _(required)_: Event type you can subscribe to. - `expiresAt` `string `: When the subscription expires, as an ISO 8601 timestamp. Only MPP-paid subscriptions expire. - `failureCount` `integer` _(required)_: Number of delivery failures in a row for this subscription, capped at the auto-disable threshold. - `filters` `object` _(required)_: Filters applied to this event type. - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). - `lastDeliveryAt` `string `: When Tempo last delivered an event successfully, as an ISO 8601 timestamp. Refreshed at most once per minute during sustained delivery. - `status` `string` _(required)_: Current state of the webhook subscription. - `updatedAt` `string ` _(required)_: When the subscription was last changed, as an ISO 8601 timestamp. #### `400`: The path parameters or request body are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Organization admin access is required. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization, webhook, or webhook capability was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx \ --request PATCH \ --header 'Content-Type: application/json' \ --data '{ "status": "active" }' ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'active' }) }) ``` ## Delete webhook `DELETE /v1/orgs/{orgId}/webhooks/{id}` ### Path parameters - `id` `string` _(required)_: Webhook subscription id (`wh_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: Deleted webhook. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `id` `string` _(required)_: Deleted webhook subscription id. #### `400`: The path parameters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Organization admin access is required. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization, webhook, or webhook capability was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx \ --request DELETE ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx', { method: 'DELETE' }) ``` ## Retry webhook delivery `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry` Replay a previous webhook delivery and update the delivery result. ### Path parameters - `deliveryId` `string` _(required)_: Delivery ID (`whd_…`) you want Tempo to replay. - `id` `string` _(required)_: Webhook subscription ID (`wh_…`). ### Responses #### `200`: Result of replaying the webhook delivery. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `delivered` `boolean` _(required)_: Whether your endpoint returned a 2xx response. - `error` `string`: Why the delivery failed, present when `delivered` is `false`. - `eventId` `string` _(required)_: Synthetic event ID (`evt_…`) Tempo sent for this ping. - `responseMs` `integer`: Round-trip delivery time in milliseconds, when the request completed. - `responseStatus` `integer`: HTTP status your endpoint returned, when Tempo received a response. #### `400`: The webhook or delivery ID is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No webhook subscription or delivery was found for those IDs. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries/whd_001718668800000_C1yJkk7CuML98HFthEDk9PsY/retry \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries/whd_001718668800000_C1yJkk7CuML98HFthEDk9PsY/retry', { method: 'POST' }) ``` ## Retry webhook delivery `POST /v1/orgs/{orgId}/webhooks/{id}/deliveries/{deliveryId}/retry` ### Path parameters - `deliveryId` `string` _(required)_: Delivery ID (`whd_…`) you want Tempo to replay. - `id` `string` _(required)_: Webhook subscription id (`wh_…`). - `orgId` `string` _(required)_: Organization id (`org_…`). ### Responses #### `200`: Result of replaying the webhook delivery. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `delivered` `boolean` _(required)_: Whether your endpoint returned a 2xx response. - `error` `string`: Why the delivery failed, present when `delivered` is `false`. - `eventId` `string` _(required)_: Synthetic event ID (`evt_…`) Tempo sent for this ping. - `responseMs` `integer`: Round-trip delivery time in milliseconds, when the request completed. - `responseStatus` `integer`: HTTP status your endpoint returned, when Tempo received a response. #### `400`: The path parameters are invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Organization admin access is required. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No accessible organization, webhook, delivery, or webhook capability was found. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries/whd_001718668800000_C1yJkk7CuML98HFthEDk9PsY/retry \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/orgs/org_1a2b3c4d5e6f7g8h9j0k1m2n/webhooks/wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx/deliveries/whd_001718668800000_C1yJkk7CuML98HFthEDk9PsY/retry', { method: 'POST' }) ``` ## Webhook event delivery `POST event` When an event you subscribed to happens, Tempo sends this signed message to your webhook URL as an HTTP POST (the same shape is used by `POST /webhooks/:id/ping` so you can test your endpoint). Always verify the `tempo-signature` header before trusting the body, then reply with any 2xx status to confirm you received it. If your endpoint returns a non-2xx status, times out, or redirects, Tempo retries with increasing delays; an endpoint that keeps failing is eventually disabled. ### Header parameters - `tempo-signature` `string` _(required)_: Signature proving the message really came from Tempo. An HMAC-SHA256 of the body using your webhook secret, formatted `t=,v1=`. Verify this before trusting the payload. - `tempo-event-id` `string` _(required)_: A unique, stable id for this event (`evt_…`). The same event may be delivered more than once, so use this id to skip duplicates. - `tempo-event-type` `string` _(required)_: What happened: `token:transfer`, `transaction:included`, `log:emitted`, `block:created`, `routes:deposit.updated`, `routes:transfer.updated`, or `ping`. ### Request body (required) (`application/json`) ### Responses #### `2XX`: Delivery acknowledged. ### Example request ```bash curl https://api.tempo.xyzevent \ --request POST \ --header 'tempo-signature: t=1786543200,v1=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8' \ --header 'tempo-event-id: evt_515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665' \ --header 'tempo-event-type: token:transfer' \ --header 'Content-Type: application/json' \ --data '{ "chainId": 4217, "createdAt": "2026-01-14T18:38:03.000Z", "id": "evt_abc123", "subscriptionId": "wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx", "data": { "address": "0x20c0000000000000000000008f5425160ebe5525", "amount": "10000", "blockNumber": 1000002, "recipient": "0x9e39034aae71fb89f66061a2602eb6efec271754", "sender": "0xe7687128b0a808c2831ff94d4f7b2fb35c65af38", "timestamp": "2026-01-14T18:38:03.685Z", "transactionHash": "0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3" }, "type": "token:transfer" }' ``` ```ts fetch('https://api.tempo.xyzevent', { method: 'POST', headers: { 'tempo-signature': 't=1786543200,v1=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'tempo-event-id': 'evt_515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665', 'tempo-event-type': 'token:transfer', 'Content-Type': 'application/json' }, body: JSON.stringify({ chainId: 4217, createdAt: '2026-01-14T18:38:03.000Z', id: 'evt_abc123', subscriptionId: 'wh_001718668800000_2ZPE2gvateYEQ0dQslgvkhjx', data: { address: '0x20c0000000000000000000008f5425160ebe5525', amount: '10000', blockNumber: 1000002, recipient: '0x9e39034aae71fb89f66061a2602eb6efec271754', sender: '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38', timestamp: '2026-01-14T18:38:03.685Z', transactionHash: '0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3' }, type: 'token:transfer' }) }) ``` # Zones Private chain operations anchored to Tempo. ## List Zone withdrawals `GET /v1/zones/withdrawals/{senderTag}` Lists parent-chain outcomes for Zone withdrawals correlated by sender tag. ### Path parameters - `senderTag` `string` _(required)_: The 32-byte correlation tag shared by withdrawals from one sender in one Zone transaction. ### Query parameters - `chainId` `string | number` _(required)_: The chain id of the Zone where the withdrawal originated. ### Responses #### `200`: Indexed parent-chain withdrawal outcomes correlated by sender tag. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Indexed parent-chain withdrawal outcomes sharing the sender tag. - `blockNumber` `integer` _(required)_: Parent-chain block containing the withdrawal processing event. - `id` `string` _(required)_: Stable withdrawal id built from the parent transaction hash and log index (`${transactionHash}-${logIndex}`). - `logIndex` `integer` _(required)_: Event position within the parent-chain block. - `status` `string` _(required)_: Whether parent-chain delivery or processing failed or completed. - `transactionHash` `string` _(required)_: Parent-chain transaction that processed the withdrawal. - `zoneStatus` `string` _(required)_: Completed once the Zone has proven it imported the parent-chain `blockNumber` containing this withdrawal. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The API key does not grant read access to the originating Zone. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The parent-chain indexer or RPC returned unavailable or malformed data. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/zones/withdrawals/0x61fc42a734cb44acef497ea0bed612c691195623244136e0739e1929569e2487?chainId=1424310003' ``` ```ts fetch('https://api.tempo.xyz/v1/zones/withdrawals/0x61fc42a734cb44acef497ea0bed612c691195623244136e0739e1929569e2487?chainId=1424310003') ``` # Tempo developer tools and infrastructure Integrating with Tempo is easy by leveraging services provided by our infrastructure partners. These partners take advantage of Tempo Transactions, TIP-20 tokens, and more. Visit their documentation for more information on how to get started. * [Bridges](#bridges) — Move assets to and from Tempo with cross-chain bridges * [Security & Compliance](#security--compliance) — Transaction scanning, threat detection, and compliance infrastructure for Tempo applications * [Orchestration](#orchestration) — Move money globally between local currencies and stablecoins. Issue, transfer, and manage stablecoins * [Data & Analytics](#data--analytics) — Query blockchain data with indexers, analytics platforms, and monitoring tools * [Block Explorers](#block-explorers) — View transactions, blocks, accounts, and token activity on Tempo * [Wallets](#wallets) — Integrate user-friendly wallet experiences directly into your application * [Smart Contract Libraries](#smart-contract-libraries) — Build with account abstraction and programmable smart contract wallets * [Node Infrastructure](#node-infrastructure) — Connect to Tempo with reliable RPC endpoints and managed node services * [Tempo SDKs](https://tempo.xyz/developers/docs/sdk) — Build on Tempo with official SDKs for TypeScript, Go, Foundry, and Rust ## Bridges ### Across [Across](https://across.to) provides fast, capital-efficient bridging for moving assets to and from Tempo. Across uses an intent-based architecture with optimistic verification, enabling near-instant cross-chain transfers with competitive fees. Bridge assets to Tempo through the [Across app](https://app.across.to/) and explore the integration docs at [docs.across.to](https://docs.across.to/). ### Bungee [Bungee](https://bungee.exchange) enables seamless swaps within and between blockchains. Bungee aggregates bridge and DEX liquidity to deliver fast, cost-efficient cross-chain transfers and swaps to and from Tempo — with a simple integration path via widget or API. Get started with the [Bungee docs](https://docs.bungee.exchange/) or try the [Bungee app](https://bungee.exchange). ### Chainlink CCIP [Chainlink Cross-Chain Interoperability Protocol (CCIP)](https://chain.link/cross-chain) enables applications to transfer tokens and messages across blockchains. CCIP connects Tempo to supported networks through active cross-chain lanes. View supported tokens, lanes, fees, and contract configuration in the [Tempo Mainnet CCIP Directory](https://docs.chain.link/ccip/directory/mainnet/chain/tempo-mainnet). ### Relay [Relay](https://relay.link) provides instant cross-chain bridging and transaction execution. Relay enables users and applications to move assets to Tempo from other chains with fast finality and low fees, powered by a network of relayers that fill orders on the destination chain. Bridge to Tempo through the [Relay app](https://relay.link) and explore the [Relay docs](https://docs.relay.link/). ### Squid [Squid](https://www.squidrouter.com) enables cross-chain swaps and bridging in a single transaction. Squid's intent-based routing engine aggregates DEXs, bridges, and market makers to find the optimal path for moving assets to and from Tempo — with sub-second execution and zero fees on stablecoin swaps. Developers can integrate cross-chain functionality via a REST API, TypeScript SDK, or drop-in widget. Get started with the [Squid docs](https://docs.squidrouter.com/) or try the [bridge app](https://app.squidrouter.com/). ## Data & Analytics ### Tempo Indexer (TIDX) [TIDX](https://tempo.xyz/developers/docs/api/indexer-api) is Tempo's hosted indexer for querying blocks, transactions, logs, token balances, and decoded events through SQL. Use the public mainnet endpoint at `https://indexer.tempo.xyz` or the testnet endpoint at `https://indexer.testnet.tempo.xyz`. The hosted indexer powers explorer-style reads and supports ClickHouse-backed analytical queries for expensive reads like holder lists and token activity. Try the [interactive TIDX example](https://tempo.xyz/developers/docs/api/indexer-api#interactive-example) or read the [TIDX README](https://github.com/tempoxyz/tidx) to run your own indexer. ### Allium [Allium](https://www.allium.so) is an enterprise blockchain data platform that delivers real-time, analytics-ready datasets through a unified schema across chains. Developers can fetch wallet, token, and price data in milliseconds without managing infrastructure, decoding raw data, or inferring transactions—making it easy to focus on building Tempo applications. Get access to Tempo data through the [Allium App](https://app.allium.so/join), explore the full API in the [Allium docs](https://docs.allium.so/), and browse real examples of production apps built on Allium [here](https://docs.allium.so/api/developer/overview). :::tip Allium has a [ready-to-use recipe](https://github.com/Allium-Science/allium-recipes/tree/main/tempo) for querying Tempo data with SQL. ::: ### Artemis [Artemis](https://about.artemis.ai/) provides a unified analytics terminal for monitoring onchain activity across stablecoins, assets, and networks. Developers use Artemis to analyze flows, liquidity, token performance, and ecosystem-level trends through a clean, queryable interface. Tempo is already supported within Artemis, with a dedicated analytics page for [Tempo Testnet](https://app.artemisanalytics.com/asset/tempo_moderato). Artemis also maintains a cross-chain stablecoin dashboard covering major USD-pegged assets across numerous networks. Stablecoins launched on Tempo will appear in the [Stablecoins dashboard](https://app.artemisanalytics.com/stablecoins). ### Chainlink [Chainlink](https://chain.link) is the industry-standard oracle platform powering the majority of DeFi and bringing capital markets onchain. The Chainlink stack provides the data, interoperability, and security needed for tokenized assets, stablecoins, payments, lending, and other advanced onchain use cases. Chainlink supports Tempo through: * **Cross-Chain Interoperability Protocol (CCIP):** A secure interoperability layer for sending messages and value across chains, enabling cross-chain user flows and multi-chain architectures.\ Explore CCIP in the [Chainlink CCIP docs](https://docs.chain.link/ccip). * **Data Streams:** Chainlink Data Streams delivers low-latency market data offchain, which can be verified onchain. This pull-based design gives dApps on-demand access to high-frequency market data backed by decentralized, fault-tolerant, and transparent infrastructure—an improvement over traditional push-based oracles that update only at fixed intervals or price thresholds.\ View the Chainlink Data Stream deployed on Tempo [here](https://explore.tempo.xyz/address/0xcE73c8ad08CBDEaCa6078BF0627C8fe0a9a536E7?tab=contract). Developers can explore CCIP, Data Streams, and the full Chainlink platform through the [Chainlink Developer Docs](https://docs.chain.link/). ### Codex [Codex](https://www.codex.io) is an enriched blockchain data API covering 70M+ tokens and 700M+ wallets across 90+ chains, including Tempo. Developers can query real-time USD pricing, token analytics, holder data, trading pair statistics, stablecoin activity, and prediction market data through a single GraphQL API — without managing infrastructure or decoding raw data. Get started with the [Codex docs](https://docs.codex.io), explore the API with the [API explorer](https://docs.codex.io/explore), and try the [TypeScript SDK](https://github.com/Codex-Data/sdk). ### CoinGecko [CoinGecko](https://www.coingecko.com) provides comprehensive cryptocurrency market data, including prices, trading volume, market capitalization, and token metadata. Developers can use the CoinGecko API to access Tempo token data for building dashboards, portfolio trackers, and analytics tools. Get started with the [CoinGecko API](https://docs.coingecko.com/reference/introduction). ### Goldsky [Goldsky](https://goldsky.com) makes it easy to access real-time Tempo data with minimal maintenance. Goldsky offers two core products for indexing and streaming onchain data: * **[Subgraphs](https://docs.goldsky.com/subgraphs/):** A fully backwards-compatible subgraph indexing solution that handles reorgs, RPC failures, and scaling automatically, with improved reliability and performance over traditional subgraph hosts. * **[Mirror](https://docs.goldsky.com/mirror/):** A simple way to replicate subgraph or chain-level streams directly into your own databases or message queues, powering flexible front-end and back-end data pipelines. Start indexing Tempo [here](https://goldsky.com/chains/tempo). ### Range [Range](https://www.range.org) powers the Stablecoin Explorer, which provides a unified view of major stablecoins across 100+ chains. Tempo is fully supported, allowing developers and users to trace stablecoin flows in a way traditional explorers cannot. Range stands out through: * **Complete cross-chain visibility**, showing the entire lifecycle of a transfer in one place * **Enriched context**, including bridge routes, verified entities, and risk signals * **Built-in compliance checks** via global sanctions lists Explore Tempo activity in the [Stablecoin Explorer](https://explorer.money/transactions?dn=tempo-testnet\&sc=INTRACHAIN\&sn=tempo-testnet). ### RedStone [RedStone](https://redstone.finance) delivers modular oracle infrastructure with a Push and Pull model for onchain price feeds. RedStone's architecture minimizes gas costs by delivering data on-demand, making it well-suited for DeFi applications, lending protocols, FX and stablecoin systems on Tempo. Explore the available Push data feeds [here](https://app.redstone.finance/push-feeds?networks=tempo\&testnets=true) and integration guides in the [RedStone docs](https://docs.redstone.finance/). ### SonarX [SonarX](https://www.sonarx.com) delivers standardized, auditable on-chain data built for institutional confidence and enterprise integration. SonarX provides indexed Tempo data from genesis to tip through instant data shares on Snowflake, Databricks, and BigQuery, as well as real-time streaming and REST APIs — all backed by a robust data quality framework and SOC 2 compliance. Start a trial at [sonarx.com](https://www.sonarx.com/trial) and explore the [SonarX docs](https://docs.sonarx.com/). ### SQD [SQD](https://sqd.ai) is a decentralized query engine and high-performance indexing toolkit for extracting and transforming on-chain data. With the Squid SDK, developers can build custom indexers for Tempo that are up to 100x faster than direct RPC indexing, with data served through the SQD Network's decentralized data layer. Get started with the [SQD docs](https://docs.sqd.ai/) and deploy indexers via [SQD Cloud](https://app.subsquid.io/). ### Zerion [Zerion](https://zerion.io/api) provides an enterprise-grade wallet data API that delivers portfolio balances, transaction history, DeFi positions, PnL tracking, and real-time webhooks — including Tempo — through a single unified interface. Developers can add comprehensive blockchain data to their applications without running any indexing infrastructure. Get a free API key from the [Zerion dashboard](https://dashboard.zerion.io/) and explore the [API documentation](https://developers.zerion.io/reference/authentication). ## Block Explorers ### Tempo Explorer Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](https://tempo.xyz/developers/docs/quickstart/connection-details). ### Tenderly [Tenderly](https://tenderly.co) delivers full-stack observability, debugging, and simulation tools for Tempo smart contract development and monitoring. With Tenderly you get real-time error tracking, EVM-level tracing, and off-chain transaction simulation — enabling you to catch bugs, analyze reverts, and inspect gas usage before transactions go live. You can enable Tempo in the [Tenderly Dashboard](https://dashboard.tenderly.co/) to use its tracing, alerts, and debugging tools with no infrastructure to manage. ## Wallets ### Embedded #### Blockradar [Blockradar](https://blockradar.co) provides non-custodial wallet infrastructure purpose-built for fintechs running stablecoin payments. The platform focuses on real financial use cases, from merchant settlement to cross-border payouts, with tools designed for payments, compliance, treasury operations, and multi-chain liquidity. Explore the full platform in the [Blockradar Docs](https://docs.blockradar.co/). **Wallet and Payment Operations:** Through one unified API, teams can issue wallets for users, merchants, or treasury; accept fiat inflows through virtual accounts; enable gasless stablecoin transactions; apply AML checks automatically; consolidate balances through configurable sweeps; and handle cross-chain movement using swap and bridge. Fintechs can start building immediately from our API or [Blockradar Dashboard](https://dashboard.blockradar.co/). For advanced flows or high-volume programs, fintechs can [book a demo](https://www.blockradar.co/contact) to walk through production architectures. #### Crossmint [Crossmint](https://www.crossmint.com) is an all-in-one platform, with unified APIs for [wallets](https://docs.crossmint.com/wallets/), [stablecoin orchestration](https://docs.crossmint.com/stablecoin-orchestration/), [checkout flows](https://docs.crossmint.com/payments), and [tokenization](https://docs.crossmint.com/minting), giving developers a single interface for everything from payments to asset management on Tempo. Crossmint delivers a gasless, seed-phrase-free UX backed by bank-grade security and compliance, along with no-code dashboards for managing programs across your team. Set up a project in the [Crossmint console](https://crossmint.com/console) and explore the [Solution Guide](https://docs.crossmint.com/solutions/overview#fintech) tailored for payment use-cases. #### Dynamic [Dynamic](https://dynamic.xyz) combines authentication, smart wallets, and key management into a flexible SDK for Tempo developers. Teams can onboard users with familiar login methods and provision Tempo-compatible wallets through Dynamic's secure infrastructure. Enable Tempo testnet in the [Dynamic dashboard](https://app.dynamic.xyz/dashboard/chains-and-networks), and create an account [here](https://www.dynamic.xyz/get-started) to start integrating Dynamic into your app. #### Para [Para](https://getpara.com) is a comprehensive wallet and authentication suite for fintech and crypto applications. It provides flexible login methods, secure MPC-backed wallets, fast authentication, and infrastructure for automating onchain activity. Para is adding Tempo chain support so developers can easily build Tempo-enabled wallets and payment flows. Get started by signing up through the [Para Dev Portal](https://developer.getpara.com/) and following the quickstart in the [Para docs](https://docs.getpara.com/v2/introduction/welcome). #### Privy [Privy](https://www.privy.io/) builds secure key management and embedded wallets so any developer can easily build secure, scalable wallets into their app. Easily spin up self-custodial wallets for users, manage your treasury wallets and more. Privy takes advantage of Tempo-native experiences to enable better stablecoin and payments experiences. Easily enable gas sponsorship, leverage webhooks for onchain events, delegated signatures, simple wallet funding, etc. You can get started now. Simply [create](https://docs.privy.io/wallets/wallets/create/create-a-wallet#param-chain-type-1) an ethereum wallet with Privy and pass in `"caip2": "eip155:4217"` when [making transactions](https://docs.privy.io/wallets/using-wallets/ethereum/send-a-transaction#usage-9). :::tip Check out the [Tempo + Privy guide](https://docs.privy.io/wallets/using-wallets/tempo/send-a-transaction) and Privy's [example](https://github.com/privy-io/examples/tree/main/examples/privy-next-tempo) peer-to-peer payments app that uses Tempo transaction memos. ::: ### Turnkey [Turnkey](https://www.turnkey.com) provides programmable key management and non-custodial wallet infrastructure for applications that need granular signing policies and automated transaction flows. With Turnkey, developers can securely sign Tempo Transactions, automate wallet operations, and build custom logic around how keys are used. Turnkey also supports sponsor-style workflows, enabling gasless or subsidized transaction flows through configurable signing policies. [Create your Turnkey account](https://app.turnkey.com/dashboard) and follow the [Turnkey Embedded Wallet Kit guide](https://docs.turnkey.com/sdks/react/getting-started) to integrate embedded wallets into your Tempo app. :::tip Turnkey has a [`with-tempo`](https://github.com/tkhq/sdk/tree/main/examples/chain-integrations/with-tempo) example in their SDK to get you started quickly. ::: ### Custodial & Institutional #### BitGo [BitGo](https://www.bitgo.com) provides institutional-grade custody, trading, and wallet infrastructure. BitGo supports Tempo with both custodial and self-custody wallet solutions, enabling enterprises to securely store, manage, and transact with Tempo-based assets under robust security and compliance controls. BitGo is a qualified custodian in the United States and globally [licensed and regulated](https://www.bitgo.com/company/licenses/). Get started through the [BitGo platform](https://www.bitgo.com) or explore their [developer docs](https://developers.bitgo.com/). #### Fireblocks [Fireblocks](https://www.fireblocks.com) provides enterprise-grade digital asset infrastructure for custody, transfers, and tokenization. Tempo is supported through Fireblocks' MPC-based signing, policy engine, and transaction API — enabling institutions to securely manage Tempo assets with configurable approval workflows and direct network connectivity. Access Tempo through the [Fireblocks console](https://console.fireblocks.io/) and explore the [Fireblocks Developer docs](https://developers.fireblocks.com/). #### Utila [Utila](https://utila.io) provides secure MPC wallet infrastructure and asset-management tooling for teams building with stablecoins and digital assets. Developers can use Utila to manage Tempo-based payments and treasury operations across multiple wallets and blockchains, all within a single policy-driven platform. Utila's MPC technology reduces counterparty risk, while its configurable approval engine gives teams granular control over how funds are moved. [Learn more](https://utila.io/product/payments/) about how Utila supports stablecoin operations on Tempo, and [request a demo](https://utila.io/request-a-demo/) if you're interested in secure MPC infrastructure. ## Smart Contract Libraries ### Pimlico [Pimlico](https://www.pimlico.io) provides smart account infrastructure for Tempo, including ERC-4337 bundlers and paymasters. With Pimlico, developers can sponsor gas fees, accept ERC-20 tokens for gas, and relay smart account transactions — enabling seamless, gasless onchain experiences for end users. Get started on the [Pimlico dashboard](https://dashboard.pimlico.io/) and explore the [Pimlico docs](https://docs.pimlico.io/). ### Safe [Safe](https://safe.global) provides a modular smart account framework used across leading Web3 applications and institutions. With Safe, developers can build Tempo applications that take advantage of multi-sig controls, programmable permissions, session keys, and automated transaction policies. ### ZeroDev [ZeroDev](https://zerodev.app) provides a powerful smart account platform for Tempo, supporting both ERC-4337 and EIP-7702. Developers can onboard users with social logins, enable gas sponsorship, and automate transactions while taking advantage of ZeroDev's chain-abstracted workflows. Its modular wallet stack also allows teams to build customized features such as custom transaction policies and tailored approval logic. Create a project in the [ZeroDev dashboard](https://dashboard.zerodev.app) and follow the [SDK quickstart](https://docs.zerodev.app/sdk/getting-started/quickstart) to integrate smart accounts into your Tempo application. ## Node Infrastructure ### Alchemy With [Alchemy](https://alchemy.com), build the fastest and most reliable Tempo applications, powered by industry-leading latency, uptime, and elastic throughput. Alchemy's global RPC infrastructure supports everything from stablecoins to tokenization and large-scale consumer apps. Sign up through the [Alchemy dashboard](https://dashboard.alchemy.com) and visit the [Alchemy docs](https://www.alchemy.com/docs/node#tldr) to start building. ### Blockdaemon [Blockdaemon](https://app.blockdaemon.com/) provides institutional-grade node and API infrastructure, along with staking and MPC wallet services. Their globally distributed platform supports enterprise-scale, production workloads with strong reliability and compliance guarantees. Sign up through the [Blockdaemon Developer Dashboard](https://app.blockdaemon.com/) and deploy a Tempo node by navigating to **Nodes & RPC → Deploy a Node**. ### Chainstack [Chainstack](https://chainstack.com) provides managed blockchain infrastructure with high-performance, secure RPC nodes. The platform offers reliable Tempo endpoints with built-in monitoring and analytics. Create an account through the [Chainstack console](https://console.chainstack.com) to deploy Tempo nodes and access RPC endpoints. ### Conduit [Conduit](https://conduit.xyz) provides high-performance RPC infrastructure for Tempo Testnet. Developers can create API keys and access Tempo Testnet endpoints through the [Conduit app](https://app.conduit.xyz). View the Tempo Testnet RPC endpoint in the [Conduit Hub](https://hub.conduit.xyz/tempo-testnet) and get started with the [Tempo RPC Quickstart](https://docs.conduit.xyz/rpc-nodes/getting-started/tempo-rpc-quickstart). ### dRPC [dRPC](https://drpc.org) provides managed Tempo RPC endpoints through NodeCloud, with smart routing, analytics, key control, and front-end protection across 180+ networks. The platform runs on 40 providers in 8 geoclusters, with a free tier and flat-rate plans starting at $10. Get started by visiting the [dRPC chain list](https://drpc.org/chainlist), and learn more about NodeCloud on the [dRPC NodeCloud page](https://drpc.org/nodecloud-multichain-rpc-management). ### Quicknode [Quicknode](https://quicknode.com) is the enterprise-grade development platform for building, scaling, and launching onchain applications with speed and reliability. Their globally optimized RPC network makes it easy to run high-performance Tempo workloads from day one. Get started on the [Tempo Chain Page](https://www.quicknode.com/chains/tempo) and follow the [QuickStart guide](https://www.quicknode.com/docs/tempo) to create your Tempo RPC endpoint. ### Validation Cloud [Validation Cloud](https://www.validationcloud.io/tempo) provides institutional-grade, full-archive RPC nodes and validator infrastructure for Tempo. With SOC 2 Type II compliance, high performance, and low latency, Validation Cloud is purpose-built for powering real-world payments and stablecoin use cases at scale. Get started at [validationcloud.io/tempo](https://www.validationcloud.io/tempo). ## Security & Compliance ### Blockaid [Blockaid](https://blockaid.io) provides real-time security infrastructure for Web3 applications. Its transaction scanning and threat detection systems identify malicious activity before users sign transactions, improving safety across wallets and interfaces. Learn how Blockaid's transaction scanning improves security by visiting their [overview page](https://www.blockaid.io/transaction-security), and reach out to their team [here](https://www.blockaid.io/contact) to get started. ### Chainalysis [Chainalysis](https://www.chainalysis.com) delivers industry-leading onchain intelligence, compliance, and security infrastructure. Through Hexagate, Chainalysis supports Tempo with real-time monitoring, anomaly detection, and threat insights to help developers and platforms better understand and manage onchain risk as the ecosystem grows. Discover how Hexagate supports Tempo [here](https://www.hexagate.com), or request a dedicated walkthrough from the Chainalysis team through their [demo form](https://www.hexagate.com/request-demo). ### Elliptic [Elliptic](https://www.elliptic.co) provides blockchain analytics and compliance solutions for detecting and preventing financial crime. Elliptic supports Tempo with transaction screening, wallet risk scoring, and regulatory compliance tools — helping platforms meet AML obligations while operating on the Tempo network. Learn more about Elliptic's compliance solutions at [elliptic.co](https://www.elliptic.co) or explore their [developer docs](https://docs.elliptic.co/). ### TRES [TRES](https://www.tres.finance) is the accounting and reconciliation layer for digital asset payments. TRES reconciles onchain settlement against internal and custodial records daily, then delivers the output in bank-grade formats such as MT940 and custom formats so finance and treasury teams can keep using their existing TMS and ERP without a rewrite. ### TRM Labs [TRM Labs](https://www.trmlabs.com) delivers blockchain intelligence and compliance infrastructure for detecting fraud, money laundering, and financial crime. TRM supports Tempo with transaction monitoring, wallet screening, and risk assessment tools that help platforms operate safely and meet regulatory requirements. Get started at [trmlabs.com](https://www.trmlabs.com) or explore their [documentation](https://docs.trmlabs.com/). ## Orchestration ### Brale [Brale](https://brale.xyz) provides infrastructure for issuing, transferring, and managing stablecoins across chains. Developers can create new stablecoins or work with existing issued assets using Brale's APIs to support on- and off-ramps, payouts, and cross-ecosystem stablecoin movement. Brale exposes two complementary APIs: * **[Stablecoin Movement & Account Management](https://docs.brale.xyz/#stablecoin-movement--account-management-apibralexyz):**\ An authenticated API for orchestrating stablecoin workflows, including issuance, transfers across accounts or chains, custody management, and integration with financial institutions. * **[Stablecoin Market Data](https://docs.brale.xyz/#stablecoin-market-data-databralexyz):**\ A public, read-only API that provides token metadata, stablecoin definitions, and price feeds. These APIs support common stablecoin workflows such as minting, redemption, swaps, payouts, and treasury operations, making Brale suitable for fintechs, exchanges, and payment platforms building on Tempo. Get started by creating an account [here](https://app.brale.xyz/buy/signup/). ### Bridge [Bridge](https://www.bridge.xyz) (a Stripe Company) provides stablecoin orchestration infrastructure for moving money between fiat and crypto rails. Bridge supports Tempo with APIs for issuance, wallets, and cross-border stablecoin transfers — enabling fintechs and platforms to build payment flows that span traditional and onchain systems. Get started with [Bridge's Tempo Integration Guide](https://apidocs.bridge.xyz/get-started/guides/move-money/tempo-integration-guide#tempo-integration-guide). # How to create and manage API keys API keys authenticate an integration and attribute its usage to a project. Every key is fixed to the production or sandbox environment in which it was created. ## Create an API key You need at least one project before creating a key. :::steps ### Choose the environment Open the organization menu in [Tempo API Console](https://console.tempo.xyz/?to=/\:org/api-keys) and switch to [**Sandbox**](https://console.tempo.xyz/?to=/\:org/api-keys%3Fenv%3Dsandbox) or [**Production**](https://console.tempo.xyz/?to=/\:org/api-keys). Check for the amber sandbox banner before continuing. ### Open the new-key form Select [**API Keys**](https://console.tempo.xyz/?to=/\:org/api-keys), then [**New key**](https://console.tempo.xyz/?to=/\:org/api-keys/new). ### Choose a project and name Select the project that will use the credential. Give the key a name that identifies its deployment or owner, such as `production-worker` or `local-development`. ### Review scopes Expand **View scopes** and keep only the permissions the integration needs. Available self-service scopes are selected by default. A key with no scopes has no access to scoped APIs. ### Create and store the key Select **Create key**, then immediately copy the revealed token into your secret manager. The plaintext value is shown only once. ::: Production keys begin with `tempo:sk:` and sandbox keys begin with `tempo_sandbox:sk:`. The API Keys page shows only the prefix and final four characters after you dismiss the reveal. ## Authenticate a request Send the token as a Bearer credential: ```bash curl 'https://api.tempo.xyz/v1/blocks' \ --header 'Authorization: Bearer tempo:sk:...' ``` You can also use the `tempo-api-key` header or configure the [Tempo API Typed Client](https://tempo.xyz/developers/docs/api/typed-client): ```ts import { Client } from 'tapimo' const client = Client.create({ apiKey: process.env.TEMPO_API_KEY, }) ``` Keep the key in a server-side environment variable or secret manager. Do not expose it in frontend code, logs, support messages, or source control. ## Restrict an API key by IP address Use an IP allowlist for server-side integrations with stable egress addresses. Tempo accepts exact IPv4 and IPv6 addresses or CIDR ranges. Set `allowedIps` when creating a key through the management API: ```http POST /v1/orgs/{orgId}/projects/{projectId}/api-keys Content-Type: application/json { "name": "production-worker", "scopes": ["data:read"], "allowedIps": ["203.0.113.7", "2001:db8::/32"] } ``` Replace an existing key's complete allowlist with `PATCH`: ```http PATCH /v1/orgs/{orgId}/projects/{projectId}/api-keys/{keyId} Content-Type: application/json { "allowedIps": ["198.51.100.0/24"] } ``` An omitted or empty `allowedIps` list means unrestricted access. Send `{ "allowedIps": [] }` to remove an existing restriction. Each list supports up to 100 entries. Requests from an address outside the allowlist, or without a trusted client IP, fail with `403 api_key_ip_forbidden`. Allowlist changes are eventually consistent and may take a minute or more to propagate globally. During an egress migration, add both the old and new ranges, wait for propagation, then remove the old range. See the [Tempo API reference](https://tempo.xyz/developers/docs/api/reference) for complete request and response schemas. ## Rotate an API key Tempo API Console does not reveal an existing token again. Rotate a credential by overlapping the old and new keys: :::steps ### Create a replacement Create a new key in the same project and environment with the scopes required by the integration. ### Update the integration Replace the old token in the integration's secret configuration and deploy or restart it as required. ### Verify the replacement Make a request with the new key and check the project's [**Usage**](https://console.tempo.xyz/?to=/\:org/usage) page. Confirm that requests are attributed to the replacement key. ### Revoke the old key Return to [**API Keys**](https://console.tempo.xyz/?to=/\:org/api-keys), open the old key's actions menu, select **Revoke key**, and confirm. Requests using that token stop authenticating. ::: ## Resolve common key problems | Problem | Check | | --- | --- | | The key cannot be found in the console | Confirm that the selected environment and project filter match the key. | | The API rejects the credential | Check that the full token was copied, has not been revoked, and is sent in a supported header. | | A scoped endpoint denies access | Open the key's project and confirm that the key was created with the required scope. Create a replacement to change scopes. | | A sandbox key appears to use public access on mainnet | Sandbox keys cannot authenticate mainnet requests. Remove the mainnet selector or use a production key. | | The plaintext token was lost | Create a replacement key. Existing tokens cannot be revealed again. | For complete credential and chain-selection behavior, see [API authentication](https://tempo.xyz/developers/docs/api/authentication). # Managing projects and environments in the API Console Projects identify the apps and integrations using your Tempo API organization. Environments separate production activity from sandbox testing without requiring a second organization. ## Work with projects Each API key belongs to one project. Project attribution lets you filter API keys and usage, and it gives you a boundary for rotating or revoking an integration's credentials. ### Create a project :::steps ### Open Projects Select [**Projects** in Tempo API Console](https://console.tempo.xyz/?to=/\:org/projects), then select [**New project**](https://console.tempo.xyz/?to=/\:org/new). ### Name the project Enter a name that identifies the app, service, or integration. Prefer names such as `checkout-service` or `treasury-dashboard` over environment names, because one project can have separate sandbox and production keys. ### Finish creating the project Select **Create project**. The project is now available when creating an API key and filtering organization usage. ::: To rename or delete a project, open its actions menu and select **Settings**. Deleting a project permanently deletes its API keys, so migrate callers to another project before confirming the deletion. ## Switch environments Open the organization menu and select **Switch to sandbox** or **Switch to production**. An amber banner remains visible while sandbox is active. The selected environment applies to the API Keys, Usage, and Billing pages: | Console state | Production | Sandbox | | --- | --- | --- | | Key prefix | `tempo:sk:...` | `tempo_sandbox:sk:...` | | Supported chains | Any chain, including mainnet | Non-mainnet chains only | | Default chain | Mainnet | Testnet | | Usage | Production requests and sponsorship | Sandbox requests and sponsorship | | Billing | Production billing state and limits | Separate sandbox billing state and limits | Projects are shared across both environments. A project can have production keys, sandbox keys, or both. :::info Switching the console environment changes which keys, usage, and billing state you see. It does not convert an existing key to another environment. ::: ## Choose an environment for a request Use sandbox while developing and testing an integration. Move to production by creating a production key, updating your secret configuration, and testing the integration against the intended chain. The API determines a key's environment from its token. See [production and sandbox API keys](https://tempo.xyz/developers/docs/api/authentication#production-and-sandbox-api-keys) for chain-selection rules. ## Filter organization pages by project API Keys and Usage are organization-wide by default. Use their project filter to narrow the page: * **API Keys** shows credentials belonging to the selected project and environment. * **Usage** shows request and sponsorship activity attributed to the selected project and environment. * **Billing** remains organization-wide and environment-specific. Unknown or removed project filters fall back to the organization-wide view. # Managing teams and access in the API Console The Team page controls who can access an organization in Tempo API Console. Membership applies across the organization's projects and both environments. ## Understand organization roles | Role | Console access | | --- | --- | | **Owner** | Manages billing, invites any role, changes member roles, removes members, and controls the organization. | | **Admin** | Invites new members with the Member role and reviews pending invitations. | | **Member** | Uses the organization without team-management controls. | An organization must retain at least one owner. The console prevents the last owner from being demoted or removed. ## Invite a member Owners and admins can send invitations. Admins can grant only the Member role. :::steps ### Open Team Select [**Team** in Tempo API Console](https://console.tempo.xyz/?to=/\:org/team). ### Enter the invitation details Under **Invite member**, enter the person's email address and choose one of the roles available to you. ### Send the invitation Select **Invite member**. The invitation appears under **Pending invitations** until it is accepted or revoked. ### Ask the recipient to accept The recipient must sign in with the invited email address and accept the invitation before it expires. ::: If the recipient cannot accept, confirm that they signed in with the exact invited email address and that the invitation is still pending. Revoke and resend an expired or incorrect invitation. ## Change a member's role Organization owners can change roles from the role selector in the members table. Promote someone to Owner before removing or demoting the organization's last current owner. Role changes take effect for organization access after the update succeeds. They do not change the scopes of existing API keys. ## Remove access * **Revoke an invitation:** open its actions menu under **Pending invitations**, select **Revoke**, and confirm. * **Remove a member:** open the member's actions menu, select **Remove**, and confirm. * **Leave an organization:** open your own actions menu and select **Leave organization**. The last owner cannot leave until another owner exists. Removing a member does not automatically revoke project API keys. Review [**API Keys**](https://console.tempo.xyz/?to=/\:org/api-keys) separately when offboarding someone who managed integration credentials. # Monitoring your API usage and billing Tempo API Console reports API activity and manages billing for fee sponsorship. Usage and billing are organization-wide, with separate state for production and sandbox. ## Review API usage Select [**Usage**](https://console.tempo.xyz/?to=/\:org/usage) to inspect activity in the selected environment. Use the project selector to show all projects or one project. The page includes: * **Request volume:** total and failed API calls in the selected window. * **Sponsorship:** sponsored transaction counts and committed fees. * **Routes:** the API routes receiving the most requests. * **API keys:** request volume attributed to each credential. Use these views to find unexpected errors, identify high-volume routes, and confirm that a newly deployed key is receiving traffic. :::info Changing the console environment changes the usage dataset. Sandbox activity does not appear in production usage, and production activity does not appear in sandbox usage. ::: ## Add a payment method Only organization owners can manage billing. :::steps ### Choose the environment Switch to the production or sandbox environment whose billing state you want to configure. ### Open Billing Select [**Billing**](https://console.tempo.xyz/?to=/\:org/billing), then find **Payment methods**. ### Add the payment method Select **Add payment method** and complete the hosted Stripe checkout. After returning to the console, the billing page may refresh briefly while the payment method is confirmed. ### Confirm billing status Check that the payment method appears and that the page reports **Active** billing. A **Past due** or **Canceled** status requires attention before billable services can continue normally. ::: When a payment method already exists, owners can select **Manage** to open the hosted billing portal. Other members must contact an organization owner. ## Configure spend limits The Billing page provides two organization-level controls for the selected environment: * **Current spend:** billable API usage in the current billing period and an optional monthly spend limit. * **Sponsorship limit:** the maximum transaction fee the API may sponsor for one transaction. Owners can set, edit, or remove these limits. A sponsorship request must remain within both the organization controls and any platform limits enforced by Tempo. :::warning Production fee sponsorship requires an active billing source. A spend or sponsorship limit does not activate billing on its own. ::: ## Investigate unexpected usage 1. Confirm that the console is showing the correct environment. 2. Open [**Usage**](https://console.tempo.xyz/?to=/\:org/usage) and filter to the affected project. 3. Compare the **Routes** and **API keys** sections to find the source. 4. Revoke a credential if its traffic is unexpected. 5. Review the Billing page and adjust spend or sponsorship limits if needed. See [API rate limits](https://tempo.xyz/developers/docs/api/rate-limits) for request quotas and response headers. For sponsored transaction integration, follow the [fee sponsorship guide](https://viem.sh/tempo/guides/sponsor-fees#sponsor-via-a-relay). # Chains Source chains and stablecoins supported by Routes. ## Get chains `GET /v1/routes/chains` Lists supported source chains and tokens. ### Responses #### `200`: Supported source chains and stablecoins. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Source chains and stablecoins supported by route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `tokens` `object[]` _(required)_: Stablecoins known on this source chain. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/chains ``` ```ts fetch('https://api.tempo.xyz/v1/routes/chains') ``` # Deposit Addresses Reusable route addresses and deposits detected at those addresses. ## List deposit addresses `GET /v1/routes/deposit-addresses` Lists reusable route deposit addresses, newest first. ### Query parameters - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. ### Responses #### `200`: A page of reusable route deposit addresses. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Route deposit addresses on this page, newest first. - `address` `string` _(required)_: Reusable source-chain deposit address. - `createdAt` `string ` _(required)_: When the deposit address was created (ISO 8601). - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `id` `string` _(required)_: Route deposit address id (`rda_…`, or legacy `fda_…`). - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `recipient` `string` _(required)_: Tempo account that receives completed deposits. - `refundAddress` `string` _(required)_: Source-chain address that receives refunds. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `status` `string` _(required)_: Whether the address can safely process new deposits. - `subsidize` `boolean` _(required)_: Whether Tempo guarantees normalized 1:1 delivery. - `updatedAt` `string ` _(required)_: When the deposit address last materially changed (ISO 8601). - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/routes/deposit-addresses?cursor=WzIzNDU2Nzg5LDBd&limit=10' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/deposit-addresses?cursor=WzIzNDU2Nzg5LDBd&limit=10') ``` ## Create deposit address `POST /v1/routes/deposit-addresses` Creates or returns a reusable deposit address for routes a Tempo account. ### Header parameters - `idempotency-key` `string` _(required)_: Opaque retry key scoped to the API key. Matching requests replay successful responses for 24 hours; changed input conflicts, in-flight requests block, and failed attempts release the key. ### Request body (required) (`application/json`) - `amount` `string` _(required)_: Amount in base units of the token the mode fixes. - `destinationToken` `string` _(required)_: Tempo destination token symbol, contract address, or token key. - `recipient` `string` _(required)_: Tempo account that receives completed deposits. - `refundAddress` `string` _(required)_: Source-chain account that receives refunds. - `sourceChain` `string` _(required)_: Source chain CAIP-2 id, slug, or alias. - `sourceToken` `string` _(required)_: Source token symbol, contract address, or token key. - `subsidize` `boolean`: Guarantees normalized 1:1 delivery through a Tempo-funded subsidy. ### Responses #### `200`: The created or matching reusable route deposit address and quote. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The organization cannot create another address or is not eligible for a Tempo-funded Routes subsidy. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No provider can provision the requested reusable address route. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: The request conflicts with an existing address, policy, or in-flight request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: The selected provider could not provision the address. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/deposit-addresses \ --request POST \ --header 'idempotency-key: routes_01k1c5j8q8p0be6j5v9m6d1e4r' \ --header 'Content-Type: application/json' \ --data '{ "amount": "1000000", "destinationToken": "usdt0", "recipient": "0x1111111111111111111111111111111111111111", "refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8", "sourceChain": "tron", "sourceToken": "usdt" }' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/deposit-addresses', { method: 'POST', headers: { 'idempotency-key': 'routes_01k1c5j8q8p0be6j5v9m6d1e4r', 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: '1000000', destinationToken: 'usdt0', recipient: '0x1111111111111111111111111111111111111111', refundAddress: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', sourceChain: 'tron', sourceToken: 'usdt' }) }) ``` ## Reconcile deposit address `POST /v1/routes/deposit-addresses/{id}/reconcile` Queues an immediate provider reconciliation for one reusable route deposit address. ### Path parameters - `id` `string` _(required)_: Route deposit address ID (`rda_…`). ### Responses #### `202`: The route deposit address was queued for reconciliation. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The API key does not grant routes write access. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No visible route deposit address exists for the identifier. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: The route deposit address is not active. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Manual routes reconciliation is not configured. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Reconciliation could not be queued. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/deposit-addresses/rda_001785792000000_2ZPE2gvateYEQ0dQslgvkhjx/reconcile \ --request POST ``` ```ts fetch('https://api.tempo.xyz/v1/routes/deposit-addresses/rda_001785792000000_2ZPE2gvateYEQ0dQslgvkhjx/reconcile', { method: 'POST' }) ``` ## Get deposit address `GET /v1/routes/deposit-addresses/{id}` Returns one reusable route deposit address. ### Path parameters - `id` `string` _(required)_: Route deposit address ID (`rda_…`). ### Responses #### `200`: The requested route deposit address. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `address` `string` _(required)_: Reusable source-chain deposit address. - `createdAt` `string ` _(required)_: When the deposit address was created (ISO 8601). - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `id` `string` _(required)_: Route deposit address id (`rda_…`, or legacy `fda_…`). - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `recipient` `string` _(required)_: Tempo account that receives completed deposits. - `refundAddress` `string` _(required)_: Source-chain address that receives refunds. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `status` `string` _(required)_: Whether the address can safely process new deposits. - `subsidize` `boolean` _(required)_: Whether Tempo guarantees normalized 1:1 delivery. - `updatedAt` `string ` _(required)_: When the deposit address last materially changed (ISO 8601). #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No visible route deposit address exists for the identifier. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/deposit-addresses/rda_001785792000000_2ZPE2gvateYEQ0dQslgvkhjx ``` ```ts fetch('https://api.tempo.xyz/v1/routes/deposit-addresses/rda_001785792000000_2ZPE2gvateYEQ0dQslgvkhjx') ``` ## Get deposit `GET /v1/routes/deposits/{id}` Returns one detected deposit and its delivery status. ### Path parameters - `id` `string` _(required)_: Route deposit id (`rdp_…`). ### Responses #### `200`: The requested route deposit. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the deposit was first detected (ISO 8601). - `depositAddressId` `string` _(required)_: Route deposit address that detected the transfer, including legacy IDs. - `detectionTrigger` `string`: How the deposit was first detected. - `destinationAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountRequired` `object`: Destination amount required for completion, when known. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `destinationTransactionHashes` `string[]`: Verified destination transaction references. - `id` `string` _(required)_: Route deposit id (`rdp_…`, or legacy `fdp_…`). - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `recipient` `string` _(required)_: Tempo account that receives the completed deposit. - `refundAddress` `string` _(required)_: Source-chain address that receives a refund. - `refundAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `refundTransactionHashes` `string[]`: Verified source-chain refund transaction references. - `sender` `string`: Observed source-chain sender, when available. - `sourceAmount` `object`: Verified source amount, when source evidence is available. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `sourceTransactionHashes` `string[]` _(required)_: Provider-observed source transaction references. - `sourceTransferIndex` `integer`: Verified transfer position within the source transaction. - `status` `string` _(required)_: Current delivery status of the deposit. - `statusReason` `object`: Why a deposit needs attention or recovery. - `code` `string` _(required)_: Stable machine-readable reason code. - `message` `string` _(required)_: Human-readable explanation of the current status. - `updatedAt` `string ` _(required)_: When the deposit last materially changed (ISO 8601). #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No visible route deposit exists for the identifier. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/deposits/rdp_001785792060000_2ZPE2gvateYEQ0dQslgvkhjx ``` ```ts fetch('https://api.tempo.xyz/v1/routes/deposits/rdp_001785792060000_2ZPE2gvateYEQ0dQslgvkhjx') ``` ## List deposits `GET /v1/routes/deposits` Lists visible deposits, optionally filtered, newest first. ### Query parameters - `depositAddress` `string`: Reusable source-chain deposit address that received the funds. - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `destinationToken` `string`: Destination token symbol, contract address, or token key. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `provider` `string`: Route provider id. - `recipient` `string`: Tempo account that received the deposits. - `sourceChain` `string`: Source chain CAIP-2 id, slug, or alias. - `sourceToken` `string`: Source token symbol, contract address, or token key. - `status` `string`: Current delivery status of the deposit. ### Responses #### `200`: A page of visible route deposits. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Detected deposits on this page, newest first. - `createdAt` `string ` _(required)_: When the deposit was first detected (ISO 8601). - `depositAddressId` `string` _(required)_: Route deposit address that detected the transfer, including legacy IDs. - `detectionTrigger` `string`: How the deposit was first detected. - `destinationAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountRequired` `object`: Destination amount required for completion, when known. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `destinationTransactionHashes` `string[]`: Verified destination transaction references. - `id` `string` _(required)_: Route deposit id (`rdp_…`, or legacy `fdp_…`). - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `recipient` `string` _(required)_: Tempo account that receives the completed deposit. - `refundAddress` `string` _(required)_: Source-chain address that receives a refund. - `refundAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `refundTransactionHashes` `string[]`: Verified source-chain refund transaction references. - `sender` `string`: Observed source-chain sender, when available. - `sourceAmount` `object`: Verified source amount, when source evidence is available. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `sourceTransactionHashes` `string[]` _(required)_: Provider-observed source transaction references. - `sourceTransferIndex` `integer`: Verified transfer position within the source transaction. - `status` `string` _(required)_: Current delivery status of the deposit. - `statusReason` `object`: Why a deposit needs attention or recovery. - `code` `string` _(required)_: Stable machine-readable reason code. - `message` `string` _(required)_: Human-readable explanation of the current status. - `updatedAt` `string ` _(required)_: When the deposit last materially changed (ISO 8601). - `meta` `object`: Extra response metadata requested with `include`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/routes/deposits?depositAddress=TJRabPrwbZy45sbavfcjinPJC18kjpRTv8&cursor=WzIzNDU2Nzg5LDBd&destinationToken=usdt0&include=totalCount&limit=10&provider=relay&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sourceChain=tron&sourceToken=usdt&status=detected' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/deposits?depositAddress=TJRabPrwbZy45sbavfcjinPJC18kjpRTv8&cursor=WzIzNDU2Nzg5LDBd&destinationToken=usdt0&include=totalCount&limit=10&provider=relay&recipient=0xbe058e1c4df8a4366a387bf595b284246a93039e&sourceChain=tron&sourceToken=usdt&status=detected') ``` # Providers Providers available through Routes. ## Get providers `GET /v1/routes/providers` Lists available transfer providers. ### Responses #### `200`: Route quote providers supported by Tempo. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Providers in Tempo's route quote catalog. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/providers ``` ```ts fetch('https://api.tempo.xyz/v1/routes/providers') ``` # Quotes Live quotes for routing assets between supported chains. ## Get quotes `GET /v1/routes/quotes` Returns live quotes for transferring stablecoins between supported chains. ### Query parameters - `destinationChain` `string`: Destination chain CAIP-2 id, slug, or alias. Defaults to Tempo. - `destinationToken` `string`: Destination token symbol, address, or token key. Defaults to USDC.e for Tempo destinations. - `provider` `string`: Only request a quote from this provider. Use an id returned by the route providers endpoint, or omit it to query every available provider. - `sourceAmount` `string` _(required)_: Positive source token amount. Base units by default, so 1 USDC is `1000000`. - `sourceAmountUnits` `string`: Whether `sourceAmount` is base units or a human-readable decimal. - `sourceChain` `string` _(required)_: Source chain alias or stable id, such as `base`, `solana`, or `eip155:8453`. - `sourceToken` `string` _(required)_: Source token symbol, contract address, or Tempo token key. Lookup is scoped to `sourceChain`; non-EVM addresses are case-sensitive. An unknown token returns an empty list. ### Responses #### `200`: Live route quotes. An empty list means no provider returned a quote. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Successful live route quotes. - `destinationAmount` `object` _(required)_: Expected amount received after provider deductions, including pool fees. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountMin` `object`: Provider-derived minimum destination amount. Symbiosis and Squid quotes use a fixed 1% slippage tolerance. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `id` `string` _(required)_: Stable deterministic route quote id. - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `quality` `object` _(required)_: Machine-readable quality and settlement-time signals for a successful quote. - `estimatedSeconds` `integer`: Estimated settlement time in seconds, when known. - `tier` `string` _(required)_: Machine-readable quote quality tier for a successful quote. - `quote` `object` _(required)_: Freshness data for a provider route quote. - `expiresAt` `string `: Timestamp after which the provider quote is no longer valid. - `sampledAt` `string ` _(required)_: Timestamp for the live quote request. - `sourceAmount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `402`: Payment required. This endpoint accepts MPP payment, and the request either exceeded free quota or needs a paid request credential. The challenge is protocol-native (handled by mppx), not the JSON error envelope: read the `WWW-Authenticate` header and retry with `Authorization: Payment `. A successful paid response carries `Payment-Receipt`. Headers: - `WWW-Authenticate` `string`: On a `402` response, the payment challenge to satisfy. Use it to build the `Authorization: Payment` credential and retry the request. #### `403`: Fresh instructions are suspended for this API-key organization. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: No quote succeeded and at least one matching provider failed unexpectedly. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/routes/quotes?destinationChain=tempo&destinationToken=usdc.e&provider=relay&sourceAmount=1000000&sourceAmountUnits=baseUnits&sourceChain=base&sourceToken=usdc' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/quotes?destinationChain=tempo&destinationToken=usdc.e&provider=relay&sourceAmount=1000000&sourceAmountUnits=baseUnits&sourceChain=base&sourceToken=usdc') ``` # Transfers Transfers routed between supported chains. ## Register source transaction `POST /v1/routes/transfers/{id}/source-transactions` Registers verified source transactions once per organization, environment, and project. ### Path parameters - `id` `string` _(required)_: Route transfer ID (`rtr_…`). ### Request body (required) (`application/json`) ### Responses #### `200`: The transfer with its verified source transactions. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the transfer was created (ISO 8601). - `destinationAmount` `object`: Expected amount received after provider deductions, including pool fees. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountMin` `object`: Minimum destination amount encoded in the transfer action. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountRequired` `object`: Destination amount required for normalized 1:1 delivery, when enabled. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `destinationTransactionHashes` `string[]`: Verified destination transaction references. - `fees` `object[]` _(required)_: Explicit fees charged separately from the routed amount; deductions reflected in `destinationAmount` are omitted. - `amount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `side` `string` _(required)_: Which side of the transfer the fee is taken from. - `token` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `type` `string` _(required)_: What the fee pays for. - `id` `string` _(required)_: Route transfer id (`rtr_…`, lexically time-ordered). - `method` `string` _(required)_: How the caller funds the transfer. - `mode` `string` _(required)_: Which side of the transfer the quoted amount fixes. - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `quote` `object` _(required)_: Freshness and validity window of the selected quote. - `expiresAt` `string ` _(required)_: When the quoted terms stop being executable (ISO 8601). - `sampledAt` `string ` _(required)_: When the provider produced the quote (ISO 8601). - `recipient` `string` _(required)_: Final beneficiary of the transfer. - `refundAddress` `string`: Source-chain refund recipient (deposit-address method). - `refundAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `refundTransactionHashes` `string[]`: Verified refund transaction references. - `sender` `string`: Source-chain account that signs the route action (transaction method). - `sourceAmount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceAmountMax` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `sourceTransactionHashes` `string[]`: Verified source transaction references. - `subsidize` `boolean`: Whether Tempo guarantees normalized 1:1 destination delivery. - `status` `string` _(required)_: Lifecycle status of the route transfer. - `statusReason` `object`: Why the transfer is in its current status, when context is needed. - `code` `string` _(required)_: Stable machine-readable reason code. - `message` `string` _(required)_: Human-readable explanation of the reason. - `updatedAt` `string ` _(required)_: When the transfer last materially changed (ISO 8601). - `version` `integer` _(required)_: Monotonic revision that increments whenever the transfer materially changes. #### `400`: The path, body, or submitted source transaction is invalid. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No visible route transfer exists for the identifier. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: The transfer or source transaction cannot be registered yet. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Source transaction reconciliation is not configured. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Source verification or reconciliation dispatch failed. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/transfers/rtr_001785729600000_2ZPE2gvateYEQ0dQslgvkhjx/source-transactions \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "transactionHash": "0x5151515151515151515151515151515151515151515151515151515151515151" }' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/transfers/rtr_001785729600000_2ZPE2gvateYEQ0dQslgvkhjx/source-transactions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transactionHash: '0x5151515151515151515151515151515151515151515151515151515151515151' }) }) ``` ## List transfers `GET /v1/routes/transfers` Lists transfers from newest to oldest. ### Query parameters - `cursor` `string`: Opaque keyset cursor from a previous response (`nextCursor`); pass it back verbatim to fetch the next page. Omit for the first (head) page. - `include` `string[]`: Comma-separated optional resources to embed, e.g. `totalCount`. - `limit` `integer`: How many items to return per page (5–50, default 10). Use `nextCursor` to fetch more. - `status` `string`: Lifecycle status of the route transfer. ### Responses #### `200`: A page of route transfers. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `data` `object[]` _(required)_: Route transfers on this page, newest first. - `createdAt` `string ` _(required)_: When the transfer was created (ISO 8601). - `destinationAmount` `object`: Expected amount received after provider deductions, including pool fees. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountMin` `object`: Minimum destination amount encoded in the transfer action. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountRequired` `object`: Destination amount required for normalized 1:1 delivery, when enabled. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `destinationTransactionHashes` `string[]`: Verified destination transaction references. - `fees` `object[]` _(required)_: Explicit fees charged separately from the routed amount; deductions reflected in `destinationAmount` are omitted. - `amount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `side` `string` _(required)_: Which side of the transfer the fee is taken from. - `token` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `type` `string` _(required)_: What the fee pays for. - `id` `string` _(required)_: Route transfer id (`rtr_…`, lexically time-ordered). - `method` `string` _(required)_: How the caller funds the transfer. - `mode` `string` _(required)_: Which side of the transfer the quoted amount fixes. - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `quote` `object` _(required)_: Freshness and validity window of the selected quote. - `expiresAt` `string ` _(required)_: When the quoted terms stop being executable (ISO 8601). - `sampledAt` `string ` _(required)_: When the provider produced the quote (ISO 8601). - `recipient` `string` _(required)_: Final beneficiary of the transfer. - `refundAddress` `string`: Source-chain refund recipient (deposit-address method). - `refundAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `refundTransactionHashes` `string[]`: Verified refund transaction references. - `sender` `string`: Source-chain account that signs the route action (transaction method). - `sourceAmount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceAmountMax` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `sourceTransactionHashes` `string[]`: Verified source transaction references. - `subsidize` `boolean`: Whether Tempo guarantees normalized 1:1 destination delivery. - `status` `string` _(required)_: Lifecycle status of the route transfer. - `statusReason` `object`: Why the transfer is in its current status, when context is needed. - `code` `string` _(required)_: Stable machine-readable reason code. - `message` `string` _(required)_: Human-readable explanation of the reason. - `updatedAt` `string ` _(required)_: When the transfer last materially changed (ISO 8601). - `version` `integer` _(required)_: Monotonic revision that increments whenever the transfer materially changes. - `meta` `object`: Extra response metadata requested with `include`. - `totalCountCapped` `boolean` _(required)_: Whether `totalCount` hit the count cap. When true, `totalCount` is a lower bound rather than an exact total. - `totalCount` `integer` _(required)_: Number of rows matching the query, exact when `totalCountCapped` is false and a lower bound (at least this many, computed up to 10000) when `totalCountCapped` is true. Independent of pagination: use `nextCursor` to page, not this count. - `nextCursor` `string` _(required)_: Pass this back as the `cursor` query parameter to fetch the next page. `null` once you have reached the end of the list. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl 'https://api.tempo.xyz/v1/routes/transfers?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&status=awaiting-source' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/transfers?cursor=WzIzNDU2Nzg5LDBd&include=totalCount&limit=10&status=awaiting-source') ``` ## Create transfer `POST /v1/routes/transfers` Creates a durable transfer between supported chains, including optional normalized destination subsidies. ### Header parameters - `idempotency-key` `string` _(required)_: Opaque retry key scoped to the API key. Matching requests replay successful responses for 24 hours; changed input conflicts, in-flight requests block, and failed attempts release the key. ### Request body (required) (`application/json`) - `amount` `string` _(required)_: Amount in base units of the token the mode fixes. - `destinationChain` `string`: Destination chain CAIP-2 id, slug, or alias. Defaults to Tempo. - `destinationToken` `string` _(required)_: Destination token symbol, contract address, or token key. - `mode` `string` _(required)_: Which side of the transfer the quoted amount fixes. - `provider` `string`: Route provider ID. Omit to select the best available provider. - `recipient` `string` _(required)_: Destination-chain account that receives the transfer. - `sender` `string` _(required)_: Source-chain account that signs the route action. - `slippageBps` `integer`: Maximum acceptable slippage in basis points. - `sourceChain` `string` _(required)_: Source chain CAIP-2 id, slug, or alias. - `sourceToken` `string` _(required)_: Source token symbol, contract address, or token key. - `subsidize` `boolean`: Guarantees normalized 1:1 delivery for exact-source Tempo USD transfers to Base or Ethereum. A Tempo Admin must enable subsidies for the organization. ### Responses #### `200`: The created route transfer with its one-time executable action. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `action` `object` _(required)_: Executable source-chain action. Never returned by transfer reads. - `createdAt` `string ` _(required)_: When the transfer was created (ISO 8601). - `destinationAmount` `object`: Expected amount received after provider deductions, including pool fees. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountMin` `object`: Minimum destination amount encoded in the transfer action. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountRequired` `object`: Destination amount required for normalized 1:1 delivery, when enabled. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `destinationTransactionHashes` `string[]`: Verified destination transaction references. - `fees` `object[]` _(required)_: Explicit fees charged separately from the routed amount; deductions reflected in `destinationAmount` are omitted. - `amount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `side` `string` _(required)_: Which side of the transfer the fee is taken from. - `token` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `type` `string` _(required)_: What the fee pays for. - `id` `string` _(required)_: Route transfer id (`rtr_…`, lexically time-ordered). - `method` `string` _(required)_: How the caller funds the transfer. - `mode` `string` _(required)_: Which side of the transfer the quoted amount fixes. - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `quote` `object` _(required)_: Freshness and validity window of the selected quote. - `expiresAt` `string ` _(required)_: When the quoted terms stop being executable (ISO 8601). - `sampledAt` `string ` _(required)_: When the provider produced the quote (ISO 8601). - `recipient` `string` _(required)_: Final beneficiary of the transfer. - `refundAddress` `string`: Source-chain refund recipient (deposit-address method). - `refundAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `refundTransactionHashes` `string[]`: Verified refund transaction references. - `sender` `string`: Source-chain account that signs the route action (transaction method). - `sourceAmount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceAmountMax` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `sourceTransactionHashes` `string[]`: Verified source transaction references. - `subsidize` `boolean`: Whether Tempo guarantees normalized 1:1 destination delivery. - `status` `string` _(required)_: Lifecycle status of the route transfer. - `statusReason` `object`: Why the transfer is in its current status, when context is needed. - `code` `string` _(required)_: Stable machine-readable reason code. - `message` `string` _(required)_: Human-readable explanation of the reason. - `updatedAt` `string ` _(required)_: When the transfer last materially changed (ISO 8601). - `version` `integer` _(required)_: Monotonic revision that increments whenever the transfer materially changes. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: The requested normalized delivery subsidy is unavailable. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No provider can prepare the requested route. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `409`: The Idempotency-Key conflicts with an existing or in-flight request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: Destination subsidy settlement is not configured. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: No provider succeeded and at least one failed unexpectedly. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `503`: Destination subsidy capacity is temporarily unavailable. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/transfers \ --request POST \ --header 'idempotency-key: routes_01k1c5j8q8p0be6j5v9m6d1e4r' \ --header 'Content-Type: application/json' \ --data '{ "amount": "1000000", "destinationToken": "usdc", "mode": "exactSource", "recipient": "0x1111111111111111111111111111111111111111", "sender": "0x2222222222222222222222222222222222222222", "sourceChain": "tempo", "sourceToken": "usdce" }' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/transfers', { method: 'POST', headers: { 'idempotency-key': 'routes_01k1c5j8q8p0be6j5v9m6d1e4r', 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: '1000000', destinationToken: 'usdc', mode: 'exactSource', recipient: '0x1111111111111111111111111111111111111111', sender: '0x2222222222222222222222222222222222222222', sourceChain: 'tempo', sourceToken: 'usdce' }) }) ``` ## Create transfer into vault `POST /v1/routes/transfers/vault` Creates a transfer into a Tempo Earn vault. ### Header parameters - `idempotency-key` `string` _(required)_: Opaque retry key scoped to the API key. Matching requests replay successful responses for 24 hours; changed input conflicts, in-flight requests block, and failed attempts release the key. ### Request body (required) (`application/json`) - `amount` `string` _(required)_: Amount in base units of the token the mode fixes. - `mode` `string` _(required)_: Which side of the transfer the quoted amount fixes. - `recipient` `string` _(required)_: Destination-chain account that receives the transfer. - `sender` `string` _(required)_: Source-chain account that signs the route action. - `slippageBps` `integer`: Maximum acceptable slippage in basis points. - `sourceChain` `string` _(required)_: Source chain CAIP-2 id, slug, or alias. - `sourceToken` `string` _(required)_: Source token symbol, contract address, or token key. - `vaultAddress` `string` _(required)_: Tempo Earn vault contract address. ### Responses #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: This route transfer destination is not implemented. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/transfers/vault \ --request POST \ --header 'idempotency-key: routes_01k1c5j8q8p0be6j5v9m6d1e4r' \ --header 'Content-Type: application/json' \ --data '{ "amount": "1000000", "mode": "exactSource", "recipient": "0x1111111111111111111111111111111111111111", "sender": "0x2222222222222222222222222222222222222222", "sourceChain": "tempo", "sourceToken": "usdce", "vaultAddress": "0xf4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4" }' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/transfers/vault', { method: 'POST', headers: { 'idempotency-key': 'routes_01k1c5j8q8p0be6j5v9m6d1e4r', 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: '1000000', mode: 'exactSource', recipient: '0x1111111111111111111111111111111111111111', sender: '0x2222222222222222222222222222222222222222', sourceChain: 'tempo', sourceToken: 'usdce', vaultAddress: '0xf4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4' }) }) ``` ## Create transfer into zone `POST /v1/routes/transfers/zone` Creates a transfer into a Tempo Zone. ### Header parameters - `idempotency-key` `string` _(required)_: Opaque retry key scoped to the API key. Matching requests replay successful responses for 24 hours; changed input conflicts, in-flight requests block, and failed attempts release the key. ### Request body (required) (`application/json`) - `amount` `string` _(required)_: Amount in base units of the token the mode fixes. - `destinationChain` `string` _(required)_: Configured Zone CAIP-2 id. - `destinationToken` `string` _(required)_: Destination token symbol, contract address, or token key. - `mode` `string` _(required)_: Which side of the transfer the quoted amount fixes. - `recipient` `string` _(required)_: Destination-chain account that receives the transfer. - `recipientFallback` `string` _(required)_: Public Tempo recipient used if the Zone credit cannot complete. - `sender` `string` _(required)_: Source-chain account that signs the route action. - `slippageBps` `integer`: Maximum acceptable slippage in basis points. - `sourceChain` `string` _(required)_: Source chain CAIP-2 id, slug, or alias. - `sourceToken` `string` _(required)_: Source token symbol, contract address, or token key. ### Responses #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `501`: This route transfer destination is not implemented. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/transfers/zone \ --request POST \ --header 'idempotency-key: routes_01k1c5j8q8p0be6j5v9m6d1e4r' \ --header 'Content-Type: application/json' \ --data '{ "amount": "1000000", "destinationChain": "eip155:1424310003", "destinationToken": "usdc", "mode": "exactSource", "recipient": "0x1111111111111111111111111111111111111111", "recipientFallback": "0x2222222222222222222222222222222222222222", "sender": "0x2222222222222222222222222222222222222222", "sourceChain": "tempo", "sourceToken": "usdce" }' ``` ```ts fetch('https://api.tempo.xyz/v1/routes/transfers/zone', { method: 'POST', headers: { 'idempotency-key': 'routes_01k1c5j8q8p0be6j5v9m6d1e4r', 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: '1000000', destinationChain: 'eip155:1424310003', destinationToken: 'usdc', mode: 'exactSource', recipient: '0x1111111111111111111111111111111111111111', recipientFallback: '0x2222222222222222222222222222222222222222', sender: '0x2222222222222222222222222222222222222222', sourceChain: 'tempo', sourceToken: 'usdce' }) }) ``` ## Get transfer `GET /v1/routes/transfers/{id}` Returns a transfer by ID. ### Path parameters - `id` `string` _(required)_: Route transfer ID (`rtr_…`). ### Responses #### `200`: The requested route transfer. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. Body (`application/json`): - `createdAt` `string ` _(required)_: When the transfer was created (ISO 8601). - `destinationAmount` `object`: Expected amount received after provider deductions, including pool fees. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountMin` `object`: Minimum destination amount encoded in the transfer action. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationAmountRequired` `object`: Destination amount required for normalized 1:1 delivery, when enabled. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `destinationChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `destinationToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `destinationTransactionHashes` `string[]`: Verified destination transaction references. - `fees` `object[]` _(required)_: Explicit fees charged separately from the routed amount; deductions reflected in `destinationAmount` are omitted. - `amount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `side` `string` _(required)_: Which side of the transfer the fee is taken from. - `token` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `type` `string` _(required)_: What the fee pays for. - `id` `string` _(required)_: Route transfer id (`rtr_…`, lexically time-ordered). - `method` `string` _(required)_: How the caller funds the transfer. - `mode` `string` _(required)_: Which side of the transfer the quoted amount fixes. - `provider` `object` _(required)_: Provider metadata embedded in a route quote. - `id` `string` _(required)_: Stable provider id. - `name` `string` _(required)_: Human-readable provider name. - `quote` `object` _(required)_: Freshness and validity window of the selected quote. - `expiresAt` `string ` _(required)_: When the quoted terms stop being executable (ISO 8601). - `sampledAt` `string ` _(required)_: When the provider produced the quote (ISO 8601). - `recipient` `string` _(required)_: Final beneficiary of the transfer. - `refundAddress` `string`: Source-chain refund recipient (deposit-address method). - `refundAmount` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `refundTransactionHashes` `string[]`: Verified refund transaction references. - `sender` `string`: Source-chain account that signs the route action (transaction method). - `sourceAmount` `object` _(required)_: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceAmountMax` `object`: A token quantity with its denomination and decimal representation. - `baseUnits` `string` _(required)_: The quantity in the token's smallest unit. - `currency` `string` _(required)_: The monetary denomination of this quantity. - `decimals` `integer` _(required)_: Decimal places used to convert `baseUnits` into `formatted`. - `formatted` `string` _(required)_: The quantity rendered in whole token units. - `sourceChain` `object` _(required)_: A normalized chain reference for route quotes. - `addressFormat` `string` _(required)_: Address encoding used by accounts and token identifiers on this chain. - `id` `string` _(required)_: CAIP-2 chain id. - `kind` `string` _(required)_: Chain execution family used for provider routing. - `name` `string` _(required)_: Human-readable chain name. - `sourceToken` `object` _(required)_: A normalized token reference for route quotes. - `address` `string` _(required)_: Contract address, mint, or issuer address on the token chain. - `currency` `string` _(required)_: Monetary denomination represented by this token. - `decimals` `integer` _(required)_: Number of decimal places this token uses. - `name` `string` _(required)_: Human-readable token name. - `standard` `string` _(required)_: Token standard on the token chain. - `symbol` `string` _(required)_: Short token ticker symbol. - `tokenKey` `string` _(required)_: Stable Tempo token key scoped to the token chain. - `verified` `boolean` _(required)_: Whether Tempo recognizes this token in its route quote inventory. - `sourceTransactionHashes` `string[]`: Verified source transaction references. - `subsidize` `boolean`: Whether Tempo guarantees normalized 1:1 destination delivery. - `status` `string` _(required)_: Lifecycle status of the route transfer. - `statusReason` `object`: Why the transfer is in its current status, when context is needed. - `code` `string` _(required)_: Stable machine-readable reason code. - `message` `string` _(required)_: Human-readable explanation of the reason. - `updatedAt` `string ` _(required)_: When the transfer last materially changed (ISO 8601). - `version` `integer` _(required)_: Monotonic revision that increments whenever the transfer materially changes. #### `400`: Invalid request. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `401`: Missing or invalid API key. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `403`: Forbidden. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `404`: No visible route transfer exists for the identifier. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `429`: Rate limit exceeded. On endpoints that accept MPP, over-quota unpaid requests return `402 Payment Required` instead. Headers: - `RateLimit-Limit` `integer`: How many requests you may make in the current time window. Not sent on cached responses or `402` payment challenges. - `RateLimit-Remaining` `integer`: How many requests you have left in the current window before you are rate-limited. Not sent on cached responses or `402` payment challenges. - `RateLimit-Reset` `integer`: When the current window resets, as a Unix timestamp in seconds. Not sent on cached responses or `402` payment challenges. - `RateLimit-Scope` `string`: Which quota this request counted against (e.g. `data:read`). Not sent on cached responses or `402` payment challenges. - `tempo-request-id` `string`: A unique id for this request, returned on every response (and as `requestId` in error bodies). Include it when contacting support so we can find your request. - `Retry-After` `integer`: How many seconds to wait before trying again. Sent with `429` (rate-limited) responses. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `500`: Internal server error. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `502`: Upstream data failure. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. #### `504`: Request timed out. The request did not complete within the 60-second deadline; retry it. Body (`application/json`): - `error` `object` _(required)_: What went wrong. - `code` `string` _(required)_: A short, stable code you can branch on in your code (e.g. `token_not_found`). - `details` `object[]`: A list of specific problems, when the error is about your request (e.g. invalid fields). - `message` `string` _(required)_: A specific thing that went wrong, in plain language (e.g. why a field failed validation). - `path` `string | number[]`: Where the problem is, as a path into your request (e.g. `["query", "limit"]`). Present for validation errors. - `message` `string` _(required)_: A human-readable explanation of what went wrong. - `requestId` `string` _(required)_: The id of this request — include it when contacting support. ### Example request ```bash curl https://api.tempo.xyz/v1/routes/transfers/rtr_001785729600000_2ZPE2gvateYEQ0dQslgvkhjx ``` ```ts fetch('https://api.tempo.xyz/v1/routes/transfers/rtr_001785729600000_2ZPE2gvateYEQ0dQslgvkhjx') ``` # TIP-0000: TIP Process ## Abstract This TIP defines the lifecycle for a Tempo Improvement Proposal, from draft through mainnet activation. It sets clear decision gates, required reviews, and ownership expectations so proposals are evaluated consistently before they are scheduled and rolled out. **External TIP submissions are not accepted at this time.** ## Motivation This process gives the team one shared path from idea to production. A consistent lifecycle improves decision quality, keeps security and ecosystem impact visible, and helps prioritize changes that solve real user problems. *** ## Specification ### Status Lifecycle `Draft` → `In Review` / `Rejected` `In Review` → `Ready for Consideration` / `Rejected` `Ready for Consideration` → `Approved` / `Backlog` / `Rejected` `Backlog` → `Ready for Consideration` / `Rejected` `Approved` → `Scheduled` → `Testnet` → `Mainnet` ### Status Definitions * `Draft`: The idea is being developed into a complete TIP and is not yet in formal review. * `In Review`: The TIP is under structured technical, security, and implications review. * `Ready for Consideration`: Required review is complete and the TIP is ready for a network upgrade call decision. * `Backlog`: The TIP is directionally supported, but deferred until there is clear product or customer pull. * `Approved`: The TIP is accepted and eligible for upgrade scheduling. * `Scheduled`: The TIP is assigned to a specific upgrade. * `Testnet`: The TIP is released on testnet and monitored against success criteria. * `Mainnet`: The TIP is released on mainnet. * `Rejected`: The TIP does not move forward in its current form. ### 1. Propose a TIP Goal: Turn an idea into a complete, reviewable specification. 1. Share the problem and proposed direction early to gather feedback. 2. Assign one TIP owner who is accountable for moving the TIP forward. 3. Pick the lowest available TIP number and create branch `tip/xxxx`. 4. Create `tip-xxxx.md` using the [TIP Template](https://github.com/tempoxyz/tempo/blob/main/tips/tip_template.md) and open a draft PR. 5. Complete the draft with: problem statement, design, assumptions, alternatives considered, threat model, expected tooling/user impact, and success criteria. 6. When ready, set status to `In Review` and request stakeholder review. ### 2. Review the TIP Goal: Validate the proposal's value, feasibility, and risk. 1. Run stakeholder review in the PR and keep the TIP updated. 2. Provide evidence that the problem is real and worth solving now. 3. Run a whiteboard session if needed to align context. 4. Complete engineering review and confirm the design is feasible and robust. 5. Complete a security review. 6. Complete an implications review for tooling, integrations, and partners, and share outcomes with affected stakeholders. 7. Obtain engineering, research, and security approval. 8. Before merging, set status to `Ready for Consideration`, then merge. ### 3. Consideration and Decision (Network Upgrade Call) Goal: Move each `Ready for Consideration` TIP to a clear decision. 1. The TIP owner flags with the network upgrade call chair that a decision is needed ahead of the call. 2. The TIP owner presents review outcomes, key tradeoffs, and major spec changes. 3. The call records one outcome: `Approved`, `Backlog`, or `Rejected`. 4. If `Approved`, confirm a target upgrade when possible. 5. If `Backlog`, record what signal is needed to revisit it. ### 4. Scheduling Criteria A TIP can move from `Approved` to `Scheduled` only if: 1. Engineering capacity is available. 2. A target upgrade is identified. 3. Inclusion timing is explicit (`Why include? Why now?`). 4. The TIP spec is complete and implementation-ready. ### 5. Ship a TIP Goal: Implement the approved TIP and prepare it for rollout. 1. Implement the TIP and keep the spec aligned with what is built. 2. Implementation can still surface learnings; updates and scoped changes are welcome. 3. If implementation materially changes the TIP, request another approval from the same engineering, research, and security stakeholders. This second approval should be fast and focused on the implementation delta. ### 6. Testnet Release Readiness Before moving an upgrade to `Testnet`: 1. Publish technical communication, including tooling and user impact. 2. Ensure dashboards and alerting are in place for success criteria. # TIP-1000: State Creation Cost Increase * **Protocol Version**: T1 ## Abstract This TIP increases the gas cost for creating new state elements, accounts, and contract code to provide economic protection against state growth spam attacks. The proposal increases the cost of writing a new state element from 20,000 gas to 250,000 gas, introduces a 250,000 gas charge for account creation (when the account's nonce is first written), and implements a new contract creation cost model: 1,000 gas per byte of contract code plus a fixed upfront contract creation cost of 500,000 gas. ## Motivation Tempo's high throughput capability (approximately 20,000 transactions per second) creates a vulnerability where an adversary could create a massive amount of state with the intent of permanently slowing the chain down. If each transaction is used to create a new account, and each account requires approximately 200 bytes of storage, then over 120 TB of storage could be created in a single year. Even if this storage is technically feasible, the database performance implications are unknown and would likely require significant R\&D on state management much earlier than needed for business requirements. The current EVM gas schedule charges 20,000 gas for writing a new state element and has no cost for creating an account. This makes state creation attacks economically viable for adversaries. By increasing these costs to 250,000 gas each, we create a meaningful economic barrier: creating 1 TB of state would cost approximately $50 million, and creating 10 TB would cost approximately $500 million (based on the assumption that a TIP-20 transfer costs 50,000 gas = 0.1 cent, implying 1 cent per 500,000 gas). ### Alternatives Considered 1. **Storage rent**: Implementing a periodic fee for holding state. This was rejected due to complexity and poor user experience. 2. **State expiry**: Automatically removing unused state after a time period. This was rejected due to technical complexity and breaking changes to existing applications. 3. **Lower cost increases**: Using smaller multipliers (e.g., 50,000 gas instead of 250,000 gas). This was rejected as it would not provide sufficient economic deterrent against well-funded attackers. ## Terminology This TIP uses the following economic unit terminology: * **Microdollars**: TIP-20 token units at 10^-6 USD precision (6 decimals). One TIP-20 token unit = 1 microdollar = 0.000001 USD = 0.0001 cents. * **Attodollars**: Gas accounting units at 10^-18 USD precision. Gas prices (basefee) are denominated in attodollars. * **Conversion**: Gas cost in microdollars = (gas × basefee in attodollars) / 10^12 These units provide precise economic accounting while maintaining human-readable dollar relationships. *** ## Specification ### Gas Cost Changes #### New State Element Creation **Current Behavior:** * Writing a new state element (SSTORE to a zero slot) costs 20,000 gas **Proposed Behavior:** * Writing a new state element (SSTORE to a zero slot) costs 250,000 gas for the state creation component (replacing 20,000 gas) * The EIP-2929 access cost is charged separately: 2,100 gas for cold access, 100 gas for warm access * Total cost for a cold zero-to-nonzero SSTORE: 2,100 + 250,000 = 252,100 gas * Total cost for a warm zero-to-nonzero SSTORE: 100 + 250,000 = 250,100 gas This applies to all storage slot writes that transition from zero to non-zero, including: * Contract storage slots * TIP-20 token balances * Nonce key storage in the Nonce precompile (when a new nonce key is first used) * Rewards-related storage (userRewardInfo mappings, reward balances) * Active key count tracking in the Nonce precompile * Any other state elements stored in the EVM state trie **Note:** Since Tempo-specific operations (nonce keys, rewards processing, etc.) ultimately use EVM storage operations (SSTORE), they are automatically subject to the new state creation pricing. The implementation must ensure all new state element creation is correctly charged at 250,000 gas, regardless of which precompile or contract creates the state. #### Account Creation **Current Behavior:** * Account creation has no explicit gas cost * The account is created implicitly when its nonce is first written **Proposed Behavior:** * Account creation incurs a 250,000 gas charge when the account's nonce is first written * This charge applies when the account is first used (e.g., sends its first transaction), not when it first receives tokens **Implementation Details:** * The charge is applied when `account.nonce` transitions from 0 to 1 * The charge also applies to other nonces with [nonce keys](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#specification) (2D nonces) * Transactions with a nonce value of 0 need to supply at least 271,000 gas and are otherwise invalid * For EOA accounts: charged on the first transaction sent from that address (when the account is first used) * For contract accounts: included in the fixed 500,000 gas CREATE cost (see Contract Creation); there is no separate account creation charge * **Important:** When tokens are transferred TO a new address, the recipient's nonce remains 0, so no account creation cost is charged. The account creation cost only applies when the account is first used (sends a transaction). * The charge is in addition to any other gas costs for the transaction #### Contract Creation **Current Behavior:** * Contract creation (CREATE/CREATE2) has a base cost of 32,000 gas plus 200 gas per byte of contract code * Total cost formula: `32,000 + (code_size × 200)` gas * Example: A 1,000 byte contract costs 32,000 + (1,000 × 200) = 232,000 gas **Proposed Behavior:** * Contract creation replaces the existing EVM per-byte cost with a new pricing model: * Each byte: 1,000 gas per byte (linear pricing) * Fixed upfront contract creation cost: 500,000 gas * This pricing applies to the contract code size (the bytecode being deployed) **Implementation Details:** * The code storage cost is calculated as: `code_size × 1,000` * Fixed upfront contract creation cost: 500,000 gas * Total contract creation cost: `(code_size × 1,000) + 500,000` gas * This replaces the existing EVM per-byte cost for contract creation (not an additional charge) * Applies to both CREATE and CREATE2 operations * The fixed 500,000 gas covers the contract account creation; there is no separate account creation charge for the contract #### Intrinsic transaction gas A transaction is invalid if the minimal costs of a (reverting) transaction can't be covered by caller's balance. Those checks are done in the transaction pool as a DOS prevention measure as well as when a transaction is first executed as part of a block. * Transaction with `nonce == 0` require an additional 250,000 gas * Tempo transactions with any `nonce_key` and `nonce == 0` require an additional 250,000 gas * Changes to EIP-7702 authorization lists: * The base cost per authorization is reduced to 12,500 gas * EIP-7702 authorisation list entries with `auth_list.nonce == 0` require an additional 250,000 gas (account creation for the nonce field) * EIP-7702 authorisation list entries going from no delegation to delegation require an additional 250,000 gas (state creation for the keccak/code hash field) * There is no refund if the account already exists * The additional initial cost for CREATE transactions that deploy a contract is increased to 500,000 from currently 32,000 (to reflect the upfront cost in contract creation) * If the first transaction in a batch is a CREATE transaction, the additional cost of 500,000 needs to be charged #### Other changes The transaction gas cap is changed from 16M to 30M to accommodate the deployment of 24kb contracts. Tempo transaction key authorisations can't determine whether it is going to create new storage or not. If the transaction cannot pay for the key authorization storage costs, the transaction reverts any authorization key that has been set. ### Gas Schedule Summary | Operation | Current Gas Cost | Proposed Gas Cost | Change | |-----------|------------------|-------------------|--------| | New state element (SSTORE zero → non-zero, state creation component) | 20,000 | 250,000 | +230,000 | | Account creation (first nonce write) | 0 | 250,000 | +250,000 | | Contract creation (per byte) | 200 | 1,000 | +800 | | Contract creation (fixed upfront cost) | Included in base | 500,000 | +500,000 | | Existing state element (SSTORE non-zero → non-zero) | 5,000 | 5,000 | No change | | Existing state element (SSTORE non-zero → zero) | -15,000 (refund) | -15,000 (refund) | No change | ### Economic Impact Analysis #### Cost Calculations Based on the assumptions: * TIP-20 transfer cost (to existing address, including base transaction and state update): 50,000 gas = 0.1 cent (1,000 microdollars) * Implied gas price: 1 cent per 500,000 gas (10,000 microdollars per 500,000 gas) **New State Element Creation:** * Gas cost: 250,000 gas * Dollar cost: 250,000 / 500,000 = **0.5 cents (5,000 microdollars) per state element** **Account Creation:** * Gas cost: 250,000 gas * Dollar cost: 250,000 / 500,000 = **0.5 cents (5,000 microdollars) per account** **Contract Creation:** * Per byte: 1,000 gas = **0.002 cents (20 microdollars) per byte** * Fixed upfront cost: 500,000 gas = **1.0 cent (10,000 microdollars)** * Example: 1,000 byte contract = (1,000 × 1,000) + 500,000 = 1,500,000 gas = **3.0 cents (30,000 microdollars)** #### Attack Cost Analysis **Creating 1 TB of state:** * 1 TB = 1,000,000,000,000 bytes * Assuming ~100 bytes per state element: 10,000,000,000 state elements * Cost: 10,000,000,000 × 0.5 cents = **$50,000,000** **Creating 10 TB of state:** * 10 TB = 10,000,000,000,000 bytes * Assuming ~100 bytes per state element: 100,000,000,000 state elements * Cost: 100,000,000,000 × 0.5 cents = **$500,000,000** These costs serve as a significant economic deterrent against state growth spam attacks. ### Impact on Normal Operations #### Transfer to New Address **Current Cost:** * TIP-20 transfer (base + operation): 50,000 gas * New state element (balance): 20,000 gas * **Total: ~70,000 gas ≈ 0.14 cents** * Note: Account creation cost does not apply here because the recipient's nonce remains 0 **Proposed Cost:** * TIP-20 transfer (base + operation): 50,000 gas * New state element (balance): 250,000 gas * **Total: ~300,000 gas ≈ 0.6 cents** * Note: Account creation cost does not apply here because the recipient's nonce remains 0 **Impact:** A transfer to a new address increases from 0.14 cents to 0.6 cents, representing a 4.3x increase. The account creation cost (0.5 cents) will be charged separately when the recipient first uses their account. #### First Use of New Account **Current Cost:** * TIP-20 transfer (base + operation + state update): 50,000 gas * Account creation: 0 gas * **Total: 50,000 gas ≈ 0.1 cents** **Proposed Cost:** * TIP-20 transfer (base + operation + state update): 50,000 gas * Account creation (nonce 0 → 1): 250,000 gas * **Total: ~300,000 gas ≈ 0.6 cents** **Impact:** The first transaction from a new account increases from 0.1 cents to 0.6 cents, representing a 6x increase. Combined with the initial transfer cost (0.6 cents), the total onboarding cost for a new user is approximately 1.2 cents. #### Transfer to Existing Address **Current Cost:** * TIP-20 transfer (base + operation + state update): 50,000 gas * **Total: 50,000 gas ≈ 0.1 cents** **Proposed Cost:** * TIP-20 transfer (base + operation + state update): 50,000 gas * **Total: 50,000 gas ≈ 0.1 cents** **Impact:** No change for transfers to existing addresses. #### Contract Deployment **Current Cost:** * Contract code storage: 32,000 gas base + 200 gas per byte * Example for 1,000 byte contract: 32,000 + (1,000 × 200) = 232,000 gas ≈ 0.46 cents **Proposed Cost:** * Contract code storage: code\_size × 1,000 gas * Fixed upfront contract creation cost: 500,000 gas * Example for 1,000 byte contract: (1,000 × 1,000) + 500,000 = 1,500,000 gas ≈ **3.0 cents** **Impact:** Contract deployment costs increase significantly, especially for larger contracts. A 100 byte contract costs (100 × 1,000) + 500,000 = 600,000 gas = 1.2 cents. ### Implementation Requirements #### Node Implementation The node implementation must: 1. **Detect new state element creation:** * Track SSTORE operations that write to a zero slot * Charge 250,000 gas instead of 20,000 gas for these operations 2. **Detect account creation:** * Track when an EOA account's nonce transitions from 0 to 1 * Charge 250,000 gas for this transition * For contract accounts, the fixed 500,000 gas CREATE cost applies instead 3. **Implement contract creation pricing:** * Replace existing EVM per-byte cost for contract code storage * Charge 1,000 gas per byte of contract code (linear pricing) * Charge a fixed upfront contract creation cost of 500,000 gas * Total formula: `(code_size × 1,000) + 500,000` * Apply to both CREATE and CREATE2 operations 4. **Maintain backward compatibility:** * Existing state operations (non-zero to non-zero, non-zero to zero) remain unchanged * Gas refunds for storage clearing remain unchanged #### Test Suite Requirements The test suite must verify: 1. **New state element creation:** * SSTORE to zero slot charges 250,000 gas * Multiple new state elements in one transaction are each charged 250,000 gas * Existing state element updates (non-zero to non-zero) remain at 5,000 gas 2. **Account creation:** * First transaction from EOA charges 250,000 gas for account creation (when nonce transitions 0 → 1) * Contract deployment does NOT charge a separate 250,000 gas for the contract's account creation (the nonce write is included in the 500,000 CREATE cost) * Transfer TO a new address does NOT charge account creation fee (recipient's nonce remains 0) * Subsequent transactions from the same account do not charge account creation fee 3. **Contract creation:** * Contract code storage replaces EVM per-byte cost with new pricing model * Each byte of contract code costs 1,000 gas (linear pricing) * Fixed upfront contract creation cost: 500,000 gas * Total cost formula: `(code_size × 1,000) + 500,000` gas * Example: 100 byte contract costs (100 × 1,000) + 500,000 = 600,000 gas * Both CREATE and CREATE2 use the same pricing 4. **Tempo-specific state creation operations:** * Nonce key creation: First use of a new nonce key (nonce key > 0) creates storage in Nonce precompile * Active key count tracking: First nonce key for an account creates active key count storage * Rewards opt-in: `setRewardRecipient` creates new `userRewardInfo` mapping entry * Rewards recipient delegation: Setting reward recipient for a new recipient creates storage * Rewards balance creation: First reward accrual to a recipient creates storage if needed * All Tempo-specific operations that create new state elements must charge 250,000 gas per new storage slot 5. **Edge cases:** * Self-destruct and recreation of account * Contracts that create accounts via CREATE/CREATE2 * Batch operations creating multiple accounts/state elements * Contract deployment with various code sizes (small, medium, large) * Multiple Tempo-specific operations in a single transaction 6. **Economic calculations:** * Verify gas costs match expected dollar amounts * Verify attack cost calculations for large-scale state creation * Verify contract creation costs match formula: `(code_size × 1,000) + 500,000` (nonce write included in CREATE cost) * Verify Tempo-specific operations charge correctly for new state creation *** ## Invariants The following invariants must always hold: 1. **State Creation Cost Invariant:** Any SSTORE operation that writes a non-zero value to a zero slot MUST charge 250,000 gas for the state creation component (not 20,000 gas). The total gas charged also includes the EIP-2929 access cost: 2,100 gas for cold access or 100 gas for warm access, resulting in a total of 252,100 gas (cold) or 250,100 gas (warm). 2. **Account Creation Cost Invariant:** The first transaction sent from an EOA (causing the sender's nonce to transition from 0 to 1) MUST charge exactly 250,000 gas for account creation. For contract accounts, the fixed 500,000 gas CREATE cost applies instead. 3. **Existing State Invariant:** SSTORE operations that modify existing non-zero state (non-zero to non-zero) MUST continue to charge 5,000 gas and MUST NOT be affected by this change. 4. **Storage Clearing Invariant:** SSTORE operations that clear storage (non-zero to zero) MUST continue to provide a 15,000 gas refund and MUST NOT be affected by this change. 5. **Gas Accounting Invariant:** The total gas charged for a transaction creating N new state elements and M new accounts (where M is the number of accounts whose nonce transitions from 0 to 1 in this transaction) MUST equal: base\_transaction\_gas + operation\_gas + (N × 250,000) + (M × 250,000). Note: Transferring tokens TO a new address does not create the account (nonce remains 0), so M = 0 in that case. 6. **Contract Creation Cost Invariant:** Contract creation (CREATE/CREATE2) MUST charge exactly `(code_size × 1,000) + 500,000` gas for code storage, replacing the existing EVM per-byte cost. This includes: 1,000 gas per byte of contract code (linear pricing) and a fixed upfront contract creation cost of 500,000 gas. There is no separate account creation charge for the contract. 7. **Economic Deterrent Invariant:** The cost to create 1 TB of state MUST be at least $50 million, and the cost to create 10 TB of state MUST be at least $500 million, based on the assumed gas price of 1 cent per 500,000 gas. ### Critical Test Cases The test suite must cover: 1. **Basic state creation:** Single SSTORE to zero slot charges 250,000 gas 2. **Multiple state creation:** Multiple SSTORE operations to zero slots each charge 250,000 gas 3. **Account creation (EOA):** First transaction from new EOA charges 250,000 gas 4. **Contract creation (CREATE):** Contract deployment via CREATE charges a fixed upfront cost of 500,000 gas (no separate account creation charge) 5. **Contract creation (CREATE2):** Contract deployment via CREATE2 charges a fixed upfront cost of 500,000 gas (no separate account creation charge) 6. **Contract creation (small):** Contract with 100 bytes charges (100 × 1,000) + 500,000 = 600,000 gas for code storage 7. **Contract creation (medium):** Contract with 1,000 bytes charges (1,000 × 1,000) + 500,000 = 1,500,000 gas for code storage 8. **Contract creation (large):** Contract with 10,000 bytes charges (10,000 × 1,000) + 500,000 = 10,500,000 gas for code storage 9. **Existing state updates:** SSTORE to existing non-zero slot charges 5,000 gas (unchanged) 10. **Storage clearing:** SSTORE clearing storage provides 15,000 gas refund (unchanged) 11. **Mixed operations:** Transaction creating both new accounts and new state elements charges correctly for both 12. **Transfer to new address:** Complete transaction cost matches expected ~300,000 gas (no account creation cost, only new state element cost) 13. **First use of new account:** Complete transaction cost matches expected ~300,000 gas (account creation cost applies) 14. **Transfer to existing address:** Complete transaction cost matches expected 50,000 gas (unchanged) 15. **Batch operations:** Multiple account creations in one transaction each charge 250,000 gas 16. **Self-destruct and recreate:** Account that self-destructs and is recreated charges account creation fee again 17. **Transfer to new address does not create account:** Transferring tokens to a new address does not charge account creation fee (only new state element fee applies) 18. **Nonce key creation:** First use of a new nonce key creates a new storage slot and charges 250,000 gas 19. **Active key count tracking:** First nonce key for an account creates storage for active key count and charges 250,000 gas 20. **Rewards opt-in:** First call to `setRewardRecipient` creates a new entry and charges 250,000 gas 21. **Rewards recipient delegation:** Setting a new reward recipient creates storage and charges 250,000 gas 22. **Rewards balance creation:** First reward accrual creates storage and charges 250,000 gas (if needed) 23. **Multiple nonce keys:** Creating multiple nonce keys in one transaction each charges 250,000 gas 24. **Nonce key and rewards combined:** Transaction creating both nonce key and rewards storage charges 250,000 gas for each new state element # TIP-1001: Place-only mode for next quote token ## Abstract This TIP adds a `createNextPair` function to the Stablecoin DEX that creates a trading pair between a base token and its `nextQuoteToken()`, along with `place` and `placeFlip` overloads that accept a book key to target specific pairs. This enables market makers to place orders on the new pair before a quote token update is finalized, providing a smooth liquidity transition. ## Motivation When a token issuer decides to change their quote token (via `setNextQuoteToken` and `completeQuoteTokenUpdate`), there is currently no way to establish liquidity on the new pair before the transition completes. This means that market makers will need to wait until the quote token has been updated before they can place orders, which could cause a period where there is no liquidity, or limited liquidity, for the token, which will interrupt swaps involving that token. By allowing pair creation against `nextQuoteToken()`, this change allows users and market makers to add liquidity to the DEX before it is used on swaps. Since swaps route through `quoteToken()` (not `nextQuoteToken()`), the new pair operates in "place-only" mode: orders can be placed and cancelled, but no swaps route through it until `completeQuoteTokenUpdate()` is called. *** ## Specification ### New functions Add the following functions to the Stablecoin DEX interface: ```solidity /// @notice Creates a trading pair between a base token and its next quote token /// @param base The base token address /// @return key The pair key for the created pair /// @dev Reverts if: /// - The base token has no next quote token staged (nextQuoteToken is zero) /// - The pair already exists /// - Either token is not USD-denominated function createNextPair(address base) external returns (bytes32 key); /// @notice Places an order on a specific pair identified by book key /// @param bookKey The pair key identifying the orderbook /// @param token The base token of the pair /// @param amount The order amount in base tokens /// @param isBid True for buy orders, false for sell orders /// @param tick The price tick for the order /// @return orderId The ID of the placed order function place(bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick) external returns (uint128 orderId); /// @notice Places a flip order on a specific pair identified by book key /// @param bookKey The pair key identifying the orderbook /// @param token The base token of the pair /// @param amount The order amount in base tokens /// @param isBid True for buy orders, false for sell orders /// @param tick The price tick for the order /// @param flipTick The price tick for the flipped order when filled /// @param internalBalanceOnly If true, only use internal balance for the flipped order /// @return orderId The ID of the placed order function placeFlip(bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick, int16 flipTick, bool internalBalanceOnly) external returns (uint128 orderId); ``` ### Behavior #### Pair creation `createNextPair(base)` creates a pair between `base` and `base.nextQuoteToken()`. The function: 1. Calls `nextQuoteToken()` on the base token 2. Reverts with `NO_NEXT_QUOTE_TOKEN` if the result is `address(0)` 3. Validates both tokens are USD-denominated (same as `createPair`) 4. Creates the pair using the same mechanism as `createPair` 5. Emits `PairCreated(key, base, nextQuoteToken)` #### Place-only mode Once the pair exists, it supports the full order lifecycle: * `place(bookKey, ...)` and `placeFlip(bookKey, ...)` allow placing orders on the pair * `cancel` and `cancelStaleOrder` work normally (they use order ID, not pair lookup) * `books` returns accurate data (it takes the book key directly) The new `place` and `placeFlip` overloads are required because the existing functions derive the pair from `token.quoteToken()`, which would look up the wrong pair. The overloads accept a `bookKey` parameter to target the correct pair. Swap functions (`swapExactAmountIn`, `swapExactAmountOut`) and quote functions (`quoteSwapExactAmountIn`, `quoteSwapExactAmountOut`) do not route through this pair because routing uses `quoteToken()` to find paths between tokens. #### After quote token update When the token issuer calls `completeQuoteTokenUpdate()`: 1. The token's `quoteToken()` changes to what was `nextQuoteToken()` 2. The token's `nextQuoteToken()` becomes `address(0)` 3. The existing pair (created via `createNextPair`) is now the active pair 4. Swaps begin routing through the pair The old pair (against the previous quote token) remains but will no longer be used for routing swaps involving this base token. Orders on it can be canceled using their ID. ### New error ```solidity /// @notice The base token has no next quote token staged error NO_NEXT_QUOTE_TOKEN(); ``` ### Events No new events. The existing `PairCreated` event is emitted by `createNextPair`, and the existing `OrderPlaced` event is emitted by the `place` and `placeFlip` overloads. *** ## Invariants * A pair created via `createNextPair` must be identical to one created via `createPair` once `completeQuoteTokenUpdate` is called * `createNextPair` must revert if `nextQuoteToken()` returns `address(0)` * `createNextPair` must revert if the pair already exists (same as `createPair`) * Orders placed on a next-quote-token pair must be executable via swaps after the quote token update completes * Swap routing must not change until `completeQuoteTokenUpdate` is called on the base token # TIP-1002: Prevent crossed orders and allow same-tick flip orders ## Abstract This TIP makes two related changes to the Stablecoin DEX: 1. **Prevent crossed orders**: Modify `place` and `placeFlip` to reject orders that would cross existing orders on the opposite side of the book. An order "crosses" when a bid is placed at a tick higher than the best ask, or an ask is placed at a tick lower than the best bid. 2. **Allow same-tick flip orders**: Relax the `placeFlip` validation to allow `flipTick` to equal `tick`, enabling flip orders that flip to the same price. ## Motivation ### Preventing crossed orders Currently, the Stablecoin DEX allows orders to be placed at any valid tick, even if they would cross existing orders. Since matching only occurs during swaps (not during order placement), crossed orders can accumulate in the order book. This is unusual behavior and could confuse market makers who are accustomed to books that do not allow crossing. By preventing crossed orders at placement time, the order book maintains a clean invariant: `best_bid_tick <= best_ask_tick`. ### Allowing same-tick flip orders Currently, `placeFlip` requires `flipTick` to be strictly on the opposite side of `tick` (e.g., for a bid, `flipTick > tick`). This prevents use cases like instant token convertibility, where an issuer wants to place flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. *** ## Specification ### Modified behavior The `place` and `placeFlip` functions (including the `bookKey` overloads from TIP-1001) are modified to check for crossing before accepting an order: * **For bids**: Revert if `tick > best_ask_tick` (when `best_ask_tick` exists) * **For asks**: Revert if `tick < best_bid_tick` (when `best_bid_tick` exists) #### Same-tick orders Orders at the same tick as the best order on the opposite side are **allowed**. This means: * A bid at `tick == best_ask_tick` is allowed * An ask at `tick == best_bid_tick` is allowed While this is non-standard behavior for most order books (which would immediately match same-tick orders), it is intentionally permitted to support flip orders that flip to the same tick (see below). ### Same-tick flip orders The `placeFlip` validation is relaxed to allow `flipTick == tick`: * **Current behavior**: For bids, `flipTick > tick` required; for asks, `flipTick < tick` required * **New behavior**: For bids, `flipTick >= tick` required; for asks, `flipTick <= tick` required This enables use cases like instant token convertibility, where an issuer places flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. ### Interaction with TIP-1001 If TIP-1001 is accepted, the crossing check only applies when the pair is **active**—that is, when the pair's quote token equals the base token's current `quoteToken()`. For pairs created via `createNextPair` (where the quote token is the base token's `nextQuoteToken()`), the crossing check is skipped. This allows orders to accumulate freely during "place-only mode" before the quote token update is finalized. Such orders would likely be arbitraged nearly instantly once the pair launches, but this prevents someone from causing a denial-of-service to one side of the book by placing an extremely aggressive order on the other side. ### New error ```solidity /// @notice The order would cross existing orders on the opposite side error ORDER_WOULD_CROSS(); ``` ### Events No new events. *** ## Invariants * On active pairs, `best_bid_tick <= best_ask_tick` after any successful `place` or `placeFlip` call * On inactive pairs (per TIP-1001), no crossing check is enforced * Flip orders may create orders at the same tick as the opposite side, potentially resulting in `best_bid_tick == best_ask_tick` # TIP-1003: Client order IDs ## Abstract This TIP adds support for optional client order IDs (`clientOrderId`) to the Stablecoin DEX. Users can specify a `uint128` identifier when placing orders, which serves as an idempotency key and a predictable handle for the order. The system-generated `orderId` is not predictable before transaction execution, making client order IDs useful for order management. ## Motivation Traditional exchanges allow users to specify a client order ID (called `ClOrdID` in FIX protocol, `cloid` in Hyperliquid) for several reasons: 1. **Idempotency**: If a transaction is submitted twice (e.g., due to network issues), the duplicate can be detected and rejected 2. **Predictable reference**: Users know the order identifier before the transaction confirms, enabling them to prepare cancel requests or track orders without waiting for confirmation 3. **Integration**: External systems can use their own ID schemes to correlate orders *** ## Specification ### New storage A new mapping tracks active client order IDs per user: ```solidity mapping(address user => mapping(uint128 clientOrderId => uint128 orderId)) public clientOrderIds; ``` ### Modified functions All order placement functions gain an optional `clientOrderId` parameter: ```solidity /// @notice Places an order with an optional client order ID /// @param token The base token of the pair /// @param amount The order amount in base tokens /// @param isBid True for buy orders, false for sell orders /// @param tick The price tick for the order /// @param clientOrderId Optional client-specified ID (0 for none) /// @return orderId The system-assigned order ID function place( address token, uint128 amount, bool isBid, int16 tick, uint128 clientOrderId ) external returns (uint128 orderId); /// @notice Places an order on a specific pair with an optional client order ID /// @dev Overload from TIP-1001 function place( bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick, uint128 clientOrderId ) external returns (uint128 orderId); /// @notice Places a flip order with an optional client order ID function placeFlip( address token, uint128 amount, bool isBid, int16 tick, int16 flipTick, bool internalBalanceOnly, uint128 clientOrderId ) external returns (uint128 orderId); /// @notice Places a flip order on a specific pair with an optional client order ID /// @dev Overload from TIP-1001 function placeFlip( bytes32 bookKey, address token, uint128 amount, bool isBid, int16 tick, int16 flipTick, bool internalBalanceOnly, uint128 clientOrderId ) external returns (uint128 orderId); ``` ### New functions ```solidity /// @notice Cancels an order by its client order ID /// @param clientOrderId The client-specified order ID function cancelByClientOrderId(uint128 clientOrderId) external; /// @notice Gets the system order ID for a client order ID /// @param user The user who placed the order /// @param clientOrderId The client-specified order ID /// @return orderId The system-assigned order ID, or 0 if not found function getOrderByClientOrderId(address user, uint128 clientOrderId) external view returns (uint128 orderId); ``` ### Behavior #### Placing orders with clientOrderId When `clientOrderId` is non-zero: 1. Check if `clientOrderIds[msg.sender][clientOrderId]` maps to an active order 2. If it does, revert with `DUPLICATE_CLIENT_ORDER_ID` 3. Otherwise, proceed with order placement and set `clientOrderIds[msg.sender][clientOrderId] = orderId` When `clientOrderId` is zero, no client order ID tracking occurs. #### Uniqueness and reuse A `clientOrderId` must be unique among a user's **active orders**. Once an order is filled or cancelled, its `clientOrderId` can be reused. This matches the standard FIX protocol behavior where `ClOrdID` uniqueness is required only for working orders. When an order reaches a terminal state (filled or cancelled), the `clientOrderIds` mapping entry is cleared. #### Flip orders When a flip order is filled and creates a new order on the opposite side: 1. The new (flipped) order inherits the original order's `clientOrderId` 2. The `clientOrderIds` mapping is updated to point to the new order ID 3. This allows users to track their position across flips using a single `clientOrderId` If the original order had no `clientOrderId` (was zero), the flipped order also has no `clientOrderId`. #### Cancellation `cancelByClientOrderId(clientOrderId)` looks up `clientOrderIds[msg.sender][clientOrderId]` and cancels that order. It reverts if no active order exists for that `clientOrderId`. ### New event ```solidity /// @notice Emitted when an order is placed (V2 with clientOrderId) /// @dev Replaces OrderPlaced for new orders event OrderPlacedV2( uint128 indexed orderId, address indexed maker, address token, uint128 amount, bool isBid, int16 tick, bool isFlipOrder, int16 flipTick, uint128 clientOrderId ); ``` `OrderPlacedV2` is identical to `OrderPlaced` but adds the `clientOrderId` field. When an order is placed, only `OrderPlacedV2` is emitted (not both events). ### New errors ```solidity /// @notice The client order ID is already in use by an active order error DUPLICATE_CLIENT_ORDER_ID(); /// @notice No active order found for the given client order ID error CLIENT_ORDER_ID_NOT_FOUND(); ``` *** ## Invariants * A non-zero `clientOrderId` maps to at most one active order per user * `clientOrderIds[user][clientOrderId]` is cleared when the order is filled or cancelled * Flip orders inherit `clientOrderId` and update the mapping atomically * `clientOrderId = 0` is reserved to mean "no client order ID" # TIP-1004: Permit for TIP-20 ## Abstract TIP-1004 adds EIP-2612 compatible `permit()` functionality to TIP-20 tokens, enabling gasless approvals via off-chain signatures. This allows users to approve token spending without submitting an on-chain transaction, with the approval being executed by any third party who submits the signed permit. ## Motivation The standard ERC-20 approval flow requires users to submit a transaction to approve a spender before that spender can transfer tokens on their behalf. Among other things, this makes it difficult for a transaction to "sweep" tokens from multiple addresses that have never sent a transaction onchain. EIP-2612 introduced the `permit()` function which allows approvals to be granted via a signed message rather than an on-chain transaction. This enables: * **Gasless approvals**: Users can sign a permit off-chain, and a relayer or the spender can submit the transaction * **Single-transaction flows**: DApps can batch the permit with the subsequent action (e.g., approve + swap) in one transaction * **Improved UX**: Users don't need to wait for or pay for a separate approval transaction Since TIP-20 aims to be a superset of ERC-20 with additional functionality, adding EIP-2612 permit support ensures TIP-20 tokens work seamlessly with existing DeFi protocols and tooling that expect permit functionality. ### Alternatives While Tempo transactions provide solutions for most of the common problems that are solved by account abstraction, they do not provide a way to transfer tokens from an address that has never sent a transaction onchain, which means it does not provide an easy way for a batched transaction to "sweep" tokens from many addresses. While we plan to have Permit2 deployed on the chain, it, too, requires an initial transaction from the address being transferred from. Adding a function for `transferWithAuthorization`, which we are also considering, would also solve this problem. But `permit` is somewhat more flexible, and we think these functions are not mutually exclusive. *** ## Specification ### New functions The following functions are added to the TIP-20 interface: ```solidity interface ITIP20Permit { /// @notice Approves `spender` to spend `value` tokens on behalf of `owner` via a signed permit /// @param owner The address granting the approval /// @param spender The address being approved to spend tokens /// @param value The amount of tokens to approve /// @param deadline Unix timestamp after which the permit is no longer valid /// @param v The recovery byte of the signature /// @param r Half of the ECDSA signature pair /// @param s Half of the ECDSA signature pair /// @dev The permit is valid only if: /// - The current block timestamp is <= deadline /// - The signature is valid and was signed by `owner` /// - The nonce in the signature matches the current nonce for `owner` /// Upon successful execution, increments the nonce for `owner` by 1. /// Emits an {Approval} event. function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; /// @notice Returns the current nonce for an address /// @param owner The address to query /// @return The current nonce, which must be included in any permit signature for this owner /// @dev The nonce starts at 0 and increments by 1 each time a permit is successfully used function nonces(address owner) external view returns (uint256); /// @notice Returns the EIP-712 domain separator for this token /// @return The domain separator bytes32 value /// @dev The domain separator is computed dynamically on each call as: /// keccak256(abi.encode( /// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), /// keccak256(bytes(name())), /// keccak256(bytes("1")), /// block.chainid, /// address(this) /// )) /// Dynamic computation ensures correct behavior after chain forks where chainId changes. function DOMAIN_SEPARATOR() external view returns (bytes32); } ``` ### EIP-712 Typed Data The permit signature must conform to EIP-712 typed structured data signing. The domain and message types are defined as follows: #### Domain Separator The domain separator is computed using the following parameters: | Parameter | Value | |-----------|-------| | name | The token's `name()` | | version | `"1"` | | chainId | The chain ID where the token is deployed | | verifyingContract | The TIP-20 token contract address | ```solidity bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes(name())), keccak256(bytes("1")), block.chainid, address(this) )); ``` #### Permit Typehash The permit message type is: ```solidity bytes32 constant PERMIT_TYPEHASH = keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); ``` #### Signature Construction To create a valid permit signature, the signer must sign the following EIP-712 digest: ```solidity bytes32 structHash = keccak256(abi.encode( PERMIT_TYPEHASH, owner, spender, value, nonces[owner], deadline )); bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, structHash )); ``` The signature `(v, r, s)` must be produced by signing `digest` with the private key of `owner`. ### Behavior #### Nonces Each address has an associated nonce that: * Starts at `0` for all addresses * Increments by `1` each time a permit is successfully executed for that address * Must be included in the permit signature to prevent replay attacks #### Deadline The `deadline` parameter is a Unix timestamp. The permit is only valid if `block.timestamp <= deadline`. This allows signers to limit the validity window of their permits. #### Pause State The `permit()` function follows the same pause behavior as `approve()`. Since setting an allowance does not move tokens, `permit()` is allowed to execute even when the token is paused. #### TIP-403 Transfer Policy The `permit()` function does not perform TIP-403 authorization checks, consistent with the behavior of `approve()`. Transfer policy checks are only enforced when tokens are actually transferred. #### Signature Validation The implementation must: 1. Verify that `block.timestamp <= deadline`, otherwise revert with `PermitExpired` 2. Retrieve the current nonce for `owner` and use it to construct the `structHash` and `digest` 3. Increment `nonces[owner]` 4. Validate the signature: * The `v` parameter must be `27` or `28`. Values of `0` or `1` are **not** normalized and will revert with `InvalidSignature`. Callers using signing libraries that produce `v ∈ {0, 1}` must add `27` before calling `permit`. * Use `ecrecover` to recover a signer address from the digest * If `ecrecover` returns a non-zero address that equals `owner`, the signature is valid (EOA case) * Otherwise, revert with `InvalidSignature` 5. Set `allowance[owner][spender] = value` 6. Emit an `Approval(owner, spender, value)` event \> **Note**: The nonce is included in the signed digest, so nonce verification is implicit in signature validation — if the wrong nonce was signed, `ecrecover` will return a different address. ### New errors ```solidity /// @notice The permit signature has expired (block.timestamp > deadline) error PermitExpired(); /// @notice The permit signature is invalid (wrong signer, malformed, or zero address recovered) error InvalidSignature(); ``` ### New events None. Successful permit execution emits the existing `Approval` event from TIP-20. *** ## Invariants * `nonces(owner)` must only ever increase, never decrease * `nonces(owner)` must increment by exactly 1 on each successful `permit()` call for that owner * A permit signature can only be used once (enforced by nonce increment) * A permit with a deadline in the past must always revert * The recovered signer from a valid permit signature must exactly match the `owner` parameter * After a successful `permit(owner, spender, value, ...)`, `allowance(owner, spender)` must equal `value` * `DOMAIN_SEPARATOR()` must be computed dynamically and reflect the current `block.chainid` ### Test Cases The test suite must cover: 1. **Happy path**: Valid permit sets allowance correctly 2. **Expired permit**: Reverts with `PermitExpired` when `deadline < block.timestamp` 3. **Invalid signature**: Reverts with `InvalidSignature` for malformed signatures 4. **Wrong signer**: Reverts with `InvalidSignature` when signature is valid but signer ≠ owner 5. **Replay protection**: Second use of same signature reverts (nonce already incremented) 6. **Nonce tracking**: Verify nonce increments correctly after each permit 7. **Zero address recovery**: Reverts with `InvalidSignature` if ecrecover returns zero address 8. **Pause state**: Permit works when token is paused 9. **Domain separator**: Verify correct EIP-712 domain separator computation 10. **Domain separator chain ID**: Verify domain separator changes if chain ID changes 11. **Max allowance**: Permit with `type(uint256).max` value works correctly 12. **Allowance override**: Permit can override existing allowance (including to zero) # TIP-1005: Fix ask swap rounding loss ## Abstract This TIP fixes a rounding bug in the `swapExactAmountIn` function when filling ask orders. Due to double-rounding, the maker can receive slightly less quote tokens than the taker paid, causing tokens to be lost. ## Motivation When a taker swaps quote tokens for base tokens against an ask order, the following calculation occurs: 1. Convert taker's `amountIn` (quote) to base: `base_out = floor(amountIn / price)` 2. Credit maker with quote: `makerReceives = ceil(base_out * price)` Due to the floor in step 1, `makerReceives` can be less than `amountIn`. For example: * Taker pays `amountIn = 102001` quote at price 1.02 (tick 2000) * `base_out = floor(102001 / 1.02) = 100000` * `makerReceives = ceil(100000 * 1.02) = 102000` * **1 token is lost** This violates the zero-sum invariant: the taker pays more than the maker receives. It also means there is no canonical amount swapped—the trade for the maker is different from the trade for the taker. *** ## Specification ### Bug location The bug is in `_fillOrdersExactIn` when processing ask orders (the `baseForQuote = false` path). Specifically, when a partial fill occurs: 1. `fillAmount` (base) is calculated by rounding down: `baseOut = (remainingIn * PRICE_SCALE) / price` 2. `_fillOrder` is called with `fillAmount` 3. Inside `_fillOrder`, the maker's quote credit is re-derived: `quoteAmount = ceil(fillAmount * price)` The re-derivation in step 3 loses the original `remainingIn` information. ### Fix For partial fills in the ask path, pass the actual `remainingIn` (quote) to `_fillOrder` and use it directly for the maker's credit, rather than re-deriving it from `fillAmount`. The fix requires: 1. Modify `_fillOrder` to accept an optional `quoteOverride` parameter for ask orders 2. In `_fillOrdersExactIn`, when partially filling an ask, pass `remainingIn` as the quote override 3. When `quoteOverride` is provided, use it directly for the maker's balance increment instead of computing `ceil(fillAmount * price)` ### Reference implementation changes The fix requires changes to two functions in [`docs/specs/src/StablecoinDEX.sol`](https://github.com/tempoxyz/tempo/blob/3994f7c231f960cb481253888f295a50502d432c/docs/specs/src/StablecoinDEX.sol): #### 1. `_fillOrder` ([line 551-556](https://github.com/tempoxyz/tempo/blob/3994f7c231f960cb481253888f295a50502d432c/docs/specs/src/StablecoinDEX.sol#L551-L556)) Add an optional `quoteOverride` parameter. When non-zero and the order is an ask, use `quoteOverride` directly for the maker's balance increment instead of computing `ceil(fillAmount * price)`. ```solidity // Before: uint128 quoteAmount = uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE); balances[order.maker][book.quote] += quoteAmount; // After: uint128 quoteAmount = quoteOverride > 0 ? quoteOverride : uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE); balances[order.maker][book.quote] += quoteAmount; ``` #### 2. `_fillOrdersExactIn` ([line 923-926](https://github.com/tempoxyz/tempo/blob/3994f7c231f960cb481253888f295a50502d432c/docs/specs/src/StablecoinDEX.sol#L923-L926)) In the partial fill branch for asks, pass `remainingIn` as the quote override: ```solidity // Before: orderId = _fillOrder(orderId, fillAmount); // After (for partial fills where fillAmount == baseOut): orderId = _fillOrder(orderId, fillAmount, remainingIn); ``` ### Affected code paths * `_fillOrdersExactIn` with `baseForQuote = false` (ask path), partial fill case only * Full fills are not affected because the quote amount is derived from `order.remaining`, not `remainingIn` * Bid swaps are not affected because the taker pays base tokens directly ### Example: Before and after **Before (buggy):** ``` amountIn = 102001 quote base_out = floor(102001 / 1.02) = 100000 makerReceives = ceil(100000 * 1.02) = 102000 Lost: 1 token ``` **After (fixed):** ``` amountIn = 102001 quote base_out = floor(102001 / 1.02) = 100000 makerReceives = 102001 (passed directly) Lost: 0 tokens ``` *** ## Invariants * Zero-sum: for any swap, `takerPaid == makerReceived` (within the same token) * Taker receives `floor(amountIn / price)` base tokens (rounds in favor of protocol) * Maker receives exactly what taker paid in quote tokens # TIP-1006: Burn At for TIP-20 Tokens ## Abstract This specification introduces a `burnAt` function to TIP-20 tokens, allowing holders of a new `BURN_AT_ROLE` to burn tokens from any address without transfer policy restrictions. This complements the existing `burnBlocked` function which is limited to burning from addresses blocked by the transfer policy. ## Motivation The existing TIP-20 burn mechanisms have the following limitations: 1. `burn()` - Only burns from the caller's own balance, requires `ISSUER_ROLE` 2. `burnBlocked()` - Can burn from other addresses, but only if the target address is blocked by the transfer policy There are legitimate use cases where token administrators may want a privileged caller to have the ability to burn tokens from any address regardless of their policy status, such as allowing a bridge contract to burn tokens that are being bridged out without requiring approval (as in the `crosschainBurn` function proposed in [ERC 7802](https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7802.md)). The `burnAt` function provides this capability with appropriate access controls via a dedicated role. *** ## Specification ### New Role A new role constant is added to TIP-20: ```solidity bytes32 public constant BURN_AT_ROLE = keccak256("BURN_AT_ROLE"); ``` This role is administered by the `DEFAULT_ADMIN_ROLE` (same as other TIP-20 roles). ### New Event ```solidity /// @notice Emitted when tokens are burned from any account. /// @param from The address from which tokens were burned. /// @param amount The amount of tokens burned. event BurnAt(address indexed from, uint256 amount); ``` ### New Function ```solidity /// @notice Burns tokens from any account. /// @dev Requires BURN_AT_ROLE. Cannot burn from protected precompile addresses. /// @param from The address to burn tokens from. /// @param amount The amount of tokens to burn. function burnAt(address from, uint256 amount) external; ``` #### Behavior 1. **Access Control**: Reverts with `Unauthorized` if caller does not have `BURN_AT_ROLE` 2. **Protected Addresses**: Reverts with `ProtectedAddress` if `from` is: * `TIP_FEE_MANAGER_ADDRESS` (0xfeEC000000000000000000000000000000000000) * `STABLECOIN_DEX_ADDRESS` (0xDEc0000000000000000000000000000000000000) 3. **Balance Check**: Reverts with `InsufficientBalance` if `from` has insufficient balance 4. **No Policy Check**: Unlike `burnBlocked`, this function does NOT check transfer policy authorization 5. **State Changes**: * Decrements `balanceOf[from]` by `amount` * Decrements `_totalSupply` by `amount` * Updates reward accounting if `from` is opted into rewards 6. **Events**: Emits `Transfer(from, address(0), amount)` and `BurnAt(from, amount)` #### Interface Addition The `ITIP20` interface is extended with: ```solidity /// @notice Returns the role identifier for burning tokens from any account. /// @return The burn-at role identifier. function BURN_AT_ROLE() external view returns (bytes32); /// @notice Burns tokens from any account. /// @param from The address to burn tokens from. /// @param amount The amount of tokens to burn. function burnAt(address from, uint256 amount) external; ``` ## Invariants 1. **Role Required**: `burnAt` must always revert if caller lacks `BURN_AT_ROLE` 2. **Protected Addresses**: `burnAt` must never succeed when `from` is a protected precompile address 3. **Supply Conservation**: After `burnAt(from, amount)`: * `totalSupply` decreases by exactly `amount` * `balanceOf[from]` decreases by exactly `amount` 4. **Balance Constraint**: `burnAt` must revert if `amount > balanceOf[from]` 5. **Reward Accounting**: If `from` is opted into rewards: * Pending rewards must be accrued to the reward recipient's `rewardBalance` before the balance changes * `from`'s `rewardPerToken` snapshot must be synced to `globalRewardPerToken` * `optedInSupply` must decrease by `amount` * Any previously accrued `rewardBalance` remains claimable 6. **Policy Independence**: `burnAt` must succeed regardless of transfer policy status of `from` ### Test Cases The test suite must verify: 1. Successful burn with `BURN_AT_ROLE` 2. Revert without `BURN_AT_ROLE` (Unauthorized) 3. Revert when burning from `TIP_FEE_MANAGER_ADDRESS` (ProtectedAddress) 4. Revert when burning from `STABLECOIN_DEX_ADDRESS` (ProtectedAddress) 5. Successful burn from policy-blocked address (same behavior as `burnBlocked`) 6. Successful burn from policy-authorized address (differs from `burnBlocked`, which reverts) 7. Revert on insufficient balance 8. Correct event emissions (`Transfer` and `BurnAt`) 9. Correct reward accounting updates (pending rewards accrued before burn, `rewardBalance` remains claimable) # TIP-1007: Fee Token Introspection ## Abstract TIP-1007 adds a `getFeeToken()` view function to the FeeManager precompile that returns the fee token address being used for the current transaction. This enables smart contracts to introspect which TIP-20 token is paying for gas fees during execution, allowing for dynamic logic based on the fee token choice. ## Motivation Tempo transactions support paying gas fees in any USD-denominated TIP-20 token via the fee token preference system. However, prior to this TIP, there was no way for a smart contract to determine which fee token is being used for the current transaction during execution. This capability was requested by a partner. It could be useful for contracts that want to: * Adjust their internal logic based on which fee token is being used * Provide fee token-aware pricing or routing decisions * Emit events or logs that include the fee token for off-chain indexing * Implement fee token-specific behavior in cross-chain messaging *** ## Specification ### New Function The following function is added to the `IFeeManager` interface: ```solidity interface IFeeManager { // ... existing functions ... /// @notice Returns the fee token being used for the current transaction /// @return The address of the TIP-20 token paying for gas fees /// @dev This value is set by the protocol before transaction execution begins. /// Returns address(0) if no fee token has been set (e.g., in eth_call /// simulations where the transaction handler does not run). function getFeeToken() external view returns (address); } ``` ### Behavior #### Fee Token Resolution The fee token returned by `getFeeToken()` is the same token that was resolved by the protocol during transaction validation, following the [fee token preference rules](https://tempo.xyz/developers/docs/protocol/fees/spec-fee#fee-token-preferences). #### Storage The fee token is stored in **transient storage** (EIP-1153) within the FeeManager precompile. This means: * The value is automatically cleared at the end of each transaction * No persistent storage writes occur, minimizing gas costs * The value is consistent across all calls within a transaction (including internal calls and subcalls) #### Timing The fee token is set by the protocol in the `validate_against_state_and_deduct_caller` handler phase, before any user code executes. This ensures the value is available throughout the entire transaction execution. #### Gas Cost Reading the fee token costs the standard warm transient storage read cost (100 gas for TLOAD). This is the cost of calling `getFeeToken()` itself; callers should account for additional gas used by the CALL opcode to invoke the precompile. #### Edge Cases | Scenario | Return Value | |----------|--------------| | Normal transaction | The resolved fee token address | | Free transaction (zero gas price) | The resolved fee token (may still be set) | | `eth_call` simulation | `address(0)` (no transaction context) | The only case where `address(0)` is returned is in simulation contexts (e.g., `eth_call`) where the protocol handler does not execute. ### Example Usage ```solidity import { IFeeManager } from "./interfaces/IFeeManager.sol"; contract FeeTokenAware { 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 fees in pathUSD } else if (feeToken != address(0)) { // User is paying fees in a different USD stablecoin } else { // No fee token context (e.g., eth_call simulation) } } } ``` ### Interface Addition The following function is added to `IFeeManager`: ```solidity /// @notice Returns the fee token being used for the current transaction /// @return The address of the TIP-20 token paying for gas fees function getFeeToken() external view returns (address); ``` *** ## Invariants * `getFeeToken()` must return a consistent value across all calls within the same transaction * `getFeeToken()` must return `address(0)` in simulation contexts (e.g., `eth_call`) where no transaction handler runs * `getFeeToken()` must be callable from `staticcall` contexts without reverting * The fee token returned must match the token used for actual fee deduction in `collectFeePreTx` and `collectFeePostTx` * Reading the fee token must not modify any state (view function) ### Test Cases The test suite must cover: 1. **Basic functionality**: `getFeeToken()` returns the correct fee token address 2. **Zero when unset**: Returns `address(0)` when no fee token is set 3. **Consistency**: Same value returned from nested calls within a transaction 4. **Static call safety**: Works correctly when called via `staticcall` 5. **Transient storage**: Value is cleared between transactions 6. **Different fee tokens**: Works with various TIP-20 fee tokens (pathUSD, USDC, etc.) 7. **Dispatch coverage**: Function selector is correctly dispatched by the precompile # TIP-1009: Expiring Nonces ## Abstract TIP-1009 introduces expiring nonces, an alternative replay protection mechanism where transactions are valid only within a specified time window. Instead of tracking sequential nonces, the protocol uses transaction hashes with expiry timestamps to prevent replay attacks. This enables use cases like gasless transactions, meta-transactions, and simplified UX where users don't need to manage nonce ordering. ## Motivation Traditional sequential nonces require careful ordering—if transaction N fails or is delayed, all subsequent transactions (N+1, N+2, ...) are blocked. This creates friction for: 1. **Gasless/Meta-transactions**: Relayers need complex nonce management across multiple users 2. **Parallel submission**: Users cannot submit multiple independent transactions simultaneously 3. **Recovery from failures**: Stuck transactions require explicit cancellation with the same nonce Expiring nonces solve these problems by using time-based validity instead of sequence-based ordering. Each transaction is uniquely identified by its hash and is valid only until a specified `validBefore` timestamp. *** ## Specification ### Nonce Key Expiring nonce transactions use a reserved nonce key: ``` TEMPO_EXPIRING_NONCE_KEY = uint256.max (2^256 - 1) ``` When a Tempo transaction specifies `nonceKey = uint256.max`, the protocol treats it as an expiring nonce transaction. ### Transaction Fields Expiring nonce transactions require: | Field | Type | Description | |-------|------|-------------| | `nonceKey` | `uint256` | Must be `uint256.max` to indicate expiring nonce mode | | `nonce` | `uint64` | Must be `0` (unused, validated for consistency) | | `validBefore` | `uint64` | Unix timestamp (seconds) after which the transaction is invalid | ### Validity Window The `validBefore` timestamp must satisfy: ``` now < validBefore <= now + MAX_EXPIRY_SECS ``` Where: * `now` is the current block timestamp * `MAX_EXPIRY_SECS = 30` seconds Transactions with `validBefore` in the past or more than 30 seconds in the future are rejected. ### Replay Protection Replay protection uses a **circular buffer** data structure in the Nonce precompile: #### Storage Layout ```solidity contract Nonce { // Existing 2D nonce storage mapping(address => mapping(uint256 => uint64)) public nonces; // slot 0 // Expiring nonce storage mapping(bytes32 => uint64) public expiringNonceSeen; // slot 1: txHash => expiry mapping(uint32 => bytes32) public expiringNonceRing; // slot 2: circular buffer uint32 public expiringNonceRingPtr; // slot 3: buffer pointer } ``` #### Circular Buffer Design The circular buffer has a fixed capacity: ``` EXPIRING_NONCE_SET_CAPACITY = 300,000 ``` This capacity is sized for 10,000 TPS × 30 seconds = 300,000 transactions, ensuring entries expire before being overwritten. #### Algorithm When processing an expiring nonce transaction: 1. **Validate expiry window**: Reject if `validBefore <= now` or `validBefore > now + 30` 2. **Replay check**: Read `expiringNonceSeen[txHash]` * If entry exists and `expiry > now`, reject as replay 3. **Get buffer position**: Read `expiringNonceRingPtr`, compute `idx = ptr % CAPACITY` 4. **Read existing entry**: Read `expiringNonceRing[idx]` to get `oldHash` 5. **Eviction check** (safety): If `oldHash != 0`: * Read `expiringNonceSeen[oldHash]` * If `expiry > now`, reject (buffer full of valid entries) * Clear `expiringNonceSeen[oldHash] = 0` 6. **Insert new entry**: * Write `expiringNonceRing[idx] = txHash` * Write `expiringNonceSeen[txHash] = validBefore` 7. **Advance pointer**: Write `expiringNonceRingPtr = ptr + 1` #### Pseudocode ```solidity function checkAndMarkExpiringNonce( bytes32 txHash, uint64 validBefore, uint64 now ) internal { // 1. Validate expiry window require(validBefore > now && validBefore <= now + 30, "InvalidExpiry"); // 2. Replay check uint64 seenExpiry = expiringNonceSeen[txHash]; require(seenExpiry == 0 || seenExpiry <= now, "Replay"); // 3-4. Get buffer position and existing entry uint32 ptr = expiringNonceRingPtr; uint32 idx = ptr % CAPACITY; bytes32 oldHash = expiringNonceRing[idx]; // 5. Eviction check (safety) if (oldHash != bytes32(0)) { uint64 oldExpiry = expiringNonceSeen[oldHash]; require(oldExpiry == 0 || oldExpiry <= now, "BufferFull"); expiringNonceSeen[oldHash] = 0; } // 6. Insert new entry expiringNonceRing[idx] = txHash; expiringNonceSeen[txHash] = validBefore; // 7. Advance pointer expiringNonceRingPtr = ptr + 1; } ``` ### Gas Costs The intrinsic gas cost for expiring nonce transactions includes: ``` EXPIRING_NONCE_GAS = 2 * COLD_SLOAD_COST + WARM_SLOAD_COST + 3 * WARM_SSTORE_RESET = 2 * 2100 + 100 + 3 * 2900 = 13,000 gas ``` **Included operations:** * 2 cold SLOADs: `seen[txHash]`, `ring[idx]` (unique slots per tx) * 1 warm SLOAD: `seen[oldHash]` (warm because we just read `ring[idx]` which points to it) * 3 SSTOREs at RESET price: `seen[oldHash]=0`, `ring[idx]`, `seen[txHash]` **Excluded operations (amortized):** * `ring_ptr` SLOAD/SSTORE: Accessed by almost every expiring nonce tx in a block, so amortized cost approaches ~200 gas. May be moved out of EVM storage in the future. **Why SSTORE\_RESET (2,900) instead of SSTORE\_SET (20,000) for `seen[txHash]`:** * SSTORE\_SET cost exists to penalize permanent state growth * Expiring nonce data is ephemeral: evicted within 30 seconds, fixed-size buffer (300k entries) * No permanent state growth, so the 20k penalty doesn't apply ### Transaction Pool Validation The transaction pool performs preliminary validation: 1. Verify `nonceKey == uint256.max` 2. Verify `nonce == 0` 3. Verify `validBefore` is present 4. Verify `validBefore > currentTime` (not expired) 5. Verify `validBefore <= currentTime + MAX_EXPIRY_SECS` (within window) 6. Query `expiringNonceSeen[txHash]` storage slot to check for existing entry Transactions failing these checks are rejected before entering the pool. ### Interaction with Other Features #### 2D Nonces Expiring nonces and 2D nonces are mutually exclusive: * `nonceKey = 0`: Protocol nonce (standard sequential) * `nonceKey = 1..uint256.max-1`: 2D nonce keys * `nonceKey = uint256.max`: Expiring nonce mode #### Access Keys (Keychain) Expiring nonces work with access key signatures. The `validBefore` provides an additional security boundary—even if an access key is compromised, transactions signed with it become invalid after the expiry window. #### Fee Tokens Expiring nonce transactions pay fees in TIP-20 fee tokens like any other Tempo transaction. *** ## Invariants ### Must Hold | ID | Invariant | Description | |----|-----------|-------------| | **E1** | No replay | A transaction hash can never be executed twice (changing `validBefore` produces a different hash) | | **E2** | Expiry enforcement | Transactions with `validBefore <= now` must be rejected | | **E3** | Window bounds | Transactions with `validBefore > now + MAX_EXPIRY_SECS` must be rejected | | **E4** | Nonce must be zero | Expiring nonce transactions must have `nonce == 0` | | **E5** | Valid before required | Expiring nonce transactions must have `validBefore` set | | **E6** | No nonce mutation | Expiring nonce txs do not increment protocol nonce or any 2D nonce | | **E7** | Concurrent independence | Multiple expiring nonce txs from same sender can execute in same block | ### Invariant Tests These invariants are tested in the Foundry invariant test suite (`TempoTransactionInvariant.t.sol`): | Handler | Tests | Description | |---------|-------|-------------| | `handler_expiringNonceBasic` | Basic flow | Execute valid expiring nonce tx | | `handler_expiringNonceReplay` | E1 | Replay must be rejected | | `handler_expiringNonceExpired` | E2 | Tx with `validBefore <= now` must be rejected | | `handler_expiringNonceWindowTooFar` | E3 | Tx with `validBefore > now + 30s` must be rejected | | `handler_expiringNonceNonZeroNonce` | E4 | Tx with `nonce != 0` must be rejected | | `handler_expiringNonceMissingValidBefore` | E5 | Tx without `validBefore` must be rejected | | `handler_expiringNonceNoNonceMutation` | E6 | Protocol and 2D nonces unchanged after execution | | `handler_expiringNonceConcurrent` | E7 | Multiple concurrent txs from same sender succeed | ### Test Cases 1. **Basic flow**: Submit transaction, verify execution, attempt replay (should fail) 2. **Expiry validation**: * `validBefore` in past → reject * `validBefore = now` → reject * `validBefore = now + 31` → reject * `validBefore = now + 30` → accept 3. **Nonce validation**: * `nonce = 0` → accept * `nonce > 0` → reject 4. **Required fields**: * `validBefore` missing → reject * `nonceKey != uint256.max` → not expiring nonce (uses 2D nonce rules) 5. **Post-expiry replay**: Submit tx, wait for expiry, submit same tx with new `validBefore` (should succeed) 6. **Buffer eviction**: Fill buffer, verify old entries are evicted when expired 7. **Concurrent transactions**: Submit multiple transactions with same `validBefore`, verify all succeed *** ## Benchmark Results Benchmarks were run to measure state savings from expiring nonces compared to 2D nonces. ### Key Findings | Metric | Value | |--------|-------| | Per-transaction state savings | ~100 bytes | | Circular buffer capacity | 300,000 entries | | Buffer fills at 5k TPS | ~60 seconds | ### Controlled Benchmark (100k transactions at 5k TPS) | Nonce Type | Final DB Size | Transactions | |------------|---------------|--------------| | 2D Nonces | 4,342.85 MB | 100,000 | | Expiring Nonces | 4,332.18 MB | 100,000 | | **Difference** | **-10.67 MB** | - | The ~107 bytes per transaction overhead includes MPT node overhead, MDBX metadata, and RLP encoding. ### Scaling Projections | TPS | Daily Transactions | Daily State Savings | |-----|-------------------|---------------------| | 5,000 | 432M | 43.2 GB | | 10,000 | 864M | 86.4 GB | After the circular buffer fills, expiring nonces maintain constant storage while 2D nonces grow by ~100 bytes per transaction. *** ## Open Questions ### Safety Check for Buffer Eviction The current implementation includes a safety check that reads `expiringNonceSeen[oldHash]` before evicting an entry from the ring buffer. This check verifies the entry is actually expired before overwriting. **Rationale for keeping the check:** * Protects against unexpected TPS spikes that could cause the buffer to fill with valid entries * Defense-in-depth: prevents replay attacks if capacity assumptions are violated * Cost is only incurred in the rare case when eviction is needed **Rationale for removing the check:** * The buffer is sized (300k entries) to guarantee entries expire before being overwritten at 10k TPS * Removes 1 SLOAD (2,100 gas) from the critical path * Simplifies the algorithm **Current decision**: Keep the check but exclude it from gas accounting (charged as if it won't trigger in normal operation). **Question**: Should this safety check be: 1. Kept with current gas accounting (not charged for the extra SLOAD)? 2. Removed entirely, trusting the capacity sizing? 3. Kept and fully charged (add 2,100 gas to `EXPIRING_NONCE_GAS`)? ### Buffer Capacity Sizing The current capacity of 300,000 assumes: * Maximum 10,000 TPS sustained * 30 second expiry window **Question**: Should the capacity be configurable per-chain or hardcoded? What happens if TPS requirements increase significantly? ### Transaction Hash Computation The transaction hash used for replay protection must be computed before signature recovery. **Question**: Should the spec explicitly define the hash computation (which fields, encoding) or reference the Tempo Transaction spec? # TIP-1010: Mainnet Gas Parameters ## Abstract This TIP specifies the initial gas parameters for Tempo mainnet, including base fee pricing, payment lane capacity, and main transaction gas limits. These parameters are calibrated to support Tempo's target of approximately 20,000 TPS for payment transactions while maintaining economically sustainable fee levels. ## Motivation Tempo is designed as a high-throughput blockchain optimized for stablecoin payments. To achieve this, the gas parameters must be carefully calibrated to: 1. **Enable high throughput**: Support ~20,000 TPS for payment transactions 2. **Maintain low fees**: Target 0.1 cent per standard TIP-20 transfer 3. **Prevent spam**: Ensure fees are high enough to deter abuse 4. **Balance capacity**: Allocate appropriate gas limits between payment lane and general transactions The parameters defined in this TIP represent the initial mainnet configuration and may be adjusted through future governance processes. *** ## Specification ### Base Fee **Value**: `2 × 10^10` attodollars (20 billion attodollars per gas) **Rationale**: * A standard TIP-20 transfer costs approximately 50,000 gas * At this basefee: 50,000 gas × 20 billion attodollars/gas = 10^15 attodollars = 1,000 microdollars = $0.001 * This targets approximately **0.1 cent (1,000 microdollars) per TIP-20 transfer** **Note on units**: Attodollars (10^-18 USD) are the gas price unit. TIP-20 tokens use 6 decimals, so 1 token unit = 1 microdollar (10^-6 USD). Conversion: attodollars / 10^12 = microdollars. **Note**: The base fee is fixed per protocol version and does not adjust dynamically based on block utilization. Unlike EIP-1559, there is no in-protocol mechanism that raises or lowers the base fee in response to congestion. Changes to the base fee require a hardfork upgrade. ### Block Gas Limit **Value**: 500,000,000 gas per block (total block gas limit) **Rationale**: * At 50,000 gas per TIP-20 transfer: `500,000,000 / 50,000 = 10,000 transfers per block` * With 500ms block time: `10,000 × 2 = 20,000 TPS` for payment transactions * This capacity supports Tempo's target throughput for payment use cases **Gas Budget Breakdown**: * **Total block gas limit**: 500,000,000 gas * **Shared gas limit** (validator subblocks): 50,000,000 gas (`block_gas_limit / 10`) * **Non-shared gas limit** (proposer pool): 450,000,000 gas (`block_gas_limit - shared_gas_limit`) * **General gas limit** (non-payment cap): 30,000,000 gas (see below) :::info **Shared capacity model**: The payment lane is non-dedicated. General and payment transactions selected by the proposer share the non-shared gas budget (450M). General transactions are capped at `general_gas_limit` (30M), guaranteeing that at least 420M gas remains available for proposer payment transactions. The remaining 50M (`shared_gas_limit`) is reserved for validator subblocks as defined in the Sub-block Specification. ::: **Constraints**: * Only transactions qualifying for the payment lane (simple TIP-20 transfers, memos, etc.) may exceed the `general_gas_limit` * Complex contract interactions use the general gas limit instead ### Main Transaction Gas Limit **Value**: 30,000,000 gas per block (`general_gas_limit`) **Rationale**: * Aligned with the transaction gas cap to ensure maximum-sized contract deployments can be included in a block * Supports general smart contract interactions beyond simple payments * Provides capacity for: * Contract deployments (including max 24KB contracts) * DEX swaps * Complex multi-step transactions * Other non-payment use cases :::warning **Transactions exceeding 16,000,000 gas are not recommended.** The elevated gas limits (30M) exist solely to accommodate maximum-sized contract deployments under TIP-1000 state creation costs. Applications should not rely on transactions consuming more than 16M gas for normal operations. When storage pricing is moved to a separate mechanism (e.g., storage rent or state expiry), the transaction gas cap is expected to return to 16,000,000 gas. ::: ### Transaction Gas Cap **Value**: 30,000,000 gas per transaction **Rationale**: * Increased from the previous 16,000,000 gas limit * Accommodates deployment of maximum-size contracts (24,576 bytes per EIP-170) under TIP-1000 state creation costs: * Base transaction cost: 21,000 gas * Calldata for initcode (up to 49,152 bytes per EIP-3860): ~500,000-800,000 gas * CREATE base cost (TIP-1000, fixed upfront contract creation cost): 500,000 gas (replaces old 32,000) * Initcode execution: variable (~3,000 gas minimum) * Contract code storage (TIP-1000): `24,576 bytes × 1,000 gas/byte = 24,576,000 gas` * **Total**: ~25,600,000-25,900,000 gas (fits within 30M limit) ### Gas Schedule Summary | Parameter | Value | Purpose | |-----------|-------|---------| | Base fee | `2 × 10^10` attodollars | Target 0.1 cent (1,000 microdollars) per TIP-20 transfer | | Total block gas limit | 500,000,000 gas/block | Total block capacity | | Non-shared gas limit | 450,000,000 gas/block | Proposer pool transactions | | Shared gas limit | 50,000,000 gas/block | Validator subblocks (see Sub-block Specification) | | General gas limit | 30,000,000 gas/block | Cap for non-payment transactions | | Transaction gas cap | 30,000,000 gas | Allow max-size contract deployment | ### Economic Analysis #### Fee Revenue Projections At full payment lane utilization: * 10,000 transfers per block × 1,000 microdollars = 10,000,000 microdollars ($10) per block * At 2 blocks/second: $20/second * Daily: ~$1,728,000 in base fees from payment lane alone #### Cost Per Operation | Operation | Gas Cost | USD Cost (at target base fee) | |-----------|----------|-------------------------------| | TIP-20 transfer (existing recipient) | 50,000 | $0.001 (0.1 cent / 1,000 microdollars) | | TIP-20 transfer (new recipient) | 300,000 | $0.006 (0.6 cent / 6,000 microdollars) | | First transaction from new account | 300,000 | $0.006 (0.6 cent / 6,000 microdollars) | | Small contract deployment (1KB) | ~1,800,000 | $0.036 (3.6 cents / 36,000 microdollars) | | Max contract deployment (24,576 bytes) | ~25,900,000 | $0.518 (~52 cents / 518,000 microdollars) | *** ## Invariants 1. **Base Fee Invariant**: The base fee is fixed at `2 × 10^10` attodollars per protocol version and can only be changed via a hardfork upgrade. At the current base fee, a TIP-20 transfer (50,000 gas) MUST cost approximately 0.1 cent (1,000 microdollars). 2. **Payment Lane Priority**: Transactions qualifying for the payment lane MUST be able to consume up to the remaining block gas capacity (total gas limit minus gas already consumed by general transactions). 3. **Shared Gas Pool**: Proposer pool transactions (payment and general) share the non-shared gas budget (450M). General transactions are additionally constrained by `general_gas_limit` (30M). The remaining 50M is reserved for validator subblocks. 4. **Transaction Gas Cap**: No single transaction MUST be allowed to consume more than the transaction gas cap (30,000,000 gas). 5. **Block Gas Validity**: A block MUST be invalid if any of the following hold: * Total gas used by proposer pool transactions (payment + general) exceeds the non-shared gas limit (450M) * Total gas used by non-payment (general) transactions exceeds the general gas limit (30M) * Total gas used by validator subblock transactions exceeds the shared gas limit (50M) ### Implementation Notes These parameters are configured at the chainspec level and applied during block validation. Future adjustments may be made through: 1. Hard fork upgrades (for significant changes) 2. Governance proposals (if on-chain governance is implemented) 3. Emergency response procedures (for critical security issues) ### Test Cases 1. **Base fee targeting**: Verify that at equilibrium, TIP-20 transfers cost approximately 0.1 cent (1,000 microdollars) 2. **Payment lane capacity**: Verify that 10,000 TIP-20 transfers can be included in a single block 3. **General gas limit**: Verify that general transactions are correctly bounded by the 30M gas limit 4. **Transaction gas cap**: Verify that transactions exceeding 30M gas are rejected 5. **Contract deployment**: Verify that a 24KB contract can be deployed within the transaction gas cap 6. **Lane separation**: Verify that payment lane and general transactions are independently tracked # TIP-1011: Enhanced Access Key Permissions ## Abstract This TIP extends Access Keys with three permission features: 1. **Periodic spending limits** that reset on fixed intervals. 2. **Call scoping** that limits what addresses a key can call and which selectors it can use. 3. **Limited calldata recipient scoping** for token transfer/approval selectors. ## Motivation Currently Access Keys support per-token limits and expiry, but miss two practical controls. ### Periodic Spending Limits One-time limits cannot express recurring allowances. **Use cases:** 1. Subscription billing (`10 USDC / month`). 2. Payroll schedules (monthly budgeted payouts). 3. Rate-limited agent/API budgets. ### Call Scoping (Target + Selector Set) Users need finer controls than "any call". They want keys like: 1. "Only call `swap()` and `exactInput()` on DEX X." 2. "Only call gameplay methods on contract Y." 3. "Only perform plain transfers, not token extension methods." 4. "Only vote() on governance contracts." **Current workaround**: Deploy a proxy contract that enforces destination/function restrictions, adding gas overhead and complexity. ### Recipient-Bound Token Calls Target + selector scoping still allows an access key to move funds to arbitrary recipients for token methods like `transfer` and `approve`. Users need a narrower policy: the key may call transfer/approve selectors, but only when the recipient/spender matches a configured address. This TIP intentionally adds a narrow calldata rule (first ABI `address` argument equality) instead of a generic calldata policy language. *** ## Specification ### Extended Data Structures Conventions used in this section: 1. Protocol/RLP structs are written with Rust-like `Option<...>` notation. 2. Solidity ABI structs are listed separately where ABI cannot directly represent protocol `Option` semantics. #### TokenLimit **Current:** ```solidity struct TokenLimit { address token; uint256 amount; } ``` **Proposed:** ```solidity struct TokenLimit { address token; uint256 amount; // One-time cap when period == 0, per-period cap when period > 0 uint64 period; // Period duration in seconds (0 = one-time limit) } ``` Design note: `period` is specified as an explicit field (instead of packed into `token`) to keep encoding/auditing straightforward and avoid migration risk for existing limit semantics. Runtime state is derived and stored by the precompile (not signed): ```text TokenLimitState { remainingInPeriod: uint256, periodEnd: uint64, } ``` Initialization and persistence: 1. `TokenLimitState` is initialized when the key is authorized (or a limit is created via root mutation), not lazily at first spend. 2. `period == 0` initializes `remainingInPeriod = limit` and `periodEnd = 0`. 3. `period > 0` initializes `remainingInPeriod = limit` and `periodEnd = authorize_time + period`. 4. For a given `(account,key,token)`, there is exactly one active `TokenLimit`; duplicate token entries in a single authorization MUST be rejected. #### CallScope Call scoping uses explicit vectors in the protocol model: ```text CallScope { target: address, selector_rules: Vec, // [] => any selector on this target } ``` Solidity ABI representation for precompile methods: ```solidity struct CallScope { address target; SelectorRule[] selectorRules; } ``` Solidity ABI and protocol semantics match directly: 1. `selectorRules = []` allows any selector on `target`. 2. `selectorRules = [r1, ...]` allows exactly the listed selectors. 3. To remove a target scope in the Solidity precompile API, callers MUST use `removeAllowedCalls(keyId, target)`. `selector_rules` behavior: 1. `[]`: allow any selector. 2. `[r1, r2, ...]`: allow exactly the listed selector rules. In the Solidity precompile API, omitting a target scope blocks that target; `selectorRules = []` does not. #### SelectorRule ```text SelectorRule { selector: bytes4, recipients: Vec
, // [] => any recipient, [a1, ...] => only listed recipients } ``` Solidity ABI representation for precompile methods: ```solidity struct SelectorRule { bytes4 selector; address[] recipients; } ``` Solidity ABI and protocol semantics match directly: 1. `recipients = []` allows any recipient for that selector. 2. `recipients = [a1, ...]` constrains the selector to that recipient set. `SelectorRule.recipients` behavior: 1. `[]` => no calldata recipient checks for this selector. 2. `[a1, a2, ...]` => enforce `arg0` recipient membership for this selector. 3. Selector rules MUST be unique per target (`selector` appears at most once). Supported constrained selectors in this TIP: 1. `0xa9059cbb` => `transfer(address,uint256)` 2. `0x095ea7b3` => `approve(address,uint256)` 3. `0x95777d59` => `transferWithMemo(address,uint256,bytes32)` If a selector rule uses `recipients = [..]`, then: 1. `target` MUST be a TIP-20 token address. 2. `selector` MUST be one of the constrained selectors above. 3. Otherwise, key authorization MUST be rejected. For these selectors, the constrained field is ABI argument `0` (the first `address` argument). Selector width is fixed at 4 bytes. 1. Each `SelectorRule.selector` MUST be exactly 4 bytes. 2. Implementations MUST revert when decoding or accepting any selector whose length is not exactly 4 bytes. 3. Selectorless calls (`calldata.length < 4`) and fallback/receive routing are scope-matchable only for address-only scopes (`selector_rules = []`). They MUST be rejected when explicit selector matching is required. 4. Contracts with non-standard selector parsing are NOT supported. Examples: 1. `{ target: 0x123, selector_rules: [{selector: 0xaabbccdd, recipients: []}, {selector: 0xeeff0011, recipients: []}] }`: allow two selectors on one target. 2. `{ target: 0x123, selector_rules: [] }`: address-only scoping (any calldata shape on `0x123`, including selectorless/fallback-style calls). 3. `allowedCalls = None`: unrestricted key. 4. `allowedCalls = Some([])`: key is authorized but cannot make scoped calls. 5. `{ target: tokenX, selector_rules: [{selector: 0xa9059cbb, recipients: [0xReceiver]}] }`: allow `transfer` only when `to == 0xReceiver`. 6. `{ target: tokenX, selector_rules: [{selector: 0xa9059cbb, recipients: [0xA, 0xB]}] }`: allow `transfer` only when `to` is in `{0xA, 0xB}`. 7. Distinct target scopes are independent: allowing selector `s` on target `A` never allows selector `s` on target `B`. #### KeyAuthorization Existing fields remain, with a trailing optional call-scope field: ```text KeyAuthorization { chain_id: u64, key_type: SignatureType, key_id: address, expiry: Option, limits: Option>, allowed_calls: Option>, // New trailing field } ``` ### Interface Changes #### Events ```solidity /// @notice Emitted when an access key spends tokens against a spending limit /// @param account The account whose key was used /// @param publicKey The public key (address) that initiated the spend /// @param token The token address being spent /// @param amount The amount spent in this transaction /// @param remainingLimit The remaining spending limit after this spend event AccessKeySpend( address indexed account, address indexed publicKey, address indexed token, uint256 amount, uint256 remainingLimit ); ``` This event MUST be emitted whenever an access-key transaction deducts from a spending limit (one-time or periodic). #### IAccountKeychain.sol ```solidity /// @notice Authorizes a key with enhanced permissions /// @param keyId The key identifier (address derived from public key) /// @param signatureType 0: secp256k1, 1: P256, 2: WebAuthn /// @param expiry Block timestamp when key expires /// @param enforceLimits Whether spending limits are enforced for this key /// @param spendingLimits Token spending limits (may include periodic limits) /// @param allowAnyCalls Whether the key is unrestricted (`true`) or scoped by `allowedCalls` (`false`) /// @param allowedCalls Per-target call scopes for this key. function authorizeKey( address keyId, SignatureType signatureType, uint64 expiry, bool enforceLimits, TokenLimit[] calldata spendingLimits, bool allowAnyCalls, CallScope[] calldata allowedCalls ) external; /// @notice Creates or replaces one target scope for a key /// @dev Root key only. If `target` does not exist, creates a new scope; otherwise replaces it atomically. /// @dev `scope.selectorRules = []` allows any selector on `scope.target`; it does not block the target. /// @dev If a selector rule has `recipients`, `target` MUST be TIP-20 and `selector` MUST be transfer/approve (+memo). /// @dev For each selector rule, `recipients = []` means no recipient restriction. function setAllowedCalls( address keyId, CallScope calldata scope ) external; /// @notice Removes one target scope for a key function removeAllowedCalls(address keyId, address target) external; /// @notice Returns whether a key is call-scoped together with its configured call scopes /// @dev `isScoped = false` means unrestricted. /// @dev `isScoped = true && calls.length == 0` means scoped deny-all. function getAllowedCalls( address account, address keyId ) external view returns (bool isScoped, CallScope[] memory calls); /// @notice Returns remaining limit for a token, accounting for period resets function getRemainingLimit( address account, address keyId, address token ) external view returns (uint256 remaining, uint64 periodEnd); ``` `getAllowedCalls(account, keyId)` semantics: 1. `isScoped = false, calls = []`: unrestricted key. 2. `isScoped = true, calls = []`: scoped key with no allowed targets. 3. `isScoped = true, calls = [c1, ...]`: scoped key with the listed allowlist. 4. Missing, revoked, or expired access keys return `isScoped = true, calls = []`. ### Semantic Behavior #### Periodic Limit Reset Logic On each spend attempt for `(account, key, token)`: 1. Implementations MUST load the configured `TokenLimit` and runtime `TokenLimitState`. 2. If `period == 0`, the limit is one-time and no period rollover is applied. 3. If `period > 0` and `block.timestamp >= periodEnd`, implementations MUST reset `remainingInPeriod` to `limit` and advance `periodEnd` by whole multiples of `period` so that `periodEnd > block.timestamp`. 4. If `amount > remainingInPeriod`, implementations MUST revert `SpendingLimitExceeded()`. 5. Otherwise, implementations MUST decrement `remainingInPeriod` by `amount`. `updateSpendingLimit(account,key,token,newLimit)` semantics: 1. MUST update the configured `limit` for that `(account,key,token)`. 2. MUST set `remainingInPeriod = newLimit`. 3. MUST NOT change `period`. 4. MUST NOT change `periodEnd`. 5. Therefore changing `period` requires re-authorizing the key (or removing and recreating that token limit entry). #### Call Validation Logic Call-scope checks use map lookups keyed by `(account_key, target, selector)` plus optional selector-level recipient allowlists. Scoped-call validation is performed in a metered pre-execution phase after transaction validation succeeds but before the first user call executes. It is not a transaction-validity condition. If any call fails scoped-call validation, the transaction execution MUST fail atomically before any user call in the batch begins. ##### Access-Key Contract Creation Ban If a transaction is signed with an access key (`key != Address::ZERO`), contract creation MUST be rejected as an invalid transaction in all configurations. This ban applies regardless of: 1. Whether `allowed_calls` is `None` or `Some(...)`. 2. Whether any target scope has `selector_rules = []` (allow-any-selector). 3. Whether the creation call appears in a batch. Only the root key (`key == Address::ZERO`) may submit contract-creation calls; this is not a global create ban. ##### Single Call Validation For each call: 1. If the transaction uses an access key and the call is contract creation, implementations MUST reject the transaction as invalid before execution. 2. If `allowed_calls = None`, implementations MUST allow the call (subject to the contract-creation ban). 3. If `allowed_calls = Some(...)`, implementations MUST enforce target and selector matching. 4. If no target scope exists for `destination`, implementations MUST fail execution before the first user call begins. 5. If the target scope is `selector_rules = []`, implementations MUST allow the call, including selectorless/fallback-style calldata. 6. If the target scope has explicit selector rules and calldata does not provide at least 4 selector bytes, implementations MUST fail execution before the first user call begins. 7. If the target scope has explicit selector rules, there MUST be a rule for the selector; otherwise implementations MUST fail execution before the first user call begins. 8. If the matched rule has `recipients = []`, implementations MUST allow the call. 9. If the matched rule has `recipients = [a1, ...]`, implementations MUST decode ABI argument `0` as an `address` and require membership in that list. 10. For a selector rule with a non-empty `recipients` list, if calldata is shorter than `4 + 32` bytes, implementations MUST fail execution before the first user call begins. 11. For a selector rule with a non-empty `recipients` list, implementations MUST enforce canonical ABI `address` encoding for argument `0` (upper 12 bytes zero) before membership check; otherwise implementations MUST fail execution before the first user call begins. ##### Batch Validation For AA transactions with multiple calls, each call MUST be validated independently in the metered pre-execution phase before execution of the first user call begins. If any call fails scope validation: 1. The batch MUST fail atomically. 2. No user call in the batch may execute. 3. The failure MUST be reported as an execution failure rather than as an invalid transaction. #### Root-Controlled Scope Updates `setAllowedCalls` MUST be root-key-only and MUST apply create-or-replace semantics per target. 1. `setAllowedCalls(keyId, [])` MUST revert; an empty scope batch is ambiguous and MUST NOT act as a no-op or mode toggle. 2. `selectorRules = []` sets `selector_rules = None` semantics (any selector allowed on `target`). 3. `removeAllowedCalls(keyId, target)` disables that target scope. 4. Implementations MUST enforce at most one scope per target for each `(account, key)`. 5. Selector rules MUST be unique by `selector` within a target scope. 6. If any rule has `recipients = Some([..])`, `target` MUST be a TIP-20 token address. 7. If any rule has `recipients = Some([..])`, its `selector` MUST be one of: 1. `0xa9059cbb` (`transfer(address,uint256)`) 2. `0x095ea7b3` (`approve(address,uint256)`) 3. `0x95777d59` (`transferWithMemo(address,uint256,bytes32)`) 8. If any rule has `recipients = Some([..])`, each recipient in that list MUST be non-zero. 9. If any rule has `recipients = Some([..])`, recipients in that list MUST be unique. 10. If any selector-rule validity rule is violated, implementations MUST reject the authorization (or revert `setAllowedCalls`). Rationale for rule 2 (`removeAllowedCalls` disables a target scope): 1. This avoids unbounded gas from deletion-time slot iteration. 2. Prior selector rows may remain in state, but the removed target scope no longer participates in matching. #### Interaction Rules 1. Keys may mix one-time and periodic token limits. 2. Spending limits and call scopes are independent checks; both must pass. 3. `updateSpendingLimit()` updates limit and `remainingInPeriod`, but does not change `period` or `periodEnd`. 4. `allowed_calls = None` is unrestricted for non-create calls; `Some([])` is scoped mode with no allowed calls. 5. Every scope has an explicit target address, so there is no wildcard-target precedence ambiguity. 6. Per-target updates are create-or-replace and duplicate target scopes are not allowed. 7. Selector-level recipient allowlists are optional and only valid for TIP-20 targets and the constrained selectors above. 8. Selector-level recipient allowlists are checked after selector match and before call execution. 9. This TIP does not introduce generic calldata predicates, offset math, or wildcard argument matching. Wallet UX recommendation (non-consensus): 1. Wallets SHOULD default to scoped keys (non-empty `selectorRules`) and require explicit user opt-in for unrestricted target scopes (`selectorRules = []`). ### Gas And Complexity Bounds This TIP only specifies the additional intrinsic gas delta for call scopes in handler-side `key_authorization` charging. Existing key-authorization charging (signature verification, existing-key read, base key write, token-limit writes, and buffer) remains unchanged. Per-transaction scoped-call matching for access-key transactions is not charged as intrinsic gas. It is charged by normal metered execution in the scoped-call pre-execution phase described above. Definitions: 1. `SSTORE_SET = sstore_set_without_load_cost`. 2. `S` = number of targets with configured call scope. 3. `K` = total selector rules across all configured targets. 4. `C` = total selector rules with a non-empty `recipients` list. 5. `W` = total recipient entries across all constrained selector rules. Scoped-call storage writes counted for intrinsic gas: 6. Restricted-mode marker: `1` slot when `allowed_calls` is `Some(...)`. 7. Each target scope writes `3` slots: target-set length, target-set value, and target-set position. 8. Each selector rule writes `3` slots: selector-set length, selector-set value, and selector-set position. 9. Each recipient-constrained selector writes `1` additional slot for recipient-set length. 10. Each recipient entry writes `2` slots: recipient-set value and recipient-set position. ```text gas_key_authorization_new = gas_key_authorization_existing + SSTORE_SET * scope_slots scope_slots = 0 if allowed_calls is None = 1 if allowed_calls is Some([]) // explicit restricted-mode marker = 1 + 3*S + 3*K + C + 2*W if allowed_calls is Some(scopes) ``` Justification for `1 + 3*S + 3*K + C + 2*W`: `1` stores restricted mode, each target scope materializes as three set writes, each selector rule materializes as three set writes, each constrained selector writes one recipient-set length slot, and each recipient writes two set-membership slots. #### Rounded Helper Overhead Implementations may also charge a small rounded helper overhead for scoped-key authorization bookkeeping that is not captured by raw storage-row counts alone. This overhead exists because fresh scope persistence includes additional bookkeeping such as clearing the empty scope tree, maintaining per-layer set metadata, and materializing recipient sets. Tempo's T4 implementation rounds this overhead upward using the same scope cardinalities: ```text extra_scope_gas = 5_000 + 7_000*S + 7_000*K + 5_000*W ``` This rounding is intentional. The design goal is to avoid materially underpricing larger scope trees while keeping pricing simple and predictable; slight overcharging is acceptable. Bounds: 1. Implementations MUST reject any selector rule with a non-empty `recipients` list whose `target` is not a TIP-20 token address. 2. Implementations MUST reject any selector rule with a non-empty `recipients` list and selector outside the fixed constrained-selector set. 3. Implementations MUST reject duplicate selector rules for the same `(target, selector)`. 4. Implementations MUST reject duplicate recipients inside a selector rule. No additional flat gas is specified here for precompile methods (`setAllowedCalls`, `getAllowedCalls`, etc.); those are charged by normal EVM metering at execution time. ### Encoding #### Signing Format Authorization digest format: ```text key_auth_digest = keccak256(rlp([ chain_id, key_type, key_id, expiry?, limits?, allowed_calls? ])) limits = rlp([token, limit]) if period == 0 = rlp([token, limit, period]) if period > 0 ``` RLP safety note: 1. Implementations MUST use canonical RLP encoding for all fields. 2. The signed payload is a typed RLP list; distinct field tuples produce distinct canonical encodings (no cross-field preimage ambiguity under canonical RLP). #### Transaction Authorization RLP ```text KeyAuthorization := RLP([ chain_id: u64, key_type: u8, key_id: address, expiry?: uint64, limits?: [TokenLimit, ...], allowed_calls?: [CallScope, ...] ]) TokenLimit := RLP([ token: address, limit: uint256, period: uint64 ]) // Canonical one-time form omits `period` entirely. // Omitted `period` decodes to `period = 0`, i.e. a non-periodic one-time spending limit. TokenLimit(one-time) := RLP([ token: address, limit: uint256 ]) CallScope := RLP([ target: address, selector_rules: [SelectorRule, ...] | [] ]) SelectorRule := RLP([ selector: bytes4, recipients: [address, ...] | [] ]) ``` Optional encoding rules: 1. Optional scalar fields (`expiry`) use `None => 0x80`. 2. `limits = None` uses `0x80`. 3. Top-level `allowed_calls = None` is canonically omitted on wire. Implementations MUST also accept explicit `0x80` for `allowed_calls = None` as equivalent non-canonical input. 4. Nested scope-list fields (`selector_rules` and `recipients`) are always encoded explicitly. Allow-all uses RLP empty list (`0xc0`). 5. Non-empty list values encode as normal lists. 6. For `TokenLimit`, one-time limits (`period == 0`) canonically use the two-field form. Implementations MUST also accept the explicit three-field form with `period = 0` as equivalent non-canonical input. 7. Each `SelectorRule.selector` MUST decode to exactly 4 bytes; otherwise the authorization MUST be rejected. *** ### Precompile Storage Changes Current layout: 1. `keys[account][keyId] -> AuthorizedKey` 2. `spending_limits[(account,keyId)][token] -> U256` Additive periodic-limit layout: | Mapping | Type | Description | |---------|------|-------------| | `spending_limits[account_key][token]` | `U256` | Remaining amount / `remainingInPeriod` | | `spending_limit_period_state[account_key][token]` | struct `{ max, period, period_end }` | Periodic limit metadata | Call-scope storage is account-scoped and represented as nested scope membership, with a key-level scoped/unrestricted flag: | Path | Type | Description | |------|------|-------------| | `key_scopes[account_key].is_scoped` | `bool` | Whether the key is unrestricted or uses scoped target membership | | `key_scopes[account_key].targets` | `Set
` | Scoped target membership | | `key_scopes[account_key].target_scopes[target].selectors` | `Set` | Explicit selector membership | | `key_scopes[account_key].target_scopes[target].selector_scopes[selector].recipients` | `Set
` | Selector-level recipient membership | Absent target or selector entries represent disabled inner scopes; implementations do not need separate target-level or selector-level mode bits. `account_key = keccak256(account || key_id)` to avoid cross-account collisions for shared key IDs. Implementations MAY maintain additional internal indexes or equivalent layouts so long as semantics remain unchanged. ### Hardfork-Gated Features The following MUST be fork-gated: 1. New `TokenLimit` decode/encode behavior. 2. `allowed_calls` decode/encode behavior. 3. `selector_rules` decode/encode behavior. 4. Periodic reset logic. 5. Call-scope validation logic. 6. Selector-rule recipient-allowlist calldata validation logic. 7. New precompile storage writes/reads for periodic + call-scope data. 8. New precompile storage writes/reads for selector-level recipient allowlists. 9. Updated precompile read APIs (`getAllowedCalls(account,key)`, richer `getRemainingLimit`). 10. New mutator function `setAllowedCalls`, which can only be called by root key. 11. Global ban on contract creation when using access keys. 12. Selector-width enforcement (`selector length == 4` only). 13. Constrained-selector allowlist and argument-0 canonical address checks. 14. TIP-20 target verification for selector rules with recipient allowlists. Pre-fork blocks MUST replay with pre-fork semantics to preserve state roots. *** ## Invariants 1. `periodEnd` is monotonic and never set to the past. 2. `remainingInPeriod <= limit` after any operation. 3. Expiry check runs before spending and call-scope checks. 4. If `key != Address::ZERO`, any contract-creation call MUST cause the transaction to be rejected as invalid before execution, regardless of `allowed_calls`. 5. `allowed_calls = None` allows all non-create calls; `allowed_calls = Some(...)` requires target+selector-rule match and otherwise causes the transaction to be rejected as invalid before execution. 6. In scoped mode, calldata must contain at least 4 selector bytes only when explicit selector matching is required; address-only scopes allow selectorless/fallback-style calldata. 7. For each `(account, key)`, target scopes are unique, selector rules are unique per target, and recipients are unique per selector rule. 8. `setAllowedCalls(..., scope.selectorRules = [])` allows any selector on that target; `removeAllowedCalls(keyId, target)` disables that target scope. 9. Selector rules with recipient allowlists are valid only for TIP-20 targets and only for the fixed constrained selector set. 10. For recipient-allowlisted rules, calldata argument `0` must be a canonically encoded ABI address and must be in the configured recipient set. 11. In the Solidity ABI, `selectorRules[i].recipients = []` means that selector has no recipient restriction. ### Test Cases 1. Periodic reset after elapsed period. 2. No rollover of unused periodic allowance. 3. Address + multi-selector scope allow. 4. Address-only allow (`selector_rules=[]`). 5. Deny when no scope matches. 6. `allowed_calls=None` allows all non-create calls. 7. `allowed_calls=Some([])` denies all calls. 8. Mixed one-time and periodic token limits. 9. Existing keys continue to function after the fork. 10. Batch validation rejects the transaction before execution when any call is invalid. 11. Shared key IDs across accounts cannot overwrite each other’s scopes. 12. Reject calls that do not provide at least 4 selector bytes when explicit selector matching is required. 13. `setAllowedCalls(..., scope.selectorRules = [])` allows any selector on that target. 14. `setAllowedCalls` create-or-replace semantics are enforced. 15. `removeAllowedCalls(keyId, target)` removes that target scope; if no target scopes remain, the key stays scoped but matches no calls. 16. Address-only scopes allow selectorless/fallback-style calls to the scoped target. 17. Access-key transactions with CREATE as first call are rejected. 18. Access-key transactions with any CREATE in a batch are rejected. 19. For constrained TIP-20 selectors (`transfer`, `approve`, `transferWithMemo`), calls succeed iff calldata argument `0` is in the configured recipient set. 20. Single-recipient and multi-recipient selector rules both enforce the same membership rule. 21. Reject the transaction before execution when a selector rule with a recipient allowlist is matched and calldata is shorter than `4 + 32` bytes. 22. Reject the transaction before execution when a selector rule with a recipient allowlist is matched and ABI argument `0` is not canonically encoded as an address. 23. Reject selector rules with recipient allowlists for selectors outside the fixed constrained-selector set. 24. Reject duplicate selector rules for the same `(target, selector)`. 25. Reject duplicate recipients within a selector rule. 26. Reject key authorization when selector rules with recipient allowlists are used on a non-TIP-20 target. ### References * [AccountKeychain docs](https://tempo.xyz/developers/docs/protocol/transactions/AccountKeychain) * [Tempo Transactions](https://tempo.xyz/developers/docs/guide/tempo-transaction) * [IAccountKeychain.sol](https://github.com/tempoxyz/tempo-std/blob/master/src/interfaces/IAccountKeychain.sol) * [GitHub Issue #1865](https://github.com/tempoxyz/tempo/issues/1865) - Periodic spending limits * [GitHub Issue #1491](https://github.com/tempoxyz/tempo/issues/1491) - Destination address scoping # TIP-1015: Compound Transfer Policies ## Abstract This TIP extends the TIP-403 policy registry to support **compound policies** that allow token issuers to specify different authorization rules for senders, recipients, and mint recipients. A compound policy references three simple policies: one for sender authorization, one for recipient authorization, and one for mint recipient authorization. Compound policies are structurally immutable once created — their constituent policy ID references cannot be changed. However, the referenced simple policies themselves remain mutable and can be modified by their respective admins, which will affect the compound policy's effective authorization behavior. ## Motivation The current TIP-403 system applies the same policy to both senders and recipients of a token transfer. However, real-world requirements often differ between sending and receiving: * **Vendor credits**: A business may issue credits that can be minted to anyone and spent by holders to a specific vendor, but cannot be transferred peer-to-peer. This requires allowing all addresses as recipients (for minting) while restricting senders to only transfer to the vendor's address. * **Sender restrictions**: An issuer may want to block sanctioned addresses from sending tokens, while allowing anyone to receive tokens (e.g., for refunds or seizure). * **Recipient restrictions**: An issuer may require recipients to be KYC-verified, while allowing any holder to send tokens out. * **Asymmetric compliance**: Different jurisdictions may have different requirements for inflows vs outflows. Compound policies enable these use cases while maintaining backward compatibility with existing simple policies. *** ## Specification ### Policy Types TIP-403 currently supports two policy types: `WHITELIST` and `BLACKLIST`. This TIP adds a third type: ```solidity enum PolicyType { WHITELIST, BLACKLIST, COMPOUND } ``` ### Compound Policy Structure A compound policy references three existing simple policies by their policy IDs: ```solidity struct CompoundPolicyData { uint64 senderPolicyId; // Policy checked for transfer senders uint64 recipientPolicyId; // Policy checked for transfer recipients uint64 mintRecipientPolicyId; // Policy checked for mint recipients } ``` All three referenced policies MUST be simple policies (WHITELIST or BLACKLIST), not compound policies. This prevents circular references and unbounded recursion. ### Storage Layout Policy data is stored in a unified `PolicyRecord` struct that contains both base policy data and compound policy data: ```solidity struct PolicyData { uint8 policyType; // 0 = WHITELIST, 1 = BLACKLIST, 2 = COMPOUND address admin; // Policy administrator (zero for compound policies — compound structure is immutable) } struct PolicyRecord { PolicyData base; // offset 0: base policy data CompoundPolicyData compound; // offset 1: compound policy data (only used when policyType == COMPOUND) } ``` The TIP403Registry storage layout: | Slot | Field | Description | |------|-------|-------------| | 0 | `policyIdCounter` | Counter for generating unique policy IDs | | 1 | `policyRecords` (private) | `mapping(uint64 => PolicyRecord)` - Policy ID to policy record | | 2 | `policySet` | `mapping(uint64 => mapping(address => bool))` - Whitelist/blacklist membership | The `policyRecords` mapping is private (not exposed in the ABI). The existing `policyData(uint64 policyId)` view function provides backwards-compatible access to `PolicyData`. For a given policy ID, storage locations are: * **PolicyData**: `keccak256(policyId, 1)` (offset 0 within PolicyRecord) * **CompoundPolicyData**: `keccak256(policyId, 1) + 1` (offset 1 within PolicyRecord) This unified layout requires only **1 keccak computation + 2 SLOADs** for compound policy authorization, compared to 2 keccak computations with separate mappings. ### Interface Additions The TIP403Registry interface is extended with the following: ```solidity interface ITIP403Registry { // ... existing interface ... // ========================================================================= // Compound Policy Creation // ========================================================================= /// @notice Creates a new compound policy (structurally immutable — references cannot be changed after creation) /// @param senderPolicyId Policy ID to check for transfer senders /// @param recipientPolicyId Policy ID to check for transfer recipients /// @param mintRecipientPolicyId Policy ID to check for mint recipients /// @return newPolicyId ID of the newly created compound policy /// @dev All three policy IDs must reference existing simple policies (not compound). /// Compound policy references are immutable — the constituent policy IDs cannot be changed after creation. /// Note: the referenced simple policies themselves remain mutable by their admins. /// Emits CompoundPolicyCreated event. function createCompoundPolicy( uint64 senderPolicyId, uint64 recipientPolicyId, uint64 mintRecipientPolicyId ) external returns (uint64 newPolicyId); // ========================================================================= // Sender/Recipient Authorization // ========================================================================= /// @notice Checks if a user is authorized as a sender under the given policy /// @param policyId Policy ID to check against /// @param user Address to check /// @return True if authorized to send, false otherwise /// @dev For simple policies: equivalent to isAuthorized() /// For compound policies: checks against the senderPolicyId function isAuthorizedSender(uint64 policyId, address user) external view returns (bool); /// @notice Checks if a user is authorized as a recipient under the given policy /// @param policyId Policy ID to check against /// @param user Address to check /// @return True if authorized to receive, false otherwise /// @dev For simple policies: equivalent to isAuthorized() /// For compound policies: checks against the recipientPolicyId function isAuthorizedRecipient(uint64 policyId, address user) external view returns (bool); /// @notice Checks if a user is authorized as a mint recipient under the given policy /// @param policyId Policy ID to check against /// @param user Address to check /// @return True if authorized to receive mints, false otherwise /// @dev For simple policies: equivalent to isAuthorized() /// For compound policies: checks against the mintRecipientPolicyId function isAuthorizedMintRecipient(uint64 policyId, address user) external view returns (bool); // ========================================================================= // Compound Policy Queries // ========================================================================= /// @notice Returns the constituent policy IDs for a compound policy /// @param policyId ID of the compound policy to query /// @return senderPolicyId Policy ID for sender checks /// @return recipientPolicyId Policy ID for recipient checks /// @return mintRecipientPolicyId Policy ID for mint recipient checks /// @dev Reverts if policyId is not a compound policy function compoundPolicyData(uint64 policyId) external view returns ( uint64 senderPolicyId, uint64 recipientPolicyId, uint64 mintRecipientPolicyId ); // ========================================================================= // Events // ========================================================================= /// @notice Emitted when a new compound policy is created /// @param policyId ID of the newly created compound policy /// @param creator Address that created the policy /// @param senderPolicyId Policy ID for sender checks /// @param recipientPolicyId Policy ID for recipient checks /// @param mintRecipientPolicyId Policy ID for mint recipient checks event CompoundPolicyCreated( uint64 indexed policyId, address indexed creator, uint64 senderPolicyId, uint64 recipientPolicyId, uint64 mintRecipientPolicyId ); // ========================================================================= // Errors // ========================================================================= /// @notice The referenced policy is not a simple policy error PolicyNotSimple(); /// @notice The referenced policy does not exist error PolicyNotFound(); } ``` ### Authorization Logic #### isAuthorizedSender ```solidity function isAuthorizedSender(uint64 policyId, address user) external view returns (bool) { PolicyRecord storage record = policyRecords[policyId]; if (record.base.policyType == PolicyType.COMPOUND) { return isAuthorized(record.compound.senderPolicyId, user); } // For simple policies, sender authorization equals general authorization return isAuthorized(policyId, user); } ``` #### isAuthorizedRecipient ```solidity function isAuthorizedRecipient(uint64 policyId, address user) external view returns (bool) { PolicyRecord storage record = policyRecords[policyId]; if (record.base.policyType == PolicyType.COMPOUND) { return isAuthorized(record.compound.recipientPolicyId, user); } // For simple policies, recipient authorization equals general authorization return isAuthorized(policyId, user); } ``` #### isAuthorizedMintRecipient ```solidity function isAuthorizedMintRecipient(uint64 policyId, address user) external view returns (bool) { PolicyRecord storage record = policyRecords[policyId]; if (record.base.policyType == PolicyType.COMPOUND) { return isAuthorized(record.compound.mintRecipientPolicyId, user); } // For simple policies, mint recipient authorization equals general authorization return isAuthorized(policyId, user); } ``` #### isAuthorized (updated) The existing `isAuthorized` function is updated to check both sender and recipient authorization: ```solidity function isAuthorized(uint64 policyId, address user) external view returns (bool) { return isAuthorizedSender(policyId, user) && isAuthorizedRecipient(policyId, user); } ``` This maintains backward compatibility: for simple policies both functions return the same result, so `isAuthorized` behaves identically to before. For compound policies, `isAuthorized` returns true only if the user is authorized as both sender and recipient. ### Required Code Changes This TIP requires exactly 6 replacements of `isAuthorized` calls: #### Direct Replacements | Location | Current | Replace With | |----------|---------|--------------| | TIP-20 `_mint` | `isAuthorized(to)` | `isAuthorizedMintRecipient(to)` | | TIP-20 `burnBlocked` | `isAuthorized(from)` | `isAuthorizedSender(from)` | | DEX `cancelStaleOrder` | `isAuthorized(maker)` | `isAuthorizedSender(maker)` | | Fee payer `can_fee_payer_transfer` | `isAuthorized(fee_payer)` | `isAuthorizedSender(fee_payer)` | #### Core Authorization Logic | Location | Current | Replace With | |----------|---------|--------------| | TIP-20 `isTransferAuthorized` | `isAuthorized(from)` | `isAuthorizedSender(from)` | | TIP-20 `isTransferAuthorized` | `isAuthorized(to)` | `isAuthorizedRecipient(to)` | All other call sites use `ensureTransferAuthorized(from, to)` which delegates to `isTransferAuthorized`, so they automatically inherit the correct behavior: * **TIP-20**: `transfer`, `transferFrom`, `transferWithMemo`, `systemTransferFrom` * **TIP-20 Rewards**: `distributeReward`, `setRewardRecipient`, `claimRewards` * **Stablecoin DEX**: `decrementBalanceOrTransferFrom`, `placeLimitOrder`, `swapExactAmountIn` ### TIP-20 Integration TIP-20 tokens MUST be updated to use the new sender/recipient authorization functions: #### Transfer Authorization (isTransferAuthorized) ```solidity function isTransferAuthorized(address from, address to) internal view returns (bool) { uint64 policyId = transferPolicyId; bool fromAuthorized = TIP403_REGISTRY.isAuthorizedSender(policyId, from); bool toAuthorized = TIP403_REGISTRY.isAuthorizedRecipient(policyId, to); return fromAuthorized && toAuthorized; } ``` #### Mint Operations Mint operations check the mint recipient policy: ```solidity function _mint(address to, uint256 amount) internal { if (!TIP403_REGISTRY.isAuthorizedMintRecipient(transferPolicyId, to)) { revert PolicyForbids(); } // ... mint logic } ``` #### Burn Blocked Operations The `burnBlocked` function checks sender authorization to verify the address is blocked: ```solidity function burnBlocked(address from, uint256 amount) external { require(hasRole(BURN_BLOCKED_ROLE, msg.sender)); // Only allow burning from addresses blocked from sending if (TIP403_REGISTRY.isAuthorizedSender(transferPolicyId, from)) { revert PolicyForbids(); } // ... burn logic } ``` ### Stablecoin DEX Integration #### Cancel Stale Order The `cancelStaleOrder` function checks sender authorization on the token escrowed by the maker, since if the order is filled, the maker will have to send that token: ```solidity function cancelStaleOrder(uint128 orderId) external { Order order = orders[orderId]; address token = order.isBid() ? book.quote : book.base; uint64 policyId = TIP20(token).transferPolicyId(); // Order is stale if maker can no longer send the escrowed token if (TIP403_REGISTRY.isAuthorizedSender(policyId, order.maker())) { revert OrderNotStale(); } _cancelOrder(order); } ``` ### Mutability Compound policies are **structurally immutable** once created — their constituent policy ID references cannot be changed, and they have no admin. However, the referenced simple policies remain independently mutable by their respective admins. Modifications to a referenced simple policy's whitelist or blacklist will immediately affect the authorization behavior of any compound policy that references it. To change which simple policies a compound policy references, token issuers must: 1. Create a new compound policy with the desired configuration 2. Update the token's `transferPolicyId` to the new policy To modify authorization behavior without changing the compound policy itself, the admin of a referenced simple policy can modify that simple policy's whitelist or blacklist directly. ### Backward Compatibility This TIP is fully backward compatible: * Existing simple policies continue to work unchanged * Tokens using simple policies will see identical behavior (since `isAuthorizedSender` and `isAuthorizedRecipient` return the same result for simple policies) * The existing `isAuthorized` function continues to work for both simple and compound policies *** ## Invariants 1. **Simple Policy Constraint**: All three policy IDs in a compound policy MUST reference simple policies (WHITELIST or BLACKLIST). Compound policies cannot reference other compound policies. 2. **Structural Immutability**: Once created, a compound policy's constituent policy ID references cannot be changed. The compound policy itself has no admin. Note that the referenced simple policies remain mutable by their respective admins. 3. **Existence Check**: `createCompoundPolicy` MUST revert if any of the referenced policy IDs does not exist. 4. **Delegation Correctness**: For simple policies, `isAuthorizedSender(p, u)` MUST equal `isAuthorizedRecipient(p, u)` MUST equal `isAuthorizedMintRecipient(p, u)`. 5. **isAuthorized Equivalence**: `isAuthorized(p, u)` MUST equal `isAuthorizedSender(p, u) && isAuthorizedRecipient(p, u)`. 6. **Built-in Policy Compatibility**: Compound policies MAY reference built-in policies (0 = always-reject, 1 = always-allow) as any of their constituent policies. 7. **Non-existent Policy Revert**: All authorization functions (`isAuthorized`, `isAuthorizedSender`, `isAuthorizedRecipient`, `isAuthorizedMintRecipient`) MUST revert with `PolicyNotFound()` when called with a policy ID that does not exist. Built-in policies (0 and 1) always exist and are exempt from this check. ### Test Cases 1. **Simple policy equivalence**: Verify that for simple policies, all four authorization functions return the same result. 2. **Compound policy creation**: Verify that compound policies can be created with valid simple policy references. 3. **Invalid creation**: Verify that `createCompoundPolicy` reverts when referencing non-existent policies or compound policies. 4. **Sender/recipient differentiation**: Verify that a compound policy with different sender/recipient policies correctly authorizes asymmetric transfers. 5. **isAuthorized behavior**: Verify that `isAuthorized` on a compound policy returns `isAuthorizedSender() && isAuthorizedRecipient()`. 6. **TIP-20 mint**: Verify that mints check `isAuthorizedMintRecipient`, not `isAuthorizedRecipient`. 7. **TIP-20 burnBlocked**: Verify that burnBlocked checks sender authorization (and allows burning from blocked senders). 8. **Vendor credits**: Verify that a compound policy with `mintRecipientPolicyId = 1` (always-allow), `senderPolicyId = 1` (always-allow), and `recipientPolicyId = vendor whitelist` allows minting to anyone but only transfers to vendors. # TIP-1016: Exempt Storage Creation from Gas Limits ## Abstract Storage creation operations (new state elements, account creation, contract code storage) continue to consume and be charged for gas, this gas does not count against block gas limit but it is capped by max tx gas limit [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825). Gas accounting uses a **reservoir model** (aligned with [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037)) that splits gas into regular and reservoir gas, ensuring the `GAS` opcode accurately reflects the regular execution budget. This allows increasing contract code pricing to 2,500 gas/byte without preventing large contract deployments, and prevents new account creation from reducing effective throughput. ## Motivation TIP-1000 increased storage creation costs to 250,000 gas per operation and 1,000 gas/byte for contract code. This created two problems: 1. **Contract deployment constraints**: 24KB contracts require ~26M gas, forcing us to: * Keep transaction gas cap at 30M (would prefer 16M) * Keep general gas limit at 30M (would prefer lower) * Limit contract code to 1,000 gas/byte (would prefer 2,500) 2. **New account throughput penalty**: TIP-20 transfer to new address costs ~300,000 gas total (~70k regular + 230k state) vs ~50,000 gas to existing. At 500M payment lane gas limit: * Without exemption (single dimension): only ~1,700 new account transfers/block = ~3,400 TPS * With reservoir model (block limits apply to regular gas only): ~7,150 new account transfers/block = ~14,300 TPS * Existing account transfers: ~10,000 transfers/block = ~20,000 TPS * \~4x throughput improvement for new accounts by exempting state gas from block limits The root cause: state gas counts against limits designed for execution time constraints. Storage creation is permanent (disk) not ephemeral (CPU), and shouldn't be bounded by per-block execution limits. ### Why a reservoir model Simply exempting state gas from protocol limits without changing EVM internals creates two problems: 1. **`GAS` opcode inaccuracy**: The `GAS` opcode would return remaining gas from `tx.gas` minus all gas consumed (regular + state), which doesn't reflect the actual regular gas budget. A transaction with a high gas limit that has used 15.9M regular gas with a 16M EIP-7825 per-transaction gas limit would see `GAS` report millions of gas remaining, but OOG after just ~100k more regular gas. 2. **Broken gas patterns**: Contracts relying on `gasleft()` for loop guards, subcall gas forwarding (63/64 rule), and relay/meta-transaction patterns would see incorrect values, potentially leading to unexpected OOG reverts. The reservoir model (from [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037)) solves this by maintaining three internal counters: * regular `remaining` gas is reflecting execution budget, used by cpu and state creation. Returned by `GAS` opcode. * `reservoir` is holding overflow can be only be used for state creation * `state_gas` is tracking cumulative state gas consumed during execution. *** ## Specification ### Gas Dimensions All operations consume gas in two dimensions: * **Regular gas** (`regular_gas`): Compute, memory, calldata, and the computational cost of storage operations (writing, hashing). This is the execution-time resource. * **State gas** (`state_gas`): The permanent storage burden of state creation operations. This is the long-term state growth resource. At the transaction level, the user pays for both. At the block level, only regular gas counts toward block and EIP-7825 max transaction gas limits; state gas is exempt. ### Storage Gas Operations Storage creation operations split their cost between regular gas (computational overhead) and state gas (permanent storage burden): | Operation | Execution Gas | Storage Gas | Total | |-----------|---------------|-------------|-------| | Cold SSTORE (zero → non-zero) | 22,200 | 230,000 | 252,200 | | Hot SSTORE (non-zero → non-zero) | 2,900 | 0 | 2,900 | | Account creation (nonce 0 → 1) | 25,000 | 225,000 | 250,000 | | Contract code storage (per byte) | 200 | 2,300 | 2,500 | | Contract creation (fixed upfront cost) | 32,000 | 468,000 | 500,000 | | EIP-7702 delegation (per auth) | 25,000 | 225,000 | 250,000 | For zero-to-non-zero `SSTORE`, Tempo keeps revm's decomposed Berlin accounting: `GAS_WARM_ACCESS` (100) plus `sstore_set_without_load_cost` (20,000), for a 20,100 regular-gas write path. When the slot is cold, the existing Berlin cold-slot access charge (`GAS_COLD_SLOAD = 2,100`) is retained on top of that write component, for a total of 22,200 regular gas before state gas. #### EIP-7702 Delegation Pricing Each EIP-7702 authorization writes a 23-byte delegation designator (`0xef0100 || address`) to the authority account's code field. This is permanent state: redelegation overwrites the account's code pointer but the old code entry persists in the code database. The base cost per authorization is **25,000 regular gas + 225,000 state gas = 250,000 total**, matching account creation. This reverts the TIP-1000 reduction to 12,500 gas per authorization. For authorizations where `auth.nonce == 0` (new account), the account creation cost (25,000 regular + 225,000 state) applies in addition to the delegation cost, for a total of 500,000 gas. #### Keychain Authorization Pricing Keychain `authorize_key` is charged as intrinsic gas (T1B+). The SSTORE components use the same regular/state split as standard EVM SSTOREs: | Component | Regular Gas | State Gas | Notes | |-----------|-------------|-----------|-------| | Signature verification | 3,000+ | 0 | ecrecover + P256/WebAuthn if applicable | | Existing key check (SLOAD) | 2,100 | 0 | Cold SLOAD | | Key slot write (SSTORE) | 20,000 | 230,000 | Zero-to-non-zero write component only; cold-slot access charged separately | | Per spending limit (SSTORE × N) | 20,000 × N | 230,000 × N | Zero-to-non-zero write component only per token limit; cold-slot access charged separately | | Buffer (TSTORE, keccak, event) | 2,000 | 0 | Computational overhead | **Total per authorization:** ~27,100 + 20,000 × N regular gas, 230,000 × (1 + N) state gas. The table above isolates the write component itself. Any first access to a cold storage slot still incurs the standard Berlin cold-access charge separately. #### Precompile and Intrinsic Storage Operations The regular/state gas split applies uniformly to all SSTORE and code deposit operations regardless of call site. Precompile storage operations route through the same path as standard EVM SSTOREs and inherit the split automatically. Intrinsic gas charges that include SSTORE costs (e.g. keychain authorization) use the same split. **Exception:** Expiring nonce writes (TIP-1009) use `WARM_SSTORE_RESET` (2,900 gas) with zero state gas because they are ephemeral — entries are evicted from a fixed-size circular buffer and do not contribute to permanent state growth. **Notes:** * Regular gas reflects computational cost (writing, hashing) and counts toward protocol limits * State gas reflects permanent storage burden and does NOT count toward protocol limits * All gas (regular + state) counts toward user's `gas_limit` and is charged at `base_fee_per_gas` * All other operations (non-state-creating) are charged entirely as regular gas * Regular gas is set to at least the pre-TIP-1000 (standard EVM) cost for each operation, ensuring that exempting state gas from limits never makes an operation cheaper against protocol limits than it was before TIP-1000 ### Transaction Validation Before transaction execution, `calculate_intrinsic_cost` returns three values: * `intrinsic_regular_gas`: Base transaction cost, calldata, access lists, and other non-state-creating intrinsic costs * `intrinsic_state_gas`: State gas components of intrinsic cost (e.g., account creation for contract deployment transactions) * `calldata_floor_gas_cost`: The [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623) calldata floor, defined as `TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata + 21000` `validate_transaction` rejects transactions where: ``` tx.gas < intrinsic_regular_gas + intrinsic_state_gas ``` or where: ``` max(intrinsic_regular_gas, calldata_floor_gas_cost) > max_transaction_gas_limit ``` The `max` ensures that calldata-heavy transactions cannot pass validation when their floor cost exceeds the per-transaction regular gas limit. The calldata floor is a regular gas concept — it does not interact with `intrinsic_state_gas` or `state_gas_reservoir`. `validate_transaction` also returns `intrinsic_regular_gas`, `intrinsic_state_gas`, and `calldata_floor_gas_cost`. ### Transaction-Level Gas Accounting (Reservoir Model) Since transactions have a single gas limit parameter (`tx.gas`), gas accounting is enforced through a **reservoir model**, in which `gas_left` and `state_gas_reservoir` are initialized as follows: ```python intrinsic_gas = intrinsic_regular_gas + intrinsic_state_gas execution_gas = tx.gas - intrinsic_gas regular_gas_budget = max_transaction_gas_limit - intrinsic_regular_gas gas_left = min(regular_gas_budget, execution_gas) state_gas_reservoir = execution_gas - gas_left ``` The `state_gas_reservoir` holds gas that exceeds the per-transaction regular gas budget (`max_transaction_gas_limit`, per EIP-7825). The two counters operate as follows: * **Regular gas** charges deduct from `gas_left` only. * **State gas** charges deduct from `state_gas_reservoir` first; when the reservoir is exhausted, from `gas_left`. * When an opcode requires both regular and state gas, the regular gas charge MUST be applied first. If the regular gas charge triggers an out-of-gas error, the state gas charge is not applied. * The **`GAS` opcode** returns `gas_left` only (excluding the reservoir). * The reservoir is passed **in full** to child frames (no 63/64 rule). On child success, the remaining `state_gas_reservoir` is returned to the parent. * On child **revert** or **exceptional halt**, all state gas consumed by the child, both from the reservoir and any that spilled into `gas_left`, is restored to the parent's reservoir. On child **exceptional halt**, only `gas_left` is consumed (zeroed). State gas is fully preserved on failure because state changes are reverted, so no state was actually grown. * **Note**: State gas that originally spilled from the reservoir into `gas_left` is restored as reservoir gas, not as `gas_left`. A child frame that performs cold SSTOREs drawing from `gas_left` (because the reservoir was exhausted) and then reverts will return that gas to the parent's reservoir, where it can only be used for future state operations — not for regular execution. This is a known consequence of the EIP-8037 design that avoids tracking the original source of state gas charges per frame. The effect is bounded: it can only convert `gas_left` that was spent on state operations into reservoir gas, and only on child failure paths. * On **exceptional halt**, remaining `gas_left` is attributed to `execution_regular_gas_used` and set to zero (all regular gas consumed), consistent with existing EVM out-of-gas semantics. The `state_gas_reservoir` is not consumed — it is returned to the parent frame or preserved at the top level, consistent with the principle that state gas pays for long-term state growth which does not occur on failure. * **System transactions** are not subject to the `max_transaction_gas_limit` cap; their entire `execution_gas` is placed in `gas_left` with `state_gas_reservoir = 0`. The two counters are returned by the transaction output. Besides the two counters, the EVM also keeps track of `execution_state_gas_used` and `execution_regular_gas_used` during block execution. `state_gas` costs are added to `execution_state_gas_used` while `regular_gas` costs are added to `execution_regular_gas_used`. These two counters are also returned by the transaction output. ### Transaction Gas Used At the end of transaction execution, the gas used before and after refunds is defined as: ```python tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir tx_gas_refund = min(tx_gas_used_before_refund // 5, tx_output.refund_counter) tx_gas_used_after_refund = max( tx_gas_used_before_refund - tx_gas_refund, calldata_floor_gas_cost ) ``` The refund cap remains at 20% of gas used. The `max` with `calldata_floor_gas_cost` ([EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)) ensures the user always pays at least the calldata floor, even if refunds would bring the total below it. Refunds apply only to user-paid gas; block-level accounting uses `tx_regular_gas` (regular gas only, no refund subtracted) — see [Block-Level Gas Accounting](#block-level-gas-accounting). **Note**: EIP-8037 uses `tx_gas_used` in the refund and post-refund formulas, but that variable is not defined in the same code block. TIP-1016 uses `tx_gas_used_before_refund` consistently to avoid ambiguity. ### Block-Level Gas Accounting At block level, only **regular gas** counts toward block gas limits. State gas is exempt — it is not tracked at the block level and does not constrain block capacity. ```python tx_regular_gas = intrinsic_regular_gas + tx_output.execution_regular_gas_used block_output.block_regular_gas_used += max(tx_regular_gas, calldata_floor_gas_cost) ``` The `max` with `calldata_floor_gas_cost` ([EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)) ensures calldata-heavy transactions consume at least the floor cost worth of block capacity. The floor applies to regular gas only — state gas remains fully exempt from block limits. Per [EIP-7778](https://eips.ethereum.org/EIPS/eip-7778), `tx_regular_gas` is the pre-refund value: `tx_gas_refund` is **not** subtracted from block accounting. This prevents block gas limit circumvention via refundable operations while preserving user incentives to clean up state. The block header `gas_used` field is set to: ```python gas_used = block_output.block_regular_gas_used ``` The block validity condition uses this value: ```python assert gas_used <= block.gas_limit, 'invalid block: too much gas used' ``` The base fee update rule uses this same value: ```python gas_used_delta = parent.gas_used - parent.gas_target ``` **Note**: Tempo has two block limits — general gas limit (~25M) for contracts and payment lane limit (500M) for simple transfers. In both lanes, only regular gas counts toward the limit; state gas is exempt. **Divergence from EIP-8037**: EIP-8037 uses a bottleneck model where `gas_used = max(block_regular_gas, block_state_gas)`, effectively capping state gas at the block gas limit. TIP-1016 instead exempts state gas entirely from block limits, relying on fixed high prices (250,000 gas per state element) as the economic deterrent for state growth. ### SSTORE Refund for Slot Restoration When a storage slot is set to a non-zero value and then restored to zero within the same transaction (0→X→0 pattern), the following are refunded via `refund_counter`: * State gas: 230,000 (the full state creation charge; EIP-8037 equivalent: `32 × cost_per_state_byte`) * Regular gas: `GAS_STORAGE_UPDATE - GAS_COLD_SLOAD - GAS_WARM_ACCESS` (EIP-8037 equivalent: 2,800; Tempo: 20,000 − 2,100 − 100 = 17,800) The refund mechanism is identical to EIP-8037. The numeric values differ because Tempo uses fixed pricing (see Storage Gas Operations table) rather than EIP-8037's dynamic `cost_per_state_byte`. The net cost after refund is `GAS_WARM_ACCESS` (100), consistent with pre-EIP-8037 `SSTORE` restoration behavior. Refunds use `refund_counter` rather than direct gas accounting decrements, so that reverted frames do not benefit from the refund. ### Revert Behavior for State Gas State gas charged for account creation (`CREATE`, `CALL` to new account, and EOA delegation) is consumed even if the frame reverts — state changes are rolled back but gas is not refunded. This is consistent with pre-EIP-8037 behavior where `GAS_NEW_ACCOUNT` was consumed on revert. This is achieved structurally: `GAS_NEW_ACCOUNT` state gas is charged in the **parent frame** before creating the child frame. On child revert, `handle_reservoir_remaining_gas` restores only the child's `state_gas_spent` to the parent's reservoir — the parent's prior charge is preserved. Similarly, `GAS_CREATE` state gas for contract deployment is charged in the parent before the child initcode runs. ### Receipt Semantics Receipt `cumulative_gas_used` tracks the cumulative sum of `tx_gas_used_after_refund` (post-refund, post-floor) across transactions. This means `receipt[i].cumulative_gas_used - receipt[i-1].cumulative_gas_used` equals the gas paid by transaction `i`. ### Contract Creation Pricing Contract code storage cost increases from 1,000 to **2,500 gas/byte** (200 regular + 2,300 state). #### Contract Deployment Cost Calculation When a contract creation transaction or opcode (`CREATE`/`CREATE2`) is executed, gas is charged differently based on whether the deployment succeeds or fails. Given bytecode `B` (length `L`) returned by initcode and `H = keccak256(B)`: **When opcode execution starts:** Always charge `GAS_CREATE` (Tempo: 32,000 regular + 468,000 state; EIP-8037: 9,000 regular + `112 × cpsb` state) **During initcode execution:** Charge the actual gas consumed by the initcode execution **Success path** (no error, not reverted, and `L ≤ MAX_CODE_SIZE`): * Charge `GAS_CODE_DEPOSIT * L` (200 regular + 2,300 state per byte) and persist `B` under `H`, then link `codeHash` to `H` **Failure paths** (REVERT, OOG/invalid during initcode, OOG during code deposit, or `L > MAX_CODE_SIZE`): * Do NOT charge `GAS_CODE_DEPOSIT * L` * No code is stored; no `codeHash` is linked to the account * The account remains unchanged or non-existent This is aligned with EIP-8037's deployment flow, where `GAS_CODE_DEPOSIT` is charged only on the success path. #### Example: 24KB Contract Deployment Operation | Regular | State gas \----------|---------|---------- Contract code | `24,576 × 200 = 4,915,200` | `24,576 × 2,300 = 56,524,800` Contract fixed upfront | `32,000` | `468,000` Deployment logic | ~2M | 0 \----------|---------|---------- **Totals:** | ~7M (counts toward protocol limits via `gas_left`) | ~57M (served from `state_gas_reservoir`, doesn't count toward protocol limits) Total gas: ~64M (user must authorize with `gas_limit >= 64M`) **Can deploy with protocol max\_transaction\_gas\_limit = 16M** (only ~7M regular gas counts) ### Examples #### TIP-20 Transfer to New Address * Transfer logic: ~50,000 regular gas * New balance slot: 20,000 regular gas + 230,000 state gas * **Total**: ~70,000 regular gas + 230,000 state gas = ~300,000 gas * User must authorize: `gas_limit >= 300,000` * Counts toward block limit: ~70,000 regular gas * Reservoir initialization (assuming `max_transaction_gas_limit = 16M`): * `intrinsic_gas = intrinsic_regular + intrinsic_state ≈ 21,000 + 0 = 21,000` * `execution_gas = 300,000 - 21,000 = 279,000` * `regular_gas_budget = 16M - 21,000 ≈ 15,979,000` * `gas_left = min(15,979,000, 279,000) = 279,000` * `state_gas_reservoir = 279,000 - 279,000 = 0` * Since total \< `max_transaction_gas_limit`, all gas fits in `gas_left`; state gas draws from `gas_left` * `GAS` opcode accurately reflects execution budget (~279,000 before execution) * Block accounting: adds ~70,000 to `block_regular_gas_used` (state gas is exempt from block limits) * Total cost: ~300,000 gas #### TIP-20 Transfer to Existing Address * Transfer logic: ~50,000 regular gas * Update existing slot: included in transfer logic * **Total**: ~50,000 regular gas * User must authorize: `gas_limit >= 50,000` * Counts toward block limit: ~50,000 regular gas * Total cost: ~50,000 gas #### Block Throughput At 500M payment lane gas limit (only regular gas counts toward block limits): * **New account transfers**: ~70k regular gas each → ~7,150 transfers/block ≈ 14,300 TPS * **Existing account transfers**: ~50k regular gas each → ~10,000 transfers/block ≈ 20,000 TPS * **Mixed workload**: Only regular gas constrains capacity. A block can contain any mix of new and existing transfers as long as total regular gas ≤ 500M. State gas doesn't reduce block capacity. * **vs TIP-1000**: ~7,150 new account transfers/block vs ~1,700 without exemption (~4x improvement) *** ## Invariants 1. **User Authorization**: Total gas used (regular + state) MUST NOT exceed `transaction.gas_limit` (prevents surprise costs) 2. **Protocol Transaction Limit**: Regular gas (via `gas_left`) MUST NOT exceed `max_transaction_gas_limit` (EIP-7825 limit, e.g. 16M) 3. **Protocol Block Limits**: Block `regular_gas` MUST NOT exceed applicable limit: * General transactions: `general_gas_limit` (25M target, currently 30M) * Payment lane transactions: `payment_lane_limit` (500M) 4. **State Gas Exemption**: State gas MUST NOT count toward protocol limits (transaction or block). State gas is uncapped at the block level. 5. **Reservoir Model**: Gas accounting MUST use the reservoir model — `gas_left` and `state_gas_reservoir` initialized from `tx.gas`, with state gas drawing from reservoir first 6. **GAS Opcode**: The `GAS` opcode MUST return `gas_left` only (excluding `state_gas_reservoir`) 7. **Reservoir Passing**: The `state_gas_reservoir` MUST be passed in full to child frames (no 63/64 rule). Unused reservoir MUST be returned to parent on child completion 8. **Exceptional Halt**: On exceptional halt, `gas_left` MUST be set to zero; `state_gas_reservoir` MUST be preserved (returned to parent or kept for refund) 9. **Regular Gas Component**: Storage creation operations MUST charge regular gas for computational overhead (writing, hashing) 10. **Total Cost**: Transaction cost MUST equal `(regular_gas + state_gas) × (base_fee_per_gas + priority_fee)` 11. **Gas Split**: Storage creation operations MUST split cost into regular gas (computational) and state gas (permanent burden) 12. **Hot vs Cold**: Hot SSTORE (non-zero → non-zero) has NO state gas component; cold SSTORE (zero → non-zero) has both 13. **Refund via Counter**: SSTORE slot restoration refunds MUST use `refund_counter`, not direct gas decrements 14. **Revert Behavior**: On child revert or exceptional halt, all state gas consumed by the child MUST be restored to the parent's `state_gas_reservoir`, **except** state gas for account creation (`GAS_NEW_ACCOUNT`) which MUST be consumed even on revert 15. **Regular Gas Floor**: The regular gas component of each storage creation operation MUST be at least the pre-TIP-1000 (standard EVM) cost for that operation (SSTORE: 20,000, account creation: 25,000, CREATE base: 32,000, code deposit: 200/byte) 16. **EIP-7702 Delegation**: Each EIP-7702 authorization MUST charge 25,000 regular gas + 225,000 state gas (250,000 total). Authorizations with `auth.nonce == 0` MUST additionally charge the account creation cost (25,000 regular + 225,000 state) 17. **Precompile Consistency**: All precompile storage operations MUST use the same gas accounting path as standard EVM SSTORE, inheriting the regular/state gas split automatically 18. **Keychain Authorization**: Keychain `authorize_key` intrinsic gas MUST split SSTORE costs using the same regular/state ratio as standard EVM SSTOREs (20,000 regular + 230,000 state per new slot) 19. **Calldata Floor (EIP-7623)**: The calldata floor (`TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata + 21000`) MUST apply to regular gas only — it MUST NOT interact with `state_gas_reservoir`. Transaction validation MUST reject when `max(intrinsic_regular_gas, calldata_floor_gas_cost) > max_transaction_gas_limit`. Post-execution `tx_gas_used_after_refund` and block `regular_gas_used` MUST be at least `calldata_floor_gas_cost` *** ## Alignment with EIP-8037 This TIP adopts the **reservoir model** from [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037) for transaction-level gas accounting, with the following Tempo-specific differences: | Aspect | EIP-8037 | TIP-1016 | |--------|----------|----------| | State gas pricing | Dynamic `cost_per_state_byte` scaling with block gas limit | Fixed costs (e.g., 230,000 per slot) — Tempo uses fixed high prices for state growth protection | | Gas cost harmonization | Harmonizes all state creation to uniform cost-per-byte | Maintains Tempo-specific pricing from TIP-1000 | | Target state growth | 100 GiB/year dynamic target | Economic deterrence via fixed high costs | | Block-level gas accounting | Bottleneck model: `max(block_regular_gas, block_state_gas)` | Regular gas only; state gas fully exempt from block limits | | Block gas limit range | 60M–300M+ (Ethereum L1 scaling) | 25M general + 500M payment lane (Tempo dual-lane) | | Quantization | Top-5 significant bits with offset for `cost_per_state_byte` | Not applicable (fixed costs) | The core EVM mechanism — reservoir model, `GAS` opcode semantics, SSTORE refund/revert behavior, contract deployment flow, and receipt semantics — is shared with EIP-8037, minimizing implementation divergence from upstream. The key divergence is at the block level: TIP-1016 exempts state gas entirely from block limits rather than using EIP-8037's bottleneck model. # ValidatorConfig V2 ## Abstract TIP-1017 defines ValidatorConfig V2, a new precompile for managing consensus participants. V2 improves lifecycle tracking so validator sets can be reconstructed for any epoch, and adds stricter input validation. It is designed to safely support permissionless validator rotation, and additionally allows separation of fee custody from day-to-day validator operations. ## Necessary background information In Tempo, validator information is stored on-chain. This includes which nodes make up the current committee, which nodes are intended to join or leave the committee, and their network information (ingress, egress). Each validator is uniquely identified by its ed25519 public key used for signing all consensus p2p messages. For consensus itself, Tempo employs bls12381 threshold cryptography, where each validator is assigned a private key share corresponding to a section of the network public key. The network key itself is undergoing a re-sharing Distributed Key Generation process every epoch, where each epoch runs for a fixed number of blocks. The outcome of the DKG process is written to last block of an epoch. The DKG outcome contains the validators that made up the committee in epoch `E-1` (called dealers), the validators that will make up the committee in `E` (called players during `E-1`), and the validators that will participate as players in the DKG process during epoch `E` to become committee members in `E+1`. To determine the next players, validators read the contract state at the end of the epoch and select all entries marked as active. The DKG outcome hence determines who the committee members *are*, and the contract states who the committee members *should be*. ## Motivation The original ValidatorConfig precompile (frequently referred to V1 from here on), was too permissive. It allowed addresses to arbitrarily change the values of their entry in the contract, potentially breaking consensus. This and other issues were: 1. **Key ownership verification**: V1 does not verify that the caller controls the private key corresponding to the public key being registered. A malicious validator could hence grief another validator by using their public key, breaking the consensus requirement that all keys be unique. 2. **Validator re-registration**: V1 allows deleted validators to be re-added with different parameters, complicating historical queries. 3. **Historical state dependency**: Because V1 contained a warmup epoch for new validators, and because these were not written to the DKG outcome, to sync a node always needed to keep up to twice the epoch length of blocks around, requiring bloated snapshots and preventing aggressive pruning. Tempo solved problems 1 and 2 by assigning validators entries anonymous addresses. Thus, only the contract owner could change or deactivate entries. ### How V2 solves these problems: * ed25519 signature verification proves key ownership at registration time * fields `addedAtHeight` and `deactivatedAtHeight` are controlled by the contract and cannot be mutated by the owner and allow historical state reconstruction. * Public keys remain reserved forever (even after deactivation) * Addresses are unique among current validators but can be reassigned via `transferValidatorOwnership` ## Specification ### Precompile Address ```solidity address constant VALIDATOR_CONFIG_V2_ADDRESS = 0xCCCCCCCC00000000000000000000000000000001; ``` ### Interface ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; /// @title IValidatorConfigV2 - Validator Config V2 Precompile Interface /// @notice Interface for managing consensus validators with append-only, deactivate-once semantics interface IValidatorConfigV2 { /// @notice Caller is not authorized. error Unauthorized(); /// @notice Active validator address already exists. error AddressAlreadyHasValidator(); /// @notice Public key already exists. error PublicKeyAlreadyExists(); /// @notice Validator was not found. error ValidatorNotFound(); /// @notice Validator is already deactivated. error ValidatorAlreadyDeactivated(); /// @notice Public key is invalid. error InvalidPublicKey(); /// @notice Validator address is invalid. error InvalidValidatorAddress(); /// @notice Ed25519 signature verification failed. error InvalidSignature(); /// @notice Contract is not initialized. error NotInitialized(); /// @notice Contract is already initialized. error AlreadyInitialized(); /// @notice Migration is not complete. error MigrationNotComplete(); /// @notice V1 has no validators to migrate. error EmptyV1ValidatorSet(); /// @notice Migration index is out of order. error InvalidMigrationIndex(); /// @notice Address is not in valid `IP:port` format. /// @param input Invalid input. /// @param backtrace Additional error context. error NotIpPort(string input, string backtrace); /// @notice Address is not a valid IP address. /// @param input Invalid input. /// @param backtrace Additional error context. error NotIp(string input, string backtrace); /// @notice Ingress IP is already in use by an active validator. /// @param ingress Conflicting ingress address. error IngressAlreadyExists(string ingress); /// @notice Validator information /// @param publicKey Ed25519 communication public key. /// @param validatorAddress Validator address. /// @param ingress Inbound address in `:` format. /// @param egress Outbound address in `` format. /// @param index Immutable validators-array position. /// @param addedAtHeight Block height when entry was added. /// @param deactivatedAtHeight Block height when entry was deactivated (`0` if active). /// @param feeRecipient The fee recipient the node will set when proposing blocks as a leader. struct Validator { bytes32 publicKey; address validatorAddress; string ingress; string egress; uint64 index; uint64 addedAtHeight; uint64 deactivatedAtHeight; address feeRecipient; } /// @notice Get active validators. /// @return validators Active validators (`deactivatedAtHeight == 0`). function getActiveValidators() external view returns (Validator[] memory validators); /// @notice Get contract owner. /// @return Owner address. function owner() external view returns (address); /// @notice Get total validators, including deactivated entries. /// @return count Validator count. function validatorCount() external view returns (uint64); /// @notice Get validator by array index. /// @param index Validators-array index. /// @return validator Validator at `index`. function validatorByIndex(uint64 index) external view returns (Validator memory); /// @notice Get validator by address. /// @param validatorAddress Validator address. /// @return validator Validator for `validatorAddress`. function validatorByAddress(address validatorAddress) external view returns (Validator memory); /// @notice Get validator by public key. /// @param publicKey Ed25519 public key. /// @return validator Validator for `publicKey`. function validatorByPublicKey(bytes32 publicKey) external view returns (Validator memory); /// @notice Get next epoch configured for a fresh DKG ceremony. /// @return epoch Epoch number, or `0` if none is scheduled. function getNextFullDkgCeremony() external view returns (uint64); /// @notice Add a new validator (owner only) /// @dev Requires Ed25519 signature over a unique digest generated from inputs. /// @param validatorAddress New validator address. /// @param publicKey Validator Ed25519 communication public key. /// @param ingress Inbound address `:`. /// @param egress Outbound address ``. /// @param feeRecipient The fee recipient the validator sets when proposing. /// @param signature Ed25519 signature proving key ownership. function addValidator( address validatorAddress, bytes32 publicKey, string calldata ingress, string calldata egress, address feeRecipient, bytes calldata signature ) external returns (uint64); /// @notice Deactivate a validator (owner or validator only). /// @dev Sets `deactivatedAtHeight` to current block height. /// @param idx Validator index. function deactivateValidator(uint64 idx) external; /// @notice Rotate a validator to a new identity (owner or validator only). /// @dev Preserves index stability by appending a copy of the existing entry and updating the entry in-place. /// @param idx Validator index to rotate. /// @param publicKey New Ed25519 communication public key. /// @param ingress New inbound address `:`. Must be different from the rotated-out validator (changing port is enough). /// @param egress New outbound address ``. /// @param signature Ed25519 signature proving new key ownership. function rotateValidator( uint64 idx, bytes32 publicKey, string calldata ingress, string calldata egress, bytes calldata signature ) external; /// @notice Update validator IP addresses (owner or validator only). /// @param idx Validator index. /// @param ingress New inbound address `:`. /// @param egress New outbound address ``. function setIpAddresses( uint64 idx, string calldata ingress, string calldata egress ) external; /// @notice Update validator fee recipient (owner or validator only). /// @param idx Validator index. /// @param feeRecipient New fee recipient. function setFeeRecipient( uint64 idx, address feeRecipient ) external; /// @notice Transfer validator entry to a new address (owner or validator only). /// @dev Reverts if `newAddress` conflicts with an active validator. /// @param idx Validator index. /// @param newAddress New validator address. function transferValidatorOwnership(uint64 idx, address newAddress) external; /// @notice Transfer contract ownership (owner only). /// @param newOwner New owner address. function transferOwnership(address newOwner) external; /// @notice Set next fresh DKG ceremony epoch (owner only). /// @param epoch Epoch where ceremony runs (`epoch + 1` uses new polynomial). function setNextFullDkgCeremony(uint64 epoch) external; /// @notice Migrate one validator by V1 index (owner only). /// @param idx V1 validator index. function migrateValidator(uint64 idx) external; /// @notice Initialize V2 and enable reads (owner only). /// @dev Requires all V1 indices to be processed. function initializeIfMigrated() external; /// @notice Check initialization state. /// @return initialized True if initialized. function isInitialized() external view returns (bool); /// @notice Get initialization block height. /// @return height Initialization height (`0` if not initialized). function getInitializedAtHeight() external view returns (uint64); } ``` ### Overview * Migration incrementally reads and copies validator entries from V1 into V2. * During migration, the consensus layer continues reading V1 until `initializeIfMigrated()` completes. * Validator history are append-only, and deactivation is one-way. * Historical validator sets are reconstructed from `addedAtHeight` and `deactivatedAtHeight`. * Validator `index` is stable for the lifetime of an entry. * Writes for post-migration operations are gated by `isInitialized()`. ### State Model V2 stores validators in one append-only array, with lookup indexes by address and public key. * `addedAtHeight`: block height where the entry becomes visible to CL epoch filtering. * `deactivatedAtHeight`: `0` means active; non-zero marks irreversible deactivation. * `index`: immutable array position assigned at creation. * `initialized`: one-way migration flag toggled by `initializeIfMigrated()`. #### Fee Recipient Separation Each validator entry includes a `feeRecipient` that can differ from the validator's control address. This enables operators to route protocol fees to a dedicated treasury wallet, while retaining a separate validator or treasury-ops multisig for operational calls. This separation reduces blast radius during key compromise: operational key exposure does not cause historically collected fees held by the custody wallet to be lost. ### Operation Semantics #### Lifecycle Operations * `addValidator`: appends a new active entry after validation and signature verification. * `deactivateValidator`: marks an existing active entry as deactivated at current block height. * `rotateValidator`: to keep `index` stable, this updates the active entry in place and appends the entry to be deactivated. Active validator count is unchanged. #### Network And Ownership Operations * `setIpAddresses`: updates ingress and egress for an active validator, enforcing address format and ingress uniqueness among active entries. * `setFeeRecipient`: updates the destination address that receives network fees from block proposing. * `transferValidatorOwnership`: rebinds a validator entry to a new address provided the address is not used by another active entry. #### Migration And Phase-Gating Operations * `migrateValidator`: copies one V1 entry into V2 in descending index order. * `initializeIfMigrated`: switches V2 to initialized state after all V1 indices have been processed. * Mutators are phase-gated: migration mutators are blocked after init, and post-init mutators are blocked before init. #### Input Validation And Safety Checks ValidatorConfig V2 enforces the following checks: 1. Validator ed25519 public keys must be unique across all validators (active and inactive). 2. Validator addresses must be unique across active validators. 3. `ingress` must be a valid `IP:port`, and unique across active validators. 4. `egress` must be a valid IP. 5. `addValidator` and `rotateValidator` require a signature from the Ed25519 key being installed. #### Ed25519 Signature Verification When adding or rotating a validator, the caller must provide an Ed25519 signature proving ownership of the public key. **Namespace:** `addValidator` uses `b"TEMPO_VALIDATOR_CONFIG_V2_ADD_VALIDATOR"` and `rotateValidator` uses `b"TEMPO_VALIDATOR_CONFIG_V2_ROTATE_VALIDATOR"`. **Messages:** ``` addValidatorMessage = keccak256( bytes8(chainId) // uint64: Prevents cross-chain replay || contractAddress // address: Prevents cross-contract replay || validatorAddress // address: Binds to specific validator address || uint8(ingress.length) // uint8: Length of ingress || ingress // string: Binds network configuration || uint8(egress.length) // uint8: Length of egress || egress // string: Binds network configuration || feeRecipient // address: Binds fee recipients when proposing. ) rotateValidatorMessage = keccak256( bytes8(chainId) // uint64: Prevents cross-chain replay || contractAddress // address: Prevents cross-contract replay || validatorAddress // address: Binds to specific validator address || uint8(ingress.length) // uint8: Length of ingress || ingress // string: Binds network configuration || uint8(egress.length) // uint8: Length of egress || egress // string: Binds network configuration ) ``` The Ed25519 signature is computed over the operation-specific message with the namespace parameter (see commonware's [signing scheme](https://github.com/commonwarexyz/monorepo/blob/abb883b4a8b42b362d4003b510bd644361eb3953/cryptography/src/ed25519/scheme.rs#L38-L40) and [union format](https://github.com/commonwarexyz/monorepo/blob/abb883b4a8b42b362d4003b510bd644361eb3953/utils/src/lib.rs#L166-L174)). ### Compatibility And Upgrade Behavior #### Changes From V1 1. V2 preserves append-only history with irreversible deactivation instead of mutable active/inactive toggling. 2. V2 enforces stronger input checks in the precompile, including signature-backed key ownership. 3. V2 keeps validator index stable across lifecycle operations. #### Consensus Layer Read Behavior The Consensus Layer checks `v2.isInitialized()` to determine which contract to read: * **`initialized == false`**: CL reads from V1. * **`initialized == true`**: CL reads from V2. This read switch is implemented in CL logic. V2 does not proxy reads to V1. ### Consensus Layer Integration **IP address changes**: `setIpAddresses` is expected to take effect in CL peer configuration on the next finalized block. **Validator addition and deactivation**: there is no warmup or cooldown in V2. Added validators are added to the DKG player set on the next epoch; deactivated validators leave on the next epoch. (both in the case of successful DKG rounds; on failure DKG still falls back to its previous state, which might include validators that are marked inactive as per the contract). **Fee recipients**: Fee recipients are included now to be used in the future in a not yet determined hardfork. #### DKG Player Selection The consensus layer determines DKG players for epoch `E+1` by reading state at `boundary(E) - 1` and filtering: ``` players(E+1) = validators.filter(v => v.addedAtHeight < boundary(E) && (v.deactivatedAtHeight == 0 || v.deactivatedAtHeight >= boundary(E)) ) ``` This enables node recovery and late joining without historical account state. ### Migration from V1 On networks that start directly with V2 (no V1 state), `initializeIfMigrated` can be called immediately when the V1 validator count is zero. Because `SSTORE` cost is high under TIP-1000, migration is done one validator at a time to reduce out-of-gas risk on large sets. #### Full Migration Steps 1. At fork activation, the V2 precompile goes live. However, CL continues reading from the V1 precompile. 2. The owner calls `migrateValidator(n-1)` with `n` being the validator count in the V1 precompile. 3. On the first migration call, V2 copies owner from V1 if unset and snapshots the V1 validator count, then continues in descending index order. The snapshotted count is used for all subsequent index validation to prevent V1 mutations from breaking migration ordering. 4. After all indices are processed, owner calls `initializeIfMigrated()`, which flips `initialized` and activates CL reads from V2. #### Migration Edge Cases 1. **Validator goes offline during migration**: admin deactivates the validator in V1. If that index has already been migrated the admin will also deactivates it in V2. 2. **Invalid V1 entry encountered**: the index is still marked as processed (migrated, overwritten, or skipped by implementation rules) so global completion is not blocked. 3. **Zero validators in V1**: The migration flow requires at least 1 validator to be in the V1 precompile. 4. **Validator count overflow**: We use uint8s to cache the number of V1 validators and the number of skipped V1 validators. V1 currently has 14 validators, so a limit of 255 is sufficient. #### Permitted Calls During Migration | Contract | Caller | Allowed calls | | --- | --- | --- | | V2 (pre-init) | owner | `deactivateValidator`, `migrateValidator`, `initializeIfMigrated` | | V2 (pre-init) | validator | `deactivateValidator` (theoretically; in Tempo these addresses are unowned) | | V2 (post-init) | any | `migrateValidator` and `initializeIfMigrated` are blocked | | V1 (during migration window) | owner and validators | all V1 calls remain available (subject to V1 authorization and key ownership assumptions) | ## Security ### Considerations * **Migration timing**: migration and `initializeIfMigrated()` should complete before an epoch boundary to avoid DKG disruption. * **Pre-migration validation**: admins should run a validation script against V1 state to detect entries that would fail V2 checks. * **State parity before init**: admins should verify V1/V2 state consistency before finalizing with `initializeIfMigrated()`. * **Signature domain separation**: signatures for `addValidator` and `rotateValidator` are bound to chain ID, precompile address, namespace, validator address, and endpoint payload. ### Race And Griefing Risks * Stable `index` values prevent races between concurrent state-changing calls. * Append-only history and permissionless rotation require query paths that remain safe as history grows. ### Testing Requirements Unit tests should cover all control-flow branches in added functions, including initialization gating, migration completion checks, and index-based query behavior under large validator sets. ### Invariants #### Identity and Uniqueness 1. **Unique active addresses**: No two active validators share the same `validatorAddress`. Deactivated addresses may be reused. 2. **Unique public keys**: No two validators (including deactivated) share the same `publicKey`. 3. **Ingress uniqueness across active validators**: In `getActiveValidators()`, no two validators share the same ingress `:`. 4. **Valid public keys**: All validators must have valid ed25519 `publicKey`s. #### Lifecycle and Storage Behavior 1. **Append-only validator array**: `validatorsArray` length can only increase. 2. **Entry index immutability**: Once a validator entry is created at index `i`, that entry can never move to another index. A previously deactivated operator may later be re-added as a new entry at a different index. 3. **Deactivate-once**: `deactivatedAtHeight` can only transition from 0 to a non-zero value, never back. 4. **Add increases exactly one entry**: A successful `addValidator` call increases `getActiveValidators().length` by exactly one. 5. **Rotation preserves active cardinality**: A successful `rotateValidator` call does not change `getActiveValidators().length`. 6. **Deactivation decrements active cardinality by one**: A successful `deactivateValidator` call decreases `getActiveValidators().length` by exactly one. #### Query Correctness 1. **Full-set reconstruction by index**: Reading `validatorByIndex(i)` for all `i` in `0..validatorCount()-1` must reconstruct exactly the ordered validator set. 2. **Validator activity consistency**: Filtering the reconstructed validator set by `deactivatedAtHeight == 0` must produce exactly `getActiveValidators()` (same members, order not important). 3. **Address round-trip for active entries**: For any `i` where `validatorByIndex(i).deactivatedAtHeight == 0`, `validatorByAddress(validatorByIndex(i).validatorAddress).index == i`. 4. **Public-key round-trip for all entries**: For any `i < validatorCount()`, `validatorByPublicKey(validatorByIndex(i).publicKey).index == i`. 5. **Index round-trip for all entries**: For any `i < validatorCount()`, `validatorByIndex(i).index == i`. #### Migration and Initialization 1. **Initialization phase gating**: Before initialization, post-init mutators are blocked; after initialization, migration mutators are blocked. 2. **Initialized once**: The `initialized` flag can only transition from `false` to `true`, never back. 3. **Migration completion gate**: Each V1 index must be processed exactly once (migrated or skipped), and `initializeIfMigrated()` stays blocked until all indices are processed. 4. **Skipped-index counter monotonicity**: `migrationSkippedCount` is monotonically non-decreasing and may only change during `migrateValidator`. 5. **DKG continuity at initialization**: On successful `initializeIfMigrated`, `getNextFullDkgCeremony()` in V2 equals the value read from V1 at that moment. 6. **Owner bootstrap during migration**: If V2 owner is unset on first migration call, owner is copied from V1 exactly once and then used for all migration authorization checks. # TIP-1020: Signature Verification Precompile ## Abstract This TIP introduces a signature verification precompile that enables contracts to verify Tempo signature types (secp256k1, P256, WebAuthn) without relying on custom verifier contracts. ## Motivation Tempo supports multiple signature schemes beyond standard secp256k1. Currently, contracts cannot verify Tempo signatures onchain without implementing custom verification logic for each signature type. Additionally, since smart contracts have to statically bind their verification logic at deployment time, developers cannot maintain forward compatibility with future Tempo account signature schemes introduced after deployment without making their contracts upgradeable. This precompile serves as a stable interface that smart contracts can use to maintain forward compatibility with future Tempo account types and signature schemes. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. ### Precompile Address ``` 0x5165300000000000000000000000000000000000 ``` ### Interface ```solidity interface ISignatureVerifier { error InvalidFormat(); error InvalidSignature(); /// @notice Recovers the signer of a Tempo signature (secp256k1, P256, WebAuthn). /// @param hash The message hash that was signed /// @param signature The encoded signature (see Tempo Transaction spec for formats) /// @return Address of the signer if valid, reverts otherwise function recover(bytes32 hash, bytes calldata signature) external view returns (address signer); /// @notice Verifies a signer against a Tempo signature (secp256k1, P256, WebAuthn). /// @param signer The input address verified against the recovered signer /// @param hash The message hash that was signed /// @param signature The encoded signature (see Tempo Transaction spec for formats) /// @return True if the input address signed, false otherwise. Reverts on invalid signatures. function verify(address signer, bytes32 hash, bytes calldata signature) external view returns (bool); } ``` ### Signature Encoding Signatures MUST be encoded using the same format as [Tempo Transaction signatures](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#signature-types): | Type | Format | Length | |------|--------|--------| | secp256k1 | `r \|\| s \|\| v` | 65 bytes | | P256 | `0x01 \|\| r \|\| s \|\| x \|\| y \|\| prehash` | 130 bytes | | WebAuthn | `0x02 \|\| webauthn_data \|\| r \|\| s \|\| x \|\| y` | 129–2049 bytes | ### Verification Logic The precompile MUST use the same verification logic as Tempo transaction signature validation. See the [Tempo Transaction Signature Validation spec](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#signature-validation) for details. #### Keychain Signature Rejection The precompile MUST reject signatures with a Keychain type prefix (`0x03` or `0x04`). Keychain signatures are multi-step, stateful verification flows that cannot be reduced to a single pure cryptographic check. Thus, if a Keychain prefix is detected, the precompile MUST revert with `InvalidFormat()`. Contracts that need to verify Keychain-based signatures can do so by composing this precompile with the AccountKeychain precompile: first, use the AccountKeychain precompile to resolve and validate the access key for the account, then use this precompile to verify the inner signature against the resolved key. This two-step pattern separates key management (stateful, account-scoped) from cryptographic verification (stateless, type-scoped), allowing each precompile to remain single-purpose. ### Calldata Limits The precompile MUST enforce strict size limits on the `signature` argument **before** any decoding or copying occurs. If the signature exceeds the limit for its type, the precompile MUST revert with `InvalidFormat()`. | Type | Exact / Max Length | |------|-------------------| | secp256k1 | exactly 65 bytes | | P256 | exactly 130 bytes | | WebAuthn | 129–2049 bytes | ### Gas Costs The precompile MUST charge gas and verify sufficient gas is available **before** performing any cryptographic verification. The precompile MUST revert with out-of-gas if the call has insufficient gas for the signature type. All calls pay the standard Tempo precompile calldata cost of 6 gas per 32-byte word (rounded up) on the full ABI-encoded input, consistent with all other Tempo precompiles. The verification gas per signature type is in-line with the [Tempo Transaction Signature Gas Schedule](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction#signature-verification-gas-schedule): | Type | Verification Gas | |------|-----------------| | secp256k1 | 3,000 | | P256 | 8,000 | | WebAuthn | 8,000 | Total gas = `input_cost(calldata_len)` + verification gas for the signature type. ## Compatibility This TIP is **additive**. It introduces a new precompile at `0x5165300000000000000000000000000000000000` and does **not** modify existing EVM opcodes, transaction formats, or any existing Ethereum precompiles. ### Backward Compatibility #### Ethereum `ecrecover` (`0x01`) This TIP does not modify `ecrecover` or any existing Ethereum precompile. `ecrecover` remains the standard tool for Ethereum-style secp256k1 address recovery. #### Developer-Facing Differences vs. `ecrecover` Solidity developers commonly use `ecrecover(hash, v, r, s)` to recover an address and then compare it to an expected signer. The Tempo signature verification precompile follows the same recover-and-return pattern but differs in one key way: `ecrecover` returns `address(0)` on invalid input or failed recovery, but the TIP-1020 precompile **reverts** on invalid signatures. Contracts that want non-reverting behavior SHOULD wrap calls using `try/catch` (high-level) or `staticcall` (low-level) and treat failure as "invalid signature". #### `v` Value Normalization For secp256k1 signatures, the precompile normalizes the recovery identifier `v`: both Ethereum-style values (`27`, `28`) and raw values (`0`, `1`) are accepted. This is intentional — `recover()` is designed to be as close to a drop-in replacement for `ecrecover` as possible, so it accepts the same `v` values. This differs from TIP-1004 (`permit()`), which requires `v ∈ {27, 28}` and reverts on `0` or `1`. #### Existing secp256k1 Signature Payloads For backwards compatibility, secp256k1 signatures are encoded as **65 bytes `r || s || v` with no type prefix**. Callers who already produce 65-byte secp256k1 signatures can reuse them directly as the `signature` argument to this precompile. ### Forward Compatibility It is expected that this precompile will be updated when other account types are introduced to maintain forward compatibility with Tempo accounts. ## Invariants | ID | Invariant | Description | |----|-----------|-------------| | **SV1** | Transaction-equivalent verification | For any signature type supported by a given function, the precompile MUST use the same cryptographic verification rules as Tempo transaction signature validation. | | **SV2** | P256 and ECDSA signature malleability resistance | P256 and ECDSA signatures MUST satisfy the low-s requirement (`s <= n/2`). Signatures with high-s values MUST be rejected. | | **SV3** | Signature size enforcement | The precompile MUST enforce per-type size limits (65 bytes secp256k1, 130 bytes P256, 129–2049 bytes WebAuthn) before any decoding or copying, preventing out-of-bounds reads and pathological resource usage. | | **SV4** | Revert on failure | On any invalid signature, invalid encoding, or unsupported type, the precompile MUST revert. | | **SV5** | Gas schedule consistency | Gas charged MUST follow the gas schedule listed above. | | **SV6** | Signature type disambiguation | Exactly 65 bytes MUST be interpreted as secp256k1 (no prefix). Any non-65-byte signature MUST be interpreted using the leading type byte. Unknown type identifiers MUST revert. | | **SV7** | Keychain signature rejection | Signatures with a Keychain type prefix (`0x03` or `0x04`) MUST be rejected. Keychain verification is achieved by composing the AccountKeychain precompile (key resolution) with this precompile (inner signature verification). | # TIP-1022: Virtual Addresses for TIP-20 Deposit Forwarding ## Abstract This TIP introduces **virtual addresses**: a reserved 20-byte address format that, when detected in TIP-20 recipient-bearing operations, causes the precompile to auto-credit a registered master wallet instead of the literal target address. This eliminates sweep transactions entirely for entities such as exchanges, ramps, and payment processors that generate per-user deposit addresses. Master registration is a one-time onchain call; deposit address derivation is fully offchain. ## Motivation * **Eliminate sweep transactions.** Entities such as exchanges, ramps, and payment processors need to offer each customer a unique deposit address. Today, funds arriving at each address must be swept back to a central wallet in separate transactions, which is a large operational cost and burden at scale. Virtual addresses auto-credit the master wallet at the protocol level, making sweeps unnecessary. * **Avoid the 250,000 gas new-account cost.** Tempo charges 250,000 gas to create state for a new address on first use. With virtual addresses, deposit addresses never create onchain state, so the first transfer to a new deposit address costs the same as any other transfer. * **Prevent state bloat.** Without virtual addresses, each customer deposit address creates a new account in the state trie. At enterprise scale (millions of deposit addresses), this is significant and permanent state growth. Virtual addresses avoid this entirely: no accounts are created, regardless of how many deposit addresses a business generates. *** ## Specification ### Address Layout Virtual addresses are standard 20-byte EVM addresses with the following reserved format: ``` [4-byte masterId] [10-byte MAGIC] [6-byte userTag] = 20 bytes total ``` | Field | Bytes | Description | |-------|-------|-------------| | **masterId** | 4 | Deterministic identifier derived from `(masterAddress, salt)` via the registration hash. This is the registry lookup key. | | **VIRTUAL\_MAGIC** | 10 | Fixed magic value `0xFDFDFDFDFDFDFDFDFDFD`. Identifies the address as virtual. | | **userTag** | 6 | Opaque per-user identifier derived offchain by the operator. 48 bits support ~2.8×10^14 unique deposit addresses per master. | #### Why This Layout? TIP-1022 intentionally places the 10-byte magic sequence in the **middle** of the address instead of at the beginning. This preserves more visually useful bytes at the front and back of the address for operators and users comparing deposit addresses in wallets, explorers, etc. The 4-byte `masterId` is kept short to preserve room for a large `userTag`, while the 10-byte magic keeps the format highly unlikely to appear accidentally. The security implications of this tradeoff are discussed in **Security Considerations**. ### Conformance and Scope TIP-1022 applies only to TIP-20 precompile recipient resolution for the entrypoints listed in **Transfer Path Modification**. TIP-1022 does **not** alter TIP-20 methods that do not carry a recipient in the TIP-20 transfer path (e.g. `approve`, `burn`, `permit`) and does not alter non-TIP-20 protocol behavior. Non-TIP-20 token transfers (e.g. ERC-20 contracts deployed on Tempo) to virtual addresses are **not** subject to TIP-1022 forwarding. Such transfers behave as standard EVM transfers to the literal address. Tokens sent this way may be irrecoverable — see **Risks and Limitations**. `setRewardRecipient` is **not** a TIP-20 transfer-path operation and is therefore not subject to TIP-1022 recipient resolution. Implementations MUST reject virtual addresses when setting reward recipients so that rewards remain tied to canonical accounts rather than aliases. ### Reserved Virtual Address Format Any address whose bytes `[4:14]` equal `VIRTUAL_MAGIC` is treated as a virtual address by the TIP-20 precompile. If a TIP-20 transfer targets such an address: * the precompile extracts the `masterId` from bytes `[0:4]` * looks up the registered master * credits the resolved master if registered * otherwise reverts with `VirtualAddressUnregistered` The literal virtual address never accumulates TIP-20 balance through standard TIP-20 transfer paths. #### Reserved Address Space Addresses matching the virtual-address format occupy a reserved TIP-20 recipient namespace. A user who happens to control an EOA or contract whose address matches this format can still exist on Tempo and can still originate ordinary EVM transactions. However, TIP-20 transfers to such an address will follow TIP-1022 recipient resolution semantics rather than crediting the literal address. Users who control such an address SHOULD NOT use it as a normal account on Tempo. ### Master ID Derivation The `masterId` is deterministic and derived from the registration hash computed during `registerVirtualMaster()`: ``` registrationHash = keccak256(abi.encodePacked(msg.sender, salt)) masterId = bytes4(registrationHash[4:8]) ``` The first 4 bytes of `registrationHash` are consumed by the proof-of-work check (see **Registration Proof of Work**); the `masterId` is extracted from bytes `[4:8]` of the same hash. The salt is a `bytes32` value chosen by the caller. Callers MUST grind the salt to satisfy the 32-bit proof-of-work requirement. The resulting `masterId` is permanently bound to the registration address. #### Why `masterId` Registrations Are Immutable TIP-1022 intentionally does not provide a mechanism to rotate or update the master address bound to a `masterId`. Allowing rotation would interact poorly with TIP-403 policies: a blacklisted master could rotate to a fresh address and resume receiving deposits, requiring policy enforcement to track `masterId`s in addition to addresses. Operators who need to change their underlying key material can register their `masterId` to an upgradeable proxy contract or multisig, allowing the controlling keys to be rotated at the contract layer without any protocol-level change. Finally, any rotation mechanism would require a timelock or similar delay to prevent an attacker who compromises a master key from silently redirecting deposits before the legitimate owner can respond — complexity that is better handled by the operator's own key management infrastructure. In the event of a `masterId` collision (two `(address, salt)` pairs mapping to the same 4-byte `masterId`), the second registration reverts with `MasterIdCollision`. The caller can retry with a different valid salt. The probability of such a collision (and the resulting need to regrind another salt) is less than 0.1% even if 4 million masterId's have already been registered. ### Registration Proof of Work Registration requires a 32-bit proof of work to make **targeted collisions against a chosen `masterId`** computationally expensive. The registration hash is computed as: ``` registrationHash = keccak256(abi.encodePacked(msg.sender, salt)) ``` The first 4 bytes of `registrationHash` MUST be zero: ``` require(bytes4(registrationHash[0:4]) == 0x00000000) // 32-bit PoW masterId = bytes4(registrationHash[4:8]) ``` This requires the caller to grind ~2^32 salt values to find a valid registration. If the first 4 bytes are not zero, the call reverts with `ProofOfWorkFailed`. This proof of work is intended to make it expensive for an attacker who sees a pending registration transaction to compute a different `(attackerAddress, salt)` pair that lands on the same `masterId` and gets mined first. With a 4-byte `masterId` and a 32-bit proof-of-work requirement, that targeted attack costs ~2^64 work. ### User Tag Derivation (Offchain) The `userTag` is an opaque 6-byte value generated offchain by the operator. The protocol does not interpret or validate it — all values including `0x000000000000` are valid. It exists solely so the operator can attribute deposits to specific users via the two-hop `Transfer` events described below. Operators maintain their own internal mapping `{internalUserId -> virtualAddress}`. No onchain transaction is needed to create a new deposit address. ### Worked Example An exchange with master address `0xABCD...1234` registers with a salt that satisfies the 32-bit PoW: * `registrationHash = keccak256(abi.encodePacked(0xABCD...1234, salt))` * `registrationHash[0:4] == 0x00000000` (PoW satisfied) * `masterId = bytes4(registrationHash[4:8])` -> e.g. `0x07A3B1C2` * For customer #103048, the exchange derives a `userTag` -> e.g. `0xD4E5A7C3F19E` ``` Virtual address = 0x07A3B1C2 FDFDFDFDFDFDFDFDFDFD D4E5A7C3F19E ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ masterId magic (10) userTag (6) ``` ### Registry Precompile Virtual address resolution requires a registry that maps `masterId -> masterAddress`. This is managed through a new precompile deployed at `0xFDC0000000000000000000000000000000000000`. The registry MUST maintain the following mapping constraints: * each `masterId` maps to at most one registered master address (one-to-one from `masterId`) * multiple `masterId`s MAY map to the same master address (many-to-one) This many-to-one design allows a single underlying wallet to register multiple `masterId`s (e.g. with different salts). A **valid master address** MUST satisfy TIP-20 recipient safety constraints: * MUST NOT be `address(0)` * MUST NOT itself match the virtual-address format (`VIRTUAL_MAGIC` at bytes `[4:14]`) * MUST NOT be a TIP-20 token address (`0x20c000....` at bytes `[0:12]`) #### Registry Storage Layout Each `masterId` maps to a single 32-byte storage slot: ``` slot = keccak256(abi.encode(masterId, REGISTRY_SLOT)) value = masterType | reserved | masterAddress ^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^^ 1 byte 11 bytes 20 bytes ``` Here `REGISTRY_SLOT` means the storage slot of the `mapping(bytes4 => bytes32)` used to store registry entries, following standard Solidity mapping layout. | Field | Bytes | Description | |-------|-------|-------------| | `masterType` | 1 | Type discriminator for future extensibility. MUST be `0x00` in this version. | | `reserved` | 11 | Reserved for future use. MUST be zeroed. | | `masterAddress` | 20 | The registered master address for this `masterId`. `address(0)` if unregistered. | This layout packs all metadata for a `masterId` into a single storage slot, enabling one SLOAD during transfer-path resolution. #### Interface ```solidity interface IAddressRegistry { // ──────────────────── Events ──────────────────── /// @notice Emitted when a new master is registered. event MasterRegistered( bytes4 indexed masterId, address indexed masterAddress ); // ──────────────────── Errors ──────────────────── /// @notice The computed masterId is already registered to a different address. error MasterIdCollision(); /// @notice The caller/new master address is invalid for virtual forwarding. error InvalidMasterAddress(); /// @notice The registration hash does not satisfy the 32-bit proof-of-work requirement. error ProofOfWorkFailed(); /// @notice The virtual address has a valid format but its masterId is not registered. error VirtualAddressUnregistered(); // ──────────────── Registration ────────────────── /// @notice Registers msg.sender as a virtual address master. /// @dev The registration hash is keccak256(abi.encodePacked(msg.sender, salt)). /// The first 4 bytes of the hash MUST be zero (32-bit proof of work). /// masterId is derived from bytes [4:8] of the registration hash. /// Reverts with ProofOfWorkFailed if the first 4 bytes are not zero. /// Reverts with InvalidMasterAddress if msg.sender is not a valid master address. /// Reverts with MasterIdCollision if the derived masterId is already taken /// by a different address. On collision, the caller can retry with a different salt. /// The same address MAY register multiple masterIds using different salts. /// @param salt Caller-chosen salt for masterId derivation. Must satisfy 32-bit PoW. /// @return masterId The derived master identifier. function registerVirtualMaster(bytes32 salt) external returns (bytes4 masterId); // ────────────────── Queries ───────────────────── /// @notice Returns the registered master address for a given masterId, or address(0) if unregistered. function getMaster(bytes4 masterId) external view returns (address); /// @notice Resolves a transfer recipient using TIP-1022 execution semantics. /// For non-virtual addresses, returns `to` unchanged. /// For virtual addresses, returns the registered master or reverts with /// VirtualAddressUnregistered. function resolveRecipient(address to) external view returns (address effectiveRecipient); /// @notice Resolves a virtual address to its registered master. /// Returns address(0) if the address does not match the virtual-address format. /// Returns address(0) if the masterId is not registered. function resolveVirtualAddress(address virtualAddr) external view returns (address master); /// @notice Returns true if the address matches the virtual-address format. function isVirtualAddress(address addr) external pure returns (bool); /// @notice Decodes a virtual address into its components. /// @return isVirtual True if the address matches the virtual-address format. /// @return masterId The 4-byte master identifier (zero if not virtual). /// @return userTag The 6-byte user tag (zero if not virtual). function decodeVirtualAddress(address addr) external pure returns (bool isVirtual, bytes4 masterId, bytes6 userTag); } ``` #### Constants | Name | Value | Description | |------|-------|-------------| | `VIRTUAL_MAGIC` | `0xFDFDFDFDFDFDFDFDFDFD` | 10-byte magic value identifying virtual addresses | | `REGISTRY_ADDRESS` | `0xFDC0000000000000000000000000000000000000` | Precompile address for the virtual-address registry | ### Transfer Path Modification The following existing TIP-20 entrypoints are modified to resolve the `to` (recipient) address before crediting: * `transfer` * `transferFrom` * `transferWithMemo` * `transferFromWithMemo` * `mint` * `mintWithMemo` * `systemTransferFrom` The `from` address on `transferFrom`, `transferFromWithMemo`, and `systemTransferFrom` is **not** affected by TIP-1022 resolution. #### Resolution Logic ```text function resolveRecipient(to: address) -> address: // Check bytes [4:14] against VIRTUAL_MAGIC if to[4:14] != VIRTUAL_MAGIC: return to masterId = to[0:4] master = registry.getMaster(masterId) if master == address(0): revert VirtualAddressUnregistered() return master ``` #### Standard Transfer Entrypoints For `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, and `systemTransferFrom`: 1. **Resolve recipient**: compute `effectiveRecipient = resolveRecipient(to)`. If `to` is not virtual, `effectiveRecipient = to`. 2. **Token-level sender check**: apply the standard TIP-403 / TIP-1015 sender authorization rules. 3. **Token-level recipient check**: apply the standard TIP-403 / TIP-1015 recipient authorization rules to `effectiveRecipient`. 4. **Apply balance changes**: debit sender, credit `effectiveRecipient`. 5. **Emit events**: per **Event Emission** (two-hop `Transfer` if virtual, single `Transfer` otherwise). If any step reverts, the enclosing TIP-20 operation MUST revert atomically with no balance changes and no events. #### Mint Entrypoints For `mint` and `mintWithMemo`: 1. **Resolve recipient**: compute `effectiveRecipient = resolveRecipient(to)`. If `to` is not virtual, `effectiveRecipient = to`. 2. **Token-level mint-recipient check**: apply the standard TIP-1015 mint-recipient authorization rules to `effectiveRecipient`. 3. **Apply balance changes**: credit `effectiveRecipient`. 4. **Emit events**: per **Event Emission**. If any step reverts, the enclosing TIP-20 operation MUST revert atomically with no balance changes and no events. #### Authorization Semantics TIP-1022 does not introduce new authorization logic in TIP-403 itself. Instead, TIP-20 transfer and mint logic MUST resolve virtual recipient addresses before invoking the existing TIP-403 / TIP-1015 checks. Concretely, for any TIP-20 entrypoint covered by TIP-1022: 1. Compute `effectiveRecipient = resolveRecipient(to)`. 2. Apply the existing sender / recipient / mint-recipient authorization rules to `effectiveRecipient`, not the literal virtual address. This preserves view/execution symmetry with the TIP-20 authorization path defined by TIP-1015: any internal TIP-20 helper such as `isTransferAuthorized(from, to)` MUST evaluate recipient authorization against the resolved master address when `to` is virtual. `balanceOf(virtualAddress)` remains literal and MUST continue to return 0. Contracts or integrators that need explicit resolution behavior outside the TIP-20 transfer path MAY call `resolveRecipient` on the registry. ### Event Emission TIP-1022 does **not** introduce new transfer-path events. The registry precompile emits `MasterRegistered`, but forwarding itself is represented using **two-hop standard `Transfer` events**: one hop showing funds arriving at the virtual address, and a second hop showing funds moving from the virtual address to the resolved master. Using standard `Transfer` events (rather than a new event type) preserves compatibility with existing indexers, block explorers, and wallets that already understand TIP-20 / ERC-20 `Transfer` events — no custom integration is required to track virtual address deposits. For transfers where the recipient is **not** a virtual address, event emission is unchanged from standard TIP-20 behavior — a single `Transfer(sender, to, amount)`. #### Deposit Forwarding (Inbound) When a transfer targets a virtual address (`to` is virtual), the precompile MUST emit two `Transfer` events in sequence: 1. `Transfer(sender, virtualAddress, amount)` — shows funds arriving at the virtual address 2. `Transfer(virtualAddress, masterAddress, amount)` — shows funds forwarding to the master The actual balance change is applied only to `masterAddress`. The virtual address never holds a balance; the first `Transfer` event is a logical representation of deposit attribution, not a real balance credit. Indexers that need deposit attribution SHOULD watch for pairs of `Transfer` events within the same transaction where the intermediate address matches the virtual-address format. The `userTag` can then be extracted from the virtual address to identify the depositor. #### Entrypoint-Specific Event Ordering * `transfer`, `transferFrom`, `systemTransferFrom`: 1. `Transfer(sender, virtualAddress, amount)` 2. `Transfer(virtualAddress, masterAddress, amount)` * `transferWithMemo`, `transferFromWithMemo`: 1. `Transfer(sender, virtualAddress, amount)` 2. `TransferWithMemo(sender, virtualAddress, amount, memo)` 3. `Transfer(virtualAddress, masterAddress, amount)` * `mint`: 1. `Transfer(address(0), virtualAddress, amount)` 2. `Mint(virtualAddress, amount)` 3. `Transfer(virtualAddress, masterAddress, amount)` * `mintWithMemo`: 1. `Transfer(address(0), virtualAddress, amount)` 2. `TransferWithMemo(address(0), virtualAddress, amount, memo)` 3. `Mint(virtualAddress, amount)` 4. `Transfer(virtualAddress, masterAddress, amount)` ### Self-Forwarding If the registered master sends tokens to one of its own virtual addresses, the transfer resolves back to the master, effectively a transfer to self. The standard TIP-20 self-transfer semantics apply (no net balance change). The two-hop `Transfer` events are still emitted: `Transfer(master, virtualAddress, amount)` followed by `Transfer(virtualAddress, master, amount)`. Indexers SHOULD NOT interpret this as net inflow when `from == masterAddress` in the first hop. ### Interaction with TIP-403 Virtual address resolution happens **before** TIP-403 / TIP-1015 authorization checks. Policy evaluation uses the resolved `masterAddress`, not the literal virtual address. * If the **master address** is not authorized to receive the token, transfers to any of its virtual addresses revert. * If the **sender** is not authorized to send the token, the transfer reverts. * Policies configured on individual virtual addresses are ignored by the TIP-20 transfer path because virtual addresses have no independent canonical TIP-20 balance. #### Rejection of Virtual Addresses in Policy Operations TIP-403 operations that accept addresses as policy members MUST reject virtual addresses rather than accepting them silently. Implementations SHOULD use a clear, informative error indicating that virtual addresses are aliases for TIP-20 forwarding and are not valid literal policy subjects. Rejecting these operations avoids the footgun where an operator configures policy on the virtual alias they see in logs or explorers instead of on the resolved master address that actually holds the funds. ### Interaction with Account-Level Features * **`balanceOf(virtualAddress)`**: Always returns 0. Virtual addresses do not hold balances. * **Nonce / transaction origination**: A contract or EOA whose address matches the virtual-address format can still exist and can still originate ordinary EVM transactions. TIP-1022 resolution applies only to the `to` field in TIP-20 precompile calls, not to transaction senders. ### Security Considerations #### 4-Byte `masterId` and 32-Bit Registration PoW A 4-byte `masterId` would be too small if its security relied only on raw namespace size. TIP-1022 does **not** rely on that. Instead, security comes from the combination of: * a 4-byte `masterId`, and * a 32-bit proof-of-work requirement on registration An attacker who sees a pending registration transaction and wants to steal that `masterId` must compute a different `(attackerAddress, salt)` pair that: 1. satisfies the 32-bit proof-of-work requirement, and 2. lands on the same 4-byte `masterId` That targeted attack costs roughly 2^64 work. Further, because registration requires proof-of-work grinding, deployment will typically happen via dedicated tooling or a managed service that: * performs the proof-of-work search, * submits the registration transaction, and * waits for confirmation or revert before the operator routes value through the resulting master ID. This does not eliminate the residual collision-risk entirely, but it substantially reduces the practical chance that an operator incorrectly believes they control a master ID that was actually registered first by an attacker. #### Why the Magic Bytes Are in the Middle The middle `VIRTUAL_MAGIC` layout is a deliberate usability tradeoff: * it leaves the first 4 bytes available for `masterId` * it leaves the last 6 bytes available for `userTag` * it avoids spending the most visually important bytes of the address on static marker data This improves address comparison in UIs while still keeping a large reserved pattern that is highly unlikely to appear accidentally. We believe this layout is superior to the other permutations in terms of the prospect of address poisoning attacks (see below). #### Contracts and EOAs Matching the Virtual Format A sufficiently resourced adversary could, in principle, grind a CREATE2 deployment or private key so that a contract or EOA lands at an address matching the virtual-address format in a `masterID` controlled by the adversary. We view this as unlikely in practice because the address must match a 10-byte fixed magic value in the middle of the address, while targeted theft of a specific registered namespace also requires colliding the 4-byte `masterId` under the registration proof-of-work design (i.e., 14 bytes totally). A stronger global reservation mechanism for problematic address ranges may still be desirable in the future. #### Policy Configuration on Virtual Addresses Virtual addresses are forwarding aliases, not canonical TIP-20 holders. Using them directly in policy configuration is misleading and dangerous because the TIP-20 transfer path evaluates policies against the resolved master address. Accordingly, TIP-403 configuration operations SHOULD reject virtual addresses with explicit errors rather than accepting them. *** ### Risks and Limitations #### Address Poisoning and UI Confusion TIP-1022 still introduces a recognizable structured address format. Wallets, block explorers, and operational tooling that truncate addresses SHOULD display enough of the address to distinguish both the `masterId` and the `userTag`; ideally they SHOULD show the full address. #### Non-TIP-20 Token Loss TIP-1022 forwarding applies exclusively to TIP-20 precompile operations. Non-TIP-20 tokens (e.g. ERC-20 contracts deployed on Tempo) transferred to a virtual address are credited to the literal virtual address by the ERC-20 contract and are irrecoverable: no recovery mechanism is defined here. This risk is mitigated by the strong incentives for token issuers to use TIP-20 on Tempo (gas-payment eligibility, access to the payment lane, and policy support), but it remains a limitation of this design. #### Non-TIP-20 Protocol Positions Minted to Virtual Addresses TIP-1022 changes only the TIP-20 transfer and mint entrypoints listed in this document. It does not change other protocol logic that accepts an address parameter and records ownership against that literal address. This creates an edge case for protocols that mint LP shares, receipt tokens, or other redeemable positions to a user-supplied to address. If such a protocol later requires the recorded holder address to burn, redeem, or withdraw, a position minted to a virtual address can become stranded even though the corresponding master account controls that virtual namespace. The Fee AMM is one example of this pattern: LP shares minted can be mited to a virtual address, but are then permanently unburnable since `burn` checks that `msg_sender==lp_address`. In short, virtual-address forwarding is only defined for the TIP-20 paths enumerated by TIP-1022; other protocols remain literal-address systems unless they explicitly say otherwise. #### Externally-Triggerable Revert on Unregistered Virtual Addresses TIP-1022 introduces a recipient-dependent revert: if the `to` address matches the virtual-address format but its `masterId` is not registered, the transfer reverts with `VirtualAddressUnregistered`. This is the first TIP-20 revert condition that an untrusted recipient address can induce — prior to TIP-1022, transfers could only revert due to sender-side conditions (insufficient balance, authorization failure). Contracts that perform batch transfers in a single transaction (e.g. payroll, airdrop, or distribution contracts) SHOULD validate recipient addresses before execution or wrap individual transfers in try/catch to prevent a single unregistered virtual address from reverting the entire batch. #### Contracts and EOAs at virtual addresses It is theoretically possible to deploy a contract or control an EOA whose address matches `VIRTUAL_MAGIC`, including by grinding CREATE2 salts or private keys. Such addresses can still exist and originate ordinary EVM transactions, but we consider this unlikely in practice because targeting the 10-byte `VIRTUAL_MAGIC` requires roughly 2^80 work, with additional cost for targeted collisions against registered virtual namespaces. *** ## Invariants ### Core Invariants 1. **No fund loss**: A TIP-20 transfer to a virtual address MUST either credit the registered master's balance by exactly the transfer amount, or revert. Funds MUST NOT be credited to the virtual address itself or lost. 2. **Revert on unregistered**: A transfer to an address matching the virtual-address format whose `masterId` is not registered MUST revert. It MUST NOT credit any account. 3. **Balance consistency**: After a successful virtual-forwarded transfer of amount `X`, `balanceOf(master)` MUST have increased by exactly `X`. 4. **Zero-balance invariant**: For every virtual address, `balanceOf(virtualAddress)` MUST equal 0 from T3 activation onwards. Pre-T3, the TIP-20 precompile does not perform virtual-address resolution, so a transfer targeting an address that matches the virtual format will credit the literal address. Such pre-T3 balances are stranded (no party can claim them) and do not violate this invariant, which applies only to the T3-and-later transfer path. The probability of anyone controlling a private key for such an address is negligible. 5. **Event consistency**: For virtual-forwarded entrypoints, the precompile MUST emit two `Transfer` events: `Transfer(sender, virtualAddress, amount)` followed by `Transfer(virtualAddress, masterAddress, amount)`. `TransferWithMemo` events MUST immediately follow their matching `Transfer` and MUST use `virtualAddress` as the recipient to preserve deposit attribution. `Mint` events MUST use `virtualAddress`. 6. **Non-virtual path unaffected**: Transfers to addresses that do not match the virtual-address format MUST behave identically to pre-TIP-1022 semantics, with no registry lookup. 7. **Deterministic masterId**: Given `registrationHash = keccak256(abi.encodePacked(registrationAddress, salt))`, the first 4 bytes of `registrationHash` MUST be zero, and `masterId` MUST equal `bytes4(registrationHash[4:8])`, where `registrationAddress` is the address that called `registerVirtualMaster()` and `salt` is the caller-supplied salt. If the PoW check fails, registration MUST revert with `ProofOfWorkFailed`. `masterId` MUST NOT depend on registration order or transaction ordering. 8. **Master ID uniqueness**: Each `masterId` MUST map to at most one registered master address. Multiple `masterId`s MAY map to the same master address. 9. **Atomic revert behavior**: If virtual resolution fails, the enclosing TIP-20 call MUST revert with no state changes and no events. 10. **View/execution symmetry**: TIP-20 authorization logic MUST evaluate recipient authorization against the resolved master address when `to` is virtual, matching execution-time recipient resolution semantics. 11. **Policy on master**: TIP-403 / TIP-1015 authorization for virtual-forwarded transfers and mints MUST check the resolved `masterAddress`. Policies set on individual virtual addresses MUST be ignored by the TIP-20 transfer path. 12. **Policy-operation rejection**: TIP-403 configuration operations that accept literal addresses as policy subjects or members MUST reject virtual addresses. # TIP-1030: Allow same-tick flip orders ## Abstract Relaxes the `placeFlip` validation to allow `flipTick == tick`, enabling flip orders that flip to the same price. This supersedes TIP-1002, extracting the "allow same-tick flip orders" portion without the "prevent crossed orders" change. ## Motivation Currently, `placeFlip` requires `flipTick` to be strictly on the opposite side of `tick` (e.g., for a bid, `flipTick > tick`). This prevents use cases like instant token convertibility, where someone wants to place flip orders on both sides at the same tick to create a stable two-sided market that automatically replenishes when orders are filled. *** ## Specification ### Modified behavior The `placeFlip` validation is relaxed to allow `flipTick == tick`: * **Current behavior**: For bids, `flipTick > tick` required; for asks, `flipTick < tick` required * **New behavior**: For bids, `flipTick >= tick` required; for asks, `flipTick <= tick` required ### Events No new events. ### New errors No new errors. ## Implications * **Locked books**: Same-tick flip orders can result in `best_bid_tick == best_ask_tick`. This forecloses any future upgrade that would forbid bids and asks from resting at the same tick, since same-tick flip orders legitimately create that state. * **MEV**: Tighter flip orders (where `flipTick` is closer to or equal to `tick`) increase the likelihood of certain kinds of MEV, such as backrunning, since the new opposite-side order appears at a better price for the backrunner. ## Invariants * Flip orders with `flipTick == tick` are accepted and behave like any other flip order * Flip orders with `flipTick` strictly on the wrong side of `tick` are still rejected # TIP-1031: Embed Consensus Context into the Block Header ## Abstract Embed consensus metadata into the `TempoHeader`. ## Motivation Consensus context is a prerequisite to newer features in Commonware. * [Deferred Verification](https://github.com/commonwarexyz/monorepo/blob/main/consensus/src/marshal/standard/deferred.rs#L487). A reduction in finalization latency by optimisitically notarizing blocks and verifying them async in the background. By embedding the context into the header, Tempo blocks can implement the required [CertifiableBlock](https://github.com/commonwarexyz/monorepo/blob/2a588e4e341548333a4bd753c016e814ae0ecca0/consensus/src/lib.rs#L57) trait. *** ## Specification When activated, a new field, `Option` is added as the **last** field of `TempoHeader`, which must be set on every subsequent block containing consensus metadata. The field follows the trailing‑optional pattern used by Ethereum's fork‑activated header fields (e.g. `base_fee_per_gas`, `blob_gas_used`): when `None`, it is omitted entirely from the RLP stream, preserving existing block hashes for pre‑activation headers. ```rust #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, RlpEncodable, RlpDecodable)] #[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Context { pub epoch: u64, pub view: u64, pub parent_view: u64, pub leader: B256, } ``` The `Context` adds 3 fields: 3 `u64` values and 1 `B256` value, totaling 56 bytes raw. The field required to construct the [context required by simplex](https://github.com/commonwarexyz/monorepo/blob/main/consensus/src/simplex/types.rs#L22), not present in this struct can be computed using properties of the block, `parent_hash` -> `parent_digest`. **RLP**: Each `u64` encodes to at most 9 bytes (1‑byte prefix + 8 bytes); each `B256` encodes to 33 bytes (1‑byte prefix + 32 bytes). With a list header, the worst‑case overhead is **60 bytes** per block. Pre‑activation headers incur zero overhead. **Compact (DB)**: The `reth_codecs::Compact` representation is comparable, using bitflag‑compressed integer widths. ### Block Production When activated the proposals must: 1. Construct the `Context` from the information provided from the consensus engine. 2. **MUST** set the context field on the header. If not activated, the context field **MUST** be `None`. ### Block Verification During `Automaton::verify()` step: 1. If not activated, the context **MUST** be `None`. 2. If activated, the context **MUST** be set and match the information provided by the consensus engine. 3. Continue with verification as-is. Important to note the immediate switch to `Deferred` is **not strictly required**. For example a validator can choose an implementation that preserves synchronous verification. This validator simply does not contribute to the reduced latency in forming the notarization certificate. ### Genesis Block The Genesis block MUST have the consensus context set to `None`. *** ## Invariants 1. **Context presence**: Every block beyond genesis when activated has a set `Context`. 2. **Context correctness**: The encoded context MUST exactly match the `Context` provided by the consensus engine when the block was proposed. Validators MUST reject blocks where the embedded context does not match. 3. **Context commitment**: Because the context is a part of the header, and the header hash is the block's digest, the context is transitively committed to by any notarization or finalization certificate over that digest. 4. **Backward incompatibility**: This is a breaking change to block verification. All nodes must upgrade at the same protocol version. Blocks produced before the upgrade do not have the set context. Any blocks proposed by a non-upgraded node will have their proposals rejected, and will incorrectly notarize blocks with an invalid context. 5. **Encoding backward compatibility**: The `context` field MUST be the last field in `TempoHeader`. When `None`, it MUST be omitted entirely from the RLP stream so that the encoded representation of pre‑activation headers remains unchanged and existing block hashes are preserved. 6. **Round-trip fidelity**: Context serializes and de-serializes into the same values. ### Test Coverage * `CertifiableBlock::context()` returns the correct context for a block built with known parameters. * Block rejection when embedded context does not match the consensus engine's context. * RLP round-trip for `TempoHeader` with `context: Some(...)` and `context: None`, and that a pre-activation header's hash is unchanged after the upgrade. # TIP-1036: T2 Hardfork Bug Fixes ## Abstract This meta TIP collects audit-driven bug fixes, security hardening, and correctness updates that activate at T2. Each item is small in isolation, but together they define the complete in-scope T2 bug-fix bundle for this TIP, while fixes already specified by other activated TIPs (such as TIP-1017) are intentionally excluded. ## Motivation Internal and external review uncovered several T2-relevant correctness and security issues across core execution paths. Because these fixes alter state-function behavior at activation boundaries, they need hardfork gating and are grouped here as one coordinated rollout. This meta TIP tracks only fixes that are not already specified by another activated TIP (for example, TIP-1017). *** ## Changes ### 1. Require `tx.origin` for AccountKeychain admin ops **PRs**: [#3202](https://github.com/tempoxyz/tempo/pull/3202) · **Author**: @legion2002, [#3250](https://github.com/tempoxyz/tempo/pull/3250) · **Author**: @0xrusowsky With T2, `authorizeKey`, `revokeKey`, and `updateSpendingLimit` require direct owner calls by enforcing both `transaction_key == Address::ZERO` and `msg_sender == tx_origin`. This blocks indirect contract-call paths from being used to perform key-admin actions with owner-level authority. If `tx_origin` is not seeded, admin ops are rejected (failed-closed). ### 2. Reject self-sponsored fee payer signatures **PR**: [#3200](https://github.com/tempoxyz/tempo/pull/3200) *(merged)* · **Author**: @legion2002 Rejects AA transactions where the `fee_payer_signature` resolves back to the sender, preventing self-sponsored signatures from bypassing fee-payer assumptions. Enforced in both txpool validation and EVM fee-payer resolution. ### 3. Check token paused in internal DEX balance swaps **PR**: [#3204](https://github.com/tempoxyz/tempo/pull/3204) · **Author**: @0xrusowsky Adds a `check_not_paused()` call in `StablecoinDEX` internal balance transfers gated behind `is_t2()`. Previously, swaps using internal DEX balances could bypass the token pause state. ### 4. Correct built-in policy type data for TIP403Registry **PR**: [#3203](https://github.com/tempoxyz/tempo/pull/3203) *(merged)* · **Author**: @0xrusowsky Built-in policies (`REJECT_ALL` / `ALLOW_ALL`) are virtual and not stored on-chain. On T2, `policyData()` now returns the correct `PolicyType` (`WHITELIST` / `BLACKLIST` respectively) and `Address::ZERO` admin for these built-in IDs instead of falling through to storage reads. ### 5. Reject legacy invalid policy types in compound sub-policies **PR**: [#3188](https://github.com/tempoxyz/tempo/pull/3188) *(merged)* · **Author**: @howydev Uses `is_simple()` instead of `!is_compound()` to validate compound policy sub-policies, rejecting legacy type-255 policies that previously passed the negated check. ### 6. Handle T2 policy errors in DEX **PR**: [#3015](https://github.com/tempoxyz/tempo/pull/3015) *(merged)* · **Author**: @0xrusowsky Updates DEX precompiles to handle the new `TIP403RegistryError::InvalidPolicyType` error returned by `policy_type()` post-T2, replacing the old `Panic(UnderOverflow)` sentinel. ### 7. Return zero remaining limit for revoked keys **PR**: [#2553](https://github.com/tempoxyz/tempo/pull/2553) *(merged)* · **Author**: @0xrusowsky `getRemainingLimit()` now returns zero for revoked or non-existent access keys instead of a stale positive value. ### 8. Nonce key gas repricing **PR**: [#2533](https://github.com/tempoxyz/tempo/pull/2533) *(merged)* · **Author**: @0xrusowsky Increases intrinsic gas costs for 2D nonce keys on T2 by adding `2 × WARM_SLOAD` to both existing-key and new-key gas to account for extended storage lookups. Base costs differ (`COLD_SLOAD + WARM_SSTORE_RESET` for existing, `COLD_SLOAD + SSTORE_SET` for new), but the T2 delta is the same. ### 9. Error with `PolicyNotFound` for non-existent policy IDs **PR**: [#2618](https://github.com/tempoxyz/tempo/pull/2618) *(merged)* · **Author**: @0xrusowsky `get_policy_data()` now reverts with `PolicyNotFound` for non-existent policy IDs instead of silently returning default values. ### 10. Refund spending limit for unused gas fees **PR**: [#2528](https://github.com/tempoxyz/tempo/pull/2528) *(merged)* · **Author**: @legion2002 Restores access key spending limits by the refunded gas amount in `transfer_fee_post_tx()`. Previously the full max fee was permanently deducted from the spending limit regardless of actual gas used. ### 11. Tick spacing checks on DEX price conversion functions **PR**: [#2513](https://github.com/tempoxyz/tempo/pull/2513) *(merged)* · **Author**: @0xKitsune Adds tick spacing validation to `tick_to_price` and `price_to_tick`, rejecting ticks that don't align with the pool's configured spacing. ### 12. Reserved liquidity transient storage check **PR**: [#2496](https://github.com/tempoxyz/tempo/pull/2496) *(merged)* · **Author**: @0xKitsune Adds a transient storage (`TSTORE`/`TLOAD`) guard to prevent reserved liquidity from being double-spent within the same transaction. ### 13. Reject zero-address ecrecover in permit **PR**: [#2786](https://github.com/tempoxyz/tempo/pull/2786) *(merged)* · **Author**: @howydev `permit()` now explicitly rejects `recovered == address(0)` before comparing against `owner`. Previously, a crafted signature recovering to `address(0)` could have been accepted if `owner` was also `address(0)`. # TIP-1038: T3 Hardfork Meta TIP ## Abstract This meta TIP collects bug fixes, gas correctness improvements, and security hardening changes that activate at T3. Each item is small in isolation, but together they define the complete in-scope T3 bug-fix bundle. ## Motivation Ongoing internal review and audit follow-ups uncovered several correctness and performance issues that require hardfork gating. Because these fixes alter state-function behavior at activation boundaries, they are grouped here as one coordinated rollout under T3. *** ## Changes ### 1. Skip redundant `setUserToken` write and event when token unchanged **PR**: [#3272](https://github.com/tempoxyz/tempo/pull/3272) · **Author**: @fgimenez `setUserToken()` unconditionally writes to storage and emits `UserTokenSet` even when the token hasn't changed. Since the event triggers a full O(pool\_size) txpool scan via `evict_invalidated_transactions`, any account can force network-wide CPU work each block by repeatedly calling `setUserToken` with the same value. T3+ adds a read-before-write guard that returns early when the stored token already matches, eliminating the redundant write, event, and pool scan. ### 2. TIP-20: verify paused state before mint and burn **PR**: [#3411](https://github.com/tempoxyz/tempo/pull/3411) · **Author**: @0xrusowsky The TIP-20 pause mechanism was missing from `mint`, `mintWithMemo`, `burn`, `burnWithMemo`, and `burnBlocked` — an unintentional omission that left token-moving operations reachable while the contract was paused. The pause flag is meant to act as a universal kill switch; leaving mint/burn unguarded undermines that guarantee and, in particular, prevents the admin from stopping a compromised `BURN_BLOCKED_ROLE` key from seizing funds. T3+ adds `check_not_paused()` to all five functions. A new `validate_mint` helper consolidates the pause check, recipient validation, and TIP-403 policy check into a single call. Administrative functions (role management, unpausing) and `transferFeePostTx` remain intentionally exempt. ### 3. Disambiguate optional AA expiry and validity timestamps **PRs**: [#3500](https://github.com/tempoxyz/tempo/pull/3500), [#3501](https://github.com/tempoxyz/tempo/pull/3501) · **Author**: @legion2002 Several AA timestamp fields were encoded as `Option` in RLP, but `None` and `Some(0)` both serialize as the empty string. That made `Some(0)` silently roundtrip to `None`, which could invert user intent by turning an immediately expired access key or transaction into one with no expiry bound. T3+ changes `key_authorization.expiry`, `valid_before`, and `valid_after` to `Option` at the primitives layer so zero-valued bounds are unrepresentable. Serde-backed JSON/request deserialization rejects `0x0` explicitly for these fields, while downstream execution and pool components convert back to plain `u64` only where comparisons or storage require it. ### 4. StablecoinDEX: check token paused in internal balance swaps **PR**: [#3204](https://github.com/tempoxyz/tempo/pull/3204) · **Author**: @0xrusowsky The StablecoinDEX `swap_exact_amount_in` and `swap_exact_amount_out` paths operate on internal DEX balances and bypass TIP-20 `transfer`, so the pause check in the token contract is never hit. A paused token could still be swapped through the DEX — including as an intermediate hop in a multi-leg route. T3+ adds `check_not_paused()` to `validate_and_build_route` for every token in the swap path, ensuring paused tokens block DEX swaps the same way they block direct transfers. ### 5. Account-keychain: clamp refunded spending limits to the configured max **PR**: [#3483](https://github.com/tempoxyz/tempo/pull/3483) · **Author**: @rakita `refund_spending_limit()` restored spending room with a saturating add, which could raise a T3 key's remaining allowance above the configured max during defensive refund paths. T3+ clamps refunded spending limits to the stored `max`, preserving the invariant that `remaining <= max` for T3 keys while leaving migrated pre-T3 rows on their legacy behavior because they do not persist a max bound. # TIP-1047: Revert code creation and set code at addresses with TIP-20 prefix ## Abstract Reject any CREATE, CREATE2, or EIP-7702 authorization that would produce or delegate to an address starting with the TIP-20 token prefix (`0x20C000000000000000000000`). Without this guard, anyone can place arbitrary bytecode at an address that the rest of the system treats as a TIP-20 token. ## Motivation Multiple system components use `is_tip20_prefix` to decide whether an address is a TIP-20 token: precompile routing, fee-token validation, stablecoin DEX, and transaction classification. Code or a delegation at a prefix-matching address would pass these checks despite not being a real token. CREATE and CREATE2 addresses are downstream of keccak; EIP-7702 authority addresses require secp256k1 point multiplication followed by keccak. Matching a 12-byte prefix requires ~2^96 work in all cases. This guard provides defense-in-depth by making such collisions fail-closed. *** ## Specification ### CREATE and CREATE2 Before setting up a create frame, compute the would-be contract address: * **CREATE**: `keccak256(rlp(caller, nonce))[12..]` * **CREATE2**: `keccak256(0xff ++ caller ++ salt ++ keccak256(init_code))[12..]` If `is_tip20_prefix(address)` is true, revert the opcode. The caller's nonce is not bumped, no value is transferred, and no init-code is executed. Base opcode gas is consumed; init-code gas is not. ### EIP-7702 Authorizations When processing EIP-7702 authorization lists, if `is_tip20_prefix(authority)` is true for a recovered authority address, skip the entry. The delegation is not applied. *** ## Invariants 1. **No new code at TIP-20 addresses**: No CREATE/CREATE2 produces a contract where `is_tip20_prefix` returns true. Applies to all depths (top-level and nested creates). 2. **No delegations to TIP-20 addresses**: No EIP-7702 authorization sets a delegation for an address where `is_tip20_prefix` returns true. 3. **Nonce preservation**: A rejected CREATE/CREATE2 does not bump the caller's nonce. ### Test coverage * CREATE2 with a salt producing a TIP-20-prefixed address reverts without executing init-code. * CREATE where the computed address has a TIP-20 prefix reverts with nonce unchanged. * EIP-7702 authorization with a TIP-20-prefixed authority is skipped. * Normal CREATE/CREATE2 producing non-TIP-20 addresses is unaffected. # Prool setup: local Tempo test environment Setup infinite pooled Tempo node instances in TypeScript using [`prool`](https://github.com/wevm/prool) by following the steps below. ::::steps ## Install Prool for Tempo :::code-group ```bash [npm] npm i prool ``` ```bash [pnpm] pnpm i prool ``` ```bash [bun] bun i prool ``` ::: * [Prool](https://github.com/wevm/prool) is a library that provides programmatic HTTP testing instances for Ethereum. ## Create a Prool instance You can programmatically start a Tempo node instance in TypeScript using `Instance.tempo`: ```ts import { Instance, Server } from 'prool' const server = Server.create({ instance: Instance.tempo(), }); // Start the node await server.start() // Instances available at: // - http://localhost:8545/1 // - http://localhost:8545/2 // - http://localhost:8545/3 // - http://localhost:8545/4 // - http://localhost:8545/n ``` :::tip You can also set up the Tempo instance using `Instance.tempo` directly if you do not need pooling. ```ts import { Instance } from 'prool'; const instance = Instance.tempo() // Start the node await instance.start() // Instance available at: http://localhost:8545 ``` ::: ## Next steps for Prool After you have set up Tempo with Prool, you can now: * Easily use Tempo in your test suite with Vitest * Run Tempo locally alongside your Vite or Next.js development server ::::