# Build with AI
Source: https://docs.sim.dune.com/build-with-ai
Build faster with Sim APIs using LLMs.
We provide several resources to help you use LLMs and AI coding assistants to build much faster with Sim APIs.
## AI Search
First, **AI Search** is built directly into this documentation site.
The search bar, powered by Mintlify, can understand natural language queries.
Ask questions about endpoints, authentication, or specific data points, and it will answer you with the most relevant, accurate information.
## Use with LLMs
### Complete Documentation for LLMs
For LLM applications such as custom agents, RAG systems, or any scenario requiring our complete documentation, we provide an optimized text file at [`https://docs.sim.dune.com/llms-full.txt`](https://docs.sim.dune.com/llms-full.txt).
### Per-Page Access
Each page on this documentation site offers several ways to access its content in LLM-friendly formats:
* **Copy Page:** Copies the fully rendered content of the current page.
* **View Markdown:** Provides a URL with the raw Markdown source. This is ideal for direct input into LLMs.
* **Open with ChatGPT:** Instantly loads the page's content into a new session with ChatGPT. Ask questions, summarize, or generate code based on the page's content.
You can retrieve the Markdown version of any documentation page by appending `.md` to its URL. For example, `/evm/activity` becomes [`https://docs.sim.dune.com/evm/activity.md`](https://docs.sim.dune.com/evm/activity.md).
You can also type `⌘C` or `Ctrl+C` to copy any page's Markdown content.
Try it now.
## Cursor Integration
To integrate our documentation directly into Cursor:
1. Go to Cursor Settings -> Features -> Docs -> Add new doc.
2. Enter "docs.sim.dune.com" in the URL field.
3. Provide a name (e.g., "@simdocs").
4. Hit confirm. The documentation will sync automatically.
5. Reference Sim APIs documentation by typing `@simdocs` (or your chosen name) in your Cursor chat window.
## OpenAPI Specifications
For a more compact format, we provide OpenAPI specifications for each of our endpoints. These files detail available parameters, request bodies, and response schemas. This format is particularly useful for LLMs and AI code generation tools to understand our API structure.
You can find our OpenAPI specifications in the following directories:
* EVM API specifications: [`/evm/openapi/`](https://github.com/duneanalytics/sim-docs/tree/main/evm/openapi)
* SVM API specifications: [`/svm/openapi/`](https://github.com/duneanalytics/sim-docs/tree/main/svm/openapi)
# Error Handling
Source: https://docs.sim.dune.com/error-handling
How to handle errors when using Sim APIs
This guide explains how to handle errors when using Sim APIs, including common error codes, troubleshooting steps, and code examples for proper error handling.
## Error Response Format
When an error occurs, Sim APIs return a standard error response format:
```json
{
"error": {
"message": "Description of what went wrong",
"code": "ERROR_CODE"
}
}
```
## Common Error Codes
| HTTP Status | Error Code | Description | Troubleshooting |
| ----------- | ----------------------- | -------------------------- | -------------------------------------------------------------------------------------------- |
| 401 | UNAUTHORIZED | Invalid or missing API key | Check that you're including the correct API key in the `X-Sim-Api-Key` header |
| 400 | BAD\_REQUEST | Malformed request | Verify the address format and other parameters in your request |
| 404 | NOT\_FOUND | Resource not found | Verify the endpoint URL and resource identifiers |
| 429 | RATE\_LIMIT\_EXCEEDED | Too many requests | Implement backoff strategies and consider upgrading your plan if you consistently hit limits |
| 500 | INTERNAL\_SERVER\_ERROR | Server-side error | Retry the request after a short delay; if persistent, contact support |
## Handling Errors in Code
Here are examples of how to properly handle errors in different programming languages:
### JavaScript
```javascript
fetch('https://api.sim.dune.com/v1/evm/balances/0xd8da6bf26964af9d7eed9e03e53415d37aa96045', {
method: 'GET',
headers: {'X-Sim-Api-Key': 'YOUR_API_KEY'}
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(`API error: ${err.error?.message || response.statusText}`);
});
}
return response.json();
})
.then(data => {
console.log('Success:', data);
// Process your data here
})
.catch(err => {
console.error('Error fetching balances:', err);
// Handle error appropriately in your application
// e.g., show user-friendly message, retry, or fallback behavior
});
```
### Python
```python
import requests
import time
def get_balances(address, api_key, max_retries=3):
url = f"https://api.sim.dune.com/v1/evm/balances/{address}"
headers = {"X-Sim-Api-Key": api_key}
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # Raises an exception for 4XX/5XX responses
return response.json()
except requests.exceptions.HTTPError as err:
status_code = err.response.status_code
error_data = {}
try:
error_data = err.response.json()
except:
pass
error_message = error_data.get('error', {}).get('message', 'Unknown error')
print(f"HTTP Error {status_code}: {error_message}")
# Handle specific error codes
if status_code == 429: # Rate limit exceeded
wait_time = min(2 ** attempt, 60) # Exponential backoff
print(f"Rate limit exceeded. Retrying in {wait_time} seconds...")
time.sleep(wait_time)
continue
elif status_code == 500: # Server error
if attempt < max_retries - 1:
wait_time = 2 ** attempt
print(f"Server error. Retrying in {wait_time} seconds...")
time.sleep(wait_time)
continue
# For other errors or if we've exhausted retries
return {"error": error_message, "status_code": status_code}
except requests.exceptions.RequestException as err:
print(f"Request error: {err}")
return {"error": "Network or connection error", "details": str(err)}
return {"error": "Max retries exceeded"}
# Usage
result = get_balances("0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "YOUR_API_KEY")
if "error" in result:
print(f"Failed to get balances: {result['error']}")
else:
print(f"Found {len(result['balances'])} token balances")
```
## Best Practices for Error Handling
1. **Always check for errors**: Don't assume API calls will succeed.
2. **Implement retry logic with backoff**: For transient errors (like rate limits or server errors), implement exponential backoff:
```javascript
async function fetchWithRetry(url, options, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
const response = await fetch(url, options);
if (response.ok) return response.json();
const error = await response.json();
// Don't retry for client errors (except rate limiting)
if (response.status < 500 && response.status !== 429) {
throw new Error(error.error?.message || response.statusText);
}
// For rate limiting or server errors, retry with backoff
retries++;
const delay = Math.min(1000 * 2 ** retries, 10000);
await new Promise(resolve => setTimeout(resolve, delay));
} catch (err) {
if (retries === maxRetries - 1) throw err;
retries++;
}
}
}
```
3. **Provide meaningful error messages**: Transform API error responses into user-friendly messages.
4. **Log errors for debugging**: Maintain detailed logs of API errors for troubleshooting.
5. **Implement fallbacks**: When possible, have fallback behavior when API calls fail.
## Debugging Tips
If you're experiencing persistent errors:
1. **Verify your API key**: Ensure it's valid and has the necessary permissions.
2. **Check request format**: Validate that your request parameters match the API specifications.
3. **Inspect full error responses**: The error message often contains specific details about what went wrong.
4. **Monitor your usage**: Check if you're approaching or exceeding rate limits.
5. **Test with cURL**: Isolate issues by testing the API directly with cURL:
```bash
curl -v -X GET "https://api.sim.dune.com/v1/evm/balances/0xd8da6bf26964af9d7eed9e03e53415d37aa96045" \
-H "X-Sim-Api-Key: YOUR_API_KEY"
```
## Need More Help?
If you're still experiencing issues after following these guidelines, please reach out through our [support channels](/support).
# Activity
Source: https://docs.sim.dune.com/evm/activity
evm/openapi/activity.json get /v1/evm/activity/{uri}
View chronologically ordered transactions including native transfers, ERC20 movements, NFT transfers, and decoded contract interactions.
export const SupportedChains = ({endpoint}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.sim.dune.com/v1/evm/supported-chains", {
method: "GET",
headers: {
"X-Sim-Api-Key": "sim_qfIvSWu7c8WBXzfCQ0SeLBGJts204uyT"
}
}).then(response => response.json()).then(setData);
}, []);
if (data === null) {
return null;
}
const chains = endpoint !== undefined ? data.chains.filter(chain => chain[endpoint]?.supported) : data.chains;
console.log("data", data);
return
name
chain_id
tags
{chains.map(chain =>
{chain.name}
{chain.chain_id}
{chain.tags.join(", ")}
)}
;
};

