Skip to main content
This guide shows how to sign and submit RebelFi transactions using Privy server wallets as your custody provider. Privy provides server-side wallet infrastructure with a simple REST API — no SDK dependency required.
For Privy setup, app configuration, and wallet policies, see the Privy Developer Docs.

How It Works

1. Plan operation        →  RebelFi returns unsigned transaction(s)
2. Send to Privy         →  POST RPC call with transaction fields
3. Privy signs & sends   →  Server-side signing + on-chain broadcast
4. Get txHash            →  Privy returns the hash immediately
5. Submit hash           →  Report the on-chain txHash back to RebelFi
Privy handles signing and broadcasting server-side via a REST API. For EVM, you call eth_sendTransaction with the transaction fields (to, data, value). For Solana, you call signAndSendTransaction with the base64-encoded serialized transaction. Both return the transaction hash immediately — no polling required.

Prerequisites

  • A Privy account with server wallets enabled
  • An App ID and App Secret from Dashboard → App settings → Basics
  • A Privy server wallet on the target network
  • The wallet address registered with RebelFi
  • No additional SDK required — uses fetch with Basic Auth

Registering a Wallet

Create a Privy server wallet (or use an existing one) and register its address with RebelFi:
import { RebelfiClient, Blockchain } from '@rebelfi/sdk';

const PRIVY_API = 'https://api.privy.io';
const privyHeaders = {
  'Authorization': `Basic ${btoa(`${APP_ID}:${APP_SECRET}`)}`,
  'privy-app-id': APP_ID,
  'Content-Type': 'application/json',
};

// Create a new server wallet (or fetch an existing one)
const wallet = await fetch(`${PRIVY_API}/v1/wallets`, {
  method: 'POST',
  headers: privyHeaders,
  body: JSON.stringify({ chain_type: 'solana' }), // or 'ethereum'
}).then((r) => r.json());

console.log(`Privy wallet: ${wallet.id} (${wallet.address})`);

// Register with RebelFi
const rebelfi = new RebelfiClient({ apiKey: process.env.REBELFI_API_KEY });
const registered = await rebelfi.wallets.register({
  walletAddress: wallet.address,
  blockchain: wallet.chain_type === 'solana'
    ? Blockchain.SOLANA
    : Blockchain.ETHEREUM,
});
The wallet address you register with RebelFi must match the Privy wallet address. Use the same wallet for all operations.

The Privy Signer

The core of the integration is a function that takes a RebelFi unsigned transaction and sends it through Privy. It handles both EVM and Solana transactions:
import type { UnsignedTransactionDetail, EvmTransactionFields } from '@rebelfi/sdk';

const PRIVY_API = 'https://api.privy.io';

// Solana CAIP-2 identifiers (Solana doesn't use numeric chain IDs)
const SOLANA_CAIP2: Record<string, string> = {
  'mainnet-beta': 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
  'devnet': 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
  'testnet': 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z',
};

function buildRpcBody(tx: UnsignedTransactionDetail, evmChainId?: string, solanaNetwork?: string) {
  const blockchain = tx.blockchain.toLowerCase();

  if (blockchain === 'solana') {
    return {
      method: 'signAndSendTransaction',
      caip2: SOLANA_CAIP2[solanaNetwork || 'devnet'],
      params: {
        transaction: tx.unsignedTransaction.serialized, // base64
        encoding: 'base64',
      },
    };
  }

  // EVM
  const evm = tx.evmTransaction!;
  return {
    method: 'eth_sendTransaction',
    caip2: `eip155:${evmChainId || '1'}`,
    params: {
      transaction: {
        to: evm.to,
        data: evm.data,
        value: evm.value || '0x0',
      },
    },
  };
}

