> ## Documentation Index
> Fetch the complete documentation index at: https://docs.range.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Scan an Unsigned Transaction

> Screens a whole Solana transaction before it is signed: simulation, Address Risk (v2) on counterparties, and address-poisoning checks.

Screens a complete transaction before the user signs it and returns one verdict for the whole transaction. Send the serialized unsigned transaction exactly as the wallet holds it before signing. Range simulates it, screens up to 50 wallets whose balances change or that gain control of an account, checks recipients for address poisoning, and runs transaction-level rules such as authority changes. This version supports Solana only.

The response uses the same severity ladder as [Score a Single Address](/risk-api/risk-v2/get-address-risk), so a wallet or dApp can apply one policy to address screens and transaction scans.

<Warning>
  **Gate on coverage, not only on `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.
</Warning>

***

## How the Scan Works

1. **Decode.** The `transaction` string is decoded with `encoding` (base64 by default). A payload that does not decode returns HTTP 400.
2. **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`.
3. **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 through `setAuthority`, `approve`, `approveChecked`, `assign`, or `assignWithSeed`, including instructions reached through cross-program invocation (CPI).
   * `static-account`: the accounts declared in the payload, used only when simulation is unavailable.

   The signer, invoked programs, and core Solana programs (System, SPL Token, Token-2022, Associated Token, Compute Budget, Memo, Stake, Vote, Address Lookup Table) are never screened. Up to 50 counterparties are screened per scan. When more are selected, permission targets take at most 25 of the slots and the rest go to the largest balance changes.
4. **Screen in parallel.** Each counterparty runs through Address Risk (v2), credited wallets are checked for address poisoning, and transaction rules run on the simulation.
5. **Combine.** `severity` is the worst single result across `counterparties` and `findings`, ignoring informational findings.