The Activity API provides a realtime feed of onchain activity for any EVM address.
The newest activity is returned first and includes:
* Native token transfers
* ERC20 token transfers with metadata (symbol, decimals)
* ERC721 (NFT) transfers with token IDs
* Contract interactions with decoded function calls
## Spam Tokens
The Activity API supports filtering out activities related to spam tokens using the `?exclude_spam_tokens` parameter. When specified, this parameter will exclude transactions and transfers involving tokens that meet spam criteria, providing a cleaner activity feed.
We include all the data needed for custom filtering in the responses, allowing you to implement your own filtering logic. For a detailed explanation of our spam filtering approach, see our [Spam Token Filtering](/spam-filtering) guide.
# Add Account Activity
Source: https://docs.sim.dune.com/evm/add-account-activity
Expand your realtime crypto wallet by integrating a dynamic feed of onchain activity.
Now that you have a wallet capable of showing realtime token balances and total portfolio value, let's enhance it by adding an *Activity* tab.
A key feature for any wallet is the ability to view past transaction activity.
This includes native currency transfers, ERC20 token movements, NFT transfers, and decoded interactions with smart contracts.
The [Activity API](/evm/activity) provides a comprehensive, realtime feed of this onchain activity, letting you implement this functionality with a single API request across 60+ EVM chains.
Access the complete source code for this wallet on GitHub
Interact with the finished wallet app
This guide assumes you've completed the first guide, [Build a Realtime Wallet](/evm/build-a-realtime-wallet).
Your project should already be set up to fetch and display token balances.
## See It in Action
You can see the activity feed in action by trying the live demo below. Click on the "Activity" tab to explore transaction history for the sample wallet:
## Fetch Wallet Activity
Let's start by adding a new `getWalletActivity` async function to our `server.js` file to fetch activity data from Sim APIs.
```javascript server.js (getWalletActivity)
async function getWalletActivity(walletAddress, limit = 25) { // Default to fetching 25 activities
if (!walletAddress) return [];
// The Activity API is currently in beta.
// We add a 'limit' query parameter to control how many activities are returned.
const url = `https://api.sim.dune.com/v1/evm/activity/${walletAddress}?limit=${limit}`;
try {
const response = await fetch(url, {
headers: {
'X-Sim-Api-Key': SIM_API_KEY, // Your API key from .env
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`Activity API request failed with status ${response.status}: ${response.statusText}`, errorBody);
throw new Error(`Activity API request failed: ${response.statusText}`);
}
const data = await response.json();
// The API returns activity items in the 'activity' key of the JSON response.
return data.activity || [];
} catch (error) {
console.error("Error fetching wallet activity:", error.message);
return []; // Return empty array on error
}
}
```
The function creates the request URL for the `/v1/evm/activity/{address}` endpoint, adding the `limit` as a query parameter.
The [Activity API](/evm/activity) conveniently packages the transaction data within an `activity` array in the response.
The array provides rich context for each event, such as `block_time`, `transaction_hash`, `from` and `to` addresses, `value`, `value_usd`, and more.
The [Activity API](/evm/activity) supports pagination via `offset` and `limit` query parameters. For a production wallet, you might implement infinite scrolling or "Load More" buttons to fetch subsequent pages of activity.
## Add Activity into the Server Route
Next, modify the `app.get('/')` route handler, add a call to `getWalletActivity`, and include its results in the data passed to `res.render`.
```javascript server.js (app.get('/') updated for activity) {16, 18, 45}
app.get('/', async (req, res) => {
const {
walletAddress = '',
tab = 'tokens'
} = req.query;
let tokens = [];
let activities = [];
let collectibles = [];
let totalWalletUSDValue = 0;
let errorMessage = null;
if (walletAddress) {
try {
[tokens, activities] = await Promise.all([
getWalletBalances(walletAddress),
getWalletActivity(walletAddress, 25) // Fetching 25 recent activities
]);
// Calculate the total USD value from the fetched tokens
if (tokens && tokens.length > 0) {
tokens.forEach(token => {
let individualValue = parseFloat(token.value_usd);
if (!isNaN(individualValue)) {
totalWalletUSDValue += individualValue;
}
});
}
totalWalletUSDValue = numbro(totalWalletUSDValue).format('$0,0.00');
} catch (error) {
console.error("Error in route handler:", error);
errorMessage = "Failed to fetch wallet data. Please try again.";
// tokens will remain empty, totalWalletUSDValue will be 0
}
}
res.render('wallet', {
walletAddress: walletAddress,
currentTab: tab,
totalWalletUSDValue: totalWalletUSDValue, // We'll calculate this in the next section
tokens: tokens,
activities: activities, // Placeholder for Guide 2
collectibles: [], // Placeholder for Guide 3
errorMessage: errorMessage
});
});
```
Our updated `app.get('/')` route handler now handles fetching of both token balances and wallet activity.
Both the `tokens` and the newly fetched `activities` arrays are then passed to the `res.render` method.
This makes the complete dataset available to our `wallet.ejs` template, enabling it to populate both the "Tokens" and "Activity" tabs with relevant, realtime onchain information.
## Show Activity in the Frontend
The final step is to update our `views/wallet.ejs` template to render the fetched activity data within the "Activity" tab.
CTRL+F for `id="activity"` and locate the section for the *Activity* tab.
It currently contains a placeholder paragraph.
Replace that entire `div` with the following EJS code:
```ejs views/wallet.ejs (Activity tab content) [expandable]
<%= new Date(activity.block_time).toLocaleDateString(); %>
<% if (activity.type === 'call') { %>
Interaction
<% } else if (activity.value) { %>
<%
let amountDisplay = '0';
const decimals = typeof activity.token_metadata?.decimals === 'number' ? activity.token_metadata.decimals : 18;
if (decimals !== null) {
const valueStr = activity.value.toString();
const padded = valueStr.padStart(decimals + 1, '0');
let intPart = padded.slice(0, -decimals);
let fracPart = padded.slice(-decimals).replace(/0+$/, '');
if (!intPart) intPart = '0';
if (fracPart) {
amountDisplay = `${intPart}.${fracPart}`;
} else {
amountDisplay = intPart;
}
const amountNum = parseFloat(amountDisplay);
if (amountNum > 0 && amountNum < 0.0001) {
amountDisplay = '<0.0001';
}
if (amountNum > 1e12 || amountDisplay.length > 12) {
amountDisplay = amountNum.toExponential(2);
}
} else {
amountDisplay = activity.id || String(activity.value);
}
// Clean up the symbol: remove $ and anything after space/dash/bracket, and limit length
let symbol = activity.token_metadata?.symbol || (activity.asset_type === 'native'
? (activity.chain_id === 1 || activity.chain_id === 8453 || activity.chain_id === 10 ? 'ETH' : 'NTV')
: (activity.id ? '' : 'Tokens'));
if (symbol) {
symbol = symbol.replace('$', '').split(/[\s\-\[]/)[0].substring(0, 8);
}
%>
<% if (activity.type === 'receive') { %>+<% } else if (activity.type === 'send') { %>-<% } %><%= amountDisplay %> <%= symbol %>
<% } %>
<% }); %>
<% } else if (walletAddress) { %>
No activity found for this wallet.
<% } else { %>
Enter a wallet address to see activity.
<% } %>
```
This EJS transforms the raw data from the Activity API into an intuitive and visual transaction history.
Here's a breakdown of how it processes each activity item:
* A **list entry** is generated for each transaction.
* An **icon** visually indicates the transaction's nature: receive (↓), send (↑), or contract call (⇆).
* A **descriptive title** is dynamically constructed using the `activity.type` (and `activity.function.name` for contract calls).
* The transaction's **timestamp** (`block_time`) is converted to a readable local date/time string.
* The **chain ID** (`chain_id`) is displayed, providing important multichain context.
Beyond these descriptive elements, the template also focuses on presenting the value and financial aspects of each transaction:
* The **transaction amount** (raw `value`) is converted into a user-friendly decimal format (e.g., "1.5 ETH"). This conversion utilizes the `decimals` property from `token_metadata`.
* For **NFTs**, if a standard decimal value isn't applicable, the template displays the `token_id`.
* The **USD value** (`value_usd`), if provided by the API, is formatted to two decimal places and shown, giving a sense of the transaction's monetary worth.
***
Restart your server by running `node server.js` and refresh the app in the browser.
When you click on the *Activity* tab, you should now see a list of the latest transactions, similar to the screenshot at the beginning of this guide.
## Conclusion
You successfully added a realtime, fully functional activity feed to your multichain wallet with a single API request.
In the next and final guide of this series, [Display NFTs & Collectibles](/evm/show-nfts-collectibles), we will complete the wallet by adding support for viewing NFT collections.
# Balances
Source: https://docs.sim.dune.com/evm/balances
evm/openapi/balances.json get /v1/evm/balances/{uri}
Access realtime token balances. Get comprehensive details about native and ERC20 tokens, including token metadata and USD valuations.
export const SupportedChains = ({endpoint}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.sim.dune.com/v1/evm/supported-chains", {
method: "GET",
headers: {
"X-Sim-Api-Key": "sim_qfIvSWu7c8WBXzfCQ0SeLBGJts204uyT"
}
}).then(response => response.json()).then(setData);
}, []);
if (data === null) {
return null;
}
const chains = endpoint !== undefined ? data.chains.filter(chain => chain[endpoint]?.supported) : data.chains;
console.log("data", data);
return
name
chain_id
tags
{chains.map(chain =>
{chain.name}
{chain.chain_id}
{chain.tags.join(", ")}
)}
;
};

The Token Balances API provides accurate and fast real time balances of the native and ERC20 tokens of accounts on supported EVM blockchains.
The Balances API only returns balances for certain low latency chains by default.
To fetch balances for *all* supported chains, use the `?chains_ids=all` query parameter.
## Token Prices
Sim looks up prices onchain. We use the most liquid onchain pair to determine a USD price. We return the available liquidity in `pool_size` as part of the response, and show a warning `low_liquidity: true` if this value is less than \$10k.
## Spam Tokens
The Balances API provides the `?exclude_spam_tokens` query parameter. You can use it filter out potential spam tokens based on various criteria, including a minimum liquidity threshold of \$100.
We also include the `pool_size` field in all responses, allowing you to implement custom filtering logic based on your specific requirements. For a detailed explanation of our spam filtering approach, see our [Spam Token Filtering](/spam-filtering) guide.
## Pagination
This endpoint is using cursor based pagination. You can use the `limit` query parameter to define the maximum page size.
Results might at times be less than the maximum page size.
The `next_offset` value is passed back by the initial response and can be used to fetch the next page of results, by passing it as the `offet` query parameter in the next request.
You can only use the value from `next_offset` to set the `offset` query parameter of the next page of results.
# Build a Realtime Chat Agent
Source: https://docs.sim.dune.com/evm/build-a-realtime-chat-agent
Create an AI-powered chat app that can answer questions about blockchain data using Dune's Sim APIs and OpenAI's function calling feature.
In this guide, you'll learn how to build **Simchat**, an AI chat agent that can provide realtime blockchain insights through natural conversation.
Users can ask questions about wallet balances, transaction history, NFT collections, and token information across 60+ EVM chains and Solana, and the agent will fetch and explain the data in a friendly way.
By combining OpenAI's LLMs with the realtime blockchain data provided by Sim APIs, you'll create a chat agent that makes onchain data accessible to everyone, regardless of their technical expertise.
Access the complete source code for Simchat on GitHub
Chat with the finished assistant
## Prerequisites
Before we begin, ensure you have:
* Node.js >= 18.0.0
* A [Sim API key](/#authentication)
* An [OpenAI API key](https://platform.openai.com/docs/api-reference/introduction)
Learn how to obtain your Sim API key
## Features
When you complete this guide, your chat agent will have these capabilities:
Automatically triggers API requests based on user queries using OpenAI's function calling feature
Retrieves native and ERC20/SPL token balances with USD values for any wallet address across EVM chains and Solana
Displays chronological wallet activity including transfers and contract interactions on EVM networks
Shows ERC721 and ERC1155 collectibles owned by wallet addresses across supported EVM chains
Provides detailed token information, pricing, and holder distributions for EVM and Solana tokens
Users ask questions in plain English about blockchain data, no technical knowledge required
## Try the Live Demo
Before diving into building, you can interact with the live chat agent app below.
Try these example questions:
* What tokens does vitalik.eth have?
* Show me the NFTs in wallet `0xd8da6bf26964af9d7eed9e03e53415d37aa96045`
* What's the price of USDC?
* Get token balances for `DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK` on Solana
## Project Setup
Let's start by creating the project structure and installing dependencies.
Open your terminal and create a new directory:
```bash
mkdir simchat
cd simchat
```
Initialize a new Node.js project:
```bash
npm init -y
npm pkg set type="module"
```
Install the required packages:
```bash
npm install express openai dotenv
```
These packages provide:
* **express**: Web server framework
* **openai**: Official OpenAI client library
* **dotenv**: Environment variable management
Create a `.env` file in your project root:
```bash
touch .env
```
Add your API keys:
```plaintext .env
# Required API keys
OPENAI_API_KEY=your_openai_api_key_here
SIM_API_KEY=your_sim_api_key_here
```
Never commit your `.env` file to version control. Add it to `.gitignore` to keep your API keys secure.
Create the main app files:
```bash
touch server.js
touch chat.html
```
The `server.js` file will handle our backend Express server and API logic, while `chat.html` contains our frontend chat interface.
Populate the `server.js` with this basic Express code:
```javascript [expandable]
import express from 'express';
import { OpenAI } from 'openai';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();
// Set up __dirname for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Initialize Express
const app = express();
app.use(express.json());
// Initialize OpenAI client
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
// Sim API key
const SIM_API_KEY = process.env.SIM_API_KEY;
// Serve the HTML file
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'chat.html'));
});
// Start server
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
```
Add the initial frontend template to `chat.html`:
```html [expandable]
Sim APIs Chat
```
Your project structure should now look like:
```
simchat/
├── server.js # Express server with OpenAI integration
├── chat.html # Chat interface
├── package.json # Project configuration
├── .env # API keys (keep private)
└── node_modules/ # Dependencies
```
Run `node server.js` in the terminal to start the server.
Visit `http://localhost:3001` to see the newly scaffolded chat app.

