Portals Foresight API
Simulate EVM transactions and get back structured token changes, gas costs, and metadata. For supported flows, no wallet connection, pre-funded balance, or manual approval transaction is required.
All API requests are made to https://foresight.portals.fi/v1
Quick Start
Simulate a USDC to DAI swap on Ethereum in under a minute.
1. Get an API key
Sign up at foresight.portals.fi to get your API key. Free tier includes 1,000 simulations per month.
2. Make your first request
curl -X POST "https://foresight.portals.fi/v1/simulate?network=ethereum" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"tx": {
"from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"to": "0x1111111254EEB25477B68fb85Ed929f73A960582",
"data": "0x12aa3caf...",
"value": "0"
}
}'
const response = await fetch("https://foresight.portals.fi/v1/simulate?network=ethereum", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": "YOUR_API_KEY",
},
body: JSON.stringify({
inputToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
inputAmount: "10000000000",
tx: {
from: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
to: "0x1111111254EEB25477B68fb85Ed929f73A960582",
data: "0x12aa3caf...",
value: "0",
},
}),
});
const result = await response.json();
console.log(result.assetChanges);
import requests
response = requests.post(
"https://foresight.portals.fi/v1/simulate",
params={"network": "ethereum"},
headers={
"Content-Type": "application/json",
"X-Api-Key": "YOUR_API_KEY",
},
json={
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"tx": {
"from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"to": "0x1111111254EEB25477B68fb85Ed929f73A960582",
"data": "0x12aa3caf...",
"value": "0",
},
},
)
result = response.json()
print(result["assetChanges"])
3. Read the response
A successful simulation returns structured asset changes — including NFT positions — gas costs, and token metadata:
{
"success": true,
"sender": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"assetChanges": [
{
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": "4217390000",
"direction": "sent",
"standard": "erc20"
},
{
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"amount": "1500000000000000000",
"direction": "sent",
"standard": "erc20"
},
{
"token": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
"amount": "1",
"direction": "received",
"standard": "erc721",
"tokenId": "1207888"
}
],
"gasUsed": "461218",
"gasCost": "2386840000000000",
"tokenMetadata": {
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": {
"name": "USD Coin",
"symbol": "USDC",
"decimals": 6
},
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
"name": "Wrapped Ether",
"symbol": "WETH",
"decimals": 18
},
"0xc36442b4a4522e871399cd717abdd847ab11fe88": {
"name": "Uniswap V3 Positions NFT-V1",
"symbol": "UNI-V3-POS",
"decimals": 0,
"standard": "erc721"
}
},
"inputTokens": [...],
"outputTokens": [...],
"events": [...],
"errors": []
}
Authentication
Authenticate requests by passing your API key in the X-Api-Key header.
X-Api-Key: your_api_key_here
The health endpoint is exempt from authentication.
Never expose your API key in client-side code. Make simulation requests from your backend server.
An invalid API key returns 401. Missing keys on x402-enabled endpoints (/v1/simulate, /v1/simulate/batch) return 402 with a payment-required header instead — see x402 Payments.
{
"success": false,
"error": "Invalid or missing API key"
}
Simulate Transaction
Simulates an EVM transaction against live chain state and returns structured results including net asset changes, gas costs, decoded events, and token metadata when available.
Query Parameters
Request Body
0x0000000000000000000000000000000000000000 or 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.
"10000000000" for 10,000 USDC (6 decimals) or "1000000000000000000" for 1 ETH (18 decimals). No decimals, no hex.
from, to, data, and value.0x-prefixed. Use "0x" for a plain native-token transfer."0" for pure ERC20 swaps.tx.from if omitted. Useful when the protocol routes outputs to a different address.customEvents response field.Response Format
All simulation responses return HTTP 200, including reverted transactions. Check the success field to determine the outcome.
Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the simulated transaction executed without reverting in the simulated state. |
sender | string | The tracked user address used to classify sent and received asset changes. Defaults to tx.from, or to recipient when provided. |
assetChanges | AssetChange[] | Net token balance changes relative to the sender/recipient. |
inputTokens | TokenAmount[] | Tokens sent by the user (subset of assetChanges where direction is "sent"). |
outputTokens | TokenAmount[] | Tokens received by the user (subset of assetChanges where direction is "received"). |
events | TokenEvent[] | Individual token transfer events from the simulated transaction. |
gasUsed | string | Gas units consumed (decimal string). |
gasCost | string | Estimated gas cost in wei based on current network conditions. |
tokenMetadata | Record? | Name, symbol, and decimals for detected non-native tokens when metadata is available, keyed by lowercase address. |
customEvents | DecodedCustomEvent[]? | Decoded custom events from simulation logs. Only present when customEvents was provided in the request. See Custom Event Decoding. |
error | string? | Human-readable revert reason. Only present when success is false. |
errors | string[] | Non-fatal warnings (usually empty). |
bypassedChecks | BypassInfo[] | Signature-gate bypasses automatically applied to make the transaction simulable. Only present when at least one bypass fired. See Bypassed checks. |
AssetChange
| Field | Type | Description |
|---|---|---|
token | string | Token contract address. 0x000...000 for the chain native token. |
amount | string | Absolute net amount in base units (always positive). For ERC721 NFTs, always "1". |
direction | "sent" | "received" | "transfer" | Direction relative to the tracked account. "sent"/"received" are used for the simulation sender and recipient. "transfer" appears only in trace responses for pass-through accounts. |
standard | string | Token standard: "erc20", "native", "erc721", or "erc1155". |
tokenId | string? | Token ID for NFTs (ERC721/ERC1155). Not present for fungible tokens. |
from | string? | Originating address. Present when the asset left a specific address (set for direction = "sent" and "transfer"). |
to | string? | Destination address. Present when the asset arrived at a specific address (set for direction = "received" and "transfer"). |
TokenEvent
| Field | Type | Description |
|---|---|---|
type | string | Event kind: transfer, approval, mint, burn, wrap, unwrap, nft_transfer, nft_mint, nft_burn. |
standard | string | Token standard: "erc20", "native", "erc721", or "erc1155". |
token | string | Token contract address. |
from | string? | Source address. Zero address for mints. Not present on approval events. |
to | string? | Destination address. Zero address for burns. Not present on approval events. |
owner | string? | Token owner. Only present on approval events. |
spender | string? | Approved spender. Only present on approval events. |
amount | string | Transfer or approval amount in base units. "1" for ERC721. |
tokenId | string? | Token ID for ERC721/ERC1155 events. |
logIndex | number | Position in the transaction's event log. |
TokenMetadata
| Field | Type | Description |
|---|---|---|
name | string | Token name (e.g., "USD Coin"). Defaults to "Unknown". |
symbol | string | Token symbol (e.g., "USDC"). Defaults to "???". |
decimals | number | Token decimals (e.g., 6 for USDC, 18 for DAI). Defaults to 18. |
standard | string? | Token standard when detected via ERC165: "erc721" or "erc1155". Not present for fungible tokens. |
TokenAmount
Used by inputTokens and outputTokens to represent the tokens sent or received by the user.
| Field | Type | Description |
|---|---|---|
token | string | Token contract address. |
amount | string | Amount in base units (always positive). |
standard | string? | Token standard ("erc721" or "erc1155"). Not present for ERC20 or native tokens. |
tokenId | string? | Token ID for NFTs (ERC721/ERC1155). Not present for fungible tokens. |
BypassInfo
Signature gates that were auto-bypassed so the simulation could proceed. Simulating against live state doesn't have real signers, so Portals Foresight rewrites state or calldata to let gated calls execute. Each entry describes what was bypassed and any residual risk.
| Field | Type | Description |
|---|---|---|
kind | string | Bypass identifier: "safe-exec", "erc20-permit", or "erc4337-handleOps". |
target | string | Contract address the bypass was applied to. |
stepIndex | number? | Optional step index when the bypass is associated with a specific step. |
safe | object | Present when kind = "safe-exec". Contains digest, signers (selected owner addresses used to satisfy the threshold), threshold, ownerCount, version, and optional guardWarning if a Safe Guard is installed (guards are not bypassed and may still revert real transactions). |
permit | object | Present when kind = "erc20-permit". Contains owner, spender, value, slot (storage slot overridden), rewrittenTo (the value written). |
erc4337 | object | Present when kind = "erc4337-handleOps". Contains entryPointVersion ("v0.6" or "v0.7"), senders (address[]), userOpCount (number), skippedOps (number of UserOps whose signature validation was skipped). |
Batch Simulation
Simulate multiple transactions in a single call with shared state. All steps execute sequentially in one simulated block — the state changes from each step carry forward to the next. Ideal for multi-step DeFi flows like leverage loops (supply → borrow → swap → re-supply).
API-key batch requests require Pro, Business, or Enterprise. x402 batch requests are accepted without a plan.
Query Parameters
Batch Request Body
The request body contains a sender, optional recipient, and an array of steps (1–50).
sender if omitted.tx field (from, to, data, value).0x000...0 or 0xEeee...eeeE.inputToken is provided.Batch Response
The batch response contains per-step results and an aggregate that mirrors the single-endpoint response shape.
| Field | Type | Description |
|---|---|---|
success | boolean | True if all steps succeeded. |
failedAtStep | number? | Index of the first failed step. Absent when all steps succeed. |
steps | StepResult[] | Per-step results (see below). |
aggregate | SimulationResult | Combined net asset changes — same shape as the single endpoint response. |
StepResult
| Field | Type | Description |
|---|---|---|
stepIndex | number | Zero-based index of this step. |
success | boolean | Whether this individual step succeeded. |
gasUsed | string | Gas consumed by this step. |
error | string? | Revert reason if step failed, otherwise absent. |
Custom Event Decoding Pro
Decode protocol-specific events (Uniswap Swap, Curve AddLiquidity, Aave LiquidationCall, etc.) from simulation logs by passing Solidity event ABI strings in the customEvents request field. API-key requests require Pro, Business, or Enterprise and return HTTP 403 on lower plans; x402 requests are allowed.
Request
Add a customEvents array to your request body. Up to the first 10 entries are attempted; invalid ABI strings are skipped. Each entry is a Solidity event signature string:
{
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"customEvents": [
"event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)"
],
"tx": { "from": "0x...", "to": "0x...", "data": "0x...", "value": "0" }
}
Response
Matching logs are returned in the customEvents response field:
{
"customEvents": [
{
"name": "Swap",
"signature": "event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)",
"address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
"logIndex": 4,
"args": {
"sender": "0x...",
"amount0In": "0",
"amount1In": "10000000000",
"amount0Out": "2918274610283746",
"amount1Out": "0",
"to": "0x..."
}
}
]
}
DecodedCustomEvent
| Field | Type | Description |
|---|---|---|
name | string | Event name (e.g., "Swap", "Sync"). |
signature | string | Minimal Solidity event ABI string. |
address | string | Contract address that emitted the event. |
logIndex | number | Position in the event log. |
args | Record<string, string> | Decoded event parameters as key-value string pairs. |
Trace Transaction
Simulate a transaction with a full execution trace — decoded call tree, best-effort storage state changes, token transfers, decoded events, gas profiling, and ABI-decoded function inputs/outputs. Returns a shareable traceId for later retrieval. Results are cached for 7 days.
All plans include traces: Free (50/month), Starter (500), Pro (2,000), Business (10,000), Enterprise (unlimited). Requests beyond quota return HTTP 429 unless paid-plan overage billing is enabled and under its cap.
For hypothetical traces where the sender does not already hold or approve a supported ERC-20 input token, provide both inputToken and inputAmount. Foresight injects the sender balance and approval for the traced call. Omit both fields for native-token calls or when replaying existing on-chain state.
Query Parameters
Request Body
from, to, data, and value.0x-prefixed. Use "0x" for a plain native-token transfer."0" for calls with no native value.inputAmount.inputToken to inject, in base units as a decimal string. Must be provided together with inputToken.true.["function swap(address,uint256)"].Request Example
curl -X POST "https://foresight.portals.fi/v1/simulate/trace?network=ethereum" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"tx": {
"from": "0xd8da6bf26964af9d7eed9e0e9f5936cde56c19d3",
"to": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
"data": "0x7ff36ab5...",
"value": "100000000000000000"
},
"includeStateDiffs": true
}'
const response = await fetch("https://foresight.portals.fi/v1/simulate/trace?network=ethereum", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": "YOUR_API_KEY",
},
body: JSON.stringify({
tx: {
from: "0xd8da6bf26964af9d7eed9e0e9f5936cde56c19d3",
to: "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
data: "0x7ff36ab5...",
value: "100000000000000000",
},
includeStateDiffs: true,
}),
});
const trace = await response.json();
console.log(trace.traceId, trace.callTrace);
import requests
response = requests.post(
"https://foresight.portals.fi/v1/simulate/trace",
params={"network": "ethereum"},
headers={
"Content-Type": "application/json",
"X-Api-Key": "YOUR_API_KEY",
},
json={
"tx": {
"from": "0xd8da6bf26964af9d7eed9e0e9f5936cde56c19d3",
"to": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
"data": "0x7ff36ab5...",
"value": "100000000000000000",
},
"includeStateDiffs": True,
},
)
trace = response.json()
print(trace["traceId"], trace["callTrace"])
Response Format
Returns full execution trace with decoded call tree, state changes, token transfers, and events.
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the transaction succeeded or reverted. |
traceId | string? | UUID for sharing and retrieving the trace later. Returned on newly generated traces from POST /v1/simulate/trace and GET /v1/simulate/trace/tx/{hash}. Cached GET /v1/simulate/trace/{id} responses may omit this field; use the requested id as the trace ID. |
gasUsed | string | Gas consumed by the transaction. |
gasCost | string | Gas cost in wei at current base fee. |
blockNumber | number | Block number used for simulation. |
network | string | The chain the trace was executed on. |
from | string | Transaction sender address. |
to | string | Transaction target address. |
txHash | string? | Original on-chain transaction hash. Only present on replayed traces (GET /v1/simulate/trace/tx/{hash}). |
inputToken | string? | Echoed ERC-20 input token when balance and approval injection was requested. |
inputAmount | string? | Echoed input amount when balance and approval injection was requested. |
abi | string[]? | Echoed custom ABI fragments when provided for decoding. |
callTrace | CallFrame | Nested call tree with decoded function names, inputs, outputs, and sub-calls. See CallFrame. |
stateDiffs | StateDiff[] | Storage slot changes with best-effort decoded variable names, types, and before/after values. See StateDiff. |
assetChanges | AssetChange[] | Net token balance changes per address (ERC20, ERC721, native). |
balanceChanges | BalanceChange[] | Per-address native-token and ERC20 balance deltas with USD valuation when price data is available. |
decodedLogs | DecodedLog[] | Event logs decoded against resolved ABIs when available. |
tokenMetadata | object | Token name, symbol, and decimals keyed by address when metadata can be resolved. |
tokenPrices | Record<address, number> | USD prices pinned to the simulation block, keyed by lowercase token address. |
tokenPricesLive | Record<address, number> | Current USD prices (best-effort, non-blocking). Only included on GET /v1/simulate/trace/:id retrievals — the initial POST returns tokenPrices only. Used for "price now vs. price at simulation" toggles. |
bypassedChecks | BypassInfo[] | Signature-gate bypasses automatically applied to make the transaction simulable. Only present when at least one bypass fired. See Bypassed checks. |
CallFrame
| Field | Type | Description |
|---|---|---|
type | string | Call type: "CALL", "DELEGATECALL", "STATICCALL", "CREATE", "CREATE2". |
from | string | Caller. |
to | string | Callee. |
value | string | Native value forwarded (wei). |
gas | string | Gas supplied to the frame. |
gasUsed | string | Gas consumed by the frame (including sub-calls). |
input | string | Raw calldata. |
output | string | Raw return data. |
error | string? | Low-level error (e.g. "execution reverted"). |
revertReason | string? | Decoded revert string when available. |
contractName | string? | Resolved contract name from verified ABI. |
functionName | string? | Decoded function name. |
decodedInput | object? | Named arg→value map. |
decodedOutput | object? | Named return→value map. |
logs | CallFrameLog[]? | Decoded events emitted inside this frame. |
calls | CallFrame[]? | Nested sub-calls. |
StateDiff
| Field | Type | Description |
|---|---|---|
address | string | Affected account. |
label | string? | Resolved contract name. |
balance | { before, after }? | Native balance delta. |
nonce | { before, after }? | Nonce delta. |
storage | StorageDiff[] | Storage slot changes. See StorageDiff. |
StorageDiff
| Field | Type | Description |
|---|---|---|
slot | string | 32-byte storage key (hex). |
before | string | Value before. |
after | string | Value after. |
decodedLabel | string? | Variable name from storage layout. |
decodedType | string? | Solidity type. |
decodedValue | { before, after }? | Decoded value (e.g. "1000000000000000000" for a uint256). |
packedMembers | PackedMember[]? | Present when the slot holds multiple packed variables. |
{
"success": true,
"traceId": "e8fb04cb-e222-42f9-876a-13408c67ac8f",
"gasUsed": "130205",
"gasCost": "16220000000000",
"blockNumber": 24769802,
"network": "ethereum",
"callTrace": {
"type": "CALL",
"from": "0xd8da...",
"to": "0x7a25...",
"contractName": "UniswapV2Router02",
"functionName": "swapExactETHForTokens(...)",
"decodedInput": { "amountOutMin": "0", "path": [...] },
"calls": [...]
},
"stateDiffs": [...],
"assetChanges": [...],
"decodedLogs": [...],
"tokenMetadata": {
"0xa0b8...": { "name": "USD Coin", "symbol": "USDC", "decimals": 6 }
},
"tokenPrices": { "0xa0b8...": 1.0001 }
}
Trace Limits
Each plan includes a monthly trace quota. Starter, Pro, and Business accounts can enable per-trace overage billing up to their configured cap.
| Plan | Traces / month | Overage |
|---|---|---|
| Free | 50 | — |
| Starter | 500 | $0.01/trace |
| Pro | 2,000 | $0.005/trace |
| Business | 10,000 | $0.003/trace |
| Enterprise | Unlimited | — |
Replay Transaction
Replay a historical on-chain transaction at its original block with full execution trace. Returns the same response format as POST /v1/simulate/trace. Counts against your monthly trace quota.
Path Parameters
0x1cddbcf4...ac8f).Query Parameters
curl "https://foresight.portals.fi/v1/simulate/trace/tx/0x1cddbcf4e1adf110e500b0df2b33853a60c722b3a173af25913aa9785f394bc8?network=ethereum" \
-H "X-Api-Key: YOUR_API_KEY"
Get Trace by ID
Retrieve a previously generated trace by its UUID. No authentication required — traces are shareable via URL. Does not count against quota (read-only cache lookup).
Traces are cached for 7 days. After expiration, returns 404. The cached response body may not include a traceId field, so clients should retain the requested UUID. View traces in the browser at https://foresight.portals.fi/trace?id=YOUR_TRACE_ID.
Path Parameters
POST /v1/simulate/trace or GET /v1/simulate/trace/tx/:txHash request.curl "https://foresight.portals.fi/v1/simulate/trace/e8fb04cb-e222-42f9-876a-13408c67ac8f"
Error Handling
Reverted transactions
A reverted simulation still returns HTTP 200 with "success": false. The error field contains the decoded revert reason.
{
"success": false,
"error": "Insufficient output amount",
"sender": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"gasUsed": "50000",
"gasCost": "500000000000000",
"assetChanges": [],
"events": [],
"errors": []
}
HTTP Error Codes
| Status | Description | Response Shape |
|---|---|---|
200 | Simulation completed (check success field). | SimulationResult |
400 | Validation error (invalid params or network). | { success: false, errors: [...] } |
401 | Invalid API key. | { success: false, error: "..." } |
402 | Payment Required — no API key present on an x402-enabled endpoint. The response includes a payment-required header describing how to retry with a USDC-on-Base signature. See x402 Payments. | {} (details in header) |
403 | Plan upgrade required (e.g., custom events or batch on Free/Starter). | { success: false, error: "..." } |
405 | Method Not Allowed — wrong HTTP verb for the endpoint. | { success: false, error } |
413 | Payload Too Large — request body exceeds 256 kB. | { success: false, error } |
429 | RPS limit, monthly quota, or overage spending cap exceeded. | { success: false, error: "..." } |
500 | Internal server error. | { success: false, error: "..." } |
503 | Service temporarily unavailable, including shutdown or auth dependency failure. | { success: false, error: "..." } |
Validation errors
Invalid request fields return HTTP 400 with an errors array listing every issue:
{
"success": false,
"errors": [
"inputToken must be a valid Ethereum address",
"tx.value must be a valid number string"
]
}
Examples that contain ... inside calldata are shortened for readability. Real requests must send complete 0x-prefixed hex calldata.
Health Check
Returns the server's health status and the networks currently available in this deployment. No authentication required.
Returns 200 when healthy or degraded, 503 when all services are down.
Status Values
| Value | HTTP Code | Description |
|---|---|---|
ok | 200 | All services healthy. |
degraded | 200 | Some RPC providers are unreachable but service is functional. |
error | 503 | Critical services are down. |
{
"status": "ok",
"timestamp": "2026-03-16T08:28:36.181Z",
"totalNetworks": 30,
"healthyNetworks": 30,
"networks": {
"healthy": ["ethereum", "polygon", "base", "optimism", ...]
}
}
Examples
ERC20 token swap
Simulate a WETH to DAI swap through a DEX router. The sender doesn't need to hold WETH — balance is injected automatically.
curl -X POST "https://foresight.portals.fi/v1/simulate?network=ethereum" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"inputAmount": "1000000000000000000",
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
"data": "0x38ed1739...",
"value": "0"
}
}'
const result = await fetch("https://foresight.portals.fi/v1/simulate?network=ethereum", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.PORTALS_API_KEY,
},
body: JSON.stringify({
inputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
inputAmount: "1000000000000000000", // 1 WETH
tx: {
from: "0x1111111111111111111111111111111111111111",
to: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", // Uniswap V2
data: "0x38ed1739...",
value: "0",
},
}),
}).then(r => r.json());
Native token transfer
Simulate a plain native-token transfer. Use the zero address (0x000...0) or the native sentinel (0xEeee...eeeE) as inputToken and set tx.value to the amount in wei.
{
"inputToken": "0x0000000000000000000000000000000000000000",
"inputAmount": "1000000000000000000",
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x2222222222222222222222222222222222222222",
"data": "0x",
"value": "1000000000000000000"
}
}
Vault deposit with separate recipient
Use the recipient field when the output tokens go to a different address than the sender.
{
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"recipient": "0x2222222222222222222222222222222222222222",
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458",
"data": "0x6e553f65...", // ERC4626 deposit(amount, receiver)
"value": "0"
}
}
NFT / CLP position mint
Simulate a Uniswap V3 concentrated liquidity position mint. The response includes the ERC721 position NFT with its tokenId.
{
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"tx": {
"from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"to": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
"data": "0x88316456...", // NonfungiblePositionManager.mint()
"value": "0"
}
}
{
"success": true,
"assetChanges": [
{
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": "4217390000",
"direction": "sent",
"standard": "erc20"
},
{
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"amount": "1500000000000000000",
"direction": "sent",
"standard": "erc20"
},
{
"token": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
"amount": "1",
"direction": "received",
"standard": "erc721",
"tokenId": "1207888"
}
],
"tokenMetadata": {
"0xc36442b4a4522e871399cd717abdd847ab11fe88": {
"name": "Uniswap V3 Positions NFT-V1",
"symbol": "UNI-V3-POS",
"decimals": 0,
"standard": "erc721"
},
...
},
...
}
Multi-chain simulation
Simulate on any supported chain by changing the network query parameter. Token addresses differ per chain.
POST /v1/simulate?network=base
{
"inputToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
"inputAmount": "1000000000",
"tx": { ... }
}
POST /v1/simulate?network=polygon
{
"inputToken": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon
"inputAmount": "1000000000",
"tx": { ... }
}
POST /v1/simulate?network=arbitrum
{
"inputToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
"inputAmount": "1000000000",
"tx": { ... }
}
Rendering results in your UI
Use tokenMetadata to display human-readable amounts:
function formatAssetChange(change, metadata) {
const meta = metadata[change.token.toLowerCase()];
// NFT: show token name + ID
if (change.standard === "erc721" || change.standard === "erc1155") {
const name = meta?.name || change.token;
const sign = change.direction === "sent" ? "-" : "+";
return `${sign}1 ${name} #${change.tokenId}`;
}
// Fungible: show decimal amount
if (!meta) return `${change.amount} ${change.token}`;
const amount = Number(change.amount) / 10 ** meta.decimals;
const sign = change.direction === "sent" ? "-" : "+";
return `${sign}${amount.toLocaleString()} ${meta.symbol}`;
}
// Output: "-4,217.39 USDC" or "+1 Uniswap V3 Positions NFT-V1 #1207888"
Batch simulation — leverage loop
Chain Aave V3 supply → borrow → swap in a single call. Only the first step needs inputToken/inputAmount since subsequent steps consume tokens from prior state.
curl -X POST "https://foresight.portals.fi/v1/simulate/batch?network=ethereum" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"sender": "0x1111111111111111111111111111111111111111",
"steps": [
{
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037...",
"value": "0"
}
},
{
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0xa415bcad...",
"value": "0"
}
},
{
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
"data": "0xc04b8d59...",
"value": "0"
}
}
]
}'
const response = await fetch(
"https://foresight.portals.fi/v1/simulate/batch?network=ethereum",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.PORTALS_API_KEY,
},
body: JSON.stringify({
sender: "0x1111111111111111111111111111111111111111",
steps: [
{
inputToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
inputAmount: "10000000000", // 10,000 USDC
tx: {
from: "0x1111111111111111111111111111111111111111",
to: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", // Aave V3
data: "0x617ba037...", // supply()
value: "0",
},
},
{
tx: {
from: "0x1111111111111111111111111111111111111111",
to: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", // Aave V3
data: "0xa415bcad...", // borrow()
value: "0",
},
},
{
tx: {
from: "0x1111111111111111111111111111111111111111",
to: "0xE592427A0AEce92De3Edee1F18E0157C05861564", // Uniswap V3
data: "0xc04b8d59...", // exactInput()
value: "0",
},
},
],
}),
}
);
const data = await response.json();
console.log(data.success); // true
console.log(data.steps.length); // 3
console.log(data.aggregate); // combined SimulationResult
import requests
response = requests.post(
"https://foresight.portals.fi/v1/simulate/batch",
params={"network": "ethereum"},
headers={
"Content-Type": "application/json",
"X-Api-Key": "YOUR_API_KEY",
},
json={
"sender": "0x1111111111111111111111111111111111111111",
"steps": [
{
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0x617ba037...",
"value": "0",
},
},
{
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
"data": "0xa415bcad...",
"value": "0",
},
},
{
"tx": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
"data": "0xc04b8d59...",
"value": "0",
},
},
],
},
)
data = response.json()
print(data["success"]) # True
print(len(data["steps"])) # 3
print(data["aggregate"]) # combined SimulationResult
{
"success": true,
"steps": [
{ "stepIndex": 0, "success": true, "gasUsed": "248103" },
{ "stepIndex": 1, "success": true, "gasUsed": "312847" },
{ "stepIndex": 2, "success": true, "gasUsed": "184529" }
],
"aggregate": {
"success": true,
"sender": "0x1111111111111111111111111111111111111111",
"assetChanges": [
{
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": "10000000000",
"direction": "sent",
"standard": "erc20"
}
],
"inputTokens": [
{ "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "amount": "10000000000" }
],
"outputTokens": [...],
"events": [...],
"gasUsed": "745479",
"gasCost": "2386840000000000",
"tokenMetadata": {
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": {
"name": "USD Coin",
"symbol": "USDC",
"decimals": 6
}
},
"errors": []
}
}
Supported Chains
Simulate transactions on 30 EVM chains with the same API and response format.
* Monad currently supports transaction replay and single-transaction simulations. Batch and multi-step simulations may be limited.
How It Works
Understanding the simulation pipeline helps you get the most out of the API.
The simulation pipeline
When you send a request, here's what happens:
We ensure the sender has enough input tokens for the simulation. If they don't, we handle it automatically behind the scenes — no tokens are actually moved or spent.
Token approvals are handled automatically for supported ERC-20 input simulations. Submit the swap or deposit calldata plus input token details.
Your transaction runs against the latest on-chain state in a fully isolated environment. No state changes persist. No other users are affected.
The raw simulation output is processed into structured net asset changes for the sender and recipient. Gas cost is estimated from live network conditions.
Token name, symbol, and decimals are resolved and cached for detected tokens when metadata is available. The response includes what your UI needs to render human-readable amounts.
Balance injection
The key feature behind Portals Foresight is automatic balance injection. The sender address doesn't need to hold supported input tokens — we provision the simulated balance so you can test flows without pre-funding wallets.
Our proprietary detection system resolves token compatibility automatically and caches the result. First simulation for a new token takes slightly longer; every subsequent simulation is near-instant.
Balance injection is designed for broad ERC20 coverage — standard tokens, proxies, rebasing tokens, vault shares, LP tokens, wrapped tokens, and more. In the rare case a token is not directly supported, the response surfaces the failure so you can retry with a different simulation strategy.
What you can simulate
Any EVM transaction that interacts with tokens or NFTs:
- DEX swaps (Uniswap, Curve, 1inch, Paraswap, etc.)
- Lending deposits and borrows (Aave, Compound, Morpho)
- Vault deposits and withdrawals (ERC4626, Yearn, Beefy)
- Concentrated liquidity positions (Uniswap V3/V4 mint, burn, collect)
- NFT purchases and sales (OpenSea/Seaport, Blur, aggregators)
- ERC1155 transfers (game items, multi-token contracts)
- Flash loan arbitrage and leverage loops
- Bridge transactions (preview source-chain side)
- Batch/multi-step flows — supply→borrow→swap chains, leverage loops (Pro for API-key requests, or x402)
- Plain native token transfers
- Any contract call that moves ERC20, ERC721, or ERC1155 tokens
Rate Limits
API-key requests to billable simulation and trace routes use the requests-per-second limit for your plan. Unauthenticated, x402, and dev-mode requests use a global per-IP fallback limiter.
| Plan | Simulations / Month | Traces / Month | Requests / Second |
|---|---|---|---|
| Free | 1,000 | 50 | 1 |
| Starter | 25,000 | 500 | 5 |
| Pro | 100,000 | 2,000 | 20 |
| Business | 500,000 | 10,000 | 50 |
| Enterprise | Unlimited | Unlimited | 100 |
Batch simulations (/simulate/batch) require Pro plans and above for API-key requests; x402 batch requests are also supported. Traces count separately from simulations.
| |||
Rate limit headers
RPS-limited responses include these standard headers:
| Header | Description |
|---|---|
RateLimit-Limit | Maximum requests per second. |
RateLimit-Remaining | Remaining requests in the current window. |
RateLimit-Reset | Seconds until the rate limit window resets. |
Request tracing
Send an X-Request-Id header to trace requests through your system. If omitted, a UUID is generated automatically. The value is echoed back in the response header.
# Your request
X-Request-Id: my-trace-id-abc123
# Response header
X-Request-Id: my-trace-id-abc123
x402 Payments
AI agents can pay per-request on supported simulation endpoints using the x402 protocol instead of an API key. Payments use USDC on Base. Trace endpoints still require API-key auth, except cached trace retrievals that are public.
Pricing
| Endpoint | Price |
|---|---|
POST /v1/simulate | $0.001 per request |
POST /v1/simulate/batch | $0.005 per request |
How it works
The x402 protocol uses HTTP 402 Payment Required to enable pay-per-request. The flow is fully automatic with compatible client libraries.
| Step | Description |
|---|---|
| 1 | Send request without API key or payment header. |
| 2 | Server returns 402 with PAYMENT-REQUIRED header (Base64 JSON: price, network, wallet). |
| 3 | Sign the payment payload with your crypto wallet. |
| 4 | Retry with PAYMENT-SIGNATURE header. |
| 5 | Server verifies payment, executes the endpoint, and settles USDC on-chain for 2xx/3xx responses. |
If the endpoint returns a 4xx or 5xx error, payment is not settled. Reverted simulations returned as HTTP 200 are successful API responses and are billable.
Client libraries
Use any x402-compatible client to handle the payment flow automatically:
| Language | Package |
|---|---|
| JavaScript | @x402/fetch or @x402/axios |
| Python | x402 |
| Go | x402 |
Example
# 1. Initial request without auth returns 402 + PAYMENT-REQUIRED header
curl -i -X POST "https://foresight.portals.fi/v1/simulate?network=ethereum" \
-H "Content-Type: application/json" \
-d '{"inputToken":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","inputAmount":"10000000000","tx":{"from":"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045","to":"0x1111111254EEB25477B68fb85Ed929f73A960582","data":"0x12aa3caf...","value":"0"}}'
# 2. Decode the PAYMENT-REQUIRED header (Base64 JSON), sign with your wallet,
# then retry with PAYMENT-SIGNATURE:
curl -X POST "https://foresight.portals.fi/v1/simulate?network=ethereum" \
-H "Content-Type: application/json" \
-H "PAYMENT-SIGNATURE: <signed-payload>" \
-d '{"inputToken":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","inputAmount":"10000000000","tx":{"from":"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045","to":"0x1111111254EEB25477B68fb85Ed929f73A960582","data":"0x12aa3caf...","value":"0"}}'
import { wrapFetch } from "@x402/fetch";
const x402Fetch = wrapFetch(fetch, walletClient);
const response = await x402Fetch(
"https://foresight.portals.fi/v1/simulate?network=ethereum",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
inputToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
inputAmount: "10000000000",
tx: {
from: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
to: "0x1111111254EEB25477B68fb85Ed929f73A960582",
data: "0x12aa3caf...",
value: "0",
},
}),
}
);
const result = await response.json();
from x402 import X402Client
# The x402 client handles the 402 challenge, signing, and retry automatically
client = X402Client(wallet=my_wallet)
body = {
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmount": "10000000000",
"tx": {
"from": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"to": "0x1111111254EEB25477B68fb85Ed929f73A960582",
"data": "0x12aa3caf...",
"value": "0",
},
}
response = client.post(
"https://foresight.portals.fi/v1/simulate",
params={"network": "ethereum"},
json=body,
)
result = response.json()
LLM Integration
Give your AI coding assistant full context on the Portals Foresight API. Copy the docs as markdown or install the Claude Code skill.
Copy as Markdown
Click the Copy as Markdown button at the top of this page to copy the entire documentation as clean markdown. Paste it into ChatGPT, Claude, Gemini, Cursor, or any LLM-powered tool as context.
Claude Code Skill
If you use Claude Code, install the Portals Foresight skill so Claude automatically knows the API when you're writing integration code.
View and copy the skill file, then save it into your project:
# Create the skill directory and save SKILL.md into it
mkdir -p .claude/skills/portals-foresight
# Then paste the copied content into:
# .claude/skills/portals-foresight/SKILL.md
Once installed, Claude Code will automatically reference the Portals Foresight API when you ask it to write simulation code, build integrations, or debug API responses. You can also invoke it directly:
# Claude Code will auto-detect when relevant, or invoke directly:
/portals-foresight
# Example prompts that trigger the skill:
"Write a function that simulates a USDC to DAI swap on Ethereum"
"Add transaction preview to our swap UI using Portals Foresight"
"Debug why my simulation is returning success: false"
The Copy as Markdown button works with any AI tool. The skill file is specific to Claude Code but the markdown content works everywhere.
API Explorer
Test every endpoint directly in your browser. The explorer comes with pre-filled examples for simulate, batch simulate, and health.
Click the Authorize button in the explorer and enter your X-Api-Key before making requests. Don't have one yet? Sign up — the free tier includes 1,000 simulations/month.