Every scan is stored for the workspace behind the API key. List them with [List Transaction Scans](/risk-api/risk-v2/list-transaction-scans) and fetch the full per-counterparty assessments with [Fetch Assessment Evidence](/risk-api/risk-v2/get-evidence) using `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](/risk-api/product-info/test-addresses). Copy it as is to try the endpoint.

<CodeGroup>
  ```bash cURL theme={null}
  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"
    }'
  ```

  ```javascript JavaScript theme={null}
  // `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();
  ```

  ```python Python theme={null}
  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()
  ```
</CodeGroup>

For a `VersionedTransaction`, encode it with `Buffer.from(transaction.serialize()).toString('base64')`; it serializes without requiring signatures.

## Example Response

```json theme={null}
{
  "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
  }
}
```

The same transfer sent to the sanctioned Solana test address `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](/risk-api/risk-v2/get-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](#findings). |
| `notes` | array | Coverage caveats as `code` and `message`. See [Notes](#notes). |
| `timestamp` | string | When the scan ran (ISO 8601). |
| `meta` | object | Coverage flags and timings. See [Meta](#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 a `signal`, 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`. |

<Note>
  `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.
</Note>

### 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:

```typescript theme={null}
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';
}
```

Show the user `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](/risk-api/product-info/rate-limits). |
| **5xx** | Internal error | Retry after a short delay. Do not treat a failed scan as clean. |

A failed simulation or a failed counterparty screen does not fail the request. The scan returns HTTP 200 with a finding and a note that describe the gap.

***

## What's Next

<CardGroup cols={2}>
  <Card title="Wallet and dApp integration" icon="wallet" href="/risk-api/guides/wallet-dapp-integration">
    Where the scan fits in a pre-sign flow.
  </Card>

  <Card title="Fetch Assessment Evidence" icon="magnifying-glass" href="/risk-api/risk-v2/get-evidence">
    Full per-counterparty assessments for a `screen_id`.
  </Card>

  <Card title="List Transaction Scans" icon="list" href="/risk-api/risk-v2/list-transaction-scans">
    Recent scans for your workspace.
  </Card>

  <Card title="Transaction Simulation" icon="flask" href="/risk-api/guides/transaction-simulation">
    Raw simulation without a risk verdict.
  </Card>
</CardGroup>


## OpenAPI

````yaml post /v2/risk/transaction/scan
openapi: 3.0.0
info:
  title: Range Risk API
  description: The Range Risk API for risk assessment of crypto addresses.
  version: 1.7.31
  contact: {}
servers:
  - url: https://api.range.org
    description: Range API Server
security:
  - Authorization: []
tags:
  - name: Risk
    description: Get risk information about a crypto address.
  - name: Risk v2
    description: >-
      Address risk assessment, workspace configuration, and signals catalogue
      (API v2).
  - name: Simulator
    description: Simulate a transaction on a network.
paths:
  /v2/risk/transaction/scan:
    post:
      tags:
        - Risk v2
      summary: Scan an Unsigned Transaction (pre-signature)
      description: >-
        Screens a whole transaction before it is signed: simulates it, screens
        every counterparty it touches through Address Risk v2, checks recipients
        for address poisoning, runs transaction-native rules, and extracts the
        declared intent. Returns one severity (worst single signal), the
        findings behind it, and the predicted effects. Evidence is retrievable
        via `GET /v2/risk/evidence/:screenId`. Solana only in this version.
      operationId: scan
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransactionScanRequestDto'
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionScanResponseDto'
components:
  schemas:
    TransactionScanRequestDto:
      type: object
      properties:
        network:
          type: string
          enum:
            - solana
          example: solana
        transaction:
          type: string
          minLength: 1
          maxLength: 4096
          description: >-
            The complete serialized unsigned transaction — account list,
            instructions, blockhash — as one encoded string (what a wallet holds
            immediately before signing).
          example: AQAAAAAAAAAA…
        encoding:
          enum:
            - base64
            - base58
          type: string
          description: Encoding of `transaction`. Defaults to base64.
        signer_address:
          type: string
          maxLength: 64
          description: >-
            The signing account. Honored only when it is a required signer of
            the payload; defaults to the fee payer derived from the payload.
        origin:
          type: string
          maxLength: 2048
          description: >-
            Origin (dApp URL) that produced this transaction. Recorded with the
            scan and forwarded to intent extraction.
      required:
        - network
        - transaction
    TransactionScanResponseDto:
      type: object
      properties:
        screen_id:
          type: string
          example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
        network:
          type: string
          enum:
            - solana
        payload_sha256:
          type: string
          description: >-
            SHA-256 of the decoded payload bytes — the scan identity of this
            (still unsigned) transaction.
        signer:
          type: string
          description: The signing account this scan assumed.
        severity:
          type: string
          enum:
            - severe
            - high
            - medium
            - low
            - none
          description: Worst single signal across counterparties and findings.
        intent:
          nullable: true
          description: >-
            LLM-extracted intent. Null when extraction was unavailable or timed
            out (see notes).
          type: object
          allOf:
            - $ref: '#/components/schemas/ScanIntentDto'
        simulation:
          nullable: true
          description: What signing actually does. Null when simulation failed.
          type: object
          allOf:
            - $ref: '#/components/schemas/ScanSimulationDto'
        counterparties:
          type: array
          items:
            $ref: '#/components/schemas/ScanCounterpartyDto'
        findings:
          type: array
          items:
            $ref: '#/components/schemas/ScanFindingDto'
        notes:
          type: array
          items:
            $ref: '#/components/schemas/ScanNoteDto'
        timestamp:
          type: string
          example: '2026-08-18T14:30:00Z'
        meta:
          $ref: '#/components/schemas/ScanMetaDto'
      required:
        - screen_id
        - network
        - payload_sha256
        - severity
        - counterparties
        - findings
        - notes
        - timestamp
        - meta
    ScanIntentDto:
      type: object
      properties:
        summary:
          type: string
          example: Swap 100 USDC for SOL on Jupiter
        action:
          type: string
          example: swap
        confidence:
          type: string
          example: high
          enum:
            - high
            - medium
            - low
      required:
        - summary
    ScanSimulationDto:
      type: object
      properties:
        simulation_success:
          type: boolean
          example: true
        asset_diffs:
          type: array
          items:
            $ref: '#/components/schemas/ScanAssetDiffDto'
        state_changes:
          description: Human-readable predicted state changes, from the simulator.
          type: array
          items:
            type: string
        programs_invoked:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              id:
                type: string
            required:
              - name
              - id
      required:
        - simulation_success
        - asset_diffs
        - state_changes
        - programs_invoked
    ScanCounterpartyDto:
      type: object
      properties:
        address:
          type: string
        source:
          type: string
          enum:
            - asset-diff
            - permission-target
            - static-account
          description: >-
            Why this account was screened. `static-account` appears only on the
            simulation-failed fallback path. Invoked programs are never screened
            — see `simulation.programs_invoked` for what the transaction calls.
        severity:
          type: string
          enum:
            - severe
            - high
            - medium
            - low
            - none
        triggered_signals:
          description: Signals that fired on this counterparty, across pillars.
          type: array
          items:
            type: string
      required:
        - address
        - source
        - severity
        - triggered_signals
    ScanFindingDto:
      type: object
      properties:
        signal:
          type: string
          example: authority-change
        severity:
          type: string
          enum:
            - severe
            - high
            - medium
            - low
        caption:
          type: string
          example: Token Authority … changed
        evidence:
          type: object
          description: Signal-specific supporting details.
      required:
        - signal
        - severity
        - caption
    ScanNoteDto:
      type: object
      properties:
        code:
          type: string
          example: coverage-truncated
        message:
          type: string
          example: Counterparty screening truncated to 50 accounts (63 selected).
      required:
        - code
        - message
    ScanMetaDto:
      type: object
      properties:
        counterparties_screened:
          type: number
          example: 12
        counterparties_screened_complete:
          type: boolean
          example: true
          description: >-
            False on any counterparty coverage gap: a screen failed (medium
            counterparty-screening-partial finding), a token account had no
            resolvable owner wallet (medium counterparty-owner-unresolved
            finding), or the selection was truncated at the cap (medium
            coverage-truncated finding). Either way the gap lifts the verdict.
            Analogous to poisoning_screened.
        coverage_truncated:
          type: boolean
          example: false
        poisoning_screened:
          type: boolean
          example: true
          description: >-
            False when the lookalike screen did not run or did not complete —
            treat as NOT SCREENED, never as clean. Vacuously true when the
            transaction credits no owner-resolvable recipients (recipients
            excluded by counterparty-owner-unresolved are covered by that
            finding, not by this flag).
        simulate_ms:
          type: number
          example: 420
        screening_ms:
          type: number
          example: 610
        poisoning_ms:
          type: number
          example: 480
        intent_ms:
          type: number
          example: 900
        total_ms:
          type: number
          example: 1450
      required:
        - counterparties_screened
        - counterparties_screened_complete
        - coverage_truncated
        - poisoning_screened
        - simulate_ms
        - screening_ms
        - poisoning_ms
        - intent_ms
        - total_ms
    ScanAssetDiffDto:
      type: object
      properties:
        account:
          type: string
          description: Account whose balance changes.
        owner:
          type: string
          description: >-
            Owner wallet when `account` is a token account; equals `account`
            otherwise.
        asset:
          type: string
          example: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
          description: Token mint, or `SOL` for native lamport changes.
        amount:
          type: number
          example: 100.5
        direction:
          type: string
          enum:
            - credit
            - debit
      required:
        - account
        - asset
        - amount
        - direction
  securitySchemes:
    Authorization:
      type: apiKey
      in: header
      name: X-API-KEY
      description: Authorization method required to allow user to access the api endpoints.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.