If you try to send a message at this point, you'll get a server error since we haven't implemented the back-end functionality yet.
If you encounter errors, make sure your `.env` file contains the correct `OPENAI_API_KEY` and `SIM_API_KEY`.
Check your terminal for any error messages from `server.js`.
## Add OpenAI LLM Chat
Now let's add the core chat functionality to our Express server using OpenAI's GPT-4o-mini.
We'll start by defining a system prompt that instructs the LLM on its role and capabilities.
Add this `SYSTEM_PROMPT` variable to your `server.js` file:
```javascript (server.js)
// System prompt that instructs the AI assistant
const SYSTEM_PROMPT = `You are a helpful assistant that can answer questions about blockchain data using Dune's Sim APIs. You have access to various functions that can fetch realtime blockchain data including:
- Token balances for wallets across 60+ EVM chains
- Transaction activity and history
- NFT collections and collectibles
- Token metadata and pricing information
- Token holder distributions
- Supported blockchain networks
When users ask about blockchain data, wallet information, token details, or transaction history, use the appropriate functions to fetch realtime data. Always provide clear, helpful explanations of the data you retrieve.
Keep your responses concise and focused. When presenting large datasets, summarize the key findings rather than listing every detail.`;
```
This system prompt sets the context for the LLM, explaining its capabilities and how it should behave when interacting with users.
Now let's implement the basic chat endpoint with Express.js that uses this system prompt.
The `/chat` endpoint will receive POST requests from our frontend chat interface, process them through the LLM, and return responses to display in the chat:
```javascript
// Basic chat endpoint
app.post('/chat', async (req, res) => {
try {
const { message } = req.body;
if (!message) return res.status(400).json({ error: 'Message is required' });
// Create conversation with system prompt
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: message }
];
// Call OpenAI
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: messages,
max_tokens: 2048
});
const assistantMessage = response.choices[0].message.content;
res.json({
message: assistantMessage
});
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({
error: 'An error occurred while processing your request',
details: error.message
});
}
});
```
Run `node server.js` again and visit `http://localhost:3001`.
You'll have a working chat interface powered by OpenAI's `gpt-4o-mini` model with a custom system prompt, but it won't be able to fetch realtime blockchain data yet.

