Skip to content

Direct Stake Integration

This guide covers managing direct stake via signMessage (for standard wallets) and via on-chain transaction (for hardware/non-standard wallets).

Constants

ts
const DS_SET_MEMO_PREFIX = 'jpool:ds-set:'
const DS_MARKER_PUBLIC_KEY = '8pXGNkCoy7GYuyddZAdXDK9TVf4Xn5Asbb2AKtHRzqLB'
const BIND_MARKER_PUBLIC_KEY = '8pXGNkCoy7GYuyddZAdXDK9TVf4Xn5Asbb2AKtHRzqLB'
const API_DIRECT_STAKE_URL = 'https://api2.jpool.one/direct-stake'
const API_BINDING_WALLET_URL = 'https://api2.jpool.one/direct-stake/wallet-binding'
const API_HC_URL = 'https://api.hc.jpool.one'

Wallet binding

Wallet binding links a wallet to a specific validator. Once bound, all JSOL in the wallet is automatically counted as direct stake to that validator. This is the simplest approach: bind the wallet once, and any subsequent JSOL activity (purchase, receipt, etc.) is automatically reflected in the direct stake.

Method A: via signMessage (standard wallets)

ts
import { PublicKey } from '@solana/web3.js'

const API_BINDING_WALLET_URL = 'https://api2.jpool.one/direct-stake/wallet-binding'

/**
 * Binds a wallet to a validator.
 *
 * @param wallet: connected wallet (publicKey + signMessage)
 * @param voteId: validator vote account
 */
async function bindWallet(
  wallet: { publicKey: PublicKey, signMessage: (message: Uint8Array) => Promise<Uint8Array> },
  voteId: string,
) {
  // 1. Build a JSON message
  const data = {
    wallet: wallet.publicKey.toBase58(),
    action: 'bindWallet',
    voteId,
    timestamp: Date.now(),
  }
  const message = JSON.stringify(data)

  // 2. Sign with the wallet
  const encodedMessage = Buffer.from(message)
  const signatureBytes = await wallet.signMessage(encodedMessage)
  const signature = Buffer.from(signatureBytes).toString('base64')

  // 3. Send to API
  const response = await fetch(`${API_BINDING_WALLET_URL}/bind`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ signature, message }),
  })

  return await response.json()
}

/**
 * Unbinds a wallet from a validator.
 */
async function unbindWallet(
  wallet: { publicKey: PublicKey, signMessage: (message: Uint8Array) => Promise<Uint8Array> },
) {
  const data = {
    wallet: wallet.publicKey.toBase58(),
    action: 'unbindWallet',
    timestamp: Date.now(),
  }
  const message = JSON.stringify(data)

  const encodedMessage = Buffer.from(message)
  const signatureBytes = await wallet.signMessage(encodedMessage)
  const signature = Buffer.from(signatureBytes).toString('base64')

  const response = await fetch(`${API_BINDING_WALLET_URL}/unbind`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ signature, message }),
  })

  return await response.json()
}

message format for bind (JSON string):

json
{
  "wallet": "YourWalletPublicKeyBase58",
  "action": "bindWallet",
  "voteId": "ValidatorVoteAccountBase58",
  "timestamp": 1712500000000
}

message format for unbind:

json
{
  "wallet": "YourWalletPublicKeyBase58",
  "action": "unbindWallet",
  "timestamp": 1712500000000
}

Method B: via on-chain transaction (hardware / non-standard wallets)

ts
import { createMemoInstruction } from '@solana/spl-memo'
import {
  Connection,
  PublicKey,
  SystemProgram,
  Transaction,
} from '@solana/web3.js'

const BIND_MARKER_PUBLIC_KEY = '8pXGNkCoy7GYuyddZAdXDK9TVf4Xn5Asbb2AKtHRzqLB'

/**
 * Binds a wallet to a validator via transaction.
 * Memo format: jpool:bind:<voteId>
 */
async function bindWalletViaTransaction(
  connection: Connection,
  wallet: {
    publicKey: PublicKey
    signTransaction: (tx: Transaction) => Promise<Transaction>
  },
  voteId: string,
) {
  const memo = `jpool:bind:${voteId}`

  const instructions = [
    SystemProgram.transfer({
      fromPubkey: wallet.publicKey,
      toPubkey: new PublicKey(BIND_MARKER_PUBLIC_KEY),
      lamports: 2,
    }),
    createMemoInstruction(memo),
  ]

  const transaction = new Transaction().add(...instructions)
  transaction.feePayer = wallet.publicKey
  transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash

  const signed = await wallet.signTransaction(transaction)
  const txSignature = await connection.sendRawTransaction(signed.serialize())
  await connection.confirmTransaction(txSignature)

  return txSignature
}

