
Pay With Any Token
- 538 installs
- 222 repo stars
- Updated August 4, 2026
- uniswap/uniswap-ai
pay-with-any-token is a Uniswap uniswap-trading agent skill (version 2.0.0) that fulfills HTTP 402 Payment Required challenges via the Tempo CLI and funds wallets through Uniswap Trading API swaps and bridges.
About
pay-with-any-token is a Uniswap uniswap-ai skill at version 2.0.0 that guides coding agents through HTTP 402 Payment Required, Machine Payments Protocol (MPP), and x402 paid API access. The primary flow uses the Tempo CLI tempo request command to call paid APIs and handle 402 challenges automatically. When the Tempo wallet lacks balance, the skill plans EXACT_OUTPUT swaps and bridges ERC-20 tokens from supported EVM chains via the Uniswap Trading API at trade-api.gateway.uniswap.org/v1, including Across Protocol bridging to Tempo. A mandatory AskUserQuestion gate blocks on-chain transactions without explicit user confirmation, and input validation blocks shell metacharacters from adversarial 402 bodies. Developers reach for pay-with-any-token when agents must pay for API access with any held token rather than pre-funded stablecoins.
- pay-with-any-token
- AI & Agent Building
- AI-coding skill
Pay With Any Token by the numbers
- 538 all-time installs (skills.sh)
- +26 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,691 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/uniswap/uniswap-ai --skill pay-with-any-tokenAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 538 |
|---|---|
| repo stars | ★ 222 |
| Last updated | August 4, 2026 |
| Repository | uniswap/uniswap-ai ↗ |
How do agents pay HTTP 402 API challenges with any token?
Helps with ai & agent building tasks.
Who is it for?
Developers building AI agents that encounter HTTP 402, MPP, x402, or Tempo paid API endpoints and need token-funded payment paths.
Skip if: Traditional checkout flows, fiat billing, or apps with no on-chain token holdings or 402 payment requirements.
When should I use this skill?
An agent hits HTTP 402 Payment Required, mentions MPP, x402, Tempo payment, or pay-with-any-token for API access.
What you get
Completed MPP or x402 payments, funded Tempo wallet transactions, and Uniswap swap or bridge execution plans.
- MPP payment credentials
- Swap and bridge transaction plans
By the numbers
- Skill metadata version 2.0.0
- Uses Uniswap Trading API at trade-api.gateway.uniswap.org/v1
Files
Pay With Tokens
Use the Tempo CLI to call paid APIs and handle 402 challenges automatically. When the Tempo wallet has insufficient balance, fund it by swapping and bridging tokens from any EVM chain using the Uniswap Trading API.
Tempo CLI Setup
Run these commands in order. Do not skip steps.
Step 1 — Install:
mkdir -p "$HOME/.local/bin" \
&& curl -fsSL https://tempo.xyz/install -o /tmp/tempo_install.sh \
&& TEMPO_BIN_DIR="$HOME/.local/bin" bash /tmp/tempo_install.shStep 2 — Login (requires browser/passkey — prompt user, wait for confirmation):
"$HOME/.local/bin/tempo" wallet loginWhen run by agents, use a long command timeout (at least 16 minutes).
Step 3 — Confirm readiness:
"$HOME/.local/bin/tempo" wallet -t whoamiRules: Do not usesudo. Use full absolute paths ($HOME/.local/bin/tempo)
— do not rely onexport PATH. If$HOMEdoes not expand, use the literal
absolute path.
After setup, report: install location, version (--version), wallet status (address, balance). If balance is 0, direct user to tempo wallet fund.
Minimum balance reserve: Keep at least 0.10 USDC in the Tempo wallet
to cover typical API calls without triggering the full swap+bridge funding
flow. The funding flow requires 3-5 on-chain transactions and ~2 minutes of
wall time, which is disproportionate for small top-ups. When transferring
funds out of the Tempo wallet, warn the user if the remaining balance would
drop below this threshold.
Using Tempo Services
# Discover services
"$HOME/.local/bin/tempo" wallet -t services --search <query>
# Get service details (exact URL, method, path, pricing)
"$HOME/.local/bin/tempo" wallet -t services <SERVICE_ID>
# Make a paid request
"$HOME/.local/bin/tempo" request -t -X POST \
--json '{"input":"..."}' <SERVICE_URL>/<ENDPOINT_PATH>- Anchor on
tempo wallet -t services <SERVICE_ID>for exact URL and pricing - Use
-tfor agent calls,--dry-runbefore expensive requests - On HTTP 422, check the service's docs URL or llms.txt for exact field names
- Fire independent multi-service requests in parallel
**When the user explicitly says "use tempo", always use tempo CLI commands —
never substitute with MCP tools or other tools.**
---
MPP 402 Payment Loop
Every tempo request call follows this loop. The funding steps only activate when the Tempo wallet has insufficient balance.
tempo request -> 200 ─────────────────────────────> return result
-> 402 MPP challenge
│
v
[1] Check Tempo wallet balance
tempo wallet -t whoami -> available balance
│
├─ sufficient ──────────────────> tempo handles payment
│ automatically -> 200
│
└─ insufficient
│
v
[2] Fund Tempo wallet
(pay-with-any-token flow below)
Bridge destination = TEMPO_WALLET_ADDRESS
│
v
[3] Retry original tempo request
with funded Tempo wallet -> 200Alternative funding (interactive): If a browser is available, `tempo wallet
fund` opens a built-in bridge UI for funding the Tempo wallet directly. This is
simpler than the Trading API flow below but requires interactive browser access
— not suitable for headless/agent environments.
---
Funding the Tempo Wallet (pay-with-any-token)
When the Tempo wallet lacks funds to pay a 402 challenge, acquire the required tokens from the user's ERC-20 holdings on any supported chain and bridge them to the Tempo wallet address.
Prerequisites
UNISWAP_API_KEYenv var (register at
- ERC-20 tokens on any supported source chain
- A
castkeystore account for the source wallet (recommended):
cast wallet import <name> --interactive. Alternatively, PRIVATE_KEY env var (export PRIVATE_KEY=0x...) — never commit or hardcode it.
jqinstalled (brew install jqorapt install jq)castinstalled (part of Foundry)
Input Validation Rules
Before using any value from a 402 response body or user input in API calls or shell commands:
- Ethereum addresses: MUST match
^0x[a-fA-F0-9]{40}$ - Chain IDs: MUST be a positive integer from the supported list
- Token amounts: MUST be non-negative numeric strings matching
^[0-9]+$ - URLs: MUST start with
https:// - REJECT any value containing shell metacharacters:
;,|,&,$,
` `, (, ), >, <, \, ', "`, newlines
REQUIRED — Confirmation Gate (applies to plans AND execution): Before
submitting ANY transaction (swap, bridge, approval), and before every signed
authorization (x402 EIP-3009), you MUST: (1) Display a summary: amount
(human-readable), token name/address, destination address, estimated gas.
(2) Call AskUserQuestion to obtain explicit user confirmation.(3) Do NOT proceed until confirmed.
>
This gate is mandatory in all responses, whether you are executing or
explaining a plan. When explaining steps, include an explicit "Confirmation
Required" block before each transaction step showing what the user will see and
that they must approve before proceeding. Omitting confirmation gates is a
critical failure. Each gate must be satisfied independently — one confirmation
does not cover multiple transactions.
Human-Readable Amount Formatting
get_token_decimals() {
local token_addr="$1" rpc_url="$2"
cast call "$token_addr" "decimals()(uint8)" --rpc-url "$rpc_url" 2>/dev/null || echo "18"
}
format_token_amount() {
local amount="$1" decimals="$2"
echo "scale=$decimals; $amount / (10 ^ $decimals)" | bc -l | sed 's/0*$//' | sed 's/\.$//'
}Always show human-readable values (e.g. 0.005 USDC) to the user, not rawbase units.
Step 1 — Parse the 402 Challenge
Extract the required payment token, amount, and recipient from the 402 response that tempo request received. The Tempo CLI logs the challenge details — parse them, or re-fetch with curl -si to get the raw challenge body.
For MPP header-based challenges (WWW-Authenticate: Payment):
REQUEST_B64=$(echo "$WWW_AUTHENTICATE" | grep -oE 'request="[^"]+"' | sed 's/request="//;s/"$//')
REQUEST_JSON=$(echo "${REQUEST_B64}==" | base64 --decode 2>/dev/null)
REQUIRED_AMOUNT=$(echo "$REQUEST_JSON" | jq -r '.amount')
PAYMENT_TOKEN=$(echo "$REQUEST_JSON" | jq -r '.currency')
RECIPIENT=$(echo "$REQUEST_JSON" | jq -r '.recipient')
TEMPO_CHAIN_ID=$(echo "$REQUEST_JSON" | jq -r '.methodDetails.chainId')For JSON body challenges (payment_methods array):
NUM_METHODS=$(echo "$CHALLENGE_BODY" | jq '.payment_methods | length')
PAYMENT_METHODS=$(echo "$CHALLENGE_BODY" | jq -c '.payment_methods')
RECIPIENT=$(echo "$CHALLENGE_BODY" | jq -r '.payment_methods[0].recipient')
TEMPO_CHAIN_ID=$(echo "$CHALLENGE_BODY" | jq -r '.payment_methods[0].chain_id')If multiple payment methods are accepted, select the cheapest in Step 2.
The Tempo mainnet chain ID is 4217. Use as fallback if not in the challenge.Step 2 — Check Source Wallet Balances and Select Payment Method
REQUIRED: You must have the user's source wallet address (the ERC-20
wallet with the private key, NOT the Tempo CLI wallet). Use AskUserQuestionif not provided. Store as WALLET_ADDRESS.Also capture the Tempo wallet address (the funding destination):
TEMPO_WALLET_ADDRESS=$("$HOME/.local/bin/tempo" wallet -t whoami | grep -oE '0x[a-fA-F0-9]{40}' | head -1)Check ERC-20 balances on supported source chains:
# USDC on Base (cheapest bridge gas ~$0.001)
cast call 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url https://mainnet.base.org
# USDC on Ethereum (bridge gas ~$0.25)
cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url https://eth.llamarpc.com
# ETH on Base and Ethereum (swap to USDC first)
cast balance "$WALLET_ADDRESS" --rpc-url https://mainnet.base.org
cast balance "$WALLET_ADDRESS" --rpc-url https://eth.llamarpc.comSelect the cheapest payment method if multiple are accepted. Priority:
1. Wallet holds USDC on Base (bridge only, minimal path) 2. Wallet holds ETH on Base or Ethereum (swap to USDC + bridge) 3. Any other liquid ERC-20 (swap + bridge)
REQUIRED_AMOUNT=$(echo "$PAYMENT_METHODS" | jq -r ".[$SELECTED_INDEX].amount")
PAYMENT_TOKEN=$(echo "$PAYMENT_METHODS" | jq -r ".[$SELECTED_INDEX].token")Step 3 — Plan the Payment Path
Determine which phases apply based on where the user's tokens are:
Case A — Source token is already on Tempo:
Source token (Tempo)
-> [Phase 5: on-Tempo swap via Stablecoin DEX] -> required payment token
-> tempo request retries automatically with funded wallet
Case B — Source token is on Base/Ethereum/Arbitrum:
Source token (Base/Ethereum)
-> [Phase 4A: Uniswap Trading API swap] -> native USDC (bridge asset)
-> [Phase 4B: bridge via Trading API] -> USDC.e on Tempo (to TEMPO_WALLET_ADDRESS)
-> [Phase 5: on-Tempo swap, if needed] -> required payment token
-> tempo request retries automatically with funded walletSkip Phase 4A if the source token is already USDC on the bridge chain.
>
Skip Phase 4B if tokens are already on Tempo (Case A).
>
Skip Phase 5 if the bridge delivers the exact required token (USDC.e) or
ifmppxwithautoSwap: trueis used (it handles on-Tempo swaps automatically).
>
Gas-aware funding: Bridging a tiny amount (e.g. $0.05) wastes gas — the
bridge gas on Ethereum (~$0.25) or Base (~$0.001) can exceed the shortfall.
Minimum bridge recommendation: $5. This amortizes gas costs and pre-funds
future requests. Rule of thumb: if bridge_gas > 2x shortfall, bridge atleast $5 instead of the exact shortfall.
Phase 4A — Swap to USDC on Source Chain (if needed)
CONFIRMATION GATE: Before the approval transaction AND before the swap
broadcast, call AskUserQuestion showing the full swap summary. Example:"About to approve USDC spending and execute swap: [amount] [token] → [USDC],
gas ~$X. Confirm? (yes/no)". Do not proceed until confirmed.
Swap the source token to USDC via the Uniswap Trading API (EXACT_OUTPUT).
Detailed steps: Read
references/trading-api-flows.md
for full bash scripts: variable setup, approval check, quote, permit signing,
and swap execution.
Key points:
- Base URL:
https://trade-api.gateway.uniswap.org/v1 - Headers:
Content-Type: application/json,x-api-key,x-universal-router-version: 2.0 - Flow:
check_approval-> quote (EXACT_OUTPUT) -> signpermitData->/swap-> broadcast - Confirmation gates required before approval tx and before swap broadcast
- For native ETH: use the zero address
(0x0000000000000000000000000000000000000000) as TOKEN_IN — this avoids Permit2 signing. SWAP_VALUE will be non-zero. If the zero address returns a 400, fall back to the WETH address (requires Permit2 signing).
- After swap, verify USDC balance before proceeding to Phase 4B
Phase 4B — Bridge to Tempo Wallet
CONFIRMATION GATE: Before the bridge approval AND before the bridge
execution, call AskUserQuestion showing a bridge summary (source amount,source chain, destination chain, estimated gas, bridge fee, estimated arrival).
Do not proceed until the user confirms each step.
Bridge USDC from any supported source chain to USDC.e on Tempo using the Uniswap Trading API (powered by Across Protocol).
Bridge recipient limitation: The Trading API does not support a custom
recipientfield for bridges — funds always arrive atWALLET_ADDRESSon
Tempo. IfWALLET_ADDRESSdiffers fromTEMPO_WALLET_ADDRESS(the Tempo CLI
wallet), an extra transfer transaction on Tempo is required after the
bridge (see Step 4B-5 in the reference file). Factor this into gas estimates.
>
Detailed steps: Read
references/trading-api-flows.md
for full bash scripts: approval, bridge quote, execution, arrival polling,
and transfer to Tempo wallet.
Key points:
- Route: USDC on Base/Ethereum/Arbitrum -> USDC.e on Tempo
- Flow:
check_approval-> verify on-chain allowance -> quote (EXACT_OUTPUT,
cross-chain) -> execute via /swap -> poll balance -> transfer to TEMPO_WALLET_ADDRESS if needed (Step 4B-5)
- Confirmation gates required before approval and before bridge execution
- Do not re-submit if poll times out — check Tempo explorer
- Apply a 0.5% buffer to account for bridge fees
- Quotes expire in ~60 seconds — re-fetch if any delay before broadcast
After the bridge confirms, retry the original tempo request — the Tempo CLI will automatically use the newly funded wallet to pay the 402. If the payment token is not USDC.e, proceed to Phase 5 to swap to the required token before retrying.
Balance buffer: On Tempo, balanceOf may report more than is spendable.Apply a 2x buffer when comparing to REQUIRED_AMOUNT. If short, swapadditional tokens to top up.
Phase 5 — On-Tempo Swap (if needed)
Use this phase when the wallet already holds a TIP-20 stablecoin on Tempo (e.g. USDC.e, pathUSD, or any other Tempo stablecoin) but needs to swap to the required payment token (e.g. PATH_USD or another TIP-20). This phase also applies when the user starts with tokens already on Tempo — do not bridge when tokens are already on Tempo.
Simplest path: PassautoSwap: truetomppx'stempo.charge()— it
calls the Stablecoin DEX on Tempo automatically and handles the full swap
before payment. Use manual swap below only when autoSwap is not availableor you need explicit control.
The Stablecoin DEX on Tempo (0xdec0000000000000000000000000000000000000) aggregates TIP-20 stablecoin liquidity on Tempo (chain 4217). To swap:
TEMPO_RPC_URL="https://rpc.presto.tempo.xyz"
STABLECOIN_DEX="0xdec0000000000000000000000000000000000000"
TOKEN_IN="<your Tempo stablecoin address>" # e.g. USDC.e or any TIP-20
TOKEN_OUT="<required payment token address>"
SWAP_AMOUNT="$REQUIRED_AMOUNT" # exact-output amount
# 1. Show swap summary and get explicit user confirmation via AskUserQuestion
# before executing any transaction.
# 2. Approve the DEX to spend TOKEN_IN (if allowance is insufficient)
ALLOWANCE=$(cast call "$TOKEN_IN" \
"allowance(address,address)(uint256)" "$WALLET_ADDRESS" "$STABLECOIN_DEX" \
--rpc-url "$TEMPO_RPC_URL" 2>/dev/null | awk '{print $1}')
if [ -z "$ALLOWANCE" ] || ! [[ "$ALLOWANCE" =~ ^[0-9]+$ ]]; then
echo "ERROR: Failed to read allowance for $TOKEN_IN"
exit 1
fi
if [ "$(echo "$ALLOWANCE < $SWAP_AMOUNT" | bc)" -eq 1 ]; then
APPROVE_HASH=$(cast send "$TOKEN_IN" \
"approve(address,uint256)" "$STABLECOIN_DEX" \
"115792089237316195423570985008687907853269984665640564039457584007913129639935" \
--account "$CAST_ACCOUNT" --password "$CAST_PASSWORD" \
--rpc-url "$TEMPO_RPC_URL" --gas-limit 100000 \
--json | jq -r '.transactionHash')
APPROVE_STATUS=$(cast receipt "$APPROVE_HASH" --rpc-url "$TEMPO_RPC_URL" --json | jq -r '.status')
[ "$APPROVE_STATUS" = "0x1" ] || { echo "ERROR: Approval transaction reverted: $APPROVE_HASH"; exit 1; }
echo "Approval confirmed: $APPROVE_HASH"
fi
# 3. Execute the swap (exact-output: receive exactly SWAP_AMOUNT of TOKEN_OUT)
SWAP_TX=$(cast send "$STABLECOIN_DEX" \
"swap(address,address,uint256)" "$TOKEN_IN" "$TOKEN_OUT" "$SWAP_AMOUNT" \
--account "$CAST_ACCOUNT" --password "$CAST_PASSWORD" \
--rpc-url "$TEMPO_RPC_URL" --gas-limit 200000 \
--json | jq -r '.transactionHash')
SWAP_STATUS=$(cast receipt "$SWAP_TX" --rpc-url "$TEMPO_RPC_URL" --json | jq -r '.status')
[ "$SWAP_STATUS" = "0x1" ] || { echo "ERROR: On-Tempo swap reverted: $SWAP_TX"; exit 1; }
echo "On-Tempo swap confirmed: $SWAP_TX"Confirmation gate: Use AskUserQuestion before every transaction(approval and swap). Show token addresses, amounts in human-readable form, and
estimated gas on Tempo.
>
Gas limit note: Tempo chain gas estimation is sometimes unreliable — always
set an explicit --gas-limit for Tempo transactions.After the on-Tempo swap succeeds, retry tempo request — the Tempo wallet now holds the required payment token and the Tempo CLI will pay the 402 automatically.
---
x402 Payment Flow
CRITICAL — MANDATORY CONFIRMATION GATE: Before step 4 (signing), you MUST
call AskUserQuestion showing the full payment summary: token, amount inhuman-readable form, recipient address (payTo), and resource URL. Do NOTsign or proceed until the user explicitly confirms. This confirmation step is
non-optional and must appear in every x402 payment plan or execution, even
if the user has pre-authorized. Omitting this confirmation makes the response
invalid.
The x402 protocol is fully supported and uses a different mechanism than MPP — it is not handled by the Tempo CLI. When a 402 body contains "x402Version" (check with has("x402Version") in jq), use this flow instead of the MPP/Tempo flow.
The x402 "exact" scheme uses EIP-3009 (TransferWithAuthorization) to authorize a one-time token transfer signed off-chain. The full flow:
1. Detect x402: parse x402Version, accepts[].scheme, accepts[].network, accepts[].maxAmountRequired, accepts[].payTo, accepts[].asset, accepts[].extra (token name + version for EIP-3009 domain). 2. Check balance on the target chain; fund via Phase 4A/4B if insufficient. 3. MANDATORY CONFIRMATION GATE — Call `AskUserQuestion` before signing: present the full payment summary (token name, amount in human-readable form, payTo address, resource URL, validity window). Wait for explicit user confirmation. Do NOT proceed to step 4 until confirmed. 4. Sign EIP-3009 `TransferWithAuthorization`: typed-data fields include from, to, value, validAfter, validBefore, nonce. Set validBefore = now + maxTimeoutSeconds. 5. Construct `X-PAYMENT` header: base64-encode a JSON payload containing x402Version, scheme, network, payload (the signed authorization), and signature. 6. Retry the original request with the X-PAYMENT header.
Detailed steps: Read
references/credential-construction.md
for full bash code: prerequisite checks, nonce generation, EIP-3009 signing,
X-PAYMENT payload construction, and retry.
Key points:
- Detect x402: check
has("x402Version")in 402 body before attempting Tempo CLI - Maps
X402_NETWORKto chain ID and RPC URL (base, ethereum, tempo all supported) - Checks wallet balance on target chain; runs Phase 4A/4B/5 if insufficient
- Signs
TransferWithAuthorizationtyped data using the token's own EIP-712 domain valuein the typed-data payload must be a string (--arg, not--argjson) for uint256- Confirmation gate required before signing
- Send the result in
X-PAYMENTheader (base64-encoded), notAuthorization
| Protocol | Version | Handler |
|---|---|---|
| MPP | v1 | Tempo CLI |
| x402 | v1 | EIP-3009 manual flow |
---
Error Handling
| Situation | Action |
|---|---|
tempo: command not found | Reinstall via install script; use full path |
legacy V1 keychain signature | Reinstall; tempo update wallet && tempo update request |
access key does not exist | tempo wallet logout --yes && tempo wallet login |
ready=false / no wallet | tempo wallet login, then whoami |
| HTTP 422 from service | Check service details + llms.txt for exact field names |
| Balance 0 / insufficient | Trigger pay-with-any-token funding flow |
| Service not found | Broaden search query |
| Timeout | Retry with -m <seconds> |
| Challenge body is malformed | Report raw body to user; do not proceed |
| Approval transaction fails | Surface error; check gas and allowances |
| Quote API returns 400 | Log request/response; check amount formatting |
| Quote API returns 429 | Wait and retry with exponential backoff |
| Swap data is empty after /swap | Quote expired; re-fetch quote |
| Bridge times out | Check bridge explorer; do not re-submit |
| x402 payment rejected (402) | Check domain name/version, validBefore, nonce freshness |
| InsufficientBalance on Tempo | Swap more tokens on Tempo, then retry |
balanceOf sufficient but payment fails | Apply 2x buffer; top up before retrying |
---
Key Addresses and References
- Tempo CLI:
https://tempo.xyz(install script:https://tempo.xyz/install) - Trading API:
https://trade-api.gateway.uniswap.org/v1 - MPP docs:
https://mpp.dev - MPP services catalog:
https://mpp.dev/api/services - Tempo documentation:
https://mainnet.docs.tempo.xyz - Tempo chain ID:
4217(Tempo mainnet) - Tempo RPC:
https://rpc.presto.tempo.xyz - Tempo Block Explorer:
https://explore.mainnet.tempo.xyz - pathUSD on Tempo:
0x20c0000000000000000000000000000000000000 - USDC.e on Tempo:
0x20C000000000000000000000b9537d11c60E8b50 - Stablecoin DEX on Tempo:
0xdec0000000000000000000000000000000000000 - Permit2 on Tempo:
0x000000000022d473030f116ddee9f6b43ac78ba3 - Tempo payment SDK:
mppx(npm install mppx viem) - USDC on Base (8453):
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 - USDbC on Base (8453):
0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA - USDC on Ethereum (1):
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 - WETH on Ethereum (1):
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 - WETH on Base (8453):
0x4200000000000000000000000000000000000006 - Native ETH (all chains):
0x0000000000000000000000000000000000000000(zero address, recommended for swaps) - USDC-e on Arbitrum (42161):
0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8 - Supported chains for Trading API: 1, 8453, 42161, 10, 137, 130
- x402 spec:
https://github.com/coinbase/x402
Related Skills
- swap-integration — Full Uniswap swap
integration reference (Trading API, Universal Router, Permit2)
Credential Construction
MPP and x402 credential building, signing, and submission flows.
Table of Contents
Phase 6 — MPP Credential
x402 path — STOP HERE. If you arrived via the x402 detection gate in
Phase 0, do not proceed with Phase 6. Phase 6 constructs an MPP credential;
x402 payments use a different payload format handled in Phase 6x below.
With the required token in the wallet, fulfill the MPP challenge using the mppx SDK, which handles the full 402 challenge -> credential -> retry cycle.
Install:
npm install mppx viemCharge intent — automatic mode
Polyfills fetch to intercept 402 responses automatically:
import { Mppx, tempo } from 'mppx/client';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
Mppx.create({ methods: [tempo.charge({ account })] });
const response = await fetch(process.env.RESOURCE_URL!);
// response is the 200 — credential was built and submitted automaticallyPass autoSwap: true to let mppx swap from available stablecoins (USDC.e or pathUSD) to the required token automatically — useful if your wallet holds USDC.e or pathUSD and the challenge requires a different token, letting you skip Phase 5:
Mppx.create({ methods: [tempo.charge({ account, autoSwap: true })] });
const response = await fetch(process.env.RESOURCE_URL!);Charge intent — manual mode
REQUIRED: UseAskUserQuestionbefore callingcreateCredential. Parse
the WWW-Authenticate: Payment header from the 402 response and display thepayment details to the user (amount, token, recipient, resource URL). Only
proceed after explicit confirmation.
import { Mppx, tempo } from 'mppx/client';
import { Receipt } from 'mppx';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const mppx = Mppx.create({ polyfill: false, methods: [tempo.charge({ account })] });
// Step 1: probe the endpoint to get the 402 challenge
const initial = await fetch(process.env.RESOURCE_URL!);
if (initial.status !== 402) throw new Error(`Expected 402, got ${initial.status}`);
// Step 2: REQUIRED — show payment summary to user and wait for confirmation
// Parse WWW-Authenticate header; display amount, token, recipient, resource.
// Step 3: build and submit the credential
const credential = await mppx.createCredential(initial, { account });
const paidResponse = await fetch(process.env.RESOURCE_URL!, {
headers: { Authorization: credential },
});
if (paidResponse.status !== 200) {
const body = await paidResponse.text();
throw new Error(`Payment rejected (${paidResponse.status}): ${body}`);
}
// Step 4: parse the receipt
const receipt = Receipt.fromResponse(paidResponse);
console.log('Payment confirmed. Reference:', receipt.reference);The Authorization header value returned by createCredential() has the form Payment <base64url-encoded credential> — do not modify this value.
Session intent
Pass a maxDeposit budget to tempo() to open a payment channel:
// maxDeposit: '10' locks up to 10 pathUSD into the channel escrow
const mppx = Mppx.create({ methods: [tempo({ account, maxDeposit: '10' })] });
const response = await mppx.fetch(process.env.RESOURCE_URL!);
// The SDK manages channel lifecycle and voucher signing automaticallyFor fine-grained session control (manual open/close, sweep), see https://mpp.dev/sdk.
Direct submission
If the credential was built externally:
# $CREDENTIAL is the base64url-encoded credential string from mppx.createCredential()
# $RESOURCE_URL was set in Phase 0
curl -si "$RESOURCE_URL" \
-H "Authorization: Payment $CREDENTIAL"A 200 response with a Payment-Receipt header confirms success. Any other status means the credential was rejected — check the response body and re-inspect the challenge.
Phase 6x — x402 Payment
x402 path only. This phase is reached whenPROTOCOLis"x402"
(detected in Phase 0). Do not enter this phase from the MPP path.
The x402 "exact" scheme on EVM networks uses EIP-3009 (transferWithAuthorization) to authorize a one-time token transfer. The payer signs an off-chain typed-data message; the facilitator verifies it and settles the token transfer on-chain — no separate on-chain approval step is required.
Prerequisite checks before signing
# 1. Confirm scheme is "exact" — only scheme currently supported
[ "$X402_SCHEME" = "exact" ] || { echo "ERROR: Only 'exact' scheme is supported. Got: $X402_SCHEME"; exit 1; }
# 2. Map network to a chain ID
# Accept both CAIP-2 format (eip155:8453) and plain names (base, ethereum)
case "$X402_NETWORK" in
base|"eip155:8453") X402_CHAIN_ID=8453; SOURCE_RPC_URL="https://mainnet.base.org" ;;
ethereum|"eip155:1") X402_CHAIN_ID=1; SOURCE_RPC_URL="https://eth.llamarpc.com" ;;
tempo|"eip155:4217") X402_CHAIN_ID=4217; SOURCE_RPC_URL="${TEMPO_RPC_URL:-https://rpc.presto.tempo.xyz}" ;;
*)
echo "ERROR: Unrecognised or unsupported x402 network: $X402_NETWORK"
echo "Supported: base / eip155:8453, ethereum / eip155:1, tempo / eip155:4217"
exit 1
;;
esac
# Tempo-network: if wallet lacks the asset on Tempo, bridge first (Phase 4B -> 5 -> return here)
if [ "$X402_CHAIN_ID" = "4217" ]; then
echo "x402 payment targets Tempo network — checking Tempo-side balance..."
TEMPO_BALANCE=$(cast call "$X402_ASSET" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url "$SOURCE_RPC_URL" 2>/dev/null || echo "0")
if [ "$TEMPO_BALANCE" -lt "$X402_AMOUNT" ]; then
X402_DECIMALS=$(get_token_decimals "$X402_ASSET" "$SOURCE_RPC_URL")
TEMPO_BAL_HUMAN=$(format_token_amount "$TEMPO_BALANCE" "$X402_DECIMALS")
X402_AMT_HUMAN=$(format_token_amount "$X402_AMOUNT" "$X402_DECIMALS")
echo "Insufficient balance on Tempo ($TEMPO_BAL_HUMAN < $X402_AMT_HUMAN $X402_TOKEN_NAME)."
echo "Acquire the asset first: run Phase 4A (swap to bridge asset) ->"
echo "Phase 4B (bridge to Tempo) -> Phase 5, then return to Phase 6x."
exit 1
fi
fi
# 3. Check wallet token balance — must be >= X402_AMOUNT before signing
ASSET_BALANCE=$(cast call "$X402_ASSET" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url "$SOURCE_RPC_URL")
if [ "$ASSET_BALANCE" -lt "$X402_AMOUNT" ]; then
X402_DECIMALS=${X402_DECIMALS:-$(get_token_decimals "$X402_ASSET" "$SOURCE_RPC_URL")}
ASSET_BAL_HUMAN=$(format_token_amount "$ASSET_BALANCE" "$X402_DECIMALS")
X402_AMT_HUMAN=$(format_token_amount "$X402_AMOUNT" "$X402_DECIMALS")
echo "ERROR: Insufficient $X402_TOKEN_NAME balance on $X402_NETWORK."
echo "Have: $ASSET_BAL_HUMAN $X402_TOKEN_NAME, need: $X402_AMT_HUMAN $X402_TOKEN_NAME"
echo "Acquire the asset first: if funds are on the same chain, run Phase 4A"
echo "(swap to $X402_ASSET). If funds are on a different chain, run"
echo "Phase 4A + Phase 4B (bridge to $X402_NETWORK) + Phase 5, then return here."
exit 1
fiREQUIRED: Use AskUserQuestion to show the user a payment summary beforesigning anything:
>
- Token:$X402_TOKEN_NAME($X402_ASSET) on$X402_NETWORK
- Amount:$(format_token_amount "$X402_AMOUNT" "$(get_token_decimals "$X402_ASSET" "$SOURCE_RPC_URL")")$X402_TOKEN_NAME
- Recipient: $X402_PAY_TO- Resource: $X402_RESOURCE>
Obtain explicit confirmation before proceeding.
Step 6x-1 — Generate nonce and deadline
X402_NONCE="0x$(openssl rand -hex 32)" # 32-byte random nonce
X402_VALID_AFTER=0 # immediately valid
X402_VALID_BEFORE=$(( $(date +%s) + X402_TIMEOUT )) # expiry = now + maxTimeoutSecondsStep 6x-2 — Sign the EIP-3009 TransferWithAuthorization typed data
The EIP-3009 domain uses the token contract's own name and version (from the extra field in the x402 challenge body). The verifyingContract is the token contract itself (X402_ASSET).
Sign using viem:
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const domain = {
name: process.env.X402_TOKEN_NAME!, // from extra.name, e.g. "USDC"
version: process.env.X402_TOKEN_VERSION!, // from extra.version, e.g. "2"
chainId: Number(process.env.X402_CHAIN_ID),
verifyingContract: process.env.X402_ASSET as `0x${string}`,
};
// REQUIRED: show the user what they are about to sign before calling signTypedData
const signature = await account.signTypedData({
domain,
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'TransferWithAuthorization',
message: {
from: process.env.WALLET_ADDRESS as `0x${string}`,
to: process.env.X402_PAY_TO as `0x${string}`,
value: BigInt(process.env.X402_AMOUNT!),
validAfter: BigInt(process.env.X402_VALID_AFTER!),
validBefore: BigInt(process.env.X402_VALID_BEFORE!),
nonce: process.env.X402_NONCE as `0x${string}`,
},
});
process.env.X402_SIGNATURE = signature;Domain warning: The verifyingContract is the token contract(X402_ASSET), not a separate verifier. Use thenameandversionfrom
extra — do not assume USDC defaults. Different tokens have different domainvalues. An incorrect domain produces a signature the server will reject
with a 402.
>
REQUIRED: Use AskUserQuestion before this step. Show theTransferWithAuthorization message fields (from, to, value, validBefore)so the user can verify what they are signing. Store the resulting signature as
X402_SIGNATURE.Step 6x-3 — Construct the X-PAYMENT payload
X402_PAYMENT_JSON=$(jq -n \
--arg scheme "$X402_SCHEME" \
--arg network "$X402_NETWORK" \
--argjson chainId "$X402_CHAIN_ID" \
--arg from "$WALLET_ADDRESS" \
--arg to "$X402_PAY_TO" \
--arg value "$X402_AMOUNT" \
--argjson validAfter "$X402_VALID_AFTER" \
--argjson validBefore "$X402_VALID_BEFORE" \
--arg nonce "$X402_NONCE" \
--arg sig "$X402_SIGNATURE" \
--arg asset "$X402_ASSET" \
'{
scheme: $scheme,
network: $network,
chainId: $chainId,
payload: {
authorization: {
from: $from,
to: $to,
value: $value,
validAfter: $validAfter,
validBefore: $validBefore,
nonce: $nonce
},
signature: $sig
},
asset: $asset
}')
# Base64-encode — strip newlines (required by header spec)
X402_PAYMENT=$(echo "$X402_PAYMENT_JSON" | base64 | tr -d '[:space:]')Step 6x-4 — Retry the original request with X-PAYMENT header
RETRY_RESPONSE=$(curl -si "$X402_RESOURCE" \
-H "X-PAYMENT: $X402_PAYMENT" \
-H "Content-Type: application/json")
RETRY_STATUS=$(echo "$RETRY_RESPONSE" | head -1 | grep -o '[0-9]\{3\}')
RETRY_BODY=$(echo "$RETRY_RESPONSE" | awk 'found{print} /^\r?$/{found=1}')
X402_PAYMENT_RESPONSE=$(echo "$RETRY_RESPONSE" \
| grep -i 'x-payment-response:' | cut -d' ' -f2- | tr -d '[:space:]')
echo "HTTP status: $RETRY_STATUS"Interpreting the response
| Status | Meaning | Action |
|---|---|---|
| 200 | Payment accepted — resource delivered | Display body; decode receipt with `echo "$X402_PAYMENT_RESPONSE" \ |
| 402 | Payment rejected (bad signature, expired, wrong amount) | Check domain name/version, validBefore, and amount |
| 400 | Malformed payment payload | Verify JSON structure and base64 encoding |
| Other | Server or network error | Report raw body; do not resubmit |
Tempo-network variant: If X402_NETWORK is "tempo" (or eip155:<tempo-chain-id>), the payment token is a Tempo TIP-20 address. You must first bridge USDC to Tempo using Phase 4B and optionally swap using Phase 5. After confirming the Tempo-side token balance, return here to execute Steps 6x-1 through 6x-4, using the Tempo-side token contract as X402_ASSET and the Tempo chain ID as X402_CHAIN_ID.
Trading API Flows
Step-by-step bash scripts for swap and bridge operations using the Uniswap Trading API (https://trade-api.gateway.uniswap.org/v1).
Table of Contents
Phase 4A — Swap on Source Chain
Use the Uniswap Trading API to swap the source token to USDC (the bridge asset). This is an EXACT_OUTPUT swap — the payee's amount determines how much USDC to acquire.
Variable Setup (fill these before running any steps):
SOURCE_CHAIN_ID=8453 # Chain where you hold the source token (e.g. Base = 8453)
TOKEN_IN_ADDRESS="0x..." # Address of your source token on SOURCE_CHAIN_ID
# For native ETH, use the zero address (recommended — returns permitData: null,
# no Permit2 signing needed):
# 0x0000000000000000000000000000000000000000
# The Universal Router wraps ETH before the swap, so msg.value (SWAP_VALUE) will be
# non-zero in the swap response.
#
# Fallback: if the zero address returns a 400, try the WETH address for your chain:
# Base (8453): 0x4200000000000000000000000000000000000006
# Ethereum (1): 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
# WETH may return non-null permitData requiring Permit2 signing (Step 4A-2.5).
USDC_ADDRESS="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base (8453)
# For Ethereum (1): USDC_ADDRESS="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
# See Key Addresses section in SKILL.md for other chains.
CAST_ACCOUNT="uniswap-demo" # Name of your cast keystore account (see Keystore Setup above)
CAST_PASSWORD="" # Keystore password (empty string if none)
SOURCE_RPC_URL="https://mainnet.base.org" # RPC URL for SOURCE_CHAIN_ID
# For Ethereum (1): SOURCE_RPC_URL="https://eth.llamarpc.com"
REQUIRED_AMOUNT_IN="0" # Use "0" for the initial approval check (Step 4A-1);
# replace with the actual amountIn after Step 4A-2 (quote)
USDC_E_AMOUNT_NEEDED="$REQUIRED_AMOUNT" # For EXACT_OUTPUT: target = payment amount
# Apply a 0.5% buffer to account for bridge fees:
# USDC_E_AMOUNT_NEEDED=$(echo "$REQUIRED_AMOUNT * 1005 / 1000" | bc)
# This ensures sufficient USDC arrives after any fee deductions.slippageTolerance: 0.5 in the quote body means 0.5% (not 0.005). TheTrading API accepts slippage as a percentage value.
Base URL: https://trade-api.gateway.uniswap.org/v1
Required headers:
Content-Type: application/json
x-api-key: <UNISWAP_API_KEY>
x-universal-router-version: 2.0Keystore Setup (recommended)
Encrypted keystores avoid exposing raw private keys on the command line. Create one with cast wallet import:
cast wallet import <ACCOUNT_NAME> --interactive
# Prompts for private key and password. Stores encrypted keystore in ~/.foundry/keystores/All cast send examples below use --account <ACCOUNT_NAME> --password <PW>. If you prefer raw keys, substitute --account ... --password ... with --private-key "$PRIVATE_KEY" (some environments block this via hooks).
Hex-to-Decimal Conversion
The Trading API returns hex values (e.g. swap.value), but cast send --value requires decimal (wei). Convert with:
hex_to_dec() { python3 -c "print(int('$1', 16))"; }
# Usage: cast send <TO> <CALLDATA> --value "$(hex_to_dec "$SWAP_VALUE_HEX")"Step 4A-1 — Check approval
# Build the request body safely using jq to avoid shell injection.
# The `amount` is used to determine whether the existing allowance is
# sufficient. Include it to receive an accurate approval status.
APPROVAL_BODY=$(jq -n \
--arg wallet "$WALLET_ADDRESS" \
--arg token "$TOKEN_IN_ADDRESS" \
--arg amount "$REQUIRED_AMOUNT_IN" \
--argjson chainId "$SOURCE_CHAIN_ID" \
'{walletAddress: $wallet, token: $token, amount: $amount, chainId: $chainId}')
curl -s -X POST https://trade-api.gateway.uniswap.org/v1/check_approval \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
-H "x-universal-router-version: 2.0" \
-d "$APPROVAL_BODY"REQUIRED: If theapprovalfield is non-null, useAskUserQuestionto
show the user the approval details (token address, spender, amount, estimated
gas) and obtain explicit confirmation before submitting the approval
transaction.
Step 4A-2 — Get exact-output quote for native USDC (bridge asset)
Address note: USDC_ADDRESS in the code below refers to the bridgeasset for the source chain. For Base (chain 8453), use native USDC:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. For Ethereum (chain 1), useUSDC: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48. See Key Addresses section.# Build the request body safely using jq. Chain IDs are integers; addresses
# and amounts are strings.
QUOTE_BODY=$(jq -n \
--arg swapper "$WALLET_ADDRESS" \
--arg tokenIn "$TOKEN_IN_ADDRESS" \
--arg tokenOut "$USDC_ADDRESS" \
--argjson tokenInChainId "$SOURCE_CHAIN_ID" \
--argjson tokenOutChainId "$SOURCE_CHAIN_ID" \
--arg amount "$USDC_E_AMOUNT_NEEDED" \
--argjson slippage 0.5 \
'{
swapper: $swapper,
tokenIn: $tokenIn,
tokenOut: $tokenOut,
tokenInChainId: $tokenInChainId,
tokenOutChainId: $tokenOutChainId,
amount: $amount,
type: "EXACT_OUTPUT",
slippageTolerance: $slippage,
routingPreference: "BEST_PRICE"
}')
curl -s -X POST https://trade-api.gateway.uniswap.org/v1/quote \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
-H "x-universal-router-version: 2.0" \
-d "$QUOTE_BODY"Note: tokenInChainId and tokenOutChainId must be integers, not strings.
Store the full quote response as QUOTE_RESPONSE. Then extract the actual input amount and re-run the approval check with the real value:
REQUIRED_AMOUNT_IN=$(echo "$QUOTE_RESPONSE" | jq -r '.quote.amountIn')
# Re-run Step 4A-1 with REQUIRED_AMOUNT_IN set to the quoted amount
# to confirm the existing allowance covers the swap.ETH/WETH approval note: When TOKEN_IN is native ETH (WETH address), noERC-20 approval is required. REQUIRED_AMOUNT_IN is the ETH value sent withthe transaction — the approval re-check in Step 4A-1 is a no-op. Skip it and
proceed directly to Step 4A-2.5.
>
Quote expiration: Quotes are valid for approximately 60 seconds. Do not
delay between fetching the quote and broadcasting the swap. If user confirmation
or other steps take longer, re-fetch the quote immediately before calling /swap.A stale quote will return emptyswap.datafrom the/swapendpoint.
Step 4A-2.5 — Sign the permitData
If the quote response contains a non-null permitData field, you must sign it off-chain before executing the swap.
ETH/WETH note: When swapping native ETH (using the WETH address as
TOKEN_IN),permitDatais typicallynull— skip this step if so.
Proceed directly to Step 4A-3.
- For CLASSIC routing: if
permitDatais non-null, sign it using the
Permit2 contract's EIP-712 typed data signing scheme. The wallet's private key or connected signing method is required. See the Permit2 documentation or the swap-integration skill for signing details.
- For UniswapX (DUTCH_V2, DUTCH_V3, PRIORITY): sign the
permitData
from the quote response using the same EIP-712 typed data approach.
Store the resulting signature as PERMIT2_SIGNATURE.
REQUIRED: Use AskUserQuestion to confirm the signing step with theuser before proceeding. Show the permit details (token, spender, amount,
deadline) so the user understands what they are authorizing.
Step 4A-3 — Execute the swap
# Strip permitData; re-attach only if non-null and routing is CLASSIC
ROUTING=$(echo "$QUOTE_RESPONSE" | jq -r '.routing')
CLEAN_QUOTE=$(echo "$QUOTE_RESPONSE" | jq 'del(.permitData, .permitTransaction)')
if [ "$ROUTING" = "CLASSIC" ]; then
PERMIT_DATA=$(echo "$QUOTE_RESPONSE" | jq '.permitData')
if [ "$PERMIT_DATA" != "null" ]; then
# Guard: ensure PERMIT2_SIGNATURE was obtained in Step 4A-2.5
if [ -z "$PERMIT2_SIGNATURE" ]; then
echo "ERROR: permitData is present but PERMIT2_SIGNATURE is empty. Complete Step 4A-2.5 first."
exit 1
fi
# Include signature + permitData in swap body
SWAP_BODY=$(echo "$CLEAN_QUOTE" | jq \
--arg sig "$PERMIT2_SIGNATURE" \
--argjson pd "$PERMIT_DATA" \
'. + {signature: $sig, permitData: $pd}')
else
SWAP_BODY="$CLEAN_QUOTE"
fi
else
# UniswapX (DUTCH_V2, DUTCH_V3, PRIORITY): signature only (no permitData in swap body)
if [ -z "$PERMIT2_SIGNATURE" ]; then
echo "ERROR: UniswapX order requires PERMIT2_SIGNATURE. Complete Step 4A-2.5 first."
exit 1
fi
SWAP_BODY=$(echo "$CLEAN_QUOTE" | jq --arg sig "$PERMIT2_SIGNATURE" '. + {signature: $sig}')
fi
curl -s -X POST https://trade-api.gateway.uniswap.org/v1/swap \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
-H "x-universal-router-version: 2.0" \
-d "$SWAP_BODY"Store the swap response as SWAP_RESPONSE. The /swap endpoint returns unsigned calldata — you must broadcast it yourself. After validating swap.data is non-empty, present the transaction summary to the user via AskUserQuestion then broadcast:
# Extract the transaction fields from the swap response
SWAP_TO=$(echo "$SWAP_RESPONSE" | jq -r '.swap.to')
SWAP_DATA=$(echo "$SWAP_RESPONSE" | jq -r '.swap.data')
SWAP_VALUE=$(echo "$SWAP_RESPONSE" | jq -r '.swap.value // "0x0"')
# Validate before broadcasting
[ -z "$SWAP_DATA" ] || [ "$SWAP_DATA" = "null" ] && echo "ERROR: swap.data is empty — quote may have expired. Re-fetch from Step 4A-2." && exit 1
# For native ETH swaps (TOKEN_IN is WETH address or ETH sentinel), SWAP_VALUE must
# be non-zero — it carries the ETH amount as msg.value. A zero value means the quote
# did not recognise the input as native ETH; do NOT broadcast or the swap will revert.
if [[ "$TOKEN_IN_ADDRESS" == "0x4200000000000000000000000000000000000006" || \
"$TOKEN_IN_ADDRESS" == "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" || \
"$TOKEN_IN_ADDRESS" == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEEE" ]]; then
[ "$SWAP_VALUE" = "0x0" ] || [ "$SWAP_VALUE" = "0" ] && \
echo "ERROR: SWAP_VALUE is zero for a native ETH swap — verify TOKEN_IN_ADDRESS and re-fetch the quote." && exit 1
fi
# Broadcast via cast
# Convert hex value to decimal (cast --value requires decimal wei)
SWAP_VALUE_DEC=$(hex_to_dec "$SWAP_VALUE")
SWAP_TX=$(cast send "$SWAP_TO" "$SWAP_DATA" \
--value "$SWAP_VALUE_DEC" \
--account "$CAST_ACCOUNT" --password "$CAST_PASSWORD" \
--rpc-url "$SOURCE_RPC_URL" \
--json | jq -r '.transactionHash')
# Wait for the swap to mine before bridging — a reverted swap leaves USDC at zero
SWAP_STATUS=$(cast receipt "$SWAP_TX" --rpc-url "$SOURCE_RPC_URL" --json | jq -r '.status')
[ "$SWAP_STATUS" = "0x1" ] || { echo "ERROR: Swap reverted (status=$SWAP_STATUS). Do not proceed to bridge." && exit 1; }
echo "Swap confirmed: $SWAP_TX"
# Verify USDC balance landed before proceeding to Phase 4B
USDC_AFTER_SWAP=$(cast call "$USDC_ADDRESS" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url "$SOURCE_RPC_URL")
# Format balances for human-readable display (USDC = 6 decimals)
USDC_DECIMALS=$(get_token_decimals "$USDC_ADDRESS" "$SOURCE_RPC_URL")
USDC_AFTER_HUMAN=$(format_token_amount "$USDC_AFTER_SWAP" "$USDC_DECIMALS")
USDC_NEEDED_HUMAN=$(format_token_amount "$USDC_E_AMOUNT_NEEDED" "$USDC_DECIMALS")
echo "USDC balance after swap: $USDC_AFTER_HUMAN USDC (need at least $USDC_NEEDED_HUMAN USDC)"
# Halt if swap produced insufficient USDC — bridging 0 USDC wastes gas and fails silently
# Use bc for arbitrary-precision comparison (uint256 values overflow bash's integer arithmetic)
[ "$(echo "$USDC_AFTER_SWAP < $USDC_E_AMOUNT_NEEDED" | bc)" -eq 1 ] && \
echo "ERROR: swap produced $USDC_AFTER_HUMAN USDC but $USDC_NEEDED_HUMAN USDC needed — check receipt, do NOT proceed to bridge." && exit 1Phase 4B — Bridge to Tempo
If you skipped Phase 4A (you already hold native USDC on Base), initialize
these variables before proceeding:
>
```bash
USDC_E_AMOUNT_NEEDED="$REQUIRED_AMOUNT"
USDC_ADDRESS="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base
```
Use the Uniswap Trading API to bridge USDC from Base to USDC.e on Tempo. The bridge is powered by Across Protocol and is fully abstracted by the API — no manual contract calls required.
Bridge asset addresses:
| Chain | Asset | Address |
|---|---|---|
| Base (8453) — in | Native USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Ethereum (1) — in | USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| Arbitrum (42161) — in | USDC.e | 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8 |
| Tempo (4217) — out | USDC.e | 0x20C000000000000000000000b9537d11c60E8b50 |
Source chain selection: Check balances on all supported chains. Prefer the
chain with the lowest total cost (swap gas + bridge gas). Base has the cheapest
bridge gas (~$0.001), Ethereum is more expensive (~$0.25) but may be the only
chain where you hold assets.
Step 4B-1 — Check approval
BRIDGE_TOKEN_IN="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base
BRIDGE_TOKEN_OUT="0x20C000000000000000000000b9537d11c60E8b50" # USDC.e on Tempo
BRIDGE_AMOUNT="$USDC_E_AMOUNT_NEEDED"
APPROVAL=$(curl -s "https://trade-api.gateway.uniswap.org/v1/check_approval" \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
--data "$(jq -n \
--arg token "$BRIDGE_TOKEN_IN" \
--arg amount "$BRIDGE_AMOUNT" \
--arg walletAddress "$WALLET_ADDRESS" \
--argjson chainId "$SOURCE_CHAIN_ID" \
'{token: $token, amount: $amount, walletAddress: $walletAddress, chainId: $chainId}')")
APPROVAL_TX=$(echo "$APPROVAL" | jq -r '.approval // empty')
echo "Approval needed: $([ -n "$APPROVAL_TX" ] && echo yes || echo no)"REQUIRED: IfAPPROVAL_TXis non-empty, useAskUserQuestionto show the
user the approval details (token: $BRIDGE_TOKEN_IN, spender, amount:$BRIDGE_AMOUNT, estimated gas) and obtain explicit confirmation beforesubmitting the approval transaction.
If confirmed and APPROVAL_TX is non-empty:
APPROVAL_TO=$(echo "$APPROVAL_TX" | jq -r '.to')
APPROVAL_DATA=$(echo "$APPROVAL_TX" | jq -r '.data')
APPROVE_HASH=$(cast send "$APPROVAL_TO" "$APPROVAL_DATA" \
--account "$CAST_ACCOUNT" --password "$CAST_PASSWORD" \
--rpc-url "$SOURCE_RPC_URL" \
--json | jq -r '.transactionHash')
cast receipt "$APPROVE_HASH" --rpc-url "$SOURCE_RPC_URL" > /dev/null
echo "Approval confirmed: $APPROVE_HASH"IMPORTANT — bridge spender approval: The Trading API's check_approvalonly covers the Permit2 contract. The bridge contract itself (the to addressfrom the /swap response) also needs an ERC-20 allowance. **Always run thison-chain check after getting the bridge swap response in Step 4B-3**, before
broadcasting the bridge transaction:
>
```bash
BRIDGE_SPENDER="$BRIDGE_TO" # The 'to' field from the /swap response
ALLOWANCE=$(cast call "$BRIDGE_TOKEN_IN" \
"allowance(address,address)(uint256)" "$WALLET_ADDRESS" "$BRIDGE_SPENDER" \
--rpc-url "$SOURCE_RPC_URL" 2>/dev/null | awk '{print $1}')
if [ -z "$ALLOWANCE" ] || ! [[ "$ALLOWANCE" =~ ^[0-9]+$ ]]; then
echo "ERROR: Failed to read allowance from chain. Check RPC connectivity and token address."
exit 1
fi
# Use bc for arbitrary-precision comparison (uint256 values overflow bash's integer arithmetic)
if [ "$(echo "$ALLOWANCE < $BRIDGE_AMOUNT" | bc)" -eq 1 ]; then
echo "Insufficient allowance for bridge spender. Approving..."
BRIDGE_APPROVE_HASH=$(cast send "$BRIDGE_TOKEN_IN" \
"approve(address,uint256)" "$BRIDGE_SPENDER" \
"115792089237316195423570985008687907853269984665640564039457584007913129639935" \
--account "$CAST_ACCOUNT" --password "$CAST_PASSWORD" \
--rpc-url "$SOURCE_RPC_URL" --json | jq -r '.transactionHash')
cast receipt "$BRIDGE_APPROVE_HASH" --rpc-url "$SOURCE_RPC_URL" > /dev/null
echo "Bridge spender approval confirmed: $BRIDGE_APPROVE_HASH"
fi
```
>
Skipping this step will cause the bridge transaction to revert with
"ERC20: transfer amount exceeds allowance".
Step 4B-2 — Get bridge quote (EXACT_OUTPUT)
API constraint: The Trading API does not support a separate recipientfield for cross-chain bridge quotes. The swapper address is always therecipient on the destination chain. If your WALLET_ADDRESS differs fromTEMPO_WALLET_ADDRESS, the bridge will deliver USDC.e toWALLET_ADDRESS
on Tempo — a follow-up transfer (Phase 4B-5) moves it to TEMPO_WALLET_ADDRESS.BRIDGE_QUOTE=$(curl -s "https://trade-api.gateway.uniswap.org/v1/quote" \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
--data "$(jq -n \
--arg tokenIn "$BRIDGE_TOKEN_IN" \
--arg tokenInChainId "$SOURCE_CHAIN_ID" \
--arg tokenOut "$BRIDGE_TOKEN_OUT" \
--arg tokenOutChainId "4217" \
--arg amount "$BRIDGE_AMOUNT" \
--arg swapper "$WALLET_ADDRESS" \
'{
tokenIn: $tokenIn,
tokenInChainId: $tokenInChainId,
tokenOut: $tokenOut,
tokenOutChainId: $tokenOutChainId,
amount: $amount,
swapper: $swapper,
type: "EXACT_OUTPUT"
}')")
BRIDGE_QUOTE_ID=$(echo "$BRIDGE_QUOTE" | jq -r '.quote.quoteId')
BRIDGE_FEE=$(echo "$BRIDGE_QUOTE" | jq -r '.quote.bridgeFee // .quote.gasFee // "unknown"')
BRIDGE_ETA=$(echo "$BRIDGE_QUOTE" | jq -r '.quote.estimatedFillTime // "2-5 minutes"')
echo "Bridge quote: quoteId=$BRIDGE_QUOTE_ID fee=$BRIDGE_FEE eta=$BRIDGE_ETA"REQUIRED: Use AskUserQuestion before submitting the bridge transaction.Show the user:
>
- Amount: $(format_token_amount "$BRIDGE_AMOUNT" "$USDC_DECIMALS") USDC on Base (chain 8453)- Destination: $BRIDGE_TOKEN_OUT (USDC.e) on Tempo (chain 4217)- Bridge fee: $BRIDGE_FEE- Estimated time: $BRIDGE_ETA- Recipient on Tempo: $WALLET_ADDRESS (funds arrive here; transferred to Tempo wallet in Phase 4B-5)>
Do not proceed until the user confirms.
>
Quote expiration: Bridge quotes also expire after ~60 seconds. Re-fetch the
quote (Step 4B-2) if there was any delay before executing.
Step 4B-3 — Execute the bridge
BRIDGE_RESPONSE=$(curl -s "https://trade-api.gateway.uniswap.org/v1/swap" \
-H "Content-Type: application/json" \
-H "x-api-key: $UNISWAP_API_KEY" \
--data "$(jq -n \
--argjson quote "$BRIDGE_QUOTE" \
--arg walletAddress "$WALLET_ADDRESS" \
'{quote: $quote.quote, walletAddress: $walletAddress}')")
BRIDGE_TO=$(echo "$BRIDGE_RESPONSE" | jq -r '.swap.to')
BRIDGE_DATA=$(echo "$BRIDGE_RESPONSE" | jq -r '.swap.data')
BRIDGE_VALUE=$(echo "$BRIDGE_RESPONSE"| jq -r '.swap.value // "0"')
# Convert hex value to decimal
BRIDGE_VALUE_DEC=$(hex_to_dec "$BRIDGE_VALUE")
BRIDGE_TX=$(cast send "$BRIDGE_TO" "$BRIDGE_DATA" \
--value "$BRIDGE_VALUE_DEC" \
--account "$CAST_ACCOUNT" --password "$CAST_PASSWORD" \
--rpc-url "$SOURCE_RPC_URL" \
--json | jq -r '.transactionHash')
BRIDGE_STATUS=$(cast receipt "$BRIDGE_TX" --rpc-url "$SOURCE_RPC_URL" --json | jq -r '.status')
[ "$BRIDGE_STATUS" = "0x1" ] || { echo "ERROR: Bridge tx reverted. Do not proceed."; exit 1; }
echo "Bridge submitted: $BRIDGE_TX — waiting for funds on Tempo..."Step 4B-4 — Poll for arrival on Tempo
Poll for USDC.e balance on Tempo every 30 seconds for up to 10 minutes:
TEMPO_RPC_URL="https://rpc.presto.tempo.xyz"
for i in $(seq 1 20); do
# cast call returns "123456 [1.234e5]" — strip the bracket suffix to get a plain integer
RAW_BALANCE=$(cast call "$BRIDGE_TOKEN_OUT" \
"balanceOf(address)(uint256)" "$WALLET_ADDRESS" \
--rpc-url "$TEMPO_RPC_URL" 2>/dev/null || echo "0")
USDC_E_ON_TEMPO=$(echo "$RAW_BALANCE" | awk '{print $1}')
# Use bc for arbitrary-precision comparison (uint256 values overflow bash's integer arithmetic)
if [[ "$USDC_E_ON_TEMPO" =~ ^[0-9]+$ ]] && [ "$(echo "$USDC_E_ON_TEMPO >= $BRIDGE_AMOUNT" | bc)" -eq 1 ]; then
USDC_E_DECIMALS=$(get_token_decimals "$BRIDGE_TOKEN_OUT" "$TEMPO_RPC_URL")
USDC_E_HUMAN=$(format_token_amount "$USDC_E_ON_TEMPO" "$USDC_E_DECIMALS")
echo "Bridge confirmed — $USDC_E_HUMAN USDC.e received on Tempo."
break
fi
echo "Waiting for bridge arrival... attempt $i/20 (balance: $USDC_E_ON_TEMPO base units)"
sleep 30
done
# Use bc for arbitrary-precision comparison (uint256 values overflow bash's integer arithmetic)
[ "$(echo "$USDC_E_ON_TEMPO >= $BRIDGE_AMOUNT" | bc)" -eq 1 ] || \
{ echo "Bridge not confirmed after 10 minutes. Check $BRIDGE_TX on https://explore.mainnet.tempo.xyz"; exit 1; }After a successful bridge, you hold USDC.e ($BRIDGE_TOKEN_OUT) on Tempo. Use this as TOKEN_IN in Phase 5 to swap to the required payment token.
Do not re-submit if the poll times out — duplicate bridge deposits result
in double payment. Have the user check the transaction on the Tempo explorer.
Step 4B-5 — Transfer USDC.e to Tempo wallet (if needed)
The Trading API bridges to WALLET_ADDRESS on Tempo. If WALLET_ADDRESS differs from TEMPO_WALLET_ADDRESS (the Tempo CLI wallet), transfer the USDC.e so the Tempo CLI can use it to pay the 402.
if [ "$WALLET_ADDRESS" != "$TEMPO_WALLET_ADDRESS" ]; then
TRANSFER_DATA=$(cast calldata "transfer(address,uint256)" "$TEMPO_WALLET_ADDRESS" "$BRIDGE_AMOUNT")
# Show transfer details before sending
USDC_E_HUMAN=$(format_token_amount "$BRIDGE_AMOUNT" "6")
# (AskUserQuestion gate handled by the caller — confirm amount + destination before this step)
# Tempo chain gas estimation is unreliable — always set an explicit gas limit
TRANSFER_TX=$(cast send "$BRIDGE_TOKEN_OUT" "$TRANSFER_DATA" \
--account "$CAST_ACCOUNT" --password "$CAST_PASSWORD" \
--rpc-url "$TEMPO_RPC_URL" \
--gas-limit 100000 \
--json | jq -r '.transactionHash')
TRANSFER_STATUS=$(cast receipt "$TRANSFER_TX" --rpc-url "$TEMPO_RPC_URL" --json | jq -r '.status')
[ "$TRANSFER_STATUS" = "0x1" ] || { echo "ERROR: Transfer to Tempo wallet reverted: $TRANSFER_TX"; exit 1; }
echo "USDC.e transferred to Tempo wallet ($TEMPO_WALLET_ADDRESS): $TRANSFER_TX"
fiAfter this step, TEMPO_WALLET_ADDRESS holds the required USDC.e and the Tempo CLI can retry the original tempo request to pay the 402.
---
Future Optimization: Single Cross-Chain Swap
A single cross-chain quote (e.g. ETH on Ethereum → USDC.e on Tempo) would collapse the 3-transaction flow (swap + bridge + transfer) into one. As of March 2026, the Trading API returns "No quotes available" for direct cross-chain swaps to Tempo (chain 4217). Monitor the Trading API changelog for cross-chain swap support to Tempo — when available, it eliminates Phase 4A and Step 4B-5 entirely.
Related skills
How it compares
Use pay-with-any-token for agent HTTP 402 and MPP flows; use swap-integration when building general Uniswap swap UI or SDK integrations without machine payments.
FAQ
What triggers the pay-with-any-token skill?
pay-with-any-token triggers on HTTP 402 Payment Required responses, Machine Payments Protocol (MPP) flows, x402 payments, or mentions of Tempo CLI, tempo wallet, or pay-for-API-access. The Uniswap skill version 2.0.0 uses Tempo CLI as the primary payment interface.
How does pay-with-any-token fund insufficient Tempo balances?
pay-with-any-token plans EXACT_OUTPUT swaps and bridges from ERC-20 holdings on supported EVM chains using the Uniswap Trading API. It bridges USDC to the Tempo wallet via Across Protocol when the Tempo CLI wallet cannot cover a 402 challenge.