## Define OpenAI Functions
To make our chatbot fetch realtime blockchain data, we need to use OpenAI's [function calling](https://platform.openai.com/docs/guides/function-calling?api-mode=responses) feature.
When the model determines it needs external data, it will call one of these functions with appropriate parameters, and we can then execute the actual API call and provide the results back to the model.
Add this `functions` array to your `server.js` file:
```javascript [expandable]
// Function definitions for OpenAI function calling
const functions = [
{
type: "function",
function: {
name: "get_token_balances",
description: "Get realtime token balances for an EVM wallet address across multiple chains. Returns native and ERC20 token balances with USD values.",
parameters: {
type: "object",
properties: {
address: {
type: "string",
description: "The wallet address to get balances for (e.g., 0xd8da6bf26964af9d7eed9e03e53415d37aa96045)"
},
exclude_spam_tokens: {
type: "boolean",
description: "Whether to exclude spam tokens from results",
default: true
}
},
required: ["address"],
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_wallet_activity",
description: "Get chronologically ordered transaction activity for an EVM wallet including transfers, contract interactions, and decoded function calls.",
parameters: {
type: "object",
properties: {
address: {
type: "string",
description: "The wallet address to get activity for"
},
limit: {
type: "number",
description: "Maximum number of activities to return (default: 25)",
default: 25
}
},
required: ["address"],
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_nft_collectibles",
description: "Get NFT collectibles (ERC721 and ERC1155) owned by an EVM wallet address.",
parameters: {
type: "object",
properties: {
address: {
type: "string",
description: "The wallet address to get NFTs for"
},
limit: {
type: "number",
description: "Maximum number of collectibles to return (default: 50)",
default: 50
}
},
required: ["address"],
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_token_info",
description: "Get detailed metadata and pricing information for a specific token on EVM chains.",
parameters: {
type: "object",
properties: {
token_address: {
type: "string",
description: "The token contract address or 'native' for native tokens"
},
chain_ids: {
type: "string",
description: "Chain IDs to search on (e.g., '1,137,8453' or 'all')",
default: "all"
}
},
required: ["token_address"],
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_token_holders",
description: "Get token holders for a specific ERC20 or ERC721 token, ranked by wallet value.",
parameters: {
type: "object",
properties: {
chain_id: {
type: "number",
description: "The chain ID where the token exists (e.g., 1 for Ethereum)"
},
token_address: {
type: "string",
description: "The token contract address"
},
limit: {
type: "number",
description: "Maximum number of holders to return (default: 100)",
default: 100
}
},
required: ["chain_id", "token_address"],
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_transactions",
description: "Get detailed transaction information for an EVM wallet address.",
parameters: {
type: "object",
properties: {
address: {
type: "string",
description: "The wallet address to get transactions for"
},
limit: {
type: "number",
description: "Maximum number of transactions to return (default: 25)",
default: 25
}
},
required: ["address"],
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_supported_chains",
description: "Get list of all supported EVM chains and their capabilities.",
parameters: {
type: "object",
properties: {},
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_svm_token_balances",
description: "Get token balances for a Solana (SVM) address. Returns native and SPL token balances with USD values.",
parameters: {
type: "object",
properties: {
address: {
type: "string",
description: "The Solana wallet address to get balances for (e.g., DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK)"
},
limit: {
type: "number",
description: "Maximum number of balances to return (default: 100)",
default: 100
},
chains: {
type: "string",
description: "Comma-separated list of chains to include, or 'all' for all supported chains",
default: "all"
}
},
required: ["address"],
additionalProperties: false
}
}
},
{
type: "function",
function: {
name: "get_svm_token_metadata",
description: "Get metadata for a Solana token mint address.",
parameters: {
type: "object",
properties: {
mint: {
type: "string",
description: "The Solana token mint address (e.g., So11111111111111111111111111111111111111112)"
}
},
required: ["mint"],
additionalProperties: false
}
}
}
];
```
Each function corresponds to a different Sim API endpoint that we'll implement next.
## Integrate Sim APIs
Now we need to connect OpenAI's function calls to actual Sim API requests. When the model calls a function like `get_token_balances`, we need to:
1. Map that OpenAI function call to the correct Sim API endpoint
2. Make the HTTP request with proper authentication
3. Return the data back to the model
We'll implement this with three components: a generic API caller, endpoint configurations, and an execution function that ties them together.
### Build the Generic API Caller
First, let's create a reusable function that handles all HTTP requests to Sim APIs:
```javascript
// Generic API call function for Sim APIs
async function apiCall(endpoint, params = {}) {
try {
const queryString = Object.keys(params).length
? '?' + new URLSearchParams(params).toString()
: '';
const response = await fetch(`https://api.sim.dune.com${endpoint}${queryString}`, {
headers: {
'X-Sim-Api-Key': SIM_API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
return await response.json();
} catch (error) {
return { error: error.message };
}
}
```
This function handles all the common functionality needed for Sim API requests:
* URL construction with query parameters
* Authentication headers
* Error handling
* JSON parsing
By centralizing this logic, we avoid code duplication and ensure consistent error handling across all API calls.
### Configure API Endpoints
Next, we'll create a more comprehensive configuration object that handles all the different parameter patterns used by Sim APIs:
```javascript
// API endpoint configurations
const API_CONFIGS = {
get_token_balances: (address, exclude_spam_tokens = true) => {
const queryParams = new URLSearchParams({ metadata: 'url,logo' });
if (exclude_spam_tokens) queryParams.append('exclude_spam_tokens', 'true');
return [`/v1/evm/balances/${address}`, queryParams];
},
get_wallet_activity: (address, limit = 25) =>
[`/v1/evm/activity/${address}`, { limit: Math.min(limit, 10) }],
get_nft_collectibles: (address, limit = 50) =>
[`/v1/evm/collectibles/${address}`, { limit: Math.min(limit, 10) }],
get_token_info: (token_address, chain_ids = 'all') =>
[`/v1/evm/token-info/${token_address}`, { chain_ids }],
get_token_holders: (chain_id, token_address, limit = 100) =>
[`/v1/evm/token-holders/${chain_id}/${token_address}`, { limit: Math.min(limit, 10) }],
get_transactions: (address, limit = 25) =>
[`/v1/evm/transactions/${address}`, { limit: Math.min(limit, 10) }],
get_supported_chains: () =>
['/v1/evm/supported-chains', {}],
get_svm_token_balances: (address, limit = 100, chains = 'all') => {
const queryParams = new URLSearchParams();
if (chains) queryParams.append('chains', chains);
if (limit) queryParams.append('limit', Math.min(limit, 20));
return [`/beta/svm/balances/${address}`, queryParams];
},
get_svm_token_metadata: (mint) =>
[`/beta/svm/token-metadata/${mint}`, {}]
};
```
This configuration handles all the different patterns of Sim APIs: simple objects for basic query parameters, `URLSearchParams` for complex query strings, multiple path parameters, and endpoints with no parameters.
### Execute Function Calls
Now we need an enhanced `callFunction` that can handle both regular objects and `URLSearchParams`:
```javascript
// Function to execute API calls based on function name
async function callFunction(name, args) {
if (!API_CONFIGS[name]) return JSON.stringify({ error: `Unknown function: ${name}` });
const [endpoint, params] = API_CONFIGS[name](...Object.values(args));
const result = await apiCall(endpoint, params);
return JSON.stringify(result);
}
```
This approach maintains the streamlined `API_CONFIGS` pattern while properly handling all the different parameter types and patterns used by the various Sim API endpoints.
The `apiCall` function can handle both `URLSearchParams` objects (for complex queries) and regular objects (for simple query parameters).
### Update the Chat Endpoint
Finally, we need to update our chat endpoint to handle function calls.
Replace your existing `/chat` endpoint with this version that includes function calling support:
```javascript [expandable]
// Enhanced chat endpoint with function calling
app.post('/chat', async (req, res) => {
try {
const { message } = req.body;
if (!message) return res.status(400).json({ error: 'Message is required' });
// Create conversation with system prompt
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: message }
];
// Call OpenAI with function definitions
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: messages,
tools: functions,
tool_choice: "auto",
max_tokens: 2048
});
let assistantMessage = response.choices[0].message;
// Handle function calls if present
if (assistantMessage.tool_calls) {
messages.push(assistantMessage);
// Execute each function call
for (const toolCall of assistantMessage.tool_calls) {
const functionResult = await callFunction(
toolCall.function.name,
JSON.parse(toolCall.function.arguments)
);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: functionResult
});
}
// Get final response with function results
const finalResponse = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: messages,
tools: functions,
tool_choice: "auto",
max_tokens: 2048
});
assistantMessage = finalResponse.choices[0].message;
}
res.json({
message: assistantMessage.content,
function_calls: assistantMessage.tool_calls || []
});
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({
error: 'An error occurred while processing your request',
details: error.message
});
}
});
```
This enhanced endpoint now supports the full function calling workflow: it sends the user's message to OpenAI with the available functions, executes any function calls that the model makes, and then sends the function results back to get the final conversational response.
Restart your server and test the function calling functionality. Try asking questions like *What tokens does vitalik.eth have?* and watch as your chat agent fetches realtime data from Sim APIs to provide accurate, up-to-date responses.
## Conclusion
You've successfully built a realtime chat agent that makes blockchain data accessible through natural conversation.
By combining OpenAI's LLMs with Sim APIs' comprehensive blockchain data, you've created a tool that can instantly fetch and explain complex onchain information across 60+ EVM chains and Solana.
This foundation provides everything you need to build your own specialized blockchain chat assistants.
Consider extending it for specific use cases like:
* **NFT Discovery Bot**: Integrate marketplace data, rarity rankings, and collection insights
* **Portfolio Manager**: Include transaction categorization, P\&L tracking, and tax reporting features
* **Trading Assistant**: Add price alerts, technical indicators, and market sentiment analysis
The [complete source code on GitHub](https://github.com/duneanalytics/sim-guides/tree/main/simchat) includes additional features like full session management and enhanced error handling that weren't covered in this guide
Explore the repository to see the additional features in action.
# Build a Realtime Wallet
Source: https://docs.sim.dune.com/evm/build-a-realtime-wallet
Create a multichain wallet that displays realtime balances, transactions, and NFTs using Sim APIs and Express.js
This is the first guide in our series on building a realtime, multichain wallet using Sim APIs.
In this one, we will lay the foundation for our wallet.
You will set up a Node project with Express.js, fetch and display token balances from 60+ EVM chains using the Balances API, and calculate the wallet's total portfolio value in USD.
Access the complete source code for this wallet on GitHub
Interact with the finished wallet app
## Prerequisites
Before we begin, make sure you have:
* Node.js >= 18.0.0
* A Sim API key
Learn how to obtain your API key to properly authenticate requests.
## Features
By the end of this series, our wallet will have four main features:
1. **Token Balances**: Realtime balance tracking with USD values using the [Balances API](/evm/balances).
2. **Total Portfolio Value**: Aggregated USD value across all chains.
3. **Wallet Activity**: Comprehensive transaction history showing transfers and contract interactions using the [Activity API](/evm/activity)
4. **NFTs**: A display of owned NFTs using the [Collectibles API](/evm/collectibles)
In this first guide, we will focus on implementing the first two: **Token Balances** and **Total Portfolio Value**.
## Try the Live Demo
Before diving into building, you can interact with the finished wallet app below.
Enter any Ethereum wallet address to explore its token balances, transaction history, and NFT collection across multiple chains.
## Project Setup
Let's start by scaffolding our project.
This initial setup will provide a basic Express.js server and frontend templates, allowing us to focus on integrating Sim APIs.
Open your terminal and create a new directory for the project:
```bash
mkdir wallet-ui
cd wallet-ui
```
Now you are in the `wallet-ui` directory.
Next, initialize a new Node.js project with npm:
```bash
npm init -y
npm pkg set type="module"
```
These commands create a `package.json` file with default values and configure it to use ES modules.
Afterward, install the required packages:
```bash
npm install express ejs dotenv numbro
```
We are using three packages for our wallet:
* **Express.js**: A popular Node.js web framework for creating our server.
* **EJS**: A simple templating engine that lets us generate dynamic HTML.
* **dotenv**: A package to load environment variables from a `.env` file.
* **numbro**: For formatting numbers and currency.
Create a new `.env` file in your project root:
```bash
touch .env
```
Open the `.env` file in your code editor and add your Sim API key:
```plaintext .env
# Your Sim API key (required)
SIM_API_KEY=your_api_key_here
```
Never commit your `.env` file to version control. If you are using Git, add `.env` to your `.gitignore` file.
Create the necessary directories for views and public assets:
```bash
mkdir views
mkdir public
```
`views` will hold our EJS templates, and `public` will serve static assets like CSS.
Now, create the core files:
```bash
touch server.js
touch views/wallet.ejs
touch public/styles.css
```
Populate `server.js` with this basic Express server code:
```javascript server.js [expandable]
import express from 'express';
import numbro from 'numbro';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
// Load environment variables
dotenv.config();
const SIM_API_KEY = process.env.SIM_API_KEY;
if (!SIM_API_KEY) {
console.error("FATAL ERROR: SIM_API_KEY is not set in your environment variables.");
process.exit(1);
}
// Set up __dirname for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Initialize Express
const app = express();
// Configure Express settings
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));
// Add our home route
app.get('/', async (req, res) => {
const {
walletAddress = '',
tab = 'tokens' // Default to tokens tab
} = req.query;
let tokens = [];
let totalWalletUSDValue = 0;
let activities = []; // For Guide 2
let collectibles = []; // For Guide 3
// In later steps, these arrays will be populated with API data.
res.render('wallet', {
walletAddress: walletAddress,
currentTab: tab,
totalWalletUSDValue: '$0.00', // Will be calculated later in this guide.
tokens: tokens,
activities: activities,
collectibles: collectibles
});
});
// Start the server
app.listen(3001, () => {
console.log(`Server running at http://localhost:3001`);
});
```
Add the initial frontend template to `views/wallet.ejs`:
```ejs views/wallet.ejs [expandable]
Sim APIs Wallet UI
Enter a wallet address above to see token balances.
<% } %>
Activity feature will be added in the next guide.
Collectibles feature will be added in a future guide.
```
Add basic styles to `public/styles.css`:
```css public/styles.css [expandable]
:root {
--font-primary: 'IBM Plex Sans', sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
--color-bg-deep: #e1e2f9;
--color-bg-container: #141829;
--color-border-primary: #2c3040;
--color-border-secondary: #222636;
--color-text-primary: #ffffff;
--color-text-secondary: #e0e0e0;
--color-text-muted: #a0a3b1;
--color-text-subtle: #808391;
--color-accent-green: #50e3c2;
--color-accent-purple: #7e87ef;
--color-accent-red: #ff7875;
--color-placeholder-bg: #3a3f58;
}
body {
font-family: var(--font-primary);
background-color: var(--color-bg-deep);
color: var(--color-text-secondary);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding-top: 0;
padding-bottom: 0;
height: 100vh;
}
.mobile-container {
width: 100%;
max-width: 420px;
height: 90vh;
max-height: 800px;
min-height: 600px;
background-color: var(--color-bg-container);
border-radius: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
align-self: center;
box-shadow: 0 8px 32px rgba(20, 24, 41, 0.18), 0 1.5px 6px rgba(20, 24, 41, 0.10);
}
/* Header Styles */
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid var(--color-border-primary);
flex-shrink: 0;
}
.profile-pic-placeholder {
width: 36px;
height: 36px;
background-color: var(--color-placeholder-bg);
border-radius: 50%;
}
.header-title {
font-family: var(--font-primary);
font-size: 1.4em;
font-weight: 600; /* IBM Plex Sans SemiBold */
color: var(--color-text-primary);
}
.settings-icon {
width: 22px;
height: 22px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23e0e0e0'%3E%3Cpath d='M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12-.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: contain;
cursor: pointer;
opacity: 0.8;
}
/* Total Balance Section */
.total-balance-section {
padding: 25px 20px;
text-align: center;
border-bottom: 1px solid var(--color-border-primary);
flex-shrink: 0;
}
.total-balance-amount {
font-family: var(--font-mono); /* Mono for large number */
font-size: 2.3em;
font-weight: 700;
margin: 0;
color: var(--color-accent-green);
}
.total-balance-label {
font-family: var(--font-primary);
font-size: 0.85em;
color: var(--color-text-muted);
margin-top: 2px;
cursor: pointer; /* Make it look clickable */
}
.total-balance-label:hover {
color: var(--color-text-primary);
}
/* Tabs Section */
.tabs {
display: flex;
border-bottom: 1px solid var(--color-border-primary);
flex-shrink: 0;
position: relative;
z-index: 1;
}
.tab-button {
font-family: var(--font-primary);
flex-grow: 1;
padding: 14px;
text-align: center;
cursor: pointer;
background-color: transparent;
border: none;
color: var(--color-text-muted);
font-size: 0.95em;
font-weight: 500; /* IBM Plex Sans Medium */
border-bottom: 3px solid transparent;
transition: color 0.2s ease, border-bottom-color 0.2s ease;
}
.tab-button:hover {
color: var(--color-text-primary);
}
.tab-button.active {
color: var(--color-text-primary);
border-bottom: 3px solid var(--color-accent-purple);
}
.tab-content {
padding: 0px 20px 20px 20px;
flex-grow: 1;
min-height: 0;
max-height: calc(100vh - 220px);
overflow-y: auto;
}
.tab-pane { display: none; }
.tab-pane.active { display: block; }
/* Tokens Tab & Activity Tab Styles */
.list-item {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid var(--color-border-secondary);
}
.list-item:last-child { border-bottom: none; }
.item-icon-placeholder {
width: 38px;
height: 38px;
background-color: #2c3040; /* Using a specific color, not var for contrast */
border-radius: 50%;
margin-right: 15px;
display: flex;
justify-content: center;
align-items: center;
font-family: var(--font-mono); /* Mono for symbols like ETH */
font-weight: 500;
font-size: 0.9em;
color: #c0c3d1; /* Using specific color */
flex-shrink: 0;
overflow: hidden; /* Prevents text overflow if symbol is too long */
}
.item-info {
flex-grow: 1;
min-width: 0; /* Prevents text overflow issues in flex children */
}
.item-name { /* Token Name, Activity Type like "Received", "Sent" */
font-family: var(--font-primary);
font-size: 1.05em;
font-weight: 500; /* IBM Plex Sans Medium */
margin: 0 0 3px 0;
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-sub-info { /* "1.2345 ETH on Ethereum", "Price: $800.00" */
font-family: var(--font-primary); /* Sans for descriptive part */
font-size: 0.85em;
color: var(--color-text-muted);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-sub-info .mono { /* Span class for monospaced parts within sub-info */
font-family: var(--font-mono);
}
.item-value-right {
text-align: right;
flex-shrink: 0;
padding-left: 10px;
}
.item-usd-value { /* USD value of holding */
font-family: var(--font-mono); /* Mono for numerical USD value */
font-size: 1.05em;
font-weight: 500;
margin: 0 0 3px 0;
color: var(--color-text-primary);
}
/* Activity Tab Specifics */
.activity-direction { /* "Received ETH", "Sent USDC" */
font-family: var(--font-primary);
font-size: 1.05em;
font-weight: 500; /* IBM Plex Sans Medium */
margin: 0 0 3px 0;
}
.activity-direction.sent { color: var(--color-accent-red); }
.activity-direction.receive { color: var(--color-accent-green); } /* Ensure class name consistency with JS */
.activity-address, .activity-timestamp {
font-family: var(--font-primary); /* Sans for "From:", "To:" */
font-size: 0.8em;
color: var(--color-text-subtle);
margin: 0;
}
.activity-address .mono, .activity-timestamp .mono { /* For address itself and timestamp value */
font-family: var(--font-mono);
}
.activity-amount-right { /* "+0.5 ETH" */
font-family: var(--font-mono); /* Mono for amount + symbol */
font-size: 1.05em;
font-weight: 500;
}
.activity-amount-right.sent { color: var(--color-accent-red); }
.activity-amount-right.receive { color: var(--color-accent-green); }
/* NFT Grid Container */
.collectibles-grid {
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 1rem;
padding-top: 20px;
width: 100%;
}
/* NFT Item Container */
.collectible-item-link {
text-decoration: none;
color: inherit;
display: block;
transition: transform 0.2s ease;
}
.collectible-item-link:hover {
transform: translateY(-2px);
}
.collectible-item {
position: relative;
border-radius: 12px;
overflow: hidden;
background: var(--color-bg-container);
border: 1px solid var(--color-border-secondary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Image Container */
.collectible-image-container {
position: relative;
width: 100%;
padding-bottom: 100%; /* Creates a square aspect ratio */
background: var(--color-placeholder-bg);
}
.collectible-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Image Placeholder */
.collectible-image-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-family: var(--font-primary);
font-weight: 600;
font-size: 0.875rem;
}
/* NFT Info Container - Static (always visible) */
.collectible-info-static {
padding: 0.75rem;
background: var(--color-bg-container);
border-top: 1px solid var(--color-border-secondary);
}
/* NFT Info Container - Hover (keep for backwards compatibility but not used) */
.collectible-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0.75rem;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.85));
color: white;
opacity: 0;
transition: opacity 0.3s ease;
}
.collectible-item-link:hover .collectible-info {
opacity: 1;
}
/* NFT Text Styles */
.collectible-name {
font-family: var(--font-primary);
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--color-text-primary);
}
.collectible-collection {
font-family: var(--font-mono);
font-size: 0.8rem;
margin-bottom: 0;
opacity: 0.9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--color-text-muted);
}
.collectible-chain {
font-family: var(--font-primary);
font-size: 0.7rem;
opacity: 0.8;
color: white;
}
```
Run `ls` in your terminal. Your project directory `wallet-ui/` should now contain:
```bash
wallet-ui/
├── server.js # Main application file with Express server
├── views/ # Directory for EJS templates
│ └── wallet.ejs # Main wallet UI template
├── public/ # Directory for static assets
│ └── styles.css # CSS styling for the wallet UI
├── package.json # Project configuration
├── package-lock.json # Dependency lock file (if `npm install` was run)
├── node_modules/ # Installed packages (if `npm install` was run)
└── .env # Your environment variables
```
Run `node server.js` in the terminal to start the server.
Visit `http://localhost:3001` to see the blank wallet.

