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

# Pay Offramp Orders on Solana

> Sign and broadcast the deposit transaction for an existing Solana offramp order.

Every offramp order returns a prepared deposit transaction that the payer must sign and broadcast before the order leaves the payment phase. On Solana, that transaction has its own format and a blockhash that can expire, so it can't be sent with the EVM steps in the quickstart. This guide shows how to sign and broadcast it — from a Crossmint wallet or your own Solana keypair — so Crossmint matches the deposit to the order and the payout can proceed.

For the complete offramp lifecycle, see the [REST API Quickstart](/offramp/quickstarts/rest-api).

## Prerequisites

You need:

* **Offramp order:** created with `payment.method: "solana"` via [Create Order](/offramp/api-reference/create-order).
* **Payer wallet:** a Crossmint wallet on `chain: "solana"` with a configured signer, or an external Solana wallet whose public key matches the order's `payment.payerAddress`.
* **Funds:** enough USDC to pay the order and enough SOL to cover the network fee.

## Pay the Order

<Tabs>
  <Tab title="Crossmint wallet">
    The prepared blockhash may expire before the user approves the transaction. To avoid signing an expired transaction, fetch a current blockhash from your server and rebuild the transaction before sending it to the wallet.

    ```tsx theme={null} theme={null}
    import { useWallet, SolanaWallet } from "@crossmint/client-sdk-react-ui";
    import { Transaction } from "@solana/web3.js";
    import bs58 from "bs58";

    type OfframpOrder = {
        orderId: string;
        payment: {
            preparation: {
                serializedTransaction: string;
            };
        };
    };

    export function PayOfframpOrder({ order }: { order: OfframpOrder }) {
        const { wallet } = useWallet();

        async function pay() {
            if (wallet == null) {
                throw new Error("Wallet not ready");
            }

            const prepared = Transaction.from(
                bs58.decode(order.payment.preparation.serializedTransaction),
            );

            const blockhash = await fetch("/api/solana-blockhash").then(
                (response) => response.text(),
            );

            const transaction = new Transaction({
                feePayer: prepared.feePayer,
                recentBlockhash: blockhash,
            });

            transaction.add(...prepared.instructions);

            const { hash } = await SolanaWallet.from(wallet).sendTransaction({
                serializedTransaction: bs58.encode(
                    transaction.serialize({
                        requireAllSignatures: false,
                        verifySignatures: false,
                    }),
                ),
            });

            console.log(`https://explorer.solana.com/tx/${hash}`, order.orderId);
        }

        return <button onClick={pay}>Cash out</button>;
    }
    ```

    This copies every instruction from the prepared transaction while replacing only its blockhash.

    See [Custom Solana Transactions](/wallets/guides/send-transaction-solana) for wallet and signer setup.
  </Tab>

  <Tab title="External wallet">
    Use this flow when you control the payer wallet's keypair. The example verifies the payer, replaces the expiring blockhash, signs the transaction, and broadcasts it through your Solana RPC endpoint. `order` is the response from [Create Order](/offramp/api-reference/create-order) or [Get Order](/offramp/api-reference/get-order).

    ```typescript theme={null} theme={null}
    import {
        Connection,
        Keypair,
        SendTransactionError,
        Transaction,
    } from "@solana/web3.js";
    import bs58 from "bs58";

    const connection = new Connection(
        process.env.SOLANA_RPC_URL!,
        "confirmed",
    );

    const payer = Keypair.fromSecretKey(
        bs58.decode(process.env.SOLANA_PRIVATE_KEY!),
    );

    type OfframpOrder = {
        orderId: string;
        payment: {
            preparation: {
                serializedTransaction: string;
            };
        };
    };

    export async function payOfframpOrder(order: OfframpOrder) {
        const prepared = Transaction.from(
            bs58.decode(order.payment.preparation.serializedTransaction),
        );

        if (prepared.feePayer?.equals(payer.publicKey) !== true) {
            throw new Error(
                `Order expects payer ${prepared.feePayer?.toBase58()}`,
            );
        }

        const { blockhash, lastValidBlockHeight } =
            await connection.getLatestBlockhash("confirmed");

        const transaction = new Transaction({
            feePayer: prepared.feePayer,
            recentBlockhash: blockhash,
        });

        transaction.add(...prepared.instructions);
        transaction.partialSign(payer);

        let signature: string;

        try {
            signature = await connection.sendRawTransaction(
                transaction.serialize(),
            );
        } catch (error) {
            if (error instanceof SendTransactionError) {
                console.error(await error.getLogs(connection));
            }

            throw error;
        }

        await connection.confirmTransaction(
            { signature, blockhash, lastValidBlockHeight },
            "confirmed",
        );

        console.log(
            `https://explorer.solana.com/tx/${signature}`,
            order.orderId,
        );
    }
    ```

    `serializedTransaction` is base58-encoded. If the transaction fails to send, `SendTransactionError.getLogs()` returns the Solana program logs.
  </Tab>
</Tabs>

## Why the Blockhash Must Be Replaced

Solana rejects transactions after their blockhash expires. The prepared transaction's blockhash expires roughly a minute after Crossmint builds it, well before the order's `quote.expiresAt`.

For this reason, do not treat `quote.expiresAt` as the transaction-signing deadline. Fetch a current blockhash immediately before signing.

Replacing the blockhash does not require a new order or quote. Copy the original transaction's fee payer and instructions into a new transaction, then sign it against the current blockhash.

## Confirm the Payout

A confirmed Solana transaction proves that the USDC left the payer's wallet. It does not prove that the fiat payout reached the recipient.

Use [Get Order](/offramp/api-reference/get-order) or subscribe to the webhooks described in [Manage Orders](/offramp/guides/manage-orders).

## Next Steps

<CardGroup cols={2}>
  <Card title="Manage Orders" icon="list-check" href="/offramp/guides/manage-orders">
    Track order phases, delivery statuses, and webhooks.
  </Card>

  <Card title="Custom Solana Transactions" icon="wallet" href="/wallets/guides/send-transaction-solana">
    Wallet and signer setup per platform.
  </Card>
</CardGroup>