async function signAndBroadcast(
  tx: UnsignedTransactionDetail,
  privyWalletId: string,
): Promise<{ txHash: string }> {
  const body = buildRpcBody(tx);

  const res = await fetch(`${PRIVY_API}/v1/wallets/${privyWalletId}/rpc`, {
    method: 'POST',
    headers: privyHeaders,
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    throw new Error(`Privy RPC failed: ${res.status} ${await res.text()}`);
  }

  const result = await res.json();
  const txHash = result.data?.hash;

  if (!txHash) {
    throw new Error(`No hash returned: ${JSON.stringify(result)}`);
  }

  return { txHash };
}
Unlike Fireblocks and Dfns, Privy returns the transaction hash immediately after broadcasting — no polling loop needed. Privy handles gas estimation and signing server-side. RebelFi’s reconciliation tracks on-chain confirmation.

Supply Example

Supply operations may produce multiple transactions (e.g., EVM token approval + deposit, or a single Solana transaction). Sign and submit them sequentially through Privy:
import { RebelfiClient } from '@rebelfi/sdk';

const rebelfi = new RebelfiClient({ apiKey: process.env.REBELFI_API_KEY });

async function supply(
  walletId: number,
  strategyId: number,
  amount: string,
  tokenAddress: string,
  privyWalletId: string,
) {
  // 1. Plan the supply — RebelFi returns unsigned transactions
  const operation = await rebelfi.operations.supply({
    walletId,
    strategyId,
    amount,
    tokenAddress,
  });
  console.log(`Operation ${operation.operationId} created`);

  // 2. Fetch the unsigned transactions
  const unsignedTxs = await rebelfi.operations.getUnsignedTransactions(
    operation.operationId,
  );
  console.log(`${unsignedTxs.length} transaction(s) to sign`);

  // 3. Sign and submit each transaction via Privy
  for (const tx of unsignedTxs) {
    console.log(`Signing: ${tx.description} (${tx.blockchain})`);
    const { txHash } = await signAndBroadcast(tx, privyWalletId);

    // 4. Report the hash back to RebelFi
    await rebelfi.transactions.submitHash({
      operationId: operation.operationId,
      txHash,
      transactionId: tx.attemptId,
    });
    console.log(`Submitted: ${tx.description} (${txHash})`);
  }

  // 5. Confirm final status
  const final = await rebelfi.operations.get(operation.operationId);
  console.log(`Final status: ${final.status}`);
  return final;
}

What Happens Step by Step

1

Plan the supply

Call operations.supply() with your wallet, strategy, amount, and token address. RebelFi returns an operation with status AWAITING_SIGNATURE.
2

Fetch unsigned transactions

Call operations.getUnsignedTransactions() to get the transactions to sign. For EVM, you’ll typically get two (approval + deposit). For Solana, you’ll get one.
3

Send via Privy

For each unsigned transaction, POST to Privy’s RPC endpoint. For EVM, call eth_sendTransaction with the to, data, and value fields. For Solana, call signAndSendTransaction with the base64-encoded transaction. Privy signs and broadcasts on-chain, returning the txHash immediately.
4

Submit hash to RebelFi

Take the txHash from Privy’s response and submit it to RebelFi via transactions.submitHash(). Include the transactionId to identify which transaction in the operation you’re submitting.
5

Confirm

RebelFi monitors the chain and updates the operation status to CONFIRMED once all transactions land.

Unwind Example

Unwinding (withdrawing from a yield strategy) follows the same pattern:
async function unwind(
  walletId: number,
  strategyId: number,
  amount: string,
  privyWalletId: string,
) {
  const operation = await rebelfi.operations.unwind({
    walletId,
    strategyId,
    amount,
  });

  const unsignedTxs = await rebelfi.operations.getUnsignedTransactions(
    operation.operationId,
  );

  for (const tx of unsignedTxs) {
    const { txHash } = await signAndBroadcast(tx, privyWalletId);

    await rebelfi.transactions.submitHash({
      operationId: operation.operationId,
      txHash,
      transactionId: tx.attemptId,
    });
  }

  return rebelfi.operations.get(operation.operationId);
}
For a full withdrawal, pass fullWithdrawal: true instead of amount:
const operation = await rebelfi.operations.unwind({
  walletId,
  strategyId,
  fullWithdrawal: true,
});

EVM vs Solana

The signAndBroadcast function handles both chains. The key difference is the RPC method and transaction format:
EVMSolana
RPC methodeth_sendTransactionsignAndSendTransaction
Chain identifiereip155:<chainId> (e.g. eip155:1)solana:<genesis-hash>
Transaction formatJSON object (to, data, value)Base64-encoded serialized transaction
Source fieldtx.evmTransactiontx.unsignedTransaction.serialized
Typical tx count2 (approve + deposit)1
Polling neededNoNo
The tx.blockchain field on each unsigned transaction tells you which chain it targets. Use this to route to the correct RPC method.

Authentication

Privy uses Basic Auth with your App ID and App Secret. Every request must include:
Authorization: Basic <base64(APP_ID:APP_SECRET)>
privy-app-id: <APP_ID>
Keep your App Secret secure. Never expose it in client-side code or public repositories.

Troubleshooting

Cause: Invalid or missing authentication headers.Solution: Ensure both the Authorization header (Basic Auth with APP_ID:APP_SECRET base64-encoded) and the privy-app-id header are set correctly.
Cause: Privy returns the txHash immediately upon broadcast. The transaction may still be pending on-chain.Solution: This is expected behavior. RebelFi monitors the chain and will update the operation status once the transaction is confirmed. You can also check transaction status via GET /v1/transactions/{transaction_id}.
Cause: Registering a Solana wallet as Ethereum (or vice versa) with RebelFi.Solution: Check the Privy wallet’s chain_type field and register with the matching Blockchain enum. A Solana Privy wallet must be registered as Blockchain.SOLANA.
Cause: Using an EVM-style chain ID for a Solana wallet, or an incorrect Solana genesis hash.Solution: For EVM, use eip155:<chainId> (e.g. eip155:11155111 for Sepolia). For Solana, use the full CAIP-2 identifier with the genesis hash (e.g. solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 for devnet).

Resources