If you encounter errors, ensure your `.env` file contains the correct `SIM_API_KEY` and that it is loaded correctly.
Also, verify the `walletAddress` in the URL is a valid EVM wallet address.
Check your terminal for any error messages from `server.js`.
Now, let's integrate the Sim API to fetch real data.
## Fetch and Show Token Balances
We will use the [Balances API](/evm/balances) to get realtime token balances for a given wallet address.
This endpoint provides comprehensive details about native and ERC20 tokens, including metadata and USD values across more than 60+ EVM chains.
First, let's create an async function in `server.js` to fetch these balances. Add this function before your `app.get('/')` route handler:
```javascript server.js (getWalletBalances) {7,9,14,28}
async function getWalletBalances(walletAddress) {
if (!walletAddress) return []; // Return empty if no address
// Construct the query parameters
// metadata=url,logo fetches token URLs and logo images
// exclude_spam_tokens filters out known spam tokens
const queryParams = `metadata=url,logo&exclude_spam_tokens`;
const url = `https://api.sim.dune.com/v1/evm/balances/${walletAddress}?${queryParams}`;
try {
const response = await fetch(url, {
headers: {
'X-Sim-Api-Key': SIM_API_KEY, // Your API key from .env
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`API request failed with status ${response.status}: ${response.statusText}`, errorBody);
throw new Error(`API request failed: ${response.statusText}`);
}
const data = await response.json();
// The API returns JSON with a "balances" key. We return that directly.
return data.balances;
} catch (error) {
console.error("Error fetching wallet balances:", error.message);
return []; // Return empty array on error
}
}
```
This function creates the API request using Node's `fetch`.
It includes your `SIM_API_KEY` in the headers and sends a GET request to the `/v1/evm/balances/{address}` endpoint.
The Balances API gives you access to various [*URL query parameters*](/evm/balances#parameter-uri) that you can include to modify the response.
We have included `metadata=url,logo` to include a token's URL and logo.
There's also `exclude_spam_tokens` to filter out common spam tokens.
The `exclude_spam_tokens` parameter provides a good baseline to filter out low-value and potentially malicious tokens, but you may want to implement custom filtering for your use case.
Learn more in our [Spam Filtering guide](/spam-filtering).
Next, modify your `app.get('/')` route handler in `server.js` to call `getWalletBalances` and pass the fetched tokens to your template:
```javascript server.js {13}
app.get('/', async (req, res) => {
const {
walletAddress = '',
tab = 'tokens'
} = req.query;
let tokens = [];
let totalWalletUSDValue = 0;
let errorMessage = null;
if (walletAddress) {
try {
tokens = await getWalletBalances(walletAddress);
} catch (error) {
console.error("Error in route handler:", error);
errorMessage = "Failed to fetch wallet data. Please try again.";
// tokens will remain empty, totalWalletUSDValue will be 0
}
}
res.render('wallet', {
walletAddress: walletAddress,
currentTab: tab,
totalWalletUSDValue: `$0.00`, // We'll calculate this in the next section
tokens: tokens,
activities: [], // Placeholder for Guide 2
collectibles: [], // Placeholder for Guide 3
errorMessage: errorMessage
});
});
```
We've updated the route to:
1. Call `getWalletBalances` if a `walletAddress` is provided.
2. Pass the retrieved `balances` to the `wallet.ejs` template.
The `views/wallet.ejs` file you created earlier is already set up to display these tokens.
Restart your server with `node server.js` and refresh your browser, providing a `walletAddress` in the URL.
For example: [`http://localhost:3001/?walletAddress=0xd8da6bf26964af9d7eed9e03e53415d37aa96045`](http://localhost:3001/?walletAddress=0xd8da6bf26964af9d7eed9e03e53415d37aa96045)
You should now see the wallet populated with token balances, logos, prices for each token, and how much of that token the wallet holds.
## Format Balances
The Balances API returns token amounts in their smallest denomination. This will be in wei for ETH-like tokens.
To display these amounts in a user-friendly way, like `1.23` ETH instead of `1230000000000000000` wei, we need to adjust the amount using the token's `decimals` property, which is also returned from the Balances API.
We can add a new property, `balanceFormatted`, to each token object.
Modify your `getWalletBalances` function in `server.js` as follows. The main change is mapping over `data.balances` to add the `balanceFormatted` property:
```javascript server.js (getWalletBalances with formatting) {24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38}
async function getWalletBalances(walletAddress) {
if (!walletAddress) return [];
const queryParams = `metadata=url,logo&exclude_spam_tokens`;
const url = `https://api.sim.dune.com/v1/evm/balances/${walletAddress}?${queryParams}`;
try {
const response = await fetch(url, {
headers: {
'X-Sim-Api-Key': SIM_API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`API request failed with status ${response.status}: ${response.statusText}`, errorBody);
throw new Error(`API request failed: ${response.statusText}`);
}
const data = await response.json();
// Return formatted values and amounts
return (data.balances || []).map(token => {
// 1. Calculate human-readable token amount
const numericAmount = parseFloat(token.amount) / Math.pow(10, parseInt(token.decimals));
// 2. Get numeric USD value
const numericValueUSD = parseFloat(token.value_usd);
// 3. Format using numbro
const valueUSDFormatted = numbro(numericValueUSD).format('$0,0.00');
const amountFormatted = numbro(numericAmount).format('0,0.[00]A');
return {
...token,
valueUSDFormatted,
amountFormatted
};
}).filter(token => token.symbol !== 'RTFKT'); // Removing Spam Tokens. Add more if you like.
} catch (error) {
console.error("Error fetching wallet balances:", error.message);
return [];
}
}
```
Now, each token object returned by `getWalletBalances` will include a `balanceFormatted` string, which our EJS template (`views/wallet.ejs`) already uses: `<%= token.balanceFormatted || token.amount %>`.
Restart the server and refresh the browser. You will now see formatted balances.
## Calculate Total Portfolio Value
The wallet's total value at the top of the UI still says `$0.00`.
Let's calculate the total USD value of the wallet and properly show it.
The Balances API provides a `value_usd` field with each token.
This field represents the total U.S. dollar value of the wallet's entire holding for that specific token.
Let's modify the `app.get('/')` route handler to iterate through the fetched tokens and sum their individual `value_usd` to calculate the `totalWalletUSDValue`.
```javascript server.js (app.get('/') with total value calculation) {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 37}
app.get('/', async (req, res) => {
const {
walletAddress = '',
tab = 'tokens'
} = req.query;
let tokens = [];
let totalWalletUSDValue = 0; // Will be updated
let errorMessage = null;
if (walletAddress) {
try {
tokens = await getWalletBalances(walletAddress);
// Calculate the total USD value from the fetched tokens
if (tokens && tokens.length > 0) {
tokens.forEach(token => {
let individualValue = parseFloat(token.value_usd);
if (!isNaN(individualValue)) {
totalWalletUSDValue += individualValue;
}
});
}
totalWalletUSDValue = numbro(totalWalletUSDValue).format('$0,0.00');
} catch (error) {
console.error("Error in route handler:", error);
errorMessage = "Failed to fetch wallet data. Please try again.";
// tokens will remain empty, totalWalletUSDValue will be 0
}
}
res.render('wallet', {
walletAddress: walletAddress,
currentTab: tab,
totalWalletUSDValue: totalWalletUSDValue, // Pass the calculated total
tokens: tokens,
activities: [], // Placeholder for Guide 2
collectibles: [], // Placeholder for Guide 3
errorMessage: errorMessage
});
});
```
We use the `reduce` method to iterate over the `tokens` array.
For each `token`, we access its `value_usd` property, parse it as a float, and add it to the running `sum`.
The calculated `totalWalletUSDValue` is then formatted to two decimal places and passed to the template.
The `views/wallet.ejs` template already has `
<%= totalWalletUSDValue %>
`, so it will display the calculated total correctly.
Restart your server and refresh the browser page with a wallet address.
You should now see the total wallet value at the top of the UI accurately reflecting the sum of all token balance USD values.
## Conclusion
You have successfully set up the basic structure of your multichain wallet and integrated Sim APIs `Balances API` endpoint to display realtime token balances and total portfolio value.
In the next guide, [Add Account Activity](/evm/add-account-activity), we will enhance this wallet by adding a transaction history feature in the UI using the [Activity API](/evm/activity).
# Collectibles
Source: https://docs.sim.dune.com/evm/collectibles
evm/openapi/collectibles.json get /v1/evm/collectibles/{address}
Retrieve EVM compatiable NFTs (ERC721 and ERC1155) that include token identifiers, token standard, chain information, balance, and basic token attributes.
export const SupportedChains = ({endpoint}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.sim.dune.com/v1/evm/supported-chains", {
method: "GET",
headers: {
"X-Sim-Api-Key": "sim_qfIvSWu7c8WBXzfCQ0SeLBGJts204uyT"
}
}).then(response => response.json()).then(setData);
}, []);
if (data === null) {
return null;
}
const chains = endpoint !== undefined ? data.chains.filter(chain => chain[endpoint]?.supported) : data.chains;
console.log("data", data);
return
name
chain_id
tags
{chains.map(chain =>
{chain.name}
{chain.chain_id}
{chain.tags.join(", ")}
)}
;
};

The Collectibles API provides information about NFTs (ERC721 and ERC1155 tokens) owned by a specific address on supported EVM blockchains.
# EVM Overview
Source: https://docs.sim.dune.com/evm/overview
Access realtime token balances. Get comprehensive details about native and ERC20 tokens, including token metadata and USD valuations.
View chronologically ordered transactions including native transfers, ERC20 movements, NFT transfers, and decoded contract interactions.
All NFT (ERC721 and ERC1155) balances, including IDs and metadata
Retrieve granular transaction details including block information, gas data, transaction types, and raw transaction values.
Get detailed metadata and realtime price information for any native asset or ERC20 token including symbol, name, decimals, supply information, USD pricing, and logo URLs.
Discover token distribution across ERC20 or ERC721 holders, ranked by wallet value.
# Show NFT Collectibles in Your Wallet
Source: https://docs.sim.dune.com/evm/show-nfts-collectibles
Complete your realtime crypto wallet by adding a visual display of a user's NFT collection.

Your wallet now displays token balances, calculates total portfolio value, and tracks detailed account activity.
To give users a holistic view of their onchain assets, the final piece is to **showcase their NFT collections**.
In this third and final guide of our wallet series, we will focus on implementing the *Collectibles* tab.
Access the complete source code for this wallet on GitHub
Interact with the finished wallet app
This guide assumes you have completed the previous guides:
1. [Build a Realtime Wallet](/evm/build-a-realtime-wallet)
2. [Add Account Activity](/evm/add-account-activity)
## Explore the NFT Collection
See the collectibles feature in action with the live demo below. Click on the "Collectibles" tab to browse the sample wallet's NFT collection:
## Fetch NFT Collectibles
Let's add a new asynchronous `getWalletCollectibles` function to `server.js` to fetch a user's NFT collection using the [Collectibles API](/evm/collectibles).
```javascript server.js (getWalletCollectibles - Sim API portion) {4}
async function getWalletCollectibles(walletAddress, limit = 50) {
if (!walletAddress) return [];
const url = `https://api.sim.dune.com/v1/evm/collectibles/${walletAddress}?limit=${limit}`;
try {
const response = await fetch(url, {
headers: {
'X-Sim-Api-Key': SIM_API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`Collectibles API request failed with status ${response.status}: ${response.statusText}`, errorBody);
throw new Error(`Collectibles API request failed: ${response.statusText}`);
}
const data = await response.json();
const collectibles = data.entries || [];
// ... (OpenSea enrichment will be added in the next section)
} catch (error) {
console.error("Error fetching wallet collectibles:", error.message);
return [];
}
}
```
The NFT data is extracted from the `entries` array within this response, providing information like contract addresses, token IDs, and chain data.
The [Collectibles API](/evm/collectibles) supports pagination using `limit` and `offset` query parameters.
For wallets with many NFTs, you can implement logic to fetch subsequent pages using the `next_offset` value returned by the API to provide a complete view of the user's collection.
## Fetch NFT Images
Sim APIs provide comprehensive blockchain metadata for NFTs, but we images to create a rich visual experience.
We'll integrate with OpenSea's API to enrich our NFT data with image URLs.
NFT image data and enhanced metadata might be coming soon to the Sim APIs, but for now you can use OpenSea APIs to grab image URLs and provide a visual NFT display for users.
### Get an OpenSea API Key
Before we can fetch NFT images from OpenSea, you'll need to obtain an [OpenSea API key](https://docs.opensea.io/reference/api-keys).
Once you receive your API key, add it to your `.env` file:
```bash .env (Add OpenSea API Key)
SIM_API_KEY=your_sim_api_key_here
OPENSEA_API_KEY=your_opensea_api_key_here
```
### Update getWalletCollectibles
Let's complete the `getWalletCollectibles` function by adding OpenSea API integration to fetch images:
```javascript server.js (getWalletCollectibles - Complete function) {4, 24}
async function getWalletCollectibles(walletAddress, limit = 50) {
if (!walletAddress) return [];
const url = `https://api.sim.dune.com/v1/evm/collectibles/${walletAddress}?limit=${limit}`;
try {
const response = await fetch(url, {
headers: {
'X-Sim-Api-Key': SIM_API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`Collectibles API request failed with status ${response.status}: ${response.statusText}`, errorBody);
throw new Error(`Collectibles API request failed: ${response.statusText}`);
}
const data = await response.json();
const collectibles = data.entries || [];
// Enrich collectibles with OpenSea image data
const enrichedCollectibles = await Promise.all(
collectibles.map(async (collectible) => {
try {
// Use the chain value directly from Sim APIs
if (collectible.chain) {
const openSeaUrl = `https://api.opensea.io/api/v2/chain/${collectible.chain}/contract/${collectible.contract_address}/nfts/${collectible.token_id}`;
const openSeaResponse = await fetch(openSeaUrl, {
headers: {
'Accept': 'application/json',
'x-api-key': process.env.OPENSEA_API_KEY
}
});
if (openSeaResponse.ok) {
const openSeaData = await openSeaResponse.json();
return {
...collectible,
image_url: openSeaData.nft?.image_url || null,
opensea_url: openSeaData.nft?.opensea_url || null,
description: openSeaData.nft?.description || null,
collection_name: openSeaData.nft?.collection || collectible.name
};
}
}
// Return original collectible if OpenSea fetch fails or no chain info
return {
...collectible,
image_url: null,
opensea_url: null,
description: null,
collection_name: collectible.name
};
} catch (error) {
console.error(`Error fetching OpenSea data for ${collectible.chain}:${collectible.contract_address}:${collectible.token_id}:`, error.message);
return {
...collectible,
image_url: null,
opensea_url: null,
description: null,
collection_name: collectible.name
};
}
})
);
// Filter out collectibles without images
return enrichedCollectibles.filter(collectible => collectible.image_url !== null);
} catch (error) {
console.error("Error fetching wallet collectibles:", error.message);
return [];
}
}
```
This enhanced function combines blockchain data from Sim APIs with rich metadata from OpenSea.
For each NFT, we make an additional API call to OpenSea using the chain and contract information provided by Sim APIs.
The function enriches each collectible with `image_url`, `opensea_url`, `description`, and `collection_name` fields, then filters to only return NFTs that have available images for display.
## Add Collectibles into the Server Route
Next, we update our main `app.get('/')` route handler in `server.js` to call this new function:
```javascript server.js (app.get('/') updated for collectibles) {16, 19, 41}
app.get('/', async (req, res) => {
const {
walletAddress = '',
tab = 'tokens'
} = req.query;
let tokens = [];
let activities = [];
let collectibles = []; // Initialize collectibles array
let totalWalletUSDValue = 0;
let errorMessage = null;
if (walletAddress) {
try {
// Fetch balances, activities, and collectibles concurrently for better performance
[tokens, activities, collectibles] = await Promise.all([
getWalletBalances(walletAddress),
getWalletActivity(walletAddress, 25), // Fetching 25 recent activities
getWalletCollectibles(walletAddress, 50) // Fetching up to 50 collectibles
]);
// Calculate total portfolio value from token balances (Guide 1)
if (tokens && tokens.length > 0) {
totalWalletUSDValue = tokens.reduce((sum, token) => {
const value = parseFloat(token.value_usd);
return sum + (isNaN(value) ? 0 : value);
}, 0);
}
} catch (error) {
console.error("Error in route handler fetching all data:", error);
errorMessage = "Failed to fetch wallet data. Please try again.";
}
}
res.render('wallet', {
walletAddress: walletAddress,
currentTab: tab,
totalWalletUSDValue: `$${totalWalletUSDValue.toFixed(2)}`,
tokens: tokens,
activities: activities,
collectibles: collectibles, // Pass collectibles to the template
errorMessage: errorMessage
});
});
```
The route handler now fetches balances, activities, and the enriched NFT collectibles data concurrently for optimal performance.
The `collectibles` array, now containing both blockchain data and image URLs, is passed to the `wallet.ejs` template.
## Display Collectibles in the Frontend
The final step is to modify `views/wallet.ejs` to render the fetched collectibles within the "Collectibles" tab.
We will use a grid layout to display NFT images with their collection names and token IDs.
In `views/wallet.ejs`, find the section for the "Collectibles" tab (you can search for `id="collectibles"`).
It currently contains a placeholder paragraph.
Replace that entire `div` with the following EJS:
```ejs views/wallet.ejs (Collectibles tab content) [expandable]
<% if (collectibles && collectibles.length > 0) { %>
```
The EJS template iterates through the `collectibles` array and displays each NFT with its enriched metadata.
Each collectible shows the `image_url` from OpenSea, the `collection_name` or fallback name, and a truncated `token_id` for identification.
If an `opensea_url` is available, the entire NFT card becomes a clickable link that opens the NFT's OpenSea page in a new tab.
***
Restart your server using `node server.js` and navigate to your wallet app in the browser.
When you click on the "Collectibles" tab, and if the wallet has NFTs, you should see the NFT collection displayed with rich visual metadata.
## Conclusion
That concludes this three-part series!
With just three API requests - [Balances](/evm/balances), [Activity](/evm/activity), and [Collectibles](/evm/collectibles) - enhanced with OpenSea metadata, you've built a fully functional, multichain wallet that displays token balances, calculates portfolio value, tracks detailed transaction activity, and showcases NFT collections with rich visual displays.
**This project serves as a solid foundation for a wallet**.
You can now expand upon it by exploring other Sim API features.
Whether you want to add more sophisticated analytics, deeper NFT insights, or advanced transaction tracking, Sim APIs provides the blockchain data you need to build the next generation of onchain apps.
# Supported Chains
Source: https://docs.sim.dune.com/evm/supported-chains
evm/openapi/supported-chains.json
Explore chains supported by Sim's EVM API endpoints.
export const SupportedChains = ({endpoint}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.sim.dune.com/v1/evm/supported-chains", {
method: "GET",
headers: {
"X-Sim-Api-Key": "sim_qfIvSWu7c8WBXzfCQ0SeLBGJts204uyT"
}
}).then(response => response.json()).then(setData);
}, []);
if (data === null) {
return null;
}
const chains = endpoint !== undefined ? data.chains.filter(chain => chain[endpoint]?.supported) : data.chains;
console.log("data", data);
return
name
chain_id
tags
{chains.map(chain =>
{chain.name}
{chain.chain_id}
{chain.tags.join(", ")}
)}
;
};
Chain support varies by API endpoint.
Use the dropdown sections below to check which chains are available for each API:
# Token Holders
Source: https://docs.sim.dune.com/evm/token-holders
evm/openapi/token-holders.json get /v1/evm/token-holders/{chain_id}/{token_address}
Discover token distribution across ERC20 or ERC721 holders, ranked by wallet value.
export const SupportedChains = ({endpoint}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.sim.dune.com/v1/evm/supported-chains", {
method: "GET",
headers: {
"X-Sim-Api-Key": "sim_qfIvSWu7c8WBXzfCQ0SeLBGJts204uyT"
}
}).then(response => response.json()).then(setData);
}, []);
if (data === null) {
return null;
}
const chains = endpoint !== undefined ? data.chains.filter(chain => chain[endpoint]?.supported) : data.chains;
console.log("data", data);
return
name
chain_id
tags
{chains.map(chain =>
{chain.name}
{chain.chain_id}
{chain.tags.join(", ")}
)}
;
};

The Token Holders API provides information about accounts holding a specific ERC20 or ERC721 token on supported EVM blockchains.
## Pagination
This endpoint uses cursor-based pagination.
You can use the `limit` query parameter to define the maximum page size.
Results might at times be less than the maximum page size.
The `next_offset` value is included in the initial response and can be used to fetch the next page of results by passing it as the `offset` query parameter in the next request.
You can only use the value from `next_offset` to set the `offset` query parameter of the next page of results.
# Token Info
Source: https://docs.sim.dune.com/evm/token-info
evm/openapi/token-info.json get /v1/evm/token-info/{uri}
Get detailed metadata and realtime price information for any native asset or ERC20 token including symbol, name, decimals, supply information, USD pricing, and logo URLs.
export const SupportedChains = ({endpoint}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.sim.dune.com/v1/evm/supported-chains", {
method: "GET",
headers: {
"X-Sim-Api-Key": "sim_qfIvSWu7c8WBXzfCQ0SeLBGJts204uyT"
}
}).then(response => response.json()).then(setData);
}, []);
if (data === null) {
return null;
}
const chains = endpoint !== undefined ? data.chains.filter(chain => chain[endpoint]?.supported) : data.chains;
console.log("data", data);
return
name
chain_id
tags
{chains.map(chain =>
{chain.name}
{chain.chain_id}
{chain.tags.join(", ")}
)}
;
};

The Tokens API provides metadata and realtime pricing information for native and ERC20 tokens on supported EVM blockchains. The API returns:
* Token metadata (symbol, name, decimals)
* Current USD pricing information
* Supply information
* Logo URLs when available
The `?chain_ids` query parameter is mandatory.
To fetch tokens for *all* supported chains, pass the `?chain_ids=all` query parameter.
You can also specify specific chains with `?chain_ids=11,250,1`.
## Token Prices
Sim looks up prices onchain. We use the most liquid onchain pair to determine a usd price. We return the available liquidity in `pool_size` as part of the response.
## Pagination
This endpoint uses cursor-based pagination. You can use the `limit` parameter to define the maximum page size.
Results might at times be less than the maximum page size.
The `next_offset` value is included in the initial response and can be utilized to fetch the next page of results by passing it as the `offset` query parameter in the next request.
You can only use the value from `next_offset` to set the `offset` parameter of the next page of results. Using your own `offset` value will not have any effect.
# Transactions
Source: https://docs.sim.dune.com/evm/transactions
evm/openapi/transactions.json get /v1/evm/transactions/{uri}
Retrieve granular transaction details including block information, gas data, transaction types, and raw transaction values.
export const SupportedChains = ({endpoint}) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.sim.dune.com/v1/evm/supported-chains", {
method: "GET",
headers: {
"X-Sim-Api-Key": "sim_qfIvSWu7c8WBXzfCQ0SeLBGJts204uyT"
}
}).then(response => response.json()).then(setData);
}, []);
if (data === null) {
return null;
}
const chains = endpoint !== undefined ? data.chains.filter(chain => chain[endpoint]?.supported) : data.chains;
console.log("data", data);
return
name
chain_id
tags
{chains.map(chain =>
{chain.name}
{chain.chain_id}
{chain.tags.join(", ")}
)}
;
};

The Transactions API allows for quick and accurate lookup of transactions associated with an address.
Transactions are ordered by descending block time, so the most recent transactions appear first.
## Pagination
This endpoint is using cursor based pagination.
You can use the `limit` parameter to define the maximum page size.
Results might at times be less than the maximum page size.
The `next_offset` value is included in the initial response and can be utilized to fetch the next page of results by passing it as the `offset` query parameter in the next request.
You can only use the value from `next_offset` to set the `offset` parameter of the next page of results. Using your own `offset` value will not have any effect.
# Developer Quickstart
Source: https://docs.sim.dune.com/index
Take your first steps with the Sim APIs
Sim APIs power wallets and onchain apps with fast, reliable access to real-time blockchain activity and ownership data.
Access data from 60+ chains with a single request.
This guide will help you make your first API call to retrieve multichain token balances for an address.
## Authentication
Sim APIs use API keys to authenticate requests.
You can create and manage your API keys in your [Sim Dashboard](https://sim.dune.com/).
To authenticate, include your API key in the `X-Sim-Api-Key` header for every request.
```bash
curl --request GET \
--header "X-Sim-Api-Key: YOUR_API_KEY"
```
All API requests must be made over HTTPS.
Calls made over plain HTTP will fail.
API requests without authentication will also fail.
Your API keys carry many privileges, so be sure to keep them secure.
Do not share your secret API keys in public places like GitHub, client-side code, and so on.
## Your First Request
Let's make your first request. We'll retrieve token balances for `0xd8da6bf26964af9d7eed9e03e53415d37aa96045` (Vitalik's wallet) across multiple EVM chains using the [Balances API](/evm/balances).
Here's how to make the API call:
```bash cURL
curl -X GET "https://api.sim.dune.com/v1/evm/balances/0xd8da6bf26964af9d7eed9e03e53415d37aa96045" \
-H "X-Sim-Api-Key: YOUR_API_KEY"
```
```javascript JavaScript
const options = {method: 'GET', headers: {'X-Sim-Api-Key': 'YOUR_API_KEY'}};
fetch('https://api.sim.dune.com/v1/evm/balances/0xd8da6bf26964af9d7eed9e03e53415d37aa96045', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python
import requests
url = "https://api.sim.dune.com/v1/evm/balances/0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
headers = {"X-Sim-Api-Key": "YOUR_API_KEY"}
response = requests.request("GET", url, headers=headers)
print(response.text)
```
Replace `YOUR_API_KEY` with your actual API key from the Sim Dashboard.
The API will return a JSON response containing an array of `balances`.
Each object in the array represents a token balance for the specified address on one of the chains, including various details.
```json Response (JSON) [expandable]
{
"balances": [
{
"address": "native",
"amount": "605371497350928252303",
"chain": "ethereum",
"decimals": 18,
"price_usd": 3042.816964922323,
"symbol": "ETH",
"value_usd": 1842034.6622198338
}
],
"next_offset": "dKMBWDLqM7vlyn5OMEXsLWp0nI4AAAABA5JLazNO7x4poVGqUwsgxgqvvIg",
"request_time": "2023-11-07T05:31:56Z",
"response_time": "2023-11-07T05:31:56Z",
"wallet_address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
}
```
With a single API request you get normalized, realtime data with enriched metadata and pricing.
## Next Steps
After making your first API call to Sim APIs, you'll either see the JSON response shown above (success!) or you might encounter an error. If you received an error, check out our [Error Handling Guide](/error-handling) for troubleshooting tips and best practices.
If your call was successful, you've seen how easily you can retrieve comprehensive, multichain data. But this is just the beginning of what's possible.
Are you ready to learn more?
Here are a few paths you can explore:
See all available EVM API endpoints. Learn how to fetch transaction histories, token metadata, and more detailed onchain activity.
Follow our practical guides to build fully-functional features like token portfolio displays, real-time activity feeds, and more for your onchain apps.
Speed up your development using Sim APIs with our LLM-friendly resources.
# Cloudflare Proxy
Source: https://docs.sim.dune.com/proxy
Learn how to set up a Cloudflare Worker to securely proxy your Sim API requests, protecting your API key from client-side exposure.
Protect your Sim API key when making requests from client-side apps by using a Cloudflare Worker as a proxy.
This worker receives requests from you app, securely adds your `SIM_API_KEY` on the server, and then forwards the requests to the Sim API endpoints.
We provide a one-click-deploy Cloudflare Worker to simplify this setup.
Find detailed instructions in our GitHub repo:
One-click deployment and comprehensive setup instructions for the Cloudflare Worker proxy.
# Spam Filtering
Source: https://docs.sim.dune.com/spam-filtering
Learn how Sim APIs filter out spam tokens and how to customize filtering for your specific needs.
When working with blockchain data, you'll encounter numerous tokens with little to no value. These "spam tokens" can clutter your application's user interface and potentially mislead users. Sim APIs provide robust, real-time spam filtering capabilities to help you deliver a cleaner, more reliable experience.
## How Sim's Spam Filtering Works
Unlike many providers that rely on static lists or third-party data, Sim uses a dynamic, on-chain approach to identify spam tokens. This results in fewer false positives and negatives, giving you more accurate data.
A token is considered legitimate (not spam) when it meets **all** of the following criteria:
1. **Non-empty name**: The token must have a name property that isn't empty
2. **Acceptable symbol**: The token must have a non-empty symbol that isn't excessively long
3. **Non-zero decimals**: The token must have a decimals property greater than zero
4. **Adequate liquidity** (when applicable): If the token has a liquidity pool, that pool must have more than \$100 in value
Sim's approach to assessing liquidity is particularly sophisticated:
* For each token, we dynamically track highest liquidity route to USDC
* We calculate the USD value of the liquidity along that route for each token upon each query
## Using Spam Filtering in API Calls
Both the [Balances](/evm/balances) and [Activity](/evm/activity) APIs support the `?exclude_spam_tokens` parameter. When included in your API call, this parameter will filter out tokens that don't meet the criteria described above.
## Customizing Spam Filtering
While the `exclude_spam_tokens` parameter provides a good baseline, you may want to implement custom filtering logic for your specific use case. Sim makes this easy by including all the data used for spam filtering in every API response. Scenarios where you might want to do this include:
1. **Different liquidity threshold**: If \$100 is too high or too low for your needs, you can filter based on the `pool_size` field using your own threshold
2. **Allowlisting specific tokens**: You may want to include certain tokens regardless of their liquidity
3. **Blocklisting specific tokens**: You may want to exclude certain tokens even if they meet all criteria
4. **Custom criteria combinations**: You can create your own combination of the available fields to define what constitutes spam in your application
## Applicable APIs
Spam token filtering is currently available for:
* [EVM Balances API](/evm/balances)
* [EVM Activity API](/evm/activity)
## Benefits of Sim's Approach
* **Real-time assessment**: Liquidity is checked at query time, not based on outdated lists
* **Fewer false positives**: Legitimate but lesser-known tokens aren't automatically excluded. New tokens are recognized as legitimate as soon as they're liquid.
* **Fewer false negatives**: New spam tokens are identified immediately based on their characteristics
* **Flexibility**: All filtering data is provided, allowing you to implement custom logic
* **Simplicity**: The `exclude_spam_tokens` parameter provides an easy default option
By leveraging Sim's spam filtering capabilities, you can provide your users with a cleaner, more focused view of blockchain data while maintaining the flexibility to customize the experience as needed.
# Support
Source: https://docs.sim.dune.com/support
## Primary Support: Intercom
Most of our support is provided through **Intercom**, directly accessible from the Sim [web portal](https://sim.dune.com/). Just look for the small chat icon in the bottom-right corner.
Through Intercom, you can:
* Chat with **Blocky**, our Sim AI support agent, powered by Dune
* Get help from the **Dune support team**, either live via chat or asynchronously via email/ticket
We want to help you successfully integrate and use Sim. This page outlines the support channels available to you.
## Developer Community
Join the official Sim [Telegram Group](https://t.me/+nvyoX5xyxNwyNjU0) to connect with other developers, ask questions, and get help from the community.
## API Status
You can check the operational status of Sim APIs and view uptime history on our [status page](https://status.sim.dune.com/).
## Rate Limits & Scaling
Information about API rate limits for your current plan can be found under Billing at [sim.dune.com](https://sim.dune.com). Need higher throughput or interested in Enterprise plans with custom limits? Contact us through Intercom.
## Other Questions
If your question isn’t answered in the docs or you’re reporting a bug, please reach out via the Intercom button in the app.
Alternatively, you can email [simsupport@dune.com](mailto:simsupport@dune.com) to contact our team.
# Balances
Source: https://docs.sim.dune.com/svm/balances
svm/openapi/balances.json get /beta/svm/balances/{uri}
Get token balances for a given SVM address
The Token Balances API provides accurate and fast real time balances of the native, SPL and SPL-2022 tokens of accounts on supported SVM blockchains.
We currently support Solana and Eclipse.
* These endpoints are authenticated with a normal Sim API key.
* Specify `?chains=all` to fetch balances for all supported chains.
* Specify `?chains=solana,eclipse` to fetch balances only for select chains.
* Token metadata includes symbols, decimals, and price information when available.
# Response Structure
The API returns a JSON object with the following top-level fields:
| Field | Description | Type |
| -------------------- | ------------------------------------------------- | ----------- |
| processing\_time\_ms | Time taken to process the request in milliseconds | number |
| wallet\_address | The queried wallet address | string |
| next\_offset | Pagination token for the next page of results | string/null |
| balances\_count | Total number of balances returned | number |
| balances | Array of token balance objects | array |
# Balance Object Fields
Each item in the `balances` array contains the following fields:
| Field | Description | Type |
| --------------- | ---------------------------------------------------------- | ----------- |
| chain | Name of blockchain of token | string |
| address | Token contract address or blockchain name for native token | string |
| amount | Amount of token owned in smallest unit | string |
| balance | Formatted amount with decimals applied | string |
| value\_usd | Current value of token owned, if available | number |
| program\_id | Program ID of the token (for SPL tokens) | string |
| decimals | Decimals of token | number |
| total\_supply | Total supply of the token | string |
| name | Name of token | string |
| symbol | Symbol of token | string |
| uri | URI to token metadata | string |
| price\_usd | Current price of token, if available | number |
| liquidity\_usd | Liquidity in USD, if available | number/null |
| pool\_type | Type of liquidity pool, if available | string/null |
| pool\_address | Address of liquidity pool, if available | string/null |
| mint\_authority | Mint authority address, if available | string/null |
# Pagination
This endpoint is using cursor based pagination. You can use the `limit` parameter to define the maximum page size.
Results might at times be less than the maximum page size.
The `next_offset` value is passed back by the initial response and can be used to fetch the next page of results, by passing it as the `offset` query parameter in the next request.
You can only use the value from `next_offset` to set the `offset` parameter of the next page of results. Using your own `offset` value will not have any effect.
# SVM Overview
Source: https://docs.sim.dune.com/svm/overview
Get accurate and fast realtime balances of native, SPL and SPL-2022 tokens on Solana and Eclipse blockchains, with token metadata and USD valuations.
Quick and accurate lookup of transactions associated with a Solana address, ordered by descending block time, with complete raw transaction data.
# Transactions
Source: https://docs.sim.dune.com/svm/transactions
svm/openapi/transactions.json get /beta/svm/transactions/{uri}
Get transactions for a given SVM address
The Transactions Endpoint allows for quick and accurate lookup of transactions associated with an address.
We currently only support Solana.
# Response Structure
The API returns a JSON object with the following top-level fields:
| Field | Description | Type |
| ------------ | --------------------------------------------- | ----------- |
| next\_offset | Pagination token for the next page of results | string/null |
| transactions | Array of transaction objects | array |
# Transaction Object Fields
Each item in the `transactions` array contains the following fields:
| Field | Description | Type |
| ---------------- | --------------------------------------------------------------- | ------ |
| address | Wallet address | string |
| block\_slot | Block's sequential index | number |
| block\_time | Timestamp of block creation (in microseconds) | number |
| chain | Name of the blockchain | string |
| raw\_transaction | Raw transaction data from the RPC node at the time of ingestion | object |
See [getTransaction RPC Method](https://solana.com/docs/rpc/http/gettransaction) for more details about `raw_transaction`.
# Ordering
The data is ordered by descending block time, so that new transactions will always be delivered first.
# Pagination
This endpoint is using cursor based pagination. You can use the `limit` parameter to define the maximum page size.
Results might at times be less than the maximum page size.
The `next_offset` value is included in the response and can be utilized to fetch the next page of results by passing it as the `offset` query parameter in the next request.
You can only use the value from `next_offset` to set the `offset` parameter of the next page of results. Using your own `offset` value will not have any effect.