/**
 * Unbinds a wallet via transaction.
 * Memo format: jpool:unbind
 */
async function unbindWalletViaTransaction(
  connection: Connection,
  wallet: {
    publicKey: PublicKey
    signTransaction: (tx: Transaction) => Promise<Transaction>
  },
) {
  const memo = 'jpool:unbind'

  const instructions = [
    SystemProgram.transfer({
      fromPubkey: wallet.publicKey,
      toPubkey: new PublicKey(BIND_MARKER_PUBLIC_KEY),
      lamports: 2,
    }),
    createMemoInstruction(memo),
  ]

  const transaction = new Transaction().add(...instructions)
  transaction.feePayer = wallet.publicKey
  transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash

  const signed = await wallet.signTransaction(transaction)
  const txSignature = await connection.sendRawTransaction(signed.serialize())
  await connection.confirmTransaction(txSignature)

  return txSignature
}

Getting current binding

ts
async function getBindingWallet(walletAddress: string) {
  const response = await fetch(`${API_BINDING_WALLET_URL}/${walletAddress}`)
  return await response.json()
}

Response:

ts
type BindingWallet = {
  id: number
  wallet: string
  voteId: string
  createdAt: Date
  amount: number
}

Setting direct stake

Method A: via signMessage (standard wallets)

For wallets whose publicKey is on the ed25519 curve and that support signMessage.

ts
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'

const DS_SET_MEMO_PREFIX = 'jpool:ds-set:'
const API_DIRECT_STAKE_URL = 'https://api2.jpool.one/direct-stake'

/**
 * Sets direct stake via message signing.
 *
 * @param wallet: connected wallet (publicKey + signMessage)
 * @param voteId: validator vote account
 * @param amountSol: amount in SOL
 */
async function setDirectStakeViaSignMessage(
  wallet: { publicKey: PublicKey, signMessage: (message: Uint8Array) => Promise<Uint8Array> },
  voteId: string,
  amountSol: number,
) {
  const amountLamports = Math.round(amountSol * LAMPORTS_PER_SOL)

  // 1. Build a JSON message
  const data = {
    action: 'setDirectStake',
    wallet: wallet.publicKey.toBase58(),
    timestamp: Date.now(),
    target: voteId,
    amount: String(amountLamports),
  }
  const message = JSON.stringify(data)

  // 2. Sign the message with the wallet
  const encodedMessage = Buffer.from(message)
  const signatureBytes = await wallet.signMessage(encodedMessage)
  const signature = Buffer.from(signatureBytes).toString('base64')

  // 3. Send to API
  const response = await fetch(`${API_DIRECT_STAKE_URL}/manage/set`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ signature, message }),
  })

  return await response.json()
}

message format (JSON string):

json
{
  "action": "setDirectStake",
  "wallet": "YourWalletPublicKeyBase58",
  "timestamp": 1712500000000,
  "target": "ValidatorVoteAccountBase58",
  "amount": "1000000000"
}

POST /manage/set request format:

json
{
  "signature": "<base64 signature of message>",
  "message": "<original JSON string>"
}

Method B: via on-chain transaction (hardware / non-standard wallets)

For wallets where signMessage is unavailable (Ledger, etc.), a transaction is created with a 2-lamport transfer to the marker address and a memo instruction.

ts
import { createMemoInstruction } from '@solana/spl-memo'
import {
  Connection,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
} from '@solana/web3.js'

const DS_SET_MEMO_PREFIX = 'jpool:ds-set:'
const DS_MARKER_PUBLIC_KEY = '8pXGNkCoy7GYuyddZAdXDK9TVf4Xn5Asbb2AKtHRzqLB'
const API_DIRECT_STAKE_URL = 'https://api2.jpool.one/direct-stake'

/**
 * Sets direct stake via an on-chain transaction.
 *
 * @param connection: Solana RPC connection
 * @param wallet: connected wallet (publicKey + signTransaction)
 * @param voteId: validator vote account
 * @param amountSol: amount in SOL
 */
async function setDirectStakeViaTransaction(
  connection: Connection,
  wallet: {
    publicKey: PublicKey
    signTransaction: (tx: Transaction) => Promise<Transaction>
  },
  voteId: string,
  amountSol: number,
) {
  const amountLamports = Math.round(amountSol * LAMPORTS_PER_SOL)

  // 1. Build memo in the format: jpool:ds-set:<voteId>:<amountLamports>
  const memo = `${DS_SET_MEMO_PREFIX}${voteId}:${amountLamports}`

  // 2. Create instructions
  const instructions = [
    // Transfer 2 lamports to the marker address (proof-of-ownership)
    SystemProgram.transfer({
      fromPubkey: wallet.publicKey,
      toPubkey: new PublicKey(DS_MARKER_PUBLIC_KEY),
      lamports: 2,
    }),
    // Memo with direct stake parameters
    createMemoInstruction(memo),
  ]

  // 3. Build and send the transaction
  const transaction = new Transaction().add(...instructions)
  transaction.feePayer = wallet.publicKey
  transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash

  const signed = await wallet.signTransaction(transaction)
  const txSignature = await connection.sendRawTransaction(signed.serialize())
  await connection.confirmTransaction(txSignature)

  // 4. Register the transaction on the API (the server reads the memo from the transaction)
  const response = await fetch(
    `${API_DIRECT_STAKE_URL}/register?signature=${txSignature}`,
    { method: 'POST' },
  )

  return await response.json()
}

