Appearance
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 walletbalances: breakdown by protocol:base: JSOL held directly in the wallet- others: JSOL deployed in the corresponding DeFi protocols
Format summary
| Operation | Method | Signed message format |
|---|---|---|
| Bind (signMessage) | POST /wallet-binding/bind | JSON: { wallet, action: "bindWallet", voteId, timestamp } |
| Bind (transaction) | on-chain | Memo: jpool:bind:<voteId> |
| Unbind (signMessage) | POST /wallet-binding/unbind | JSON: { wallet, action: "unbindWallet", timestamp } |
| Unbind (transaction) | on-chain | Memo: jpool:unbind |
| Set (signMessage) | POST /manage/set | JSON: { action, wallet, timestamp, target, amount } |
| Set (transaction) | on-chain + POST /register?signature=<tx> | Memo: jpool:ds-set:<voteId>:<lamports> |
| Deposit with target | memo in deposit transaction | direct:<voteId> |
| JSOL portfolio | GET /balance/jsol-balances-detailed?wallets=<wallet> | N/a |