Historical Accounts
getHistoricalTokenAccountsByOwner
List the SPL Token and Token-2022 accounts a Solana wallet owned at a past slot or right after a specific transaction, with mint filters and pagination.
POST
/
getHistoricalTokenAccountsByOwner
curl --request POST \
--url 'https://mainnet.helius-rpc.com/?api-key=' \
--header 'Content-Type: application/json' \
--data '
{
"jsonrpc": "2.0",
"id": "1",
"method": "getHistoricalTokenAccountsByOwner",
"params": [
"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
{
"slot": 445000000
},
{
"encoding": "jsonParsed",
"limit": 2
}
]
}
'import requests
url = "https://mainnet.helius-rpc.com/?api-key="
payload = {
"jsonrpc": "2.0",
"id": "1",
"method": "getHistoricalTokenAccountsByOwner",
"params": [
"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
{ "slot": 445000000 },
{
"encoding": "jsonParsed",
"limit": 2
}
]
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
id: '1',
method: 'getHistoricalTokenAccountsByOwner',
params: [
'86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY',
{slot: 445000000},
{encoding: 'jsonParsed', limit: 2}
]
})
};
fetch('https://mainnet.helius-rpc.com/?api-key=', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://mainnet.helius-rpc.com/?api-key=",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'jsonrpc' => '2.0',
'id' => '1',
'method' => 'getHistoricalTokenAccountsByOwner',
'params' => [
'86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY',
[
'slot' => 445000000
],
[
'encoding' => 'jsonParsed',
'limit' => 2
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://mainnet.helius-rpc.com/?api-key="
payload := strings.NewReader("{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"method\": \"getHistoricalTokenAccountsByOwner\",\n \"params\": [\n \"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\",\n {\n \"slot\": 445000000\n },\n {\n \"encoding\": \"jsonParsed\",\n \"limit\": 2\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://mainnet.helius-rpc.com/?api-key=")
.header("Content-Type", "application/json")
.body("{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"method\": \"getHistoricalTokenAccountsByOwner\",\n \"params\": [\n \"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\",\n {\n \"slot\": 445000000\n },\n {\n \"encoding\": \"jsonParsed\",\n \"limit\": 2\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://mainnet.helius-rpc.com/?api-key=")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"method\": \"getHistoricalTokenAccountsByOwner\",\n \"params\": [\n \"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\",\n {\n \"slot\": 445000000\n },\n {\n \"encoding\": \"jsonParsed\",\n \"limit\": 2\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"jsonrpc": "2.0",
"id": "1",
"result": {
"context": {
"slot": 468129571
},
"value": {
"accounts": [
{
"pubkey": "EKyGnXReqmmXyqBHmMjV2CKCLafJS47xyhXXZ5WuFMxD",
"account": {
"lamports": 531589475273,
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"data": [
"<string>"
],
"executable": false,
"rentEpoch": 123,
"space": 82,
"lastUpdatedAt": {
"slot": 444999837,
"transactionIndex": 700
}
}
}
],
"paginationKey": "11112zAgXhc6hGfdfr5anSY91mq7Cs4HHpSVEQc4ASG:EKyGnXReqmmXyqBHmMjV2CKCLafJS47xyhXXZ5WuFMxD"
}
}
}{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32005,
"message": "Too many requests"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Internal error"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32002,
"message": "Service unavailable"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32003,
"message": "Gateway timeout"
},
"id": "1"
}getHistoricalTokenAccountsByOwner is in Beta. Parameters, pricing, response formats, and data coverage may change.Overview
getHistoricalTokenAccountsByOwner returns the SPL Token and Token-2022 accounts a wallet owned at a past slot, or immediately after a specific transaction in that slot. It’s a Helius-exclusive RPC method. Standard getTokenAccountsByOwner only returns current state.
With jsonParsed encoding, token amounts use each mint’s decimals as of the requested position, so balances reflect what the wallet held at that point in time.
History is available from slot 444,959,999 (September 7, 2026) onward, which is the state at the start of epoch 1030.
getHistoricalTokenAccountsByOwner is available on mainnet for Business plans and above, and costs 0 credits per call during the beta period. Pricing is subject to change.
How it differs from getTokenAccountsByOwner
- The mint or program filter is optional. With no filter, you get every token account the wallet owned. To narrow results, set either
mintorprogramIdin the configuration object (the third parameter), not both. - The second parameter is the query object that selects the position in history, not a mint or program filter.
result.valueis an object withaccountsandpaginationKey, not an array.
Choose a position in history
The second parameter is a query object:- Omitted or
{}: the latest indexed slot, capped at the requested commitment. { "slot": 445000000 }: the end of slot 445,000,000.{ "slot": 445000000, "transactionIndex": 10 }: immediately after the transaction at index 10 in that slot executed.
Example request
This request returns the wallet’s USDC token accounts at the end of slot 445,000,000:curl https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "getHistoricalTokenAccountsByOwner",
"params": [
"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
{ "slot": 445000000 },
{ "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "encoding": "jsonParsed" }
]
}'
Paginate through all token accounts
Results are ordered by mint, then by token account address. Each response returnsvalue.accounts and value.paginationKey, a cursor in the form <mint>:<tokenAccount>. Pass it back in the next request until it’s null.
A page can be short, or even empty, and still include a
paginationKey. Only a null paginationKey means you’ve reached the end.slot when you paginate. If you omit it, each page reads the latest indexed slot, so pages can come from different points in time.
const url = `https://mainnet.helius-rpc.com/?api-key=${process.env.HELIUS_API_KEY}`;
async function getTokenAccountsAt(owner: string, slot: number) {
const accounts = [];
let paginationKey: string | null = null;
do {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: "1",
method: "getHistoricalTokenAccountsByOwner",
params: [
owner,
{ slot },
{ encoding: "jsonParsed", limit: 1000, ...(paginationKey && { paginationKey }) },
],
}),
});
const { result, error } = await response.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
accounts.push(...result.value.accounts);
paginationKey = result.value.paginationKey;
} while (paginationKey);
return accounts;
}
const tokenAccounts = await getTokenAccountsAt("86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY", 445000000);
Read the response
Each entry invalue.accounts has the token account’s pubkey and an account object. The account object has the same fields as getAccountInfo, plus lastUpdatedAt:
lastUpdatedAt.slotandlastUpdatedAt.transactionIndexidentify the write that produced this state.lastUpdatedAt.slotof 444,959,999 with anulltransactionIndexmeans the token account hasn’t changed since history coverage began.context.slotis the latest slot Helius had indexed when it served the request, not the slot you queried.
Errors
| Code | Meaning |
|---|---|
-32018 | The slot is before history coverage begins (slot 444,959,999) |
-32004 | The slot is newer than the requested commitment allows. Use confirmed or query an older slot |
-32602 | Invalid parameters, such as both mint and programId, a programId that isn’t a token program, or a paginationKey whose mint doesn’t match mint |
-32601 | Method not found. Call your Helius mainnet or Gatekeeper endpoint; the method isn’t available on devnet |
Related methods
Historical Accounts Guide
How positions, coverage, and lastUpdatedAt work for historical account methods
getHistoricalAccountInfo
Read a single account at a past slot
getTransactionsForAddress
Find the slot and transactionIndex of the transactions you want to inspect
getTokenAccountsByOwnerV2
Paginate through a wallet’s current token accounts
Request Parameters
Base-58 encoded address of the wallet that owned the token accounts. Pass the owner wallet, not a token account.
Slot to read state at. Returns the account as of the end of this slot. The earliest supported slot is 444959999, the state at the start of epoch 1030. When omitted, reads the latest indexed slot, capped at the requested commitment.
Position of a transaction within the slot. Returns the account state immediately after that transaction executed, instead of at the end of the slot. Requires slot.
Return only token accounts owned by this token program. Can’t be combined with mint.
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DATokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Encoding format for account data. With jsonParsed, token amounts use the mint’s decimals as of the requested position. base58 and binary are not supported.
base64base64+zstdjsonParsed
Cursor from the previous response’s paginationKey, in the form
<mint>:<tokenAccount>. When you also set mint, the cursor’s mint must match it.Authorizations
Your Helius API key. You can get one for free in the dashboard.
Body
application/json
Array containing the owner wallet address, an optional query object that selects the position in history, and an optional configuration object.
Required array length:
1 - 3 elementsBase-58 encoded address of the wallet that owned the token accounts. Pass the owner wallet, not a token account.
Example:
"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY"
Was this page helpful?
⌘I
getHistoricalTokenAccountsByOwner
curl --request POST \
--url 'https://mainnet.helius-rpc.com/?api-key=' \
--header 'Content-Type: application/json' \
--data '
{
"jsonrpc": "2.0",
"id": "1",
"method": "getHistoricalTokenAccountsByOwner",
"params": [
"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
{
"slot": 445000000
},
{
"encoding": "jsonParsed",
"limit": 2
}
]
}
'import requests
url = "https://mainnet.helius-rpc.com/?api-key="
payload = {
"jsonrpc": "2.0",
"id": "1",
"method": "getHistoricalTokenAccountsByOwner",
"params": [
"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY",
{ "slot": 445000000 },
{
"encoding": "jsonParsed",
"limit": 2
}
]
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
id: '1',
method: 'getHistoricalTokenAccountsByOwner',
params: [
'86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY',
{slot: 445000000},
{encoding: 'jsonParsed', limit: 2}
]
})
};
fetch('https://mainnet.helius-rpc.com/?api-key=', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://mainnet.helius-rpc.com/?api-key=",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'jsonrpc' => '2.0',
'id' => '1',
'method' => 'getHistoricalTokenAccountsByOwner',
'params' => [
'86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY',
[
'slot' => 445000000
],
[
'encoding' => 'jsonParsed',
'limit' => 2
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://mainnet.helius-rpc.com/?api-key="
payload := strings.NewReader("{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"method\": \"getHistoricalTokenAccountsByOwner\",\n \"params\": [\n \"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\",\n {\n \"slot\": 445000000\n },\n {\n \"encoding\": \"jsonParsed\",\n \"limit\": 2\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://mainnet.helius-rpc.com/?api-key=")
.header("Content-Type", "application/json")
.body("{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"method\": \"getHistoricalTokenAccountsByOwner\",\n \"params\": [\n \"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\",\n {\n \"slot\": 445000000\n },\n {\n \"encoding\": \"jsonParsed\",\n \"limit\": 2\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://mainnet.helius-rpc.com/?api-key=")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"method\": \"getHistoricalTokenAccountsByOwner\",\n \"params\": [\n \"86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY\",\n {\n \"slot\": 445000000\n },\n {\n \"encoding\": \"jsonParsed\",\n \"limit\": 2\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"jsonrpc": "2.0",
"id": "1",
"result": {
"context": {
"slot": 468129571
},
"value": {
"accounts": [
{
"pubkey": "EKyGnXReqmmXyqBHmMjV2CKCLafJS47xyhXXZ5WuFMxD",
"account": {
"lamports": 531589475273,
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"data": [
"<string>"
],
"executable": false,
"rentEpoch": 123,
"space": 82,
"lastUpdatedAt": {
"slot": 444999837,
"transactionIndex": 700
}
}
}
],
"paginationKey": "11112zAgXhc6hGfdfr5anSY91mq7Cs4HHpSVEQc4ASG:EKyGnXReqmmXyqBHmMjV2CKCLafJS47xyhXXZ5WuFMxD"
}
}
}{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32005,
"message": "Too many requests"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Internal error"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32002,
"message": "Service unavailable"
},
"id": "1"
}{
"jsonrpc": "2.0",
"error": {
"code": -32003,
"message": "Gateway timeout"
},
"id": "1"
}