How to Add Transaction Preview to a MetaMask Snap Using Tenderly Simulation API
Learn how to add a "transaction preview" option to the MetaMask wallet with the Tenderly Simulation API and give users a way to preview the outcome of their transactions before signing them.
This tutorial demonstrates how to add a "transaction preview" option to a wallet using Tenderly Simulation API. We’ll show you how to add this option to the MetaMask wallet.
The "transaction preview" feature allows users to see the exact outcome of their transactions before submitting them to the production network. Wallets equipped with this feature make it easier for users to understand the financial implications of their transactions before making a commitment.
Simulations are executed on a fork of the latest state of the blockchain in a safe, risk-free environment. This helps wallet users prevent sending malicious or error-prone transactions.
You can find the complete demo repository on GitHub:
Tenderly MetaMask Snap Demo
This tutorial is for demonstration purposes only, specifically to illustrate how to add a transaction preview option to a MetaMask Snap. It is not intended nor recommended for any other usage.
This project shows you how to integrate transaction simulations into the MetaMask wallet. We’ll be working with MetaMask Snaps.
You’ll learn how to connect to the Tenderly Simulation API, send the transaction data, retrieve the simulation results, and display them to the user via the MetaMask UI. This includes asset changes in dollars, native-asset balance changes, output value, storage changes, event logs, and call traces.
Simulated transactions can then also be opened in Tenderly for further debugging and testing.
Below is a list of features that we’ll be adding to the MetaMask Snap. Similar features can also be added to any wallet to help users build confidence when sending transactions.
Feature | Description |
---|---|
Simulation debugging | Auto-generated link to inspect and debug the simulation in Tenderly. |
Publicly sharable simulations | Ability to enable/disable public sharing of simulations. |
Asset changes displayed in dollar value | Human-readable data, including asset changes automatically converted into dollars |
Native-asset balance changes | Ability to track changes in their native-asset balance during the execution of the contract |
Output value | See the result of the contract call, displaying what the contract is set to return. |
Storage changes | See any changes made to the contract's storage during the execution |
Event logs | See events that were emitted during the contract's execution |
Call traces | Breakdown of all the function calls that happened during the execution |
Before you start building the MetaMask Snap, make sure to disable the “real” version of MetaMask and install the MetaMask Flask Development Plugin.
Let’s start building! 👷♂️
yarn create @metamask/snap project-name
OR
npm create @metamask/snap project-name
From the root of the newly created project, install the project dependencies using Yarn.
yarn
Start the development server.
yarn start
Once you’ve installed the Snap, find the
/packages/snap/snap.manifest.json
file and set the permissions as shown below. Learn about MetaMask permissions here.{
// ...
"initialPermissions": {
"snap_dialog": {},
"endowment:rpc": {
"dapps": true,
"snaps": false
},
"endowment:transaction-insight": {
"allowTransactionOrigin": true
},
"endowment:network-access": {},
"snap_manageState": {},
"endowment:ethereum-provider": {
"dapps": true,
"snaps": true
}
}
}
When you run the app and try to connect the wallet, a permission request will pop up.
Click Connect and Approve & install buttons to continue.

Install MetaMask Snap Permissions
The transaction preview option in the MetaMask Snap is powered by the Tenderly Simulation API.
Before you can start using the API, you need to generate access tokens. Follow these steps:
- 2.
If you need help with API authentication, please follow this guide:

