
Solana Rpc
- 200 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
solana-rpc is a Claude Code skill for direct Solana blockchain interaction via JSON-RPC, covering account lookups, token balances, transaction submission, and program queries.
About
solana-rpc is a Claude Code skill for interacting directly with the Solana blockchain over JSON-RPC. It documents core read methods (account and token balances, transactions, blocks, program accounts, priority fees), write methods (send and simulate transactions), commitment levels, and provider tradeoffs. A developer uses it as the low-level foundation when higher-level APIs like Birdeye or Helius do not expose the needed data.
- Direct Solana JSON-RPC read/write methods with Python examples
- Account, token, transaction, block, and program-account queries
- Commitment levels and provider comparison for trading
Solana Rpc by the numbers
- 200 all-time installs (skills.sh)
- Ranked #95 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
solana-rpc capabilities & compatibility
Public RPC free but unreliable; recommended providers Helius/QuickNode from ~$49/mo.
- Capabilities
- solana rpc · blockchain query · transaction submission · token balances
- Use cases
- api development · trading
- Runs
- Runs locally
- Pricing
- Bring your own API key
- Requires keys
- SOLANA_RPC_URLPROVIDERENDPOINTEGHELIUSQUICKNODE
What solana-rpc says it does
The Solana JSON-RPC API provides direct read/write access to the blockchain.
**Recommendation**: Use Helius or QuickNode for development. Never use public RPC for production trading.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill solana-rpcAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 200 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Query Solana accounts, tokens, transactions, and submit transactions directly via JSON-RPC.
Who is it for?
Low-level Solana reads and writes when higher-level data APIs lack the required data.
Skip if: Production trading over the public mainnet-beta RPC, which the skill warns is rate limited and unreliable.
When should I use this skill?
You need direct chain access for account state, token balances, or to submit and confirm a transaction.
What you get
Working JSON-RPC calls that read chain state and submit transactions with explicit commitment control.
- Reusable Solana RPC read/write helper functions
By the numbers
- Compares 6 RPC providers
- Transaction size hard limit 1232 bytes
- 3 commitment levels documented
Files
Solana RPC — Direct Blockchain Interaction
The Solana JSON-RPC API provides direct read/write access to the blockchain. Use it for account state queries, token balance lookups, transaction building and submission, and program account enumeration. This is the low-level foundation when higher-level APIs (Birdeye, Helius, SolanaTracker) don't have the data you need.
Quick Start
import httpx
RPC = os.getenv("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
def rpc_call(method: str, params: list = None) -> dict:
resp = httpx.post(RPC, json={
"jsonrpc": "2.0", "id": 1,
"method": method, "params": params or [],
}, timeout=30.0)
return resp.json()
# Get SOL balance
result = rpc_call("getBalance", ["WALLET_PUBKEY"])
sol_balance = result["result"]["value"] / 1e9
# Get latest blockhash
result = rpc_call("getLatestBlockhash")
blockhash = result["result"]["value"]["blockhash"]RPC Providers
| Provider | Free Tier | Paid | Notes |
|---|---|---|---|
| Helius | 50K credits/day | $49+/mo | Enhanced RPCs, DAS API |
| QuickNode | Limited | $49+/mo | Multi-chain, WebSocket |
| Triton | No free tier | ~$300+/mo | Yellowstone gRPC bundled |
| Shyft | Limited | $49+/mo | Yellowstone gRPC bundled |
| Alchemy | 300M CU/mo | Scaling | Good free tier |
| Public (mainnet-beta) | Free | — | Rate limited, unreliable |
Recommendation: Use Helius or QuickNode for development. Never use public RPC for production trading.
Core Read Methods
Account & Balance
# SOL balance (in lamports, divide by 1e9 for SOL)
getBalance(pubkey, {commitment: "confirmed"})
# Full account info (data, owner, lamports, executable)
getAccountInfo(pubkey, {encoding: "jsonParsed"})
# Multiple accounts in one call
getMultipleAccounts([pubkey1, pubkey2], {encoding: "jsonParsed"})Token Accounts
# All SPL token accounts owned by a wallet
getTokenAccountsByOwner(wallet_pubkey, {
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
}, {"encoding": "jsonParsed"})
# Token balance for a specific token account
getTokenAccountBalance(token_account_pubkey)
# Largest token accounts (top holders)
getTokenLargestAccounts(mint_pubkey)
# Total supply of a token
getTokenSupply(mint_pubkey)Transaction Data
# Get parsed transaction by signature
getTransaction(signature, {
"encoding": "jsonParsed",
"maxSupportedTransactionVersion": 0,
})
# Recent transaction signatures for an address
getSignaturesForAddress(pubkey, {
"limit": 20,
"before": "optional_signature", # pagination cursor
})
# Transaction status
getSignatureStatuses([sig1, sig2])Block & Slot
# Current slot
getSlot({commitment: "confirmed"})
# Block data
getBlock(slot, {
"encoding": "jsonParsed",
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
})
# Latest blockhash (needed for tx building)
getLatestBlockhash({commitment: "confirmed"})
# Slot leader schedule
getLeaderSchedule()Program Accounts
# All accounts owned by a program (with filters)
getProgramAccounts(program_pubkey, {
"encoding": "jsonParsed",
"filters": [
{"dataSize": 165}, # Filter by account data size
{"memcmp": { # Filter by data content
"offset": 32,
"bytes": "base58_encoded_value",
}},
],
})Warning: getProgramAccounts without filters can return millions of results and timeout. Always use dataSize and/or memcmp filters.
Priority Fees
# Recent priority fee estimates
getRecentPrioritizationFees([account_pubkey])
# Returns array of { slot, prioritizationFee } for recent slots
# Minimum rent for account
getMinimumBalanceForRentExemption(data_length)Write Methods
Send Transaction
# Send a signed, serialized transaction
sendTransaction(base64_tx, {
"encoding": "base64",
"skipPreflight": False,
"preflightCommitment": "confirmed",
"maxRetries": 3,
})
# Simulate before sending
simulateTransaction(base64_tx, {
"encoding": "base64",
"sigVerify": False,
"commitment": "confirmed",
})Transaction Confirmation
import time
def confirm_transaction(rpc_url: str, signature: str, timeout: float = 30.0) -> bool:
"""Poll for transaction confirmation."""
start = time.time()
while time.time() - start < timeout:
result = rpc_call("getSignatureStatuses", [[signature]])
statuses = result.get("result", {}).get("value", [None])
if statuses[0] is not None:
status = statuses[0]
if status.get("err"):
return False
if status.get("confirmationStatus") in ("confirmed", "finalized"):
return True
time.sleep(0.5)
return FalseCommitment Levels
| Level | Description | Use When |
|---|---|---|
processed | Single node confirmation | Speed over safety |
confirmed | Supermajority (2/3+) | Default for trading |
finalized | Maximum supermajority + 31 slots | Critical operations |
Always specify commitment explicitly. Default varies by provider.
Common Patterns
Get All Token Holdings for a Wallet
def get_wallet_tokens(wallet: str) -> list[dict]:
"""Get all SPL token holdings with metadata."""
result = rpc_call("getTokenAccountsByOwner", [
wallet,
{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
{"encoding": "jsonParsed"},
])
tokens = []
for acct in result["result"]["value"]:
info = acct["account"]["data"]["parsed"]["info"]
tokens.append({
"mint": info["mint"],
"amount": int(info["tokenAmount"]["amount"]),
"decimals": info["tokenAmount"]["decimals"],
"ui_amount": info["tokenAmount"]["uiAmount"],
})
return [t for t in tokens if t["amount"] > 0]Get Top Holders of a Token
def get_top_holders(mint: str) -> list[dict]:
"""Get the 20 largest holders of a token."""
result = rpc_call("getTokenLargestAccounts", [mint])
supply_result = rpc_call("getTokenSupply", [mint])
total_supply = int(supply_result["result"]["value"]["amount"])
holders = []
for acct in result["result"]["value"]:
amount = int(acct["amount"])
holders.append({
"address": acct["address"],
"amount": amount,
"decimals": acct["decimals"],
"ui_amount": acct["uiAmount"],
"percentage": amount / total_supply * 100 if total_supply > 0 else 0,
})
return holdersBatch RPC Calls
def rpc_batch(calls: list[tuple[str, list]]) -> list[dict]:
"""Execute multiple RPC calls in a single HTTP request."""
payload = [
{"jsonrpc": "2.0", "id": i, "method": method, "params": params}
for i, (method, params) in enumerate(calls)
]
resp = httpx.post(RPC, json=payload, timeout=30.0)
results = resp.json()
results.sort(key=lambda r: r["id"])
return [r.get("result") for r in results]Key Program IDs
| Program | ID |
|---|---|
| System | 11111111111111111111111111111111 |
| SPL Token | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
| Token-2022 | TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb |
| Associated Token | ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL |
| Raydium AMM | 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 |
| Raydium CLMM | CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK |
| Orca Whirlpool | whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc |
| Meteora DLMM | LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo |
| PumpFun | 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P |
| Jupiter v6 | JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 |
| Compute Budget | ComputeBudget111111111111111111111111111111 |
When to Use Direct RPC vs Higher-Level APIs
| Need | Use |
|---|---|
| Token balance check | Direct RPC (getTokenAccountsByOwner) |
| Top 20 holders | Direct RPC (getTokenLargestAccounts) |
| Historical OHLCV | Birdeye or SolanaTracker |
| Parsed transaction history | Helius Enhanced Transactions |
| Token metadata (name, image) | Helius DAS API |
| Real-time streaming | Yellowstone gRPC |
| Wallet PnL tracking | SolanaTracker |
| Token risk scoring | SolanaTracker |
| Cross-chain data | DexScreener or CoinGecko |
Files
References
references/methods.md— Complete RPC method reference with parameters and response schemasreferences/error_handling.md— Error codes, rate limits, timeout handling, retry strategiesreferences/providers.md— RPC provider comparison with pricing and features
Scripts
scripts/wallet_scanner.py— Scan wallet for all token holdings with balancesscripts/token_holders.py— Get top holders and concentration metrics for any token
Solana RPC — Error Handling & Rate Limits
JSON-RPC Error Codes
| Code | Message | Cause | Action |
|---|---|---|---|
| -32600 | Invalid request | Malformed JSON | Fix request format |
| -32601 | Method not found | Typo in method name | Check method name |
| -32602 | Invalid params | Wrong parameter types | Check param format |
| -32003 | Transaction simulation failed | Tx would fail on-chain | Check simulation logs |
| -32004 | Block not available | Slot was skipped | Try adjacent slot |
| -32005 | Node is behind | Node not synced | Use different node or retry |
| -32007 | Transaction precompile verification failure | Bad signature | Re-sign transaction |
| -32009 | Transaction has already been processed | Duplicate submission | Tx already landed |
| -32010 | Transaction version unsupported | Missing version param | Add maxSupportedTransactionVersion: 0 |
| -32014 | Slot was skipped | Leader didn't produce block | Normal, try next slot |
| -32015 | No snapshot | Node doesn't have snapshot | Use different node |
Rate Limits by Provider
| Provider | Requests/sec | Daily Limit | Batch Size |
|---|---|---|---|
| Public mainnet-beta | ~10-40 | Unlimited | 10 |
| Helius Free | ~50 | 50K credits | 100 |
| Helius Paid | ~500+ | By plan | 100 |
| QuickNode | By plan | By plan | 100 |
| Alchemy | ~25 CU/s free | 300M CU/mo | 100 |
Retry Strategy
import time
import httpx
from typing import Any
def rpc_call_with_retry(
rpc_url: str,
method: str,
params: list,
max_retries: int = 3,
timeout: float = 30.0,
) -> dict[str, Any]:
"""Make an RPC call with exponential backoff retry.
Args:
rpc_url: RPC endpoint URL.
method: RPC method name.
params: Method parameters.
max_retries: Maximum retry attempts.
timeout: Request timeout in seconds.
Returns:
The 'result' field from the response.
Raises:
RuntimeError: On persistent failure or RPC error.
"""
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
for attempt in range(max_retries):
try:
resp = httpx.post(rpc_url, json=payload, timeout=timeout)
if resp.status_code == 429:
wait = 2.0 * (attempt + 1)
time.sleep(wait)
continue
if resp.status_code >= 500:
time.sleep(1.0 * (attempt + 1))
continue
resp.raise_for_status()
data = resp.json()
if "error" in data:
err = data["error"]
code = err.get("code", 0)
# Retriable errors
if code in (-32005, -32014):
time.sleep(1.0)
continue
raise RuntimeError(f"RPC error {code}: {err.get('message')}")
return data.get("result", {})
except httpx.TimeoutException:
if attempt < max_retries - 1:
time.sleep(2.0 * (attempt + 1))
continue
raise
raise RuntimeError(f"RPC call {method} failed after {max_retries} retries")Common Pitfalls
1. Versioned transactions require explicit opt-in
# Wrong — will fail on versioned (v0) transactions
rpc_call("getTransaction", [sig])
# Right
rpc_call("getTransaction", [sig, {
"encoding": "jsonParsed",
"maxSupportedTransactionVersion": 0,
}])2. getProgramAccounts without filters
This can return millions of accounts and timeout or OOM:
# Wrong — returns ALL token accounts
rpc_call("getProgramAccounts", ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"])
# Right — filter by data size and/or memcmp
rpc_call("getProgramAccounts", ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", {
"filters": [{"dataSize": 165}],
"encoding": "jsonParsed",
}])3. Lamport vs SOL confusion
All RPC values are in lamports (1 SOL = 1,000,000,000 lamports). All token amounts are in raw units (check decimals).
sol = lamports / 1e9
token_amount = raw_amount / (10 ** decimals)4. Commitment level defaults
Different providers may default to different commitment levels. Always specify explicitly:
rpc_call("getBalance", [pubkey, {"commitment": "confirmed"}])5. Blockhash expiration
Blockhashes expire after ~60-90 seconds. If building transactions:
- Fetch blockhash right before signing
- Submit immediately after signing
- Monitor
lastValidBlockHeightto know when to retry
6. Null results
Many methods return null for accounts/transactions that don't exist:
result = rpc_call("getAccountInfo", [pubkey])
if result["value"] is None:
print("Account does not exist")Solana RPC — Method Reference
All methods use POST to the RPC endpoint with JSON-RPC 2.0 format.
Request Format
{
"jsonrpc": "2.0",
"id": 1,
"method": "METHOD_NAME",
"params": [/* method-specific */]
}Response Format
{
"jsonrpc": "2.0",
"id": 1,
"result": { /* method-specific */ }
}On error:
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32600, "message": "Invalid request" }
}---
Account Methods
getBalance
Returns SOL balance in lamports (1 SOL = 1,000,000,000 lamports).
- Params:
[pubkey, {commitment}] - Result:
{ context: { slot }, value: lamports }
getAccountInfo
Returns all account data including owner program, data, and executable flag.
- Params:
[pubkey, {encoding, commitment, dataSlice}] - Encodings:
base58(slow),base64,base64+zstd,jsonParsed - Result:
{ context, value: { data, executable, lamports, owner, rentEpoch } } - Note:
jsonParsedonly works for known programs (Token, System, etc.)
getMultipleAccounts
Batch lookup for up to 100 accounts.
- Params:
[[pubkey1, pubkey2, ...], {encoding, commitment}] - Result:
{ context, value: [account1, account2, ...] }
---
Token Methods
getTokenAccountsByOwner
All SPL token accounts owned by a wallet.
- Params:
[owner_pubkey, {mint | programId}, {encoding, commitment}] - Filter by mint:
{"mint": "TOKEN_MINT"}— accounts for a specific token - Filter by program:
{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"}— all SPL tokens
getTokenLargestAccounts
Top 20 largest holders of a token.
- Params:
[mint_pubkey, {commitment}] - Result:
{ value: [{ address, amount, decimals, uiAmount, uiAmountString }] }
getTokenSupply
Total supply of a token.
- Params:
[mint_pubkey, {commitment}] - Result:
{ value: { amount, decimals, uiAmount, uiAmountString } }
getTokenAccountBalance
Balance of a specific token account.
- Params:
[token_account_pubkey, {commitment}] - Result:
{ value: { amount, decimals, uiAmount } }
---
Transaction Methods
getTransaction
Full transaction data by signature.
- Params:
[signature, {encoding, commitment, maxSupportedTransactionVersion}] - Note: Must set
maxSupportedTransactionVersion: 0for versioned transactions.
getSignaturesForAddress
Recent signatures involving an address.
- Params:
[pubkey, {limit, before, until, commitment}] - Result:
[{ signature, slot, err, memo, blockTime, confirmationStatus }] - Limit: Max 1000 per call. Use
beforefor pagination.
getSignatureStatuses
Check confirmation status of transactions.
- Params:
[[sig1, sig2, ...], {searchTransactionHistory}] - Result:
{ value: [{ slot, confirmations, err, confirmationStatus } | null] }
sendTransaction
Submit a signed transaction.
- Params:
[base64_tx, {encoding, skipPreflight, preflightCommitment, maxRetries, minContextSlot}] - Result: Transaction signature string
- Note:
skipPreflight: trueskips simulation (faster but riskier).
simulateTransaction
Simulate without submitting.
- Params:
[base64_tx, {encoding, sigVerify, commitment, replaceRecentBlockhash}] - Result:
{ err, logs, accounts, unitsConsumed, returnData }
---
Block & Slot Methods
getSlot
Current slot number.
- Params:
[{commitment}]
getBlock
Full block data.
- Params:
[slot, {encoding, transactionDetails, rewards, commitment, maxSupportedTransactionVersion}] - transactionDetails:
full,signatures,accounts,none
getLatestBlockhash
Current blockhash for transaction building.
- Params:
[{commitment}] - Result:
{ value: { blockhash, lastValidBlockHeight } }
isBlockhashValid
Check if a blockhash is still valid.
- Params:
[blockhash, {commitment}]
getBlockHeight
Current block height.
- Params:
[{commitment}]
---
Program Methods
getProgramAccounts
All accounts owned by a program.
- Params:
[program_pubkey, {encoding, commitment, filters, dataSlice, withContext}] - Filters:
[{ dataSize: N }, { memcmp: { offset, bytes } }] - Warning: Without filters, this can return millions of accounts. Always filter.
getRecentPrioritizationFees
Priority fee estimates from recent blocks.
- Params:
[[account_pubkey1, ...]] - Result:
[{ slot, prioritizationFee }]— fee in micro-lamports per compute unit
getMinimumBalanceForRentExemption
SOL needed to keep an account alive.
- Params:
[data_length_bytes] - Result: Lamports required
---
Subscription Methods (WebSocket)
Connect via WebSocket (wss:// version of RPC URL).
accountSubscribe / accountUnsubscribe
Watch for account data changes.
- Params:
[pubkey, {encoding, commitment}]
logsSubscribe / logsUnsubscribe
Watch for transaction logs.
- Params:
[{mentions: [pubkey]} | "all" | "allWithVotes", {commitment}]
signatureSubscribe / signatureUnsubscribe
Watch for transaction confirmation.
- Params:
[signature, {commitment}]
slotSubscribe / slotUnsubscribe
Watch for slot changes.
- Params:
[]
---
Batch Requests
Send multiple calls in one HTTP request:
[
{"jsonrpc":"2.0","id":0,"method":"getBalance","params":["ADDR1"]},
{"jsonrpc":"2.0","id":1,"method":"getBalance","params":["ADDR2"]},
{"jsonrpc":"2.0","id":2,"method":"getBalance","params":["ADDR3"]}
]Response is an array in the same order. Most providers support up to 100 calls per batch.
Solana RPC — Provider Comparison
Provider Overview
| Provider | Free Tier | Entry Paid | Best For |
|---|---|---|---|
| Helius | 50K credits/day | $49/mo (500K) | Best all-around, DAS API included |
| QuickNode | 50 req/s | $49/mo | Multi-chain, marketplace add-ons |
| Triton | No free | ~$300/mo | Yellowstone bundled, low latency |
| Shyft | Limited | $49/mo | Yellowstone + RabbitStream |
| Alchemy | 300M CU/mo | Scaling | Large free tier, enterprise |
| Chainstack | 3M req/mo | $29/mo | Budget option |
| Public | Free | — | Testing only |
Detailed Comparison
Helius
- URL format:
https://mainnet.helius-rpc.com/?api-key=YOUR_KEY - Strengths: DAS API, Enhanced Transactions, Webhooks, Priority Fee API
- Free tier: 50K credits/day (most calls = 1 credit)
- Paid: Developer $49/mo (500K), Business $199/mo (2M), Professional $999/mo (10M)
- WebSocket: Included
- Yellowstone: LaserStream on Professional tier ($999/mo)
QuickNode
- URL format:
https://YOUR_ENDPOINT.solana-mainnet.quiknode.pro/YOUR_KEY - Strengths: Multi-chain (100+ chains), marketplace add-ons
- Free tier: 50 req/s, 10M API credits/mo
- Paid: Build $49/mo, Scale $299/mo
- WebSocket: Included
- Yellowstone: Available as add-on
Triton (by Triton One)
- URL format: Custom endpoint provided
- Strengths: Lowest latency, Yellowstone (Dragon's Mouth) bundled, DeShed
- Free tier: None
- Paid: ~$300/mo+ (contact sales)
- Yellowstone: Dragon's Mouth gRPC included
- Best for: Professional/HFT operations
Shyft
- URL format:
https://rpc.shyft.to?api_key=YOUR_KEY - Strengths: Yellowstone + RabbitStream (pre-exec streaming)
- Free tier: Limited
- Paid: $49/mo (Growth), $199/mo (Premium)
- Yellowstone: Included on paid tiers
- RabbitStream: Pre-execution data streaming
Alchemy
- URL format:
https://solana-mainnet.g.alchemy.com/v2/YOUR_KEY - Strengths: Large free tier, auto-scaling, analytics dashboard
- Free tier: 300M compute units/mo
- Paid: Growth (scaling), Enterprise
- Note: CU-based pricing, different methods cost different CUs
Choosing a Provider
For development / research
Use Helius free tier — best DX, DAS API, and Enhanced Transactions included.
For production trading
Use Helius paid or Triton — low latency, reliable, Yellowstone available.
For multi-chain projects
Use QuickNode or Alchemy — both support 100+ chains.
For HFT / latency-sensitive
Use Triton — lowest latency, Yellowstone bundled, bare-metal options.
Public RPC Endpoints
For testing only — rate limited, unreliable, no SLA:
https://api.mainnet-beta.solana.comhttps://api.devnet.solana.comhttps://api.testnet.solana.com
Never use public endpoints for trading. They are rate limited (10-40 req/s), may drop requests under load, and provide no uptime guarantees.
Environment Setup
# Add to your shell profile (~/.zshrc or ~/.bashrc)
export SOLANA_RPC_URL="https://mainnet.helius-rpc.com/?api-key=YOUR_KEY"
# For development, also set devnet
export SOLANA_DEVNET_URL="https://api.devnet.solana.com"import os
RPC = os.getenv("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")#!/usr/bin/env python3
"""Get top holders and concentration metrics for any Solana token.
Uses direct RPC calls (getTokenLargestAccounts, getTokenSupply) to
fetch holder data and compute concentration metrics: top-N percentage,
Gini coefficient, HHI, and Nakamoto coefficient.
Usage:
python scripts/token_holders.py
TOKEN_ADDRESS="TokenMint..." python scripts/token_holders.py
Dependencies:
uv pip install httpx
Environment Variables:
SOLANA_RPC_URL: Solana RPC endpoint (default: public mainnet)
TOKEN_ADDRESS: Token mint address (default: BONK)
"""
import os
import sys
import time
from typing import Any, Optional
import httpx
# ── Configuration ───────────────────────────────────────────────────
RPC_URL = os.getenv("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
TOKEN_ADDRESS = os.getenv(
"TOKEN_ADDRESS",
"DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", # BONK
)
# ── RPC Helper ──────────────────────────────────────────────────────
def rpc_call(method: str, params: Optional[list] = None) -> dict[str, Any]:
"""Make a JSON-RPC call to Solana with retry.
Args:
method: RPC method name.
params: Method parameters.
Returns:
The 'result' field from the response.
Raises:
RuntimeError: On persistent RPC error.
"""
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []}
for attempt in range(3):
try:
resp = httpx.post(RPC_URL, json=payload, timeout=30.0)
if resp.status_code == 429:
time.sleep(2.0 * (attempt + 1))
continue
resp.raise_for_status()
data = resp.json()
if "error" in data:
err = data["error"]
raise RuntimeError(f"RPC error: {err.get('message')}")
return data.get("result", {})
except httpx.TimeoutException:
if attempt < 2:
time.sleep(2.0)
continue
raise
raise RuntimeError(f"RPC {method} failed after retries")
# ── Data Fetching ───────────────────────────────────────────────────
def get_token_supply(mint: str) -> dict:
"""Get total supply of a token.
Args:
mint: Token mint address.
Returns:
Supply dict with amount, decimals, uiAmount.
"""
result = rpc_call("getTokenSupply", [mint])
return result.get("value", {})
def get_largest_accounts(mint: str) -> list[dict]:
"""Get top 20 largest token accounts.
Args:
mint: Token mint address.
Returns:
List of holder dicts sorted by amount descending.
"""
result = rpc_call("getTokenLargestAccounts", [mint])
return result.get("value", [])
# ── Concentration Metrics ───────────────────────────────────────────
def top_n_percentage(amounts: list[int], total_supply: int, n: int) -> float:
"""Percentage held by top N holders.
Args:
amounts: Sorted list of holder amounts (largest first).
total_supply: Total token supply.
n: Number of top holders.
Returns:
Percentage (0-100).
"""
if total_supply == 0:
return 0.0
top_n = sum(amounts[:n])
return top_n / total_supply * 100
def gini_coefficient(amounts: list[int]) -> float:
"""Calculate Gini coefficient for holder distribution.
Args:
amounts: List of holder amounts (any order).
Returns:
Gini coefficient (0 = equal, 1 = maximally unequal).
"""
if not amounts or all(a == 0 for a in amounts):
return 0.0
sorted_a = sorted(amounts)
n = len(sorted_a)
total = sum(sorted_a)
if total == 0:
return 0.0
cumsum = sum((i + 1) * a for i, a in enumerate(sorted_a))
return (2 * cumsum) / (n * total) - (n + 1) / n
def hhi(amounts: list[int]) -> float:
"""Herfindahl-Hirschman Index for concentration.
Args:
amounts: List of holder amounts.
Returns:
HHI value (0-10000). Higher = more concentrated.
"""
total = sum(amounts)
if total == 0:
return 0.0
shares = [a / total * 100 for a in amounts]
return sum(s ** 2 for s in shares)
def nakamoto_coefficient(amounts: list[int]) -> int:
"""Minimum holders needed for >50% control.
Args:
amounts: Sorted list of holder amounts (largest first).
Returns:
Number of holders needed for majority control.
"""
total = sum(amounts)
if total == 0:
return 0
threshold = total * 0.51
cumulative = 0
for i, amount in enumerate(amounts):
cumulative += amount
if cumulative >= threshold:
return i + 1
return len(amounts)
def classify_risk(top_10_pct: float, gini_val: float, hhi_val: float) -> str:
"""Classify concentration risk level.
Args:
top_10_pct: Percentage held by top 10.
gini_val: Gini coefficient.
hhi_val: HHI value.
Returns:
Risk level string.
"""
if top_10_pct > 80 or hhi_val > 5000:
return "EXTREME"
if top_10_pct > 50 or hhi_val > 2500:
return "HIGH"
if top_10_pct > 30 or hhi_val > 1500:
return "MODERATE"
return "LOW"
# ── Display ─────────────────────────────────────────────────────────
def print_report(
mint: str,
supply: dict,
holders: list[dict],
total_supply: int,
amounts: list[int],
) -> None:
"""Print formatted holder analysis report.
Args:
mint: Token mint address.
supply: Token supply info.
holders: Top holder list from RPC.
total_supply: Total supply in raw units.
amounts: Sorted amounts (largest first).
"""
decimals = supply.get("decimals", 0)
ui_supply = supply.get("uiAmount", 0)
print(f"\n{'='*60}")
print(f"TOKEN HOLDER ANALYSIS")
print(f"{'='*60}")
print(f" Mint: {mint}")
print(f" Supply: {ui_supply:,.0f} ({decimals} decimals)")
# Top holders table
print(f"\n--- Top 20 Holders ---")
print(f" {'#':>3} {'Account':<20} {'Amount':>16} {'% Supply':>10}")
print(f" {'─'*3} {'─'*20} {'─'*16} {'─'*10}")
for i, h in enumerate(holders, 1):
addr = h.get("address", "?")
addr_short = addr[:8] + "..." + addr[-4:]
amount = int(h.get("amount", 0))
ui = h.get("uiAmount", 0)
pct = amount / total_supply * 100 if total_supply > 0 else 0
amount_str = f"{ui:,.2f}" if ui < 1e9 else f"{ui:,.0f}"
print(f" {i:>3} {addr_short:<20} {amount_str:>16} {pct:>9.2f}%")
# Concentration metrics
t1 = top_n_percentage(amounts, total_supply, 1)
t5 = top_n_percentage(amounts, total_supply, 5)
t10 = top_n_percentage(amounts, total_supply, 10)
t20 = top_n_percentage(amounts, total_supply, 20)
gini_val = gini_coefficient(amounts)
hhi_val = hhi(amounts)
naka = nakamoto_coefficient(amounts)
print(f"\n--- Concentration Metrics ---")
print(f" Top 1 holder: {t1:.2f}%")
print(f" Top 5 holders: {t5:.2f}%")
print(f" Top 10 holders: {t10:.2f}%")
print(f" Top 20 holders: {t20:.2f}%")
print(f" Gini coeff: {gini_val:.4f}")
print(f" HHI: {hhi_val:.1f}")
print(f" Nakamoto coeff: {naka} (holders for >50%)")
# Risk assessment
risk = classify_risk(t10, gini_val, hhi_val)
print(f"\n--- Risk Assessment ---")
print(f" Concentration Risk: {risk}")
if risk == "EXTREME":
print(" [!!] Extremely concentrated — few wallets control majority")
print(" High rug/dump risk. Not suitable for significant positions.")
elif risk == "HIGH":
print(" [!] Highly concentrated — top holders can significantly impact price")
print(" Use small position sizes and tight stops.")
elif risk == "MODERATE":
print(" [i] Moderate concentration — typical for newer tokens")
print(" Monitor top holder activity for large sells.")
else:
print(" [ok] Well distributed — lower concentration risk")
# Note about limitations
print(f"\n--- Notes ---")
print(" - RPC returns max 20 holders (getTokenLargestAccounts)")
print(" - Some holders may be pool/program accounts, not individuals")
print(" - For deeper analysis (100+ holders, bundlers), use SolanaTracker API")
print()
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run token holder analysis."""
print(f"Analyzing holders for: {TOKEN_ADDRESS}")
print(f"RPC: {RPC_URL[:40]}...")
print("Fetching supply...")
supply = get_token_supply(TOKEN_ADDRESS)
total_supply = int(supply.get("amount", "0"))
if total_supply == 0:
print("Could not fetch token supply. Check the mint address.")
sys.exit(1)
time.sleep(0.5)
print("Fetching top holders...")
holders = get_largest_accounts(TOKEN_ADDRESS)
if not holders:
print("No holder data returned.")
sys.exit(1)
amounts = sorted([int(h.get("amount", 0)) for h in holders], reverse=True)
print_report(TOKEN_ADDRESS, supply, holders, total_supply, amounts)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Scan a Solana wallet for all token holdings using direct RPC.
Fetches SOL balance and all SPL token accounts, then displays a
summary of holdings sorted by value (if price data available via
a simple DexScreener lookup).
Usage:
python scripts/wallet_scanner.py
WALLET_ADDRESS="YourWallet..." python scripts/wallet_scanner.py
Dependencies:
uv pip install httpx
Environment Variables:
SOLANA_RPC_URL: Solana RPC endpoint (default: public mainnet)
WALLET_ADDRESS: Wallet public key to scan
"""
import os
import sys
import time
from typing import Any, Optional
import httpx
# ── Configuration ───────────────────────────────────────────────────
RPC_URL = os.getenv("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
WALLET_ADDRESS = os.getenv("WALLET_ADDRESS", "")
if not WALLET_ADDRESS:
print("Set WALLET_ADDRESS environment variable")
print(" Example: WALLET_ADDRESS=\"YourWallet...\" python scripts/wallet_scanner.py")
sys.exit(1)
# ── RPC Helper ──────────────────────────────────────────────────────
def rpc_call(method: str, params: Optional[list] = None) -> dict[str, Any]:
"""Make a JSON-RPC call to Solana.
Args:
method: RPC method name.
params: Method parameters.
Returns:
The 'result' field from the response.
Raises:
RuntimeError: On RPC error.
"""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params or [],
}
for attempt in range(3):
try:
resp = httpx.post(RPC_URL, json=payload, timeout=30.0)
if resp.status_code == 429:
time.sleep(2.0 * (attempt + 1))
continue
resp.raise_for_status()
data = resp.json()
if "error" in data:
err = data["error"]
raise RuntimeError(f"RPC error: {err.get('message', 'unknown')}")
return data.get("result", {})
except httpx.TimeoutException:
if attempt < 2:
time.sleep(2.0)
continue
raise
raise RuntimeError(f"RPC call {method} failed after retries")
def rpc_batch(calls: list[tuple[str, list]]) -> list[dict]:
"""Execute multiple RPC calls in a single HTTP request.
Args:
calls: List of (method, params) tuples.
Returns:
List of result dicts in the same order.
"""
payload = [
{"jsonrpc": "2.0", "id": i, "method": method, "params": params}
for i, (method, params) in enumerate(calls)
]
resp = httpx.post(RPC_URL, json=payload, timeout=30.0)
resp.raise_for_status()
results = resp.json()
results.sort(key=lambda r: r.get("id", 0))
return [r.get("result") for r in results]
# ── Token Scanning ──────────────────────────────────────────────────
def get_sol_balance(wallet: str) -> float:
"""Get SOL balance for a wallet.
Args:
wallet: Wallet public key.
Returns:
SOL balance.
"""
result = rpc_call("getBalance", [wallet, {"commitment": "confirmed"}])
lamports = result.get("value", 0)
return lamports / 1e9
def get_token_accounts(wallet: str) -> list[dict]:
"""Get all SPL token accounts for a wallet.
Args:
wallet: Wallet public key.
Returns:
List of token account dicts with mint, amount, decimals.
"""
# SPL Token program
result = rpc_call("getTokenAccountsByOwner", [
wallet,
{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
{"encoding": "jsonParsed"},
])
tokens = []
for acct in result.get("value", []):
parsed = acct.get("account", {}).get("data", {}).get("parsed", {}).get("info", {})
amount_info = parsed.get("tokenAmount", {})
raw_amount = int(amount_info.get("amount", "0"))
if raw_amount == 0:
continue
tokens.append({
"mint": parsed.get("mint", ""),
"account": acct.get("pubkey", ""),
"amount": raw_amount,
"decimals": amount_info.get("decimals", 0),
"ui_amount": amount_info.get("uiAmount", 0),
})
# Also check Token-2022
try:
result_2022 = rpc_call("getTokenAccountsByOwner", [
wallet,
{"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"},
{"encoding": "jsonParsed"},
])
for acct in result_2022.get("value", []):
parsed = acct.get("account", {}).get("data", {}).get("parsed", {}).get("info", {})
amount_info = parsed.get("tokenAmount", {})
raw_amount = int(amount_info.get("amount", "0"))
if raw_amount == 0:
continue
tokens.append({
"mint": parsed.get("mint", ""),
"account": acct.get("pubkey", ""),
"amount": raw_amount,
"decimals": amount_info.get("decimals", 0),
"ui_amount": amount_info.get("uiAmount", 0),
"token_2022": True,
})
except Exception:
pass # Token-2022 query may fail on some providers
return tokens
def get_prices_dexscreener(mints: list[str]) -> dict[str, float]:
"""Get USD prices for tokens from DexScreener (free, no auth).
Args:
mints: List of token mint addresses.
Returns:
Dict of mint -> USD price.
"""
prices = {}
# DexScreener supports up to 30 addresses per call
for i in range(0, len(mints), 30):
batch = mints[i:i + 30]
joined = ",".join(batch)
try:
resp = httpx.get(
f"https://api.dexscreener.com/tokens/v1/solana/{joined}",
timeout=15.0,
)
if resp.status_code == 200:
pairs = resp.json() if isinstance(resp.json(), list) else resp.json().get("pairs", [])
# Group by base token, take highest liquidity pair
for pair in pairs:
mint = pair.get("baseToken", {}).get("address", "")
price = float(pair.get("priceUsd", "0") or "0")
liq = (pair.get("liquidity") or {}).get("usd", 0) or 0
if mint and price > 0:
if mint not in prices or liq > prices.get(f"_liq_{mint}", 0):
prices[mint] = price
prices[f"_liq_{mint}"] = liq
time.sleep(1.0)
except Exception:
pass
# Clean up liquidity tracking keys
return {k: v for k, v in prices.items() if not k.startswith("_liq_")}
# ── Display ─────────────────────────────────────────────────────────
def format_usd(value: float) -> str:
"""Format a USD value for display."""
if abs(value) >= 1_000_000:
return f"${value / 1e6:.2f}M"
elif abs(value) >= 1_000:
return f"${value / 1e3:.2f}K"
return f"${value:.2f}"
def print_report(
wallet: str,
sol_balance: float,
tokens: list[dict],
prices: dict[str, float],
) -> None:
"""Print formatted wallet scan report.
Args:
wallet: Wallet address.
sol_balance: SOL balance.
tokens: List of token holdings.
prices: Dict of mint -> USD price.
"""
# SOL price
sol_mint = "So11111111111111111111111111111111111111112"
sol_price = prices.get(sol_mint, 0)
sol_value = sol_balance * sol_price
print(f"\n{'='*60}")
print(f"WALLET SCAN")
print(f"{'='*60}")
print(f" Address: {wallet}")
print(f" SOL: {sol_balance:.4f} SOL", end="")
if sol_value:
print(f" ({format_usd(sol_value)})")
else:
print()
print(f" Tokens: {len(tokens)} non-zero holdings")
if not tokens:
print("\n No token holdings found.")
return
# Enrich with prices and sort by value
enriched = []
for t in tokens:
price = prices.get(t["mint"], 0)
value = t["ui_amount"] * price if price else 0
enriched.append({**t, "price": price, "value": value})
enriched.sort(key=lambda t: t["value"], reverse=True)
total_value = sol_value + sum(t["value"] for t in enriched)
print(f"\n--- Token Holdings ---")
print(f" {'Mint':<20} {'Amount':>14} {'Price':>12} {'Value':>12}")
print(f" {'─'*20} {'─'*14} {'─'*12} {'─'*12}")
displayed = 0
for t in enriched:
mint_short = t["mint"][:8] + "..." + t["mint"][-4:]
amount_str = f"{t['ui_amount']:.4f}" if t["ui_amount"] < 1e6 else f"{t['ui_amount']:,.0f}"
if t["price"]:
price_str = f"${t['price']:.6f}" if t["price"] < 0.01 else f"${t['price']:.4f}"
value_str = format_usd(t["value"])
else:
price_str = "—"
value_str = "—"
t2022 = " [T22]" if t.get("token_2022") else ""
print(f" {mint_short:<20} {amount_str:>14} {price_str:>12} {value_str:>12}{t2022}")
displayed += 1
if displayed >= 20:
remaining = len(enriched) - displayed
if remaining > 0:
print(f"\n ... and {remaining} more tokens (showing top 20 by value)")
break
if total_value > 0:
print(f"\n Total Portfolio Value: {format_usd(total_value)}")
print(f" SOL: {format_usd(sol_value)} ({sol_value / total_value * 100:.1f}%)")
token_value = total_value - sol_value
print(f" Tokens: {format_usd(token_value)} ({token_value / total_value * 100:.1f}%)")
print()
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run wallet scanner."""
print(f"Scanning wallet: {WALLET_ADDRESS}")
print(f"RPC: {RPC_URL[:40]}...")
print("Fetching SOL balance...")
sol_balance = get_sol_balance(WALLET_ADDRESS)
print("Fetching token accounts...")
tokens = get_token_accounts(WALLET_ADDRESS)
# Get prices from DexScreener (free, no auth)
prices: dict[str, float] = {}
if tokens:
mints = list(set(t["mint"] for t in tokens))
# Add SOL for price reference
mints.append("So11111111111111111111111111111111111111112")
print(f"Fetching prices for {len(mints)} tokens...")
prices = get_prices_dexscreener(mints)
print_report(WALLET_ADDRESS, sol_balance, tokens, prices)
if __name__ == "__main__":
main()
Related skills
FAQ
Which commitment level should trading use?
The skill recommends 'confirmed' (supermajority) as the default for trading, with 'processed' for speed and 'finalized' for critical operations.
Why avoid getProgramAccounts without filters?
Without dataSize or memcmp filters it can return millions of results and time out, so the skill says to always filter.