---
title: Transaction Structure
description:
  Anatomy of a Solana transaction including signatures, message format, header,
  account addresses, blockhash, compiled instructions, binary encoding, and size
  budget.
url: /docs/core/transactions/transaction-structure
type: conceptual
prerequisites:
  - /docs/core/transactions
  - /docs/core/instructions/instruction-structure
related:
  - /docs/core/transactions/versioned-transactions
  - /docs/core/transactions/transaction-pipeline
  - /docs/core/fees/fee-structure
  - /docs/core/constants-reference
---

{/* TOC: Signatures, Message (Header, Account addresses, Recent blockhash, Instructions), Transaction binary format, SOL transfer example */}

<Callout type="info" title="Summary">
  A transaction has signatures + a message. The message contains a header,
  account addresses, recent blockhash, and compiled instructions. Max serialized
  size: 1,232 bytes.
</Callout>

A
[`Transaction`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/transaction/src/lib.rs#L193)
has two top-level fields:

<WithMentions>

- [`signatures`](mention:signatures): An array of signatures
- [`message`](mention:message): Transaction information, including the list of
  instructions to be processed

```rust title="Transaction"
pub struct Transaction {
    // !mention signatures
    pub signatures: Vec<Signature>,
    // !mention message
    pub message: Message,
}
```

</WithMentions>

![Diagram showing the two parts of a transaction](/assets/docs/core/transactions/tx_format.png)

The total serialized size of a transaction must not exceed
[`PACKET_DATA_SIZE`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/packet/src/lib.rs#L32)
(1,232 bytes). This limit equals 1,280 bytes (the IPv6 minimum MTU) minus 48
bytes for network headers (40 bytes IPv6 + 8 bytes fragment header). The 1,232
bytes include both the [`signatures`](#signatures) array and the
[`message`](#message) struct.

![Diagram showing the transaction format and size limits](/assets/docs/core/transactions/issues_with_legacy_txs.png)

## Signatures

The `signatures` field is a compact-encoded array of
[`Signature`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/signature/src/lib.rs#L34)
values. Each `Signature` is a 64-byte Ed25519 signature of the serialized
`Message`, signed with the signer account's private key. One signature is
required for every [signer account](#account-addresses) referenced by the
transaction's instructions.

Each signature is produced by a private key. Where that key lives — a local
keypair, a cloud HSM or KMS, or a managed wallet service — is a production
design decision. See
[Signing in Production](/docs/core/transactions/signing-in-production).

The first signature in the array belongs to the **fee payer**, the account that
pays the transaction
[base fee and prioritization fee](/docs/core/fees/fee-structure#base-fee). This
first signature also serves as the **transaction ID**, used to look up the
transaction on the network. The transaction ID is commonly referred to as the
**transaction signature**.

Fee payer requirements:

- Must be the first account in the message (index 0) and a signer.
- Must be a System Program-owned account or a nonce account (validated by
  [`validate_fee_payer`](https://github.com/anza-xyz/agave/blob/v3.1.8/svm/src/account_loader.rs#L369-L417)).
- Must hold enough lamports to cover `rent_exempt_minimum + total_fee`;
  otherwise the transaction fails with _rs`InsufficientFundsForFee`_.

## Message

The `message` field is a
[`Message`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/message/src/legacy.rs#L161)
struct containing the transaction's payload:

<WithMentions>

- [`header`](mention:message-header): The message [header](#header)
- [`account_keys`](mention:account-addresses): An array of
  [account addresses](#account-addresses) required by the transaction's
  instructions
- [`recent_blockhash`](mention:recent-blockhash): A
  [blockhash](#recent-blockhash) that acts as a timestamp for the transaction
- [`instructions`](mention:instructions): An array of
  [instructions](#instructions)

```rust title="Message"
pub struct Message {
    /// The message header, identifying signed and read-only `account_keys`.
    // !mention message-header
    pub header: MessageHeader,

    /// All the account keys used by this transaction.
    #[serde(with = "short_vec")]
    // !mention account-addresses
    pub account_keys: Vec<Pubkey>,

    /// The id of a recent ledger entry.
    // !mention recent-blockhash
    pub recent_blockhash: Hash,

    /// Programs that will be executed in sequence and committed in
    /// one atomic transaction if all succeed.
    #[serde(with = "short_vec")]
    // !mention instructions
    pub instructions: Vec<CompiledInstruction>,
}
```

</WithMentions>

### Header

<WithMentions>

The `header` field is a
[`MessageHeader`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/message/src/lib.rs#L109)
struct with three `u8` fields that partition the `account_keys` array into
permission groups:

- [`num_required_signatures`](mention:num_required_signatures): Total number of
  signatures required by the transaction.
- [`num_readonly_signed_accounts`](mention:num_readonly_signed_accounts): Number
  of signed accounts that are read-only.
- [`num_readonly_unsigned_accounts`](mention:num_readonly_unsigned_accounts):
  Number of unsigned accounts that are read-only.

```rust title="MessageHeader"
pub struct MessageHeader {
    /// The number of signatures required for this message to be considered
    /// valid. The signers of those signatures must match the first
    /// `num_required_signatures` of [`Message::account_keys`].
    // !mention num_required_signatures
    pub num_required_signatures: u8,

    /// The last `num_readonly_signed_accounts` of the signed keys are read-only
    /// accounts.
    // !mention num_readonly_signed_accounts
    pub num_readonly_signed_accounts: u8,

    /// The last `num_readonly_unsigned_accounts` of the unsigned keys are
    /// read-only accounts.
    // !mention num_readonly_unsigned_accounts
    pub num_readonly_unsigned_accounts: u8,
}
```

</WithMentions>

<Callout type="info" title="Legacy and versioned message prefixes">
  In a legacy transaction message, the first message byte is
  `num_required_signatures`, followed by the other two `MessageHeader` bytes. In
  a versioned transaction message, the first byte is a version prefix instead;
  the three-byte `MessageHeader` starts immediately after that prefix. See
  [versioned transactions](/docs/core/transactions/versioned-transactions) for
  the full v0 message layout.
</Callout>

![Diagram showing the three parts of the message header](/assets/docs/core/transactions/message_header.png)

### Account addresses

The
[`account_keys`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/message/src/legacy.rs#L168)
field is a compact-encoded array of public keys. Each entry identifies an
account used by at least one of the transaction's instructions. The array must
include every account and must follow this strict ordering:

1. Signer + Writable
2. Signer + Read-only
3. Non-signer + Writable
4. Non-signer + Read-only

<Callout>
  This strict ordering allows the `account_keys` array to be combined with the
  three counts in the message's [`header`](#header) to determine the permissions
  for each account without storing per-account metadata flags. The header counts
  partition the array into the four permission groups listed above.
</Callout>

![Diagram showing the order of the account addresses array](/assets/docs/core/transactions/compat_array_of_account_addresses.png)

### Recent blockhash

The `recent_blockhash` field is a 32-byte hash that serves two purposes:

1. **Timestamp**: proves the transaction was created recently.
2. **Deduplication**: prevents the same transaction from being processed twice.

A blockhash expires after 150 slots. If the blockhash is no longer valid when
the transaction arrives, it is rejected with _rs`BlockhashNotFound`_, unless it
is a valid [durable nonce transaction](/docs/core/transactions/durable-nonces).

<Callout>
  The [`getLatestBlockhash`](/docs/rpc/http/getlatestblockhash) RPC method
  allows you to get the current blockhash and last block height at which the
  blockhash will be valid.
</Callout>

### Instructions

The
[`instructions`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/message/src/legacy.rs#L176)
field is a compact-encoded array of
[`CompiledInstruction`](https://github.com/anza-xyz/solana-sdk/blob/clock%40v2.2.3/message/src/compiled_instruction.rs#L21)
structs. Each _rs`CompiledInstruction`_ references accounts by index into the
`account_keys` array rather than by full public key. It contains:

<WithMentions>

1. [`program_id_index`](mention:program-id-index): Index into `account_keys`
   identifying the program to invoke.
2. [`accounts`](mention:account-indexes): Array of indices into `account_keys`
   specifying the accounts to pass to the program.
3. [`data`](mention:instruction-data): Byte array containing the instruction
   discriminator and serialized arguments.

```rust title="CompiledInstruction"
pub struct CompiledInstruction {
    /// Index into the transaction keys array indicating the program account that executes this instruction.
    // !mention program-id-index
    pub program_id_index: u8,
    /// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
    #[serde(with = "short_vec")]
    // !mention account-indexes
    pub accounts: Vec<u8>,
    /// The program input data.
    #[serde(with = "short_vec")]
    // !mention instruction-data
    pub data: Vec<u8>,
}
```

</WithMentions>

![Compact array of Instructions](/assets/docs/core/transactions/compact_array_of_ixs.png)

## Transaction binary format

Transactions are serialized using a compact encoding scheme. All variable-length
arrays (signatures, account keys, instructions) are prefixed with a compact-u16
length encoding. This format uses 1 byte for values 0-127 and 2-3 bytes for
larger values.

**Legacy transaction layout** (on the wire):

| Field                     | Size                          | Description                                     |
| ------------------------- | ----------------------------- | ----------------------------------------------- |
| `num_signatures`          | 1-3 bytes (compact-u16)       | Number of signatures                            |
| `signatures`              | `num_signatures` x 64 bytes   | Ed25519 signatures                              |
| `num_required_signatures` | 1 byte                        | `MessageHeader` field 1                         |
| `num_readonly_signed`     | 1 byte                        | `MessageHeader` field 2                         |
| `num_readonly_unsigned`   | 1 byte                        | `MessageHeader` field 3                         |
| `num_account_keys`        | 1-3 bytes (compact-u16)       | Number of static account keys                   |
| `account_keys`            | `num_account_keys` x 32 bytes | Public keys                                     |
| `recent_blockhash`        | 32 bytes                      | Blockhash                                       |
| `num_instructions`        | 1-3 bytes (compact-u16)       | Number of instructions                          |
| `instructions`            | variable                      | Array of [compiled instructions](#instructions) |

Each compiled instruction is serialized as:

| Field              | Size                    | Description                |
| ------------------ | ----------------------- | -------------------------- |
| `program_id_index` | 1 byte                  | Index into account keys    |
| `num_accounts`     | 1-3 bytes (compact-u16) | Number of account indices  |
| `account_indices`  | `num_accounts` x 1 byte | Account key indices        |
| `data_len`         | 1-3 bytes (compact-u16) | Length of instruction data |
| `data`             | `data_len` bytes        | Opaque instruction data    |

### Size calculation

Given _rs`PACKET_DATA_SIZE`_ = 1,232 bytes, the available space can be
calculated:

```
Total = 1232 bytes
 - compact-u16(num_sigs)          # 1 byte
 - num_sigs * 64                  # signature bytes
 - 3                              # message header
 - compact-u16(num_keys)          # 1 byte
 - num_keys * 32                  # account key bytes
 - 32                             # recent blockhash
 - compact-u16(num_ixs)           # 1 byte
 - sum(instruction_sizes)         # per-instruction overhead + data
```

## Example: SOL transfer transaction

The diagram below shows how transactions and instructions work together to allow
users to interact with the network. In this example, SOL is transferred from one
account to another.

The sender account's
[metadata](/docs/core/instructions/instruction-structure#account-metadata)
indicates that it must sign for the transaction. This allows the System Program
to deduct lamports. Both the sender and recipient accounts must be writable, in
order for their lamport balance to change. To execute this instruction, the
sender's wallet sends the transaction containing its signature and the message
containing the SOL transfer instruction.

![SOL transfer diagram](/assets/docs/core/transactions/sol-transfer.svg)

After the transaction is sent, the System Program processes the transfer
instruction and updates the lamport balance of both accounts.

![SOL transfer process diagram](/assets/docs/core/transactions/sol-transfer-process.svg)

<Callout type="warn" title="Verify the recipient before sending SOL">

A System Program transfer adds lamports to **any** account. There is no
protocol-level check that the recipient can move the SOL back out. Lamports can
only be moved out by the account's owning program, so sending SOL to a
[token mint](/docs/tokens/basics/create-mint), a program, or a
[PDA](/docs/core/pda) you don't control **risks permanent loss of funds** — only
an authority specified by the owning program can return them. SOL sent to a
[token account](/docs/tokens/basics/create-token-account) is recoverable only by
that account's owner, never by the sender.

SPL **token** transfers are partly self-protecting: the Token Program rejects a
transfer whose accounts don't match the expected mint. Native **SOL** transfers
have no such guard, so the sender must verify the recipient before signing. See
[Verify Address](/docs/payments/send-payments/verify-address) for the full
classification logic.

</Callout>

The example below shows the code relevant to the above diagrams. See the System
Program's
[`transfer` function](https://github.com/anza-xyz/agave/blob/v3.1.8/programs/system/src/system_processor.rs#L210).

<CodeTabs storage="sol-transfer" flags="r">

```ts !! title="Kit"
import { createClient, generateKeyPairSigner, lamports } from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";

const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)))
  .use(systemProgram());

const sender = client.payer;
const recipient = await generateKeyPairSigner();

const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL

// Check balance before transfer
const { value: preBalance1 } = await client.rpc
  .getBalance(sender.address)
  .send();
const { value: preBalance2 } = await client.rpc
  .getBalance(recipient.address)
  .send();

// !mark(1:6)
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
  source: sender,
  destination: recipient.address,
  amount: transferAmount // 0.01 SOL in lamports
});

const transactionSignature = await client.sendTransaction([
  transferInstruction
]);

// Check balance after transfer
const { value: postBalance1 } = await client.rpc
  .getBalance(sender.address)
  .send();
const { value: postBalance2 } = await client.rpc
  .getBalance(recipient.address)
  .send();

console.log(
  "Sender prebalance:",
  Number(preBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
  "Recipient prebalance:",
  Number(preBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log(
  "Sender postbalance:",
  Number(postBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
  "Recipient postbalance:",
  Number(postBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log("Transaction Signature:", transactionSignature.context.signature);
```

```ts !! title="Legacy"
import {
  LAMPORTS_PER_SOL,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
  Keypair,
  Connection
} from "@solana/web3.js";

// Create a connection to cluster
const connection = new Connection("http://localhost:8899", "confirmed");

// Generate sender and recipient keypairs
const sender = Keypair.generate();
const recipient = new Keypair();

// Fund sender with airdrop
const airdropSignature = await connection.requestAirdrop(
  sender.publicKey,
  LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature, "confirmed");

// Check balance before transfer
const preBalance1 = await connection.getBalance(sender.publicKey);
const preBalance2 = await connection.getBalance(recipient.publicKey);

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL

// !mark(1:6)
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = SystemProgram.transfer({
  fromPubkey: sender.publicKey,
  toPubkey: recipient.publicKey,
  lamports: transferAmount * LAMPORTS_PER_SOL // Convert transferAmount to lamports
});

// Add the transfer instruction to a new transaction
const transaction = new Transaction().add(transferInstruction);

// Send the transaction to the network
const transactionSignature = await sendAndConfirmTransaction(
  connection,
  transaction,
  [sender] // signer
);

// Check balance after transfer
const postBalance1 = await connection.getBalance(sender.publicKey);
const postBalance2 = await connection.getBalance(recipient.publicKey);

console.log("Sender prebalance:", preBalance1 / LAMPORTS_PER_SOL);
console.log("Recipient prebalance:", preBalance2 / LAMPORTS_PER_SOL);
console.log("Sender postbalance:", postBalance1 / LAMPORTS_PER_SOL);
console.log("Recipient postbalance:", postBalance2 / LAMPORTS_PER_SOL);
console.log("Transaction Signature:", transactionSignature);
```

```rs !! title="Rust"
use anyhow::Result;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{
    native_token::LAMPORTS_PER_SOL, signature::Signer, signer::keypair::Keypair,
    transaction::Transaction,
};
use solana_system_interface::instruction::transfer;

#[tokio::main]
async fn main() -> Result<()> {
    // Create a connection to cluster
    let connection = RpcClient::new_with_commitment(
        "http://localhost:8899".to_string(),
        CommitmentConfig::confirmed(),
    );

    // Generate sender and recipient keypairs
    let sender = Keypair::new();
    let recipient = Keypair::new();

    // Fund sender with airdrop
    let airdrop_signature = connection
        .request_airdrop(&sender.pubkey(), LAMPORTS_PER_SOL)
        .await?;
    loop {
        let confirmed = connection.confirm_transaction(&airdrop_signature).await?;
        if confirmed {
            break;
        }
    }

    // Check balance before transfer
    let pre_balance1 = connection.get_balance(&sender.pubkey()).await?;
    let pre_balance2 = connection.get_balance(&recipient.pubkey()).await?;

    // Define the amount to transfer
    let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOL

    // Create a transfer instruction for transferring SOL from sender to recipient
    let transfer_instruction = transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);

    // Add the transfer instruction to a new transaction
    let mut transaction =
        Transaction::new_with_payer(&[transfer_instruction], Some(&sender.pubkey()));
    let blockhash = connection.get_latest_blockhash().await?;
    transaction.sign(&[&sender], blockhash);

    // Send the transaction to the network
    let transaction_signature = connection
        .send_and_confirm_transaction(&transaction)
        .await?;

    // Check balance after transfer
    let post_balance1 = connection.get_balance(&sender.pubkey()).await?;
    let post_balance2 = connection.get_balance(&recipient.pubkey()).await?;

    println!(
        "Sender prebalance: {}",
        pre_balance1 as f64 / LAMPORTS_PER_SOL as f64
    );
    println!(
        "Recipient prebalance: {}",
        pre_balance2 as f64 / LAMPORTS_PER_SOL as f64
    );
    println!(
        "Sender postbalance: {}",
        post_balance1 as f64 / LAMPORTS_PER_SOL as f64
    );
    println!(
        "Recipient postbalance: {}",
        post_balance2 as f64 / LAMPORTS_PER_SOL as f64
    );
    println!("Transaction Signature: {}", transaction_signature);

    Ok(())
}
```

</CodeTabs>

The following example shows the structure of a transaction that contains a
single SOL transfer instruction.

<CodeTabs storage="sol-transfer" flags="r">

```ts !! title="Kit"
import {
  createClient,
  generateKeyPairSigner,
  lamports,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  pipe,
  signTransactionMessageWithSigners,
  getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";

const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)))
  .use(systemProgram());

const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();

const sender = client.payer;
const recipient = await generateKeyPairSigner();

// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL

// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
  source: sender,
  destination: recipient.address,
  amount: transferAmount
});

// Create transaction message
const transactionMessage = pipe(
  createTransactionMessage({ version: 0 }),
  (tx) => setTransactionMessageFeePayerSigner(sender, tx),
  (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
  (tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);

const signedTransaction =
  await signTransactionMessageWithSigners(transactionMessage);

// Decode the messageBytes
const compiledTransactionMessage =
  getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);

console.log(JSON.stringify(compiledTransactionMessage, null, 2));
```

```ts !! title="Legacy"
import {
  LAMPORTS_PER_SOL,
  SystemProgram,
  Transaction,
  Keypair,
  Connection
} from "@solana/web3.js";

const connection = new Connection("http://localhost:8899", "confirmed");
const { blockhash, lastValidBlockHeight } =
  await connection.getLatestBlockhash();

// Generate sender and recipient keypairs
const sender = Keypair.generate();
const recipient = new Keypair();

// Define the amount to transfer
const transferAmount = 0.01; // 0.01 SOL

// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = SystemProgram.transfer({
  fromPubkey: sender.publicKey,
  toPubkey: recipient.publicKey,
  lamports: transferAmount * LAMPORTS_PER_SOL // Convert transferAmount to lamports
});

const transaction = new Transaction({
  blockhash,
  lastValidBlockHeight,
  feePayer: sender.publicKey
}).add(transferInstruction);
transaction.sign(sender);

const compiledMessage = transaction.compileMessage();
console.log(JSON.stringify(compiledMessage, null, 2));
```

```rs !! title="Rust"
use anyhow::Result;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{
    native_token::LAMPORTS_PER_SOL, signature::Signer, signer::keypair::Keypair,
    transaction::Transaction,
};
use solana_system_interface::instruction::transfer;

#[tokio::main]
async fn main() -> Result<()> {
    let connection = RpcClient::new_with_commitment(
        "http://localhost:8899".to_string(),
        CommitmentConfig::confirmed(),
    );

    // Fetch the latest blockhash and last valid block height
    let blockhash = connection.get_latest_blockhash().await?;

    // Generate sender and recipient keypairs
    let sender = Keypair::new();
    let recipient = Keypair::new();

    // Create a transfer instruction for transferring SOL from sender to recipient
    let transfer_instruction = transfer(
        &sender.pubkey(),
        &recipient.pubkey(),
        LAMPORTS_PER_SOL / 100, // 0.01 SOL
    );

    let mut transaction =
        Transaction::new_with_payer(&[transfer_instruction], Some(&sender.pubkey()));
    transaction.sign(&[&sender], blockhash);

    println!("{:#?}", transaction);

    Ok(())
}
```

</CodeTabs>

<WithMentions>
The code below shows the output from the previous code snippets.
The format differs between SDKs,
but notice that each instruction contains the same required information.

<CodeTabs storage="sol-transfer">

```json !! title="Kit"
{
  "version": 0,
  // !mention header
  "header": {
    "numSignerAccounts": 1,
    "numReadonlySignerAccounts": 0,
    "numReadonlyNonSignerAccounts": 1
  },
  // !mention account_keys
  "staticAccounts": [
    "HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa",
    "5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL",
    "11111111111111111111111111111111"
  ],
  // !mention recent_blockhash
  "lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1",
  // !mention instructions
  "instructions": [
    {
      "programAddressIndex": 2,
      "accountIndices": [0, 1],
      "data": {
        "0": 2,
        "1": 0,
        "2": 0,
        "3": 0,
        "4": 128,
        "5": 150,
        "6": 152,
        "7": 0,
        "8": 0,
        "9": 0,
        "10": 0,
        "11": 0
      }
    }
  ]
}
```

```json !! title="Legacy"
{
  // !mention header
  "header": {
    "numRequiredSignatures": 1,
    "numReadonlySignedAccounts": 0,
    "numReadonlyUnsignedAccounts": 1
  },
  // !mention account_keys
  "accountKeys": [
    "EPLUagqZZAuAtJ5LSbK7eeXjqeTdesd4q8WhoqVrfG3g",
    "9Txf5pi5jzm7FydFAsQafk7xn5wY9yN2UNm5LW15qvcK",
    "11111111111111111111111111111111"
  ],
  "recentBlockhash": "2qYPgehzMKXcMt4Ku1tKAk9DACKUbtYEY9EUEN42cseT",
  // !mention instructions
  "instructions": [
    {
      "programIdIndex": 2,
      "accounts": [0, 1],
      "data": "3Bxs4NN8M2Yn4TLb"
    }
  ],
  "indexToProgramIds": {}
}
```

```json !! title="Rust"
{
  "signatures": [
    "2fPXZtQGWWj6suxfc55FBQiexS8hEhNELqasSL5DRYa1RB1GChHz86Cyy8ukiVwA6qbq91P4cY1FuvTuYtmTHmJP"
  ],
  "message": {
    // !mention header
    "header": {
      "num_required_signatures": 1,
      "num_readonly_signed_accounts": 0,
      "num_readonly_unsigned_accounts": 1
    },
    "account_keys": [
      "9CpbtdXfUTgLMJL8DEAeEm8thERJPwDuruohjvUuzY7m",
      "6jELNgS8Q35sF4QZCvwgyKGaKrbcm8P5QcNWUyAb5ekJ",
      "11111111111111111111111111111111"
    ],
    "recent_blockhash": "3P7CVQ9nwXx4B37MvBzghzbcM9K9p5xo7ivDE8W78dCi",
    // !mention instructions
    "instructions": [
      {
        "program_id_index": 2,
        "accounts": [0, 1],
        "data": [2, 0, 0, 0, 128, 150, 152, 0, 0, 0, 0, 0]
      }
    ]
  }
}
```

</CodeTabs>
</WithMentions>

### Verify the recipient before transferring

Because a SOL transfer succeeds into any account, check the recipient before
signing. Fetch the account and only send to a System Program wallet (or an
unfunded
[on-curve](/docs/payments/send-payments/verify-address#on-curve-addresses)
address); reject mints, token accounts, programs, and PDAs you don't control.

<CodeTabs flags="r">

```ts !! title="Kit"
import {
  type Address,
  createSolanaRpc,
  fetchJsonParsedAccount,
  isOffCurveAddress
} from "@solana/kit";

const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");

const SYSTEM_PROGRAM = "11111111111111111111111111111111" as Address;

/**
 * Throws if `recipient` cannot safely receive native SOL.
 *
 * Only System Program wallets (or unfunded on-curve addresses) are safe. Any
 * other account locks the lamports because no authority can debit them.
 */
async function assertSafeSolRecipient(recipient: Address): Promise<void> {
  const account = await fetchJsonParsedAccount(rpc, recipient);

  if (!account.exists) {
    // Off-curve = a PDA with no account; reject conservatively.
    if (isOffCurveAddress(recipient)) {
      throw new Error(
        "Recipient is a PDA with no account; SOL would be locked"
      );
    }
    // On-curve = an unfunded wallet, safe to fund.
    return;
  }

  if (account.programAddress !== SYSTEM_PROGRAM) {
    throw new Error(
      `Recipient is owned by ${account.programAddress}, not a wallet; SOL would be locked`
    );
  }
}

// A wallet: safe.
await assertSafeSolRecipient(
  "H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" as Address
);

// The USDC mint: rejected before any SOL leaves the sender.
await assertSafeSolRecipient(
  "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as Address
);
```

</CodeTabs>

<Callout>
  This snippet checks native SOL recipients. For the full classification that
  also handles SPL token sends (token accounts, ATAs, Token-2022), see [Verify
  Address](/docs/payments/send-payments/verify-address).
</Callout>

## Fetching transaction details

After submission, retrieve transaction details using the transaction signature
and the [getTransaction](/docs/rpc/http/gettransaction) RPC method.

<Callout>
  You can also find the transaction using [Solana
  Explorer](https://explorer.solana.com).
</Callout>

```json title="Transaction Data"
{
  "blockTime": 1745196488,
  "meta": {
    "computeUnitsConsumed": 150,
    "err": null,
    "fee": 5000,
    "innerInstructions": [],
    "loadedAddresses": {
      "readonly": [],
      "writable": []
    },
    "logMessages": [
      "Program 11111111111111111111111111111111 invoke [1]",
      "Program 11111111111111111111111111111111 success"
    ],
    "postBalances": [989995000, 10000000, 1],
    "postTokenBalances": [],
    "preBalances": [1000000000, 0, 1],
    "preTokenBalances": [],
    "rewards": [],
    "status": {
      "Ok": null
    }
  },
  "slot": 13049,
  "transaction": {
    "message": {
      "header": {
        "numReadonlySignedAccounts": 0,
        "numReadonlyUnsignedAccounts": 1,
        "numRequiredSignatures": 1
      },
      "accountKeys": [
        "8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ",
        "7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma",
        "11111111111111111111111111111111"
      ],
      "recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf",
      "instructions": [
        {
          "accounts": [0, 1],
          "data": "3Bxs4NN8M2Yn4TLb",
          "programIdIndex": 2,
          "stackHeight": null
        }
      ],
      "indexToProgramIds": {}
    },
    "signatures": [
      "3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"
    ]
  },
  "version": "legacy"
}
```

The raw response identifies accounts by index and stores inner (CPI)
instructions as encoded blobs. To resolve these into addresses and walk the full
instruction tree, see
[Transaction Introspection](/docs/core/transactions/transaction-introspection).