Generate Access Token from Authorization page
The core logic that makes the Snap work is stored in three files:
- Credentials Access (
credentials-access.ts
): Manages the Tenderly API authentication and access. - Simulation (
simulation.ts
): The core logic for interacting with the Tenderly API to simulate transactions and fetch simulation results. - Formatter (
formatter.ts
): Logic for parsing simulation results and formatting them in a user-friendly way in the MetaMask UI.
We can store all methods responsible for requesting, updating, and fetching Tenderly API credentials in a file called credentials-access.ts. View the source code here.
The most important methods include:
fetchCredentials()
: This function retrieves the credentials of a Tenderly project. If no credentials are stored, it triggershandleUpdateTenderlyCredentials
to request new ones.handleUpdateTenderlyCredentials()
: This function handles the process of updating Tenderly project credentials. It gets new credentials by callingrequestNewTenderlyCredentials
and then saves them using thesnap_manageState
method with an'update'
operation.requestNewTenderlyCredentials()
: This function requests new Tenderly credentials. It callsrequestCredentials
to receive the raw credentials data and verifies its correctness before returning an object withaccountId
,projectId
, andaccessToken
.
snap_manageState is a built-in method that allows the Snap to persist up to 100 MB of data to the disk. Learn more here.
Next, we need to create the logic that sends transaction data to the Tenderly Simulation API, simulates it, and retrieves the results.
We’ll use the
simulate
API endpoint for that.https://api.tenderly.co/api/v1/account/{accountId}/project/{projectId}/simulate
To keep things organized, let’s store the simulation logic in a file called simulation.ts. View the source code here.
To send transaction data to the Tenderly Simulation API, we need to create two key methods:
- simulate(): This is the main function that handles the simulation of a transaction. It fetches the API access credentials, submits the transaction data to the Tenderly Simulation API, and handles any errors returned by the API.
export async function simulate(
transaction: { [key: string]: Json },
transactionOrigin: string,
): Promise<Panel> {
const credentials = await fetchCredentials(transactionOrigin);
if (!credentials) {
return panel([text('🚨 Tenderly access token updated. Please try again.')]);
}
const simulationResponse = await submitSimulation(transaction, credentials);
const err = catchError(simulationResponse, credentials);
return err || formatResponse(simulationResponse, credentials);
}
- submitSimulation(): This function sends a request to the API along with the transaction data to be simulated.
When the transaction is simulated, we can make it publicly accessible by calling the share API endpoint. Making the simulation shareable allows you to copy the link to the transaction and send it to anyone. Shared transactions can be viewed without a Tenderly account.
async function submitSimulation(
transaction: { [key: string]: Json },
credentials: TenderlyCredentials,
) {
const chainId = await ethereum.request({ method: 'eth_chainId' });
const response = await fetch(
`https://api.tenderly.co/api/v1/account/${credentials.accountId}/project/${credentials.projectId}/simulate`,
{
method: 'POST',
body: JSON.stringify({
from: transaction.from,
to: transaction.to,
input: transaction.data,
gas: hex2int(transaction.gas),
value: hex2int(transaction.value),
network_id: hex2int(chainId as string),
save: true,
save_if_fails: true,
simulation_type: 'full',
generate_access_list: false,
source: 'tenderly-metamask-snap',
}),
headers: {
'Content-Type': 'application/json',
'X-Access-Key': credentials.accessToken,
},
},
);
const parsedResponse = await response.json();
// Make the simulation publicly accessible
if (parsedResponse?.simulation?.id) {
await fetch(
`https://api.tenderly.co/api/v1/account/${credentials.accountId}/project/${credentials.projectId}/simulations/${parsedResponse.simulation.id}/share`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Access-Key': credentials.accessToken,
},
},
);
}
return parsedResponse;
}
Once the API returns the simulation results, we need to parse this data and format it in a user-friendly way to be displayed in the MetaMask UI.
The core formatting methods include:
formatResponse()
: This function receives the raw data and credentials of a Tenderly project simulation, calls the individual formatter functions for each relevant section (like balance changes, output value, asset changes, etc.), and returns a panel with all the formatted outputs.formatBalanceDiff()
: This function generates a panel showing balance changes for each account involved in the transaction.formatOutputValue()
: This function creates a panel that presents the output values of the transaction if any exist. It also decodes the output, if possible.formatAssetChanges()
: This function formats a panel to show any asset changes, differentiating between ERC20, ERC721, and other changes.formatStorageChanges()
: This function creates a panel that lists any changes to storage that occurred during the transaction. It uniquely formats addresses and nested data structures for clarity.formatEventLogs()
: This function presents the event logs, if they exist, for each transaction. The logs are formatted for readability and include input values.formatCallTrace()
: This function produces a formatted visual hierarchy of call traces, showing nested calls recursively.formatSimulationUrl()
: This function returns a link to the full details of the simulation on the Tenderly Dashboard and a separate shareable link.
The MetaMask Snap mono repo provides a set of predefined UI elements and layout configurations. We'll use these elements to extend the existing MetaMask UI to display the simulation results.
When you install our MetaMask example, we show you how to run different types of simulations:
- Successful transaction simulation with a predefined payload (successful ERC-20 transfer and NFT transfer)
- Failed transaction simulation with a predefined payload
- Custom transaction simulation with any payload