Memo format: jpool:ds-set:<voteId>:<amountLamports>

Example: jpool:ds-set:DPmsofVJ1UMRZADgwYAHotJnazMwohHzRHSoomL6Qcao:5000000000

Memo on SOL deposit

When depositing SOL into the pool, you can specify a target validator via a memo in the deposit transaction. This instruction is added to the transaction that mints new JSOL. The direct stake will be increased by the amount of newly minted JSOL after the transaction is parsed.

This approach is useful when wallet binding is not used and you want to stake into the pool and increase the direct stake in a single action. Setting direct stake accounts for the JSOL balance that the system already sees. It is designed for portfolio management and is not suitable for calling immediately after receiving JSOL. You need to wait 5-10 minutes. You can verify the current balance using the JSOL wallet portfolio endpoint.

ts
import { createMemoInstruction } from '@solana/spl-memo'

const DIRECT_STAKE_PREFIX = 'direct:'

// Direct stake to a specific validator
function createDirectDepositMemo(voteId: string) {
  return createMemoInstruction(`${DIRECT_STAKE_PREFIX}${voteId}`)
  // Result: "direct:<voteId>"
}

Determining wallet type

Which method to use depends on the wallet's capabilities:

ts
function isStandardWallet(publicKey: PublicKey, signMessage?: Function): boolean {
  return PublicKey.isOnCurve(publicKey.toBytes()) && !!signMessage
}

// Usage:
if (isStandardWallet(wallet.publicKey, wallet.signMessage)) {
  // Method A: signMessage
  await setDirectStakeViaSignMessage(wallet, voteId, amount)
} else {
  // Method B: on-chain transaction
  await setDirectStakeViaTransaction(connection, wallet, voteId, amount)
}

JSOL wallet portfolio

This endpoint shows how the system currently sees a wallet's JSOL balance, including tokens deployed in DeFi protocols.

INFO

When using Setting direct stake, the system does not update the balance instantly after receiving JSOL. Use this endpoint to verify that the system has recognized your JSOL before setting a direct stake.

GET https://api.hc.jpool.one/balance/jsol-balances-detailed?wallets=<wallet>

Multiple wallets can be passed as a comma-separated list.

Request example:

ts
const API_HC_URL = 'https://api.hc.jpool.one'

async function getJsolPortfolio(walletAddress: string) {
  const response = await fetch(
    `${API_HC_URL}/balance/jsol-balances-detailed?wallets=${walletAddress}`,
  )
  return await response.json()
}

Response example:

json
{
  "total": {
    "4Q726AFLu7gug4Q5k9ZeL7AyTLDMVscgWronrkqqg4D3": 0
  },
  "balances": {
    "4Q726AFLu7gug4Q5k9ZeL7AyTLDMVscgWronrkqqg4D3": {
      "base": 0,
      "jupiter": 0,
      "orca": 0,
      "raydium": 0,
      "project-0": 0,
      "solayer": 0,
      "saber": 0,
      "kamino": 0,
      "save-finance": 0,
      "meteora": 0,
      "meteora-dlmm": 0,
      "raydium-clmm": 0
    }
  }
}
  • total: total JSOL balance (in lamports) per wallet
  • balances: breakdown by protocol:
    • base: JSOL held directly in the wallet
    • others: JSOL deployed in the corresponding DeFi protocols

Format summary

OperationMethodSigned message format
Bind (signMessage)POST /wallet-binding/bindJSON: { wallet, action: "bindWallet", voteId, timestamp }
Bind (transaction)on-chainMemo: jpool:bind:<voteId>
Unbind (signMessage)POST /wallet-binding/unbindJSON: { wallet, action: "unbindWallet", timestamp }
Unbind (transaction)on-chainMemo: jpool:unbind
Set (signMessage)POST /manage/setJSON: { action, wallet, timestamp, target, amount }
Set (transaction)on-chain + POST /register?signature=<tx>Memo: jpool:ds-set:<voteId>:<lamports>
Deposit with targetmemo in deposit transactiondirect:<voteId>
JSOL portfolioGET /balance/jsol-balances-detailed?wallets=<wallet>N/a