curl --request POST \
--url https://api.range.org/v2/risk/transaction/scan \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: <api-key>' \
--data '
{
"network": "solana",
"transaction": "AQAAAAAAAAAA…",
"signer_address": "<string>",
"origin": "<string>"
}
'import requests
url = "https://api.range.org/v2/risk/transaction/scan"
payload = {
"network": "solana",
"transaction": "AQAAAAAAAAAA…",
"signer_address": "<string>",
"origin": "<string>"
}
headers = {
"X-API-KEY": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-KEY': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
network: 'solana',
transaction: 'AQAAAAAAAAAA…',
signer_address: '<string>',
origin: '<string>'
})
};
fetch('https://api.range.org/v2/risk/transaction/scan', 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://api.range.org/v2/risk/transaction/scan",
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([
'network' => 'solana',
'transaction' => 'AQAAAAAAAAAA…',
'signer_address' => '<string>',
'origin' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-KEY: <api-key>"
],
]);
$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://api.range.org/v2/risk/transaction/scan"
payload := strings.NewReader("{\n \"network\": \"solana\",\n \"transaction\": \"AQAAAAAAAAAA…\",\n \"signer_address\": \"<string>\",\n \"origin\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-KEY", "<api-key>")
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://api.range.org/v2/risk/transaction/scan")
.header("X-API-KEY", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"network\": \"solana\",\n \"transaction\": \"AQAAAAAAAAAA…\",\n \"signer_address\": \"<string>\",\n \"origin\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.range.org/v2/risk/transaction/scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-KEY"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"network\": \"solana\",\n \"transaction\": \"AQAAAAAAAAAA…\",\n \"signer_address\": \"<string>\",\n \"origin\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"screen_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"network": "solana",
"payload_sha256": "<string>",
"severity": "severe",
"counterparties": [
{
"address": "<string>",
"source": "asset-diff",
"severity": "severe",
"triggered_signals": [
"<string>"
]
}
],
"findings": [
{
"signal": "authority-change",
"severity": "severe",
"caption": "Token Authority … changed",
"evidence": {}
}
],
"notes": [
{
"code": "coverage-truncated",
"message": "Counterparty screening truncated to 50 accounts (63 selected)."
}
],
"timestamp": "2026-08-18T14:30:00Z",
"meta": {
"counterparties_screened": 12,
"counterparties_screened_complete": true,
"coverage_truncated": false,
"poisoning_screened": true,
"simulate_ms": 420,
"screening_ms": 610,
"poisoning_ms": 480,
"intent_ms": 900,
"total_ms": 1450
},
"signer": "<string>",
"intent": {
"summary": "Swap 100 USDC for SOL on Jupiter",
"action": "swap",
"confidence": "high"
},
"simulation": {
"simulation_success": true,
"asset_diffs": [
{
"account": "<string>",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": 100.5,
"direction": "credit",
"owner": "<string>"
}
],
"state_changes": [
"<string>"
],
"programs_invoked": [
{
"name": "<string>",
"id": "<string>"
}
]
}
}Scan an Unsigned Transaction
Screens a whole Solana transaction before it is signed: simulation, Address Risk (v2) on counterparties, and address-poisoning checks.
curl --request POST \
--url https://api.range.org/v2/risk/transaction/scan \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: <api-key>' \
--data '
{
"network": "solana",
"transaction": "AQAAAAAAAAAA…",
"signer_address": "<string>",
"origin": "<string>"
}
'import requests
url = "https://api.range.org/v2/risk/transaction/scan"
payload = {
"network": "solana",
"transaction": "AQAAAAAAAAAA…",
"signer_address": "<string>",
"origin": "<string>"
}
headers = {
"X-API-KEY": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-KEY': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
network: 'solana',
transaction: 'AQAAAAAAAAAA…',
signer_address: '<string>',
origin: '<string>'
})
};
fetch('https://api.range.org/v2/risk/transaction/scan', 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://api.range.org/v2/risk/transaction/scan",
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([
'network' => 'solana',
'transaction' => 'AQAAAAAAAAAA…',
'signer_address' => '<string>',
'origin' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-KEY: <api-key>"
],
]);
$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://api.range.org/v2/risk/transaction/scan"
payload := strings.NewReader("{\n \"network\": \"solana\",\n \"transaction\": \"AQAAAAAAAAAA…\",\n \"signer_address\": \"<string>\",\n \"origin\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-KEY", "<api-key>")
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://api.range.org/v2/risk/transaction/scan")
.header("X-API-KEY", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"network\": \"solana\",\n \"transaction\": \"AQAAAAAAAAAA…\",\n \"signer_address\": \"<string>\",\n \"origin\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.range.org/v2/risk/transaction/scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-KEY"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"network\": \"solana\",\n \"transaction\": \"AQAAAAAAAAAA…\",\n \"signer_address\": \"<string>\",\n \"origin\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"screen_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"network": "solana",
"payload_sha256": "<string>",
"severity": "severe",
"counterparties": [
{
"address": "<string>",
"source": "asset-diff",
"severity": "severe",
"triggered_signals": [
"<string>"
]
}
],
"findings": [
{
"signal": "authority-change",
"severity": "severe",
"caption": "Token Authority … changed",
"evidence": {}
}
],
"notes": [
{
"code": "coverage-truncated",
"message": "Counterparty screening truncated to 50 accounts (63 selected)."
}
],
"timestamp": "2026-08-18T14:30:00Z",
"meta": {
"counterparties_screened": 12,
"counterparties_screened_complete": true,
"coverage_truncated": false,
"poisoning_screened": true,
"simulate_ms": 420,
"screening_ms": 610,
"poisoning_ms": 480,
"intent_ms": 900,
"total_ms": 1450
},
"signer": "<string>",
"intent": {
"summary": "Swap 100 USDC for SOL on Jupiter",
"action": "swap",
"confidence": "high"
},
"simulation": {
"simulation_success": true,
"asset_diffs": [
{
"account": "<string>",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": 100.5,
"direction": "credit",
"owner": "<string>"
}
],
"state_changes": [
"<string>"
],
"programs_invoked": [
{
"name": "<string>",
"id": "<string>"
}
]
}
}severity. meta.poisoning_screened: false means recipients were not checked for lookalikes, and meta.counterparties_screened_complete: false means at least one counterparty was not screened. Neither is a clean result. A gap in counterparty screening already raises severity to at least medium. A poisoning check that did not run does not, so check that flag separately.How the Scan Works
-
Decode. The
transactionstring is decoded withencoding(base64 by default). A payload that does not decode returns HTTP 400. -
Simulate. The transaction is simulated against current chain state. It does not need to be signed. The predicted balance changes, state changes, and invoked programs are returned in
simulation. -
Select counterparties. Range picks the wallets to screen:
asset-diff: wallets whose balance changes, with token accounts resolved to their owner wallet.permission-target: accounts that gain control throughsetAuthority,approve,approveChecked,assign, orassignWithSeed, including instructions reached through cross-program invocation (CPI).static-account: the accounts declared in the payload, used only when simulation is unavailable.
- Screen in parallel. Each counterparty runs through Address Risk (v2), credited wallets are checked for address poisoning, and transaction rules run on the simulation.
-
Combine.
severityis the worst single result acrosscounterpartiesandfindings, ignoring informational findings.
screen_id.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
network | string | Yes | Must be solana. |
transaction | string | Yes | The complete serialized unsigned transaction (account list, instructions, blockhash) as one encoded string. Up to 4,096 characters; a Solana transaction is at most 1,232 bytes. Legacy and versioned transactions are accepted. |
encoding | string | No | base64 (default) or base58. |
signer_address | string | No | The account that will sign. Defaults to the fee payer. A value that is not a required signer of the transaction is ignored: the fee payer is used and the signer-address-ignored note is returned. |
origin | string | No | The dApp URL that produced the transaction. Stored with the scan. Up to 2,048 characters. |
Example Request
This transaction sends 0.5 SOL to the clean Solana address from Test Addresses. Copy it as is to try the endpoint.curl -X POST "https://api.range.org/v2/risk/transaction/scan" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"network": "solana",
"transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDfowIh2C/3h3dzzLBfyCbgkLuUqrxMfrNiNDqLG0LBvJM1r0sCJ2KySbkJLj1EmFlWXx/7Y4DeP3JYWhV/axT1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIAAQwCAAAAAGXNHQAAAAA=",
"encoding": "base64"
}'
// `transaction` is a @solana/web3.js Transaction, before signing.
const encoded = transaction
.serialize({ requireAllSignatures: false, verifySignatures: false })
.toString('base64');
const response = await fetch('https://api.range.org/v2/risk/transaction/scan', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
network: 'solana',
transaction: encoded,
encoding: 'base64',
origin: 'https://app.example.com',
}),
});
const scan = await response.json();
import requests
response = requests.post(
"https://api.range.org/v2/risk/transaction/scan",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"network": "solana",
"transaction": encoded_transaction, # base64 string of the unsigned transaction
"encoding": "base64",
},
)
scan = response.json()
VersionedTransaction, encode it with Buffer.from(transaction.serialize()).toString('base64'); it serializes without requiring signatures.
Example Response
{
"screen_id": "c1f3d878-a6e0-4c93-a4ea-dca26189b6c6",
"network": "solana",
"payload_sha256": "c0242054122a717af894b90f31b6cdcded6dc4cc9869583fecc552bcebc56d05",
"signer": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"severity": "none",
"intent": null,
"simulation": {
"simulation_success": true,
"asset_diffs": [
{
"account": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"asset": "SOL",
"amount": 0.5,
"direction": "debit"
},
{
"account": "6AwuGoRLd54NTjAWeYZBVHnK4reK78FYpsqe6Z2PvU27",
"asset": "SOL",
"amount": 0.5,
"direction": "credit"
}
],
"state_changes": ["Sent 0.5 SOL", "Received 0.5 SOL"],
"programs_invoked": [
{ "name": "system", "id": "11111111111111111111111111111111" }
]
},
"counterparties": [
{
"address": "6AwuGoRLd54NTjAWeYZBVHnK4reK78FYpsqe6Z2PvU27",
"source": "asset-diff",
"severity": "none",
"triggered_signals": []
}
],
"findings": [],
"notes": [],
"timestamp": "2026-10-02T09:05:19.815Z",
"meta": {
"counterparties_screened": 1,
"counterparties_screened_complete": true,
"coverage_truncated": false,
"poisoning_screened": true,
"simulate_ms": 106,
"screening_ms": 450,
"poisoning_ms": 1295,
"intent_ms": 0,
"total_ms": 1412
}
}
42RLPACwZPx3vYYmxSueqsogfynBDqXK298EDsNoyoHi returns that counterparty with severity: "severe" and ofac-sanctioned in triggered_signals, so the scan severity is severe.
Response Fields
| Field | Type | Description |
|---|---|---|
screen_id | string | ID of this scan. Pass it to Fetch Assessment Evidence. |
network | string | Always solana. |
payload_sha256 | string | SHA-256 of the decoded transaction bytes as sent, signature slots included. The same payload always gives the same value; the signed transaction hashes differently. |
signer | string | The signing account the scan assumed: signer_address when it is a required signer, otherwise the fee payer. |
severity | string | none, low, medium, high, or severe. The worst single result across counterparties and findings. |
intent | null | Reserved for the transaction’s extracted intent (summary, action, confidence). Intent extraction is not enabled, so intent is always null. |
simulation | object | null | Predicted effects of executing the transaction. null when the transaction could not be simulated. |
counterparties | array | Each screened wallet with its source, Address Risk (v2) severity, and triggered_signals. |
findings | array | Transaction-level signals. See Findings. |
notes | array | Coverage caveats as code and message. See Notes. |
timestamp | string | When the scan ran (ISO 8601). |
meta | object | Coverage flags and timings. See Meta. |
Simulation
| Field | Type | Description |
|---|---|---|
simulation_success | boolean | false when the transaction would fail if submitted. A failed transaction also raises the simulation-error finding. |
asset_diffs | array | One entry per balance change: account, owner (the wallet behind a token account, when resolved), asset (token mint, or SOL), amount in token units, and direction (credit or debit). |
state_changes | array | Human-readable predicted changes, for example Sent 0.5 SOL. |
programs_invoked | array | Programs the transaction calls, as name and id. Programs are listed here and are not screened. |
Findings
Each finding has asignal, a severity, a human-readable caption, and signal-specific evidence.
| Signal | Severity | Fires when |
|---|---|---|
address-poisoning | high, medium, low | A recipient imitates another address. evidence names the address and the address it resembles. |
authority-change | medium | An instruction changes a token account or mint authority (setAuthority), directly or through CPI. |
owner-reassignment | medium | An instruction reassigns an account’s owner program (assign, assignWithSeed), directly or through CPI. |
simulation-error | medium | The simulation ran and the transaction failed. |
high-instruction-count | medium | The transaction has more than 12 instructions. |
simulation-unavailable | medium | Range could not simulate the transaction. Only the declared accounts were screened. |
coverage-truncated | medium | More accounts were selected than the 50-account cap allows. The rest were not screened. |
counterparty-owner-unresolved | medium | A credited token account could not be resolved to its owner wallet, so that recipient was not screened. |
counterparty-screening-partial | medium | At least one counterparty screen did not complete. evidence.addresses lists them. |
large-transfer | low | A credit exceeds 1 unit of its asset. Informational only; it never affects severity. |
simulation-error and simulation-unavailable are different. simulation-error is a property of the transaction: it would fail onchain. simulation-unavailable is a gap in Range’s coverage: the scan could not predict what the transaction does.Notes
| Code | Meaning |
|---|---|
signer-address-ignored | signer_address is not a required signer of the transaction; the fee payer was used. |
simulation-unavailable | Predicted balance and permission changes could not be produced. |
coverage-truncated | Counterparty screening stopped at 50 accounts. |
counterparty-owner-unresolved | Some token accounts had no resolvable owner and were excluded from counterparty and poisoning screening. |
counterparty-screening-partial | Some counterparty screens failed. Their severity reads none; treat them as not screened. |
poisoning-not-screened | The address-poisoning check did not complete. |
poisoning-coverage-truncated | The transaction credits more than 100 wallets; only the first 100 were checked for poisoning. |
Meta
| Field | Type | Description |
|---|---|---|
counterparties_screened | integer | Number of entries in counterparties. |
counterparties_screened_complete | boolean | false when any selected counterparty was not screened: a failed screen, an unresolved token account owner, or the 50-account cap. |
coverage_truncated | boolean | true when the 50-account cap dropped accounts. |
poisoning_screened | boolean | false when the poisoning check did not run or did not complete. Treat recipients as not checked. Also true when there were no recipients to check. |
simulate_ms, screening_ms, poisoning_ms, intent_ms, total_ms | integer | Time spent in each stage, in milliseconds. Screening and poisoning run in parallel. intent_ms is 0 while intent extraction is not enabled. |
Acting on the Result
A pre-sign gate for a wallet or dApp:function decide(scan: {
severity: 'none' | 'low' | 'medium' | 'high' | 'severe';
meta: { poisoning_screened: boolean };
}) {
if (scan.severity === 'severe' || scan.severity === 'high') return 'block';
if (!scan.meta.poisoning_screened) return 'warn'; // recipients not checked for lookalikes
if (scan.severity === 'medium') return 'warn'; // includes coverage gaps
return 'allow';
}
findings[].caption and every counterparty with a severity above none. Do not warn on every finding: large-transfer fires on most transfers and is context, not risk.
Errors
| HTTP Code | Cause | Recommended Action |
|---|---|---|
| 400 | network is not solana, transaction is empty or over 4,096 characters, encoding is not base64 or base58, or the payload does not decode (Invalid transaction payload) | Send the serialized unsigned transaction with the matching encoding. |
| 401 | Missing or invalid API key, or the key has no access to the Risk API | Check the key in the Range app. |
| 429 | Rate limit exceeded | Back off and retry. See Rate Limits. |
| 5xx | Internal error | Retry after a short delay. Do not treat a failed scan as clean. |
What’s Next
Wallet and dApp integration
Fetch Assessment Evidence
screen_id.List Transaction Scans
Transaction Simulation
Authorizations
Authorization method required to allow user to access the api endpoints.
Body
solana "solana"
The complete serialized unsigned transaction — account list, instructions, blockhash — as one encoded string (what a wallet holds immediately before signing).
1 - 4096"AQAAAAAAAAAA…"
Encoding of transaction. Defaults to base64.
base64, base58 The signing account. Honored only when it is a required signer of the payload; defaults to the fee payer derived from the payload.
64Origin (dApp URL) that produced this transaction. Recorded with the scan and forwarded to intent extraction.
2048Response
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
solana SHA-256 of the decoded payload bytes — the scan identity of this (still unsigned) transaction.
Worst single signal across counterparties and findings.
severe, high, medium, low, none Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
"2026-08-18T14:30:00Z"
Show child attributes
Show child attributes
The signing account this scan assumed.
LLM-extracted intent. Null when extraction was unavailable or timed out (see notes).
Show child attributes
Show child attributes
What signing actually does. Null when simulation failed.
Show child attributes
Show child attributes
Was this page helpful?