Tenderly Snap UI
For the purpose of this tutorial, we’ll show you how to run simulations with a predefined payload and how to add your custom payload and execute the simulation.
To initiate a simulation, we’ll write a function that will send the predefined transaction data from our wallet address and call the
eth_sendTransaction
RPC method.// packages/site/src/utils/snap.ts
export const sendTransaction = async (data: any): Promise<any> => {
try {
const [from] = (await window.ethereum.request({
method: 'eth_requestAccounts',
})) as string[];
if (!from) {
return Promise.reject(Error('Failed to get an account'));
}
// https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction
return window.ethereum.request({
method: 'eth_sendTransaction',
params: [
{
...data,
from,
},
],
});
} catch (e) {
console.error(e);
return e;
}
};
The
eth_sendTransaction
method call will trigger the onTransaction
handler, which will call our custom simulate
function, which was defined in simulation.ts
.// packages/snap/src/index.ts
export const onTransaction: OnTransactionHandler = async ({
transaction,
transactionOrigin,
}) => {
if (!isObject(transaction) || !hasProperty(transaction, 'to')) {
return {
content: {
value: 'Unknown transaction type',
type: NodeType.Text,
},
};
}
const simulationResponse = await simulate(
transaction,
transactionOrigin || '',
);
return {
content: {
children: simulationResponse.children,
type: NodeType.Panel,
},
};
};
In the
onTransaction
handler, the simulate
function is called with the transaction and its origin as arguments.The
simulate
function is responsible for fetching access credentials, submitting the transaction data to the Tenderly API, handling any errors, and formatting the API response for output.The results of the simulation are displayed within the MetaMask UI. The images below show a successful ERC20 token transfer of 1 USDC to
demo.eth
. The output is generated by the functions defined in the formatter.ts
file.
ERC20 Transfer - send 1 USDC to demo.eth
For example, the Asset Changes block, which shows us the dollar value changes resulting from the simulation, is generated by the
formatAssetChanges()
function.You also get a link to the simulated transaction which you can open in Tenderly and inspect it further with Debugger or resimulate it with different values.
This link is generated by the
formatSimulationUrl()
function defined in the formatter.ts
file.export function formatSimulationUrl(
data: any,
credentials: TenderlyCredentials,
): Component[] {
const simulationUrl = `https://dashboard.tenderly.co/${credentials.accountId}/${credentials.projectId}/simulator/${data.simulation?.id}`;
const sharedSimulationUrl = `https://dashboard.tenderly.co/shared/simulation/${data.simulation?.id}`;
return [
heading('Tenderly Dashboard:'),
text('See full simulation details in Tenderly.'),
text(
`**Status:** ${data.transaction?.status ? 'Success ✅' : 'Failed ❌'}`,
),
copyable(`${simulationUrl}`),
text('Share simulation details with others! 🤗'),
copyable(`${sharedSimulationUrl}`),
];
}
For the second example, we’ll simulate a failed transaction with a predefined payload.
The image below shows a failed ERC20 token transfer of 1,000,000 USDC to
demo.eth
.
Preview of the failed transaction
We also get a link to the Tenderly Dashboard, where the user can inspect the stack trace, events, state changes, gas consumption, etc. This is particularly useful for understanding why the transaction failed.
The user can smoothly continue debugging the failed transaction and resimulating it to test different solutions.
To run a simulation with a custom payload, you need to add an object that aligns with the Ethereum transaction specification.

Custom payload UI
It can contain the following fields:
from
:DATA
, 20 Bytes - The address the transaction is sent from.to
:DATA
, 20 Bytes - (optional when creating a new contract) The address the transaction is directed to.gas
:QUANTITY
- (optional, default: 90000) Integer of the gas provided for the transaction execution. It will return unused gas.gasPrice
:QUANTITY
- (optional, default: To-Be-Determined) Integer of the gasPrice used for each paid gas.value
:QUANTITY
- (optional) Integer of the value sent with this transaction.data
:DATA
- The compiled code of a contract OR the hash of the invoked method signature and encoded parameters.nonce
:QUANTITY
- (optional) Integer of a nonce. This allows you to overwrite your own pending transactions that use the same nonce.
All numerical values should be hexadecimal strings, and addresses should be Ethereum addresses (20 bytes).

Send a custom payload
After entering the custom payload, users can initiate the simulation using the same
sendTransaction
function as before. This passes the custom payload to the onTransaction
handler, which then triggers the simulate
function to run the simulation.In this tutorial, you learned how to add a transaction preview option to a MetaMask Snap powered by the Tenderly Simulation API. You can find the source code for this project on GitHub.
Transaction simulation on Tenderly can be initiated in three ways, depending on your project and needs. Learn how to integrate simulations into your dapps with these help resources: