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

> ## Agent Instructions
> Bitnob API requests authenticate with an `X-Auth-Client` API key header against https://api.bitnob.com.
> Always use the sandbox environment for testing. Never put live API keys in sample code.
> Money amounts are integers in the smallest unit of the currency (satoshis for BTC, cents for fiat). Never use floats.
> Every state-changing request takes a client-supplied `reference` and must be treated as idempotent on retry.
> Quotes expire. Re-quote rather than reusing a stale quote id.

# Send your first sponsored transfer on Solana

> Quote a gasless USDC transfer on Solana devnet, sign it with the sender's key, submit the signature, and track it to completed.

This guide sends 1.00 USDC on Solana devnet from an address that holds no SOL. Solana is the simplest scheme to start with: one signature over one message.

## Before you start

* A sandbox API key exported as `PLATFORM_API_KEY`. See [Set up your account](https://www.bitnob.dev/docs/platform/account-setup.md).
* A test sender: a Solana keypair holding devnet USDC and **no SOL**. Get devnet USDC from [Circle's faucet](https://faucet.circle.com). Export the base58 secret key as `SENDER_SECRET_KEY`. Use a key you made for testing only.

  The sender must already hold the token. A quote for an address that has never held USDC returns `400 invalid_request` with "sender has no token account for this asset".
* A recipient address on devnet.
* Node.js 20 or later, with `@solana/web3.js`, `tweetnacl`, and `bs58` installed.

**1. Request a quote**

[Create sponsored transfer](https://www.bitnob.dev/api-reference/platform/sponsored-transfers/create-sponsored-transfer-quote.md) takes the chain, asset, sender, recipient, and amount. It requires an `Idempotency-Key`. Use one per transfer, so a retry returns the same quote instead of creating a second one.

```bash theme={null}
curl https://api.platform.bitnob.com/v1/sponsored-transfers \
  -H "X-API-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: transfer-0001" \
  -d '{
    "chain": "solana",
    "asset": "USDC",
    "from": "Sender1111111111111111111111111111111111111",
    "to": "Recipient1111111111111111111111111111111111",
    "amount": "1.00"
  }'
```

```json theme={null}
{
  "object": "sponsored_transfer",
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "quoted",
  "chain": "solana",
  "asset": "USDC",
  "from": "Sender1111111111111111111111111111111111111",
  "to": "Recipient1111111111111111111111111111111111",
  "recipient_amount": "1",
  "fee": { "amount": "0.004", "asset": "USDC", "usd": "0.004" },
  "total_debit": "1.004",
  "expires_at": "2026-05-16T10:33:00Z",
  "sign": {
    "scheme": "solana_ed25519",
    "signer": "Sender1111111111111111111111111111111111111",
    "message_base64": "AQACBf3m..."
  }
}
```

The recipient gets `1` and the sender is debited `1.004`. The fee here is an example: use the one in your quote. Before going on, check that `sign.scheme` is `solana_ed25519`.

**2. Show the user what they're signing**

In a real app, show `recipient_amount`, `fee`, `total_debit`, and the recipient before you ask for a signature, and get the user's confirmation. Request the quote when they're ready to confirm: it expires at `expires_at`, about three minutes later.

**3. Sign the message**

Decode `message_base64` and sign the raw bytes with the sender's key. Don't change or hash the message first.

```javascript sign.mjs theme={null}
import { Keypair } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";

export function signSolanaQuote(quote) {
  if (quote.sign.scheme !== "solana_ed25519") {
    throw new Error(`Unexpected scheme ${quote.sign.scheme}`);
  }

  const sender = Keypair.fromSecretKey(bs58.decode(process.env.SENDER_SECRET_KEY));
  if (sender.publicKey.toBase58() !== quote.sign.signer) {
    throw new Error("This key is not the quote's signer");
  }

  const message = Buffer.from(quote.sign.message_base64, "base64");
  const signature = nacl.sign.detached(message, sender.secretKey);
  return bs58.encode(signature);
}
```

In production the user's wallet does this step. Test your wallet integration in sandbox first: it must sign the bytes exactly as given, without adding a prefix.

**4. Submit the signature**

[Submit signatures](https://www.bitnob.dev/api-reference/platform/sponsored-transfers/submit-signatures.md) takes the base58 signature as `signature`. It also requires an `Idempotency-Key`.

```bash theme={null}
curl https://api.platform.bitnob.com/v1/sponsored-transfers/$TRANSFER_ID/submit \
  -H "X-API-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: transfer-0001-submit" \
  -d '{ "signature": "5Hx3kq...base58sig" }'
```

A `202` means Bitnob verified the signature, added its own, and queued the transfer for broadcast. A `422 signature_invalid` means the signature didn't match. The quote is still valid, so sign again and resubmit.

**5. Track it to completed**

Poll [Get sponsored transfer](https://www.bitnob.dev/api-reference/platform/sponsored-transfers/get-sponsored-transfer.md) until the status is terminal.

```bash theme={null}
curl https://api.platform.bitnob.com/v1/sponsored-transfers/$TRANSFER_ID \
  -H "X-API-Key: $PLATFORM_API_KEY"
```

The status moves from `validating` to `submitted` to `pending`, and then to `completed`. Once it's `pending`, `tx_hash` is set and you can look the transaction up on a devnet explorer. The sender's SOL balance is still zero.

## Put it together

```javascript transfer.mjs theme={null}
import { signSolanaQuote } from "./sign.mjs";

const api = (path, init = {}) =>
  fetch(`https://api.platform.bitnob.com/v1${path}`, {
    ...init,
    headers: {
      "X-API-Key": process.env.PLATFORM_API_KEY,
      "Content-Type": "application/json",
      ...init.headers,
    },
  }).then(async (res) => ({ status: res.status, body: await res.json() }));

const transferKey = `transfer-${crypto.randomUUID()}`;

const quote = await api("/sponsored-transfers", {
  method: "POST",
  headers: { "Idempotency-Key": transferKey },
  body: JSON.stringify({
    chain: "solana",
    asset: "USDC",
    from: process.env.SENDER_ADDRESS,
    to: process.env.RECIPIENT_ADDRESS,
    amount: "1.00",
  }),
});

const signature = signSolanaQuote(quote.body);

const submitted = await api(`/sponsored-transfers/${quote.body.id}/submit`, {
  method: "POST",
  headers: { "Idempotency-Key": `${transferKey}-submit` },
  body: JSON.stringify({ signature }),
});
if (submitted.status !== 202) throw new Error(JSON.stringify(submitted.body));

const terminal = new Set(["completed", "failed", "expired", "cancelled"]);
let transfer = submitted.body;
while (!terminal.has(transfer.status)) {
  await new Promise((r) => setTimeout(r, 3000));
  transfer = (await api(`/sponsored-transfers/${quote.body.id}`)).body;
}
console.log(transfer.status, transfer.tx_hash, transfer.failure_code ?? "");
```

Sponsored transfers aren't in the list of webhook event types yet, so polling is how you track them. Back off as the transfer ages: every few seconds while `pending`, and stop at a terminal status.

## Next steps

* Send on EVM chains, which need two signatures: [Sign on EVM chains](https://www.bitnob.dev/docs/platform/gas-sponsorship/sign-on-evm-chains.md).
* Send USDT on Tron, with the one-time approval: [Sign on Tron](https://www.bitnob.dev/docs/platform/gas-sponsorship/sign-on-tron.md).
* Handle expiry, budgets, and on-chain failures: [Handle failures and expiry](https://www.bitnob.dev/docs/platform/gas-sponsorship/handle-failures-and-expiry.md).


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