Users
How it works
A user account is the onchain account that holds positions, orders, and collateral. Each wallet can create several of them, called subaccounts, identified by a numeric ID starting at 0. Every subaccount is a separate account with its own margin calculation: collateral and positions in subaccount 0 neither back nor endanger subaccount 1. Cross-margin applies only within one subaccount, where its positions and balances net against each other.
The account is a Solana PDA owned by the Velocity program, and it stores perp positions, spot balances (deposits and borrows), open orders, and leverage settings. Every write, placing an order, depositing, trading, is a mutation of that account. Subaccounts share only the wallet authority and one UserStats account, which carries volume, fee tier, and delegate permissions for all of them.
That isolation is what makes subaccounts worth using: separating strategies, keeping a risky book away from a conservative one, or handing a bot one subaccount while the owner trades another by hand.
SDK Usage
Most write actions go through VelocityClient. Account-level reads (positions, orders, health) go through a User, obtained from the client.
Initialize a user account
Creating a subaccount returns both the transaction signature and the new account's address. getNextSubAccountId() returns the next free ID for this client, so the caller does not have to track it.
// Assumes `velocityClient` is constructed and subscribed.
const subAccountId = await velocityClient.getNextSubAccountId();
const [txSig, userAccountPublicKey] = await velocityClient.initializeUserAccount(
subAccountId,
"my-account"
);
console.log(txSig, userAccountPublicKey.toBase58());Two rules bind when a referrer is involved, and both reject rather than recording nothing.
Passing a referrer at initialization requires that referrer's subaccount 0, and that
account's authority must match the authority on its own UserStats, or the instruction
fails with InvalidReferrer. Claiming a referrer name through initialize_referrer_name
has the same subaccount 0 requirement and additionally requires pool_id == 0.
A referrer is also recorded once and never rewritten, so a user who was referred cannot be re-referred by deleting and recreating their accounts.
Get a User and subscribe
velocityClient.getUser() returns the User for the active subaccount; pass a subaccount ID to get a specific one. Call subscribe() on it before reading, and fetchAccounts() to force a refresh from RPC rather than waiting for the next subscription update.
const user = velocityClient.getUser(); // active subaccount
const otherUser = velocityClient.getUser(1); // subaccount 1
await user.subscribe();
await user.fetchAccounts(); // force-refresh from RPCThe raw onchain UserAccount is readable from either side. user.getUserAccount() reads it off a User; velocityClient.getUserAccount() is a shortcut for the active subaccount.
const account = velocityClient.getUser().getUserAccount();
const sameAccount = velocityClient.getUserAccount(); // active subaccount
console.log(account.orders.length, account.perpPositions.length);Derive the account address
User accounts are Program Derived Addresses: deterministic addresses generated from the program ID, the wallet authority, and the subaccount ID. Deriving one takes no RPC call, which matters when the address is needed before the account exists, such as when building a transaction or fanning out parallel queries. See Program Structure for the seeds and the rest of the account layout.
const userAccountPublicKey = await velocityClient.getUserAccountPublicKey(0);
console.log(userAccountPublicKey.toBase58());Query orders and positions
These accessors all read the cached account, so they cost nothing and return undefined rather than throwing when the thing does not exist. getTokenAmount returns a signed value: positive for a deposit, negative for a borrow.
import { BN } from "@velocity-exchange/sdk";
const user = velocityClient.getUser();
const tokenAmount = user.getTokenAmount(0); // spot market 0
const isDeposit = tokenAmount.gte(new BN(0));
const isBorrow = tokenAmount.lt(new BN(0));
console.log({ tokenAmount: tokenAmount.toString(), isDeposit, isBorrow });const position = velocityClient.getUser().getPerpPosition(0); // undefined if flat
console.log(position?.baseAssetAmount.toString());Orders are addressable two ways: by the program's own orderId, or by the userOrderId set at placement. getOpenOrders() returns everything currently resting.
const user = velocityClient.getUser();
const byOrderId = user.getOrder(1); // program-assigned orderId
const byUserOrderId = user.getOrderByUserOrderId(1); // caller-assigned tag
const open = user.getOpenOrders();
console.log(open.length);The active subaccount
The active subaccount is the one VelocityClient methods operate on when no subaccount ID is named. It defaults to 0. velocityClient.getUser(), getUserAccount(), getSpotPosition(marketIndex), and placePerpOrder(orderParams) all resolve against it.
Change it with switchActiveUser(), or bypass it by passing a subaccount ID to the methods that accept one.
await velocityClient.switchActiveUser(1);
const user1 = velocityClient.getUser(); // now subaccount 1Update delegate
A delegate is another wallet that can trade on behalf of a user account without being able to withdraw funds, which is how a bot gets a position to manage while withdrawal authority stays with the owner. See Delegated Accounts for the trader-facing description.
import { PublicKey } from "@solana/web3.js";
// Pass PublicKey.default to clear the delegate.
await velocityClient.updateUserDelegate(new PublicKey("<DELEGATE_PUBKEY>"), 0);A delegate can sign most trading instructions on a subaccount's behalf: place orders, cancel orders, modify orders. withdraw always requires the owning authority's own signature, so a delegate can never move funds out of the protocol.
Delegate-initiated internal transfers
A delegate can move a subaccount's collateral to another of the owner's subaccounts via transferDepositByDelegate, but only after the owner opts in. The opt-in is a bit in delegatePermissions on the owner's UserStats account. It gates delegate transfers across all of that owner's subaccounts at once, and it does not affect direct deposits, withdrawals, or trading. While the bit is clear the program rejects transferDepositByDelegate; while it is set the delegate can move collateral between the owner's subaccounts. Read the owner's UserStats account for the bit's setting.
The owner opts in first, signing with their own wallet rather than the delegate's:
// Run with a VelocityClient whose wallet is the account owner (authority).
await velocityClient.updateUserAllowDelegateTransfer(true);Then the delegate can transfer between the owner's subaccounts:
// Run with a VelocityClient constructed with `authority: <OWNER_PUBKEY>` and a
// delegate wallet, per the delegated-accounts note in Setup.
const marketIndex = 0; // spot market 0 is the quote asset
const amount = velocityClient.convertToSpotPrecision(marketIndex, 100);
await velocityClient.transferDepositByDelegate(
amount,
marketIndex,
0, // fromSubAccountId (owner's subaccount)
1 // toSubAccountId (owner's subaccount)
);Without the opt-in, the program rejects the call regardless of which subaccount the delegate is authorized on. See Transfers for the rest of the transfer surface.
Update margin settings
Margin settings control how a subaccount uses leverage and borrows against collateral. Both updaters take an array, so one transaction can change several subaccounts. marginRatio is scaled by MARGIN_PRECISION (1e4), and it is a minimum margin ratio, so a larger number means less leverage: 10000 caps the account at 1x, 5000 at 2x, 2000 at 5x. See Margin.
await velocityClient.updateUserMarginTradingEnabled([
{ marginTradingEnabled: true, subAccountId: 0 },
]);// marginRatio is scaled by MARGIN_PRECISION (1e4).
await velocityClient.updateUserCustomMarginRatio([
{ marginRatio: 5000, subAccountId: 0 }, // 2x max leverage
]);Delete a user account
A subaccount with no assets and no liabilities can be deleted to reclaim its rent.
await velocityClient.deleteUser(1);