SWIFT API
SWIFT is Velocity’s offchain signed-order protocol: it lets market makers receive signed taker orders over WebSocket before they hit the onchain JIT auction. This enables ultra-low-latency market making. In the SDK, SWIFT orders are called “signed-message” orders (SignedMsgOrderParams, isSignedMsgOrder, SwiftOrderSubscriber) — the “SWIFT” name is the product-facing term for the same feature.
How SWIFT works
Flow overview
Taker signs order offchain
Order is signed with the user’s private key and broadcast to the SWIFT WebSocket feed.
Makers receive order via WebSocket
Order arrives before hitting onchain auction, and makers inspect direction, size, and auction params.
Makers submit place-and-make
Maker order is submitted onchain with ed25519 verification and atomically fills the taker.
Key benefit: Market makers see orders 100-500ms before they land onchain, allowing faster response times and better fills.
SWIFT vs standard JIT: why the latency matters
In the standard JIT flow, the taker submits a transaction to Solana → it lands onchain → your RPC/gRPC subscription fires → you compute and submit your fill. This takes 1-2 seconds minimum.
With SWIFT, the taker broadcasts the signed order offchain to the SWIFT WebSocket simultaneously with submitting it onchain. You receive it over WebSocket in 100-500ms, before it even lands onchain. This head start lets you:
- Price more accurately: you see the order while the oracle is fresher
- Win more auctions: your fill tx can land at the same slot or even before other makers who rely on onchain feeds
- React to flow faster: critical for inventory management and toxic flow avoidance
The tradeoff: you need fast infrastructure (dedicated RPC node, low-latency WebSocket connection) to capitalize on this advantage. If your fill tx doesn’t land quickly, the latency edge is wasted.
Subscribing to SWIFT orders
Basic subscription
Endpoint is provisional.
SwiftOrderSubscriberdefaultsendpointbased onvelocityEnvwhen you don’t pass one explicitly:wss://swift.velocity.exchange/wsformainnet-beta,wss://swift.master.velocity.exchange/wsfordevnet(both hardcoded insdk/src/swift/swiftOrderSubscriber.ts). These are real values shipped in the SDK today, but the hostnames haven’t been confirmed as final/DNS-live production endpoints — check with the team before depending on them in production.
import { SwiftOrderSubscriber, loadKeypair } from "@velocity-exchange/sdk";
const swiftSubscriber = new SwiftOrderSubscriber({
velocityClient,
velocityEnv: "mainnet-beta",
marketIndexes: [0, 1, 2], // SOL, BTC, ETH perp markets to listen to
keypair: loadKeypair("<KEYPAIR_PATH>"), // used for WebSocket auth
// endpoint: "wss://swift.velocity.exchange/ws", // optional -- defaults based on velocityEnv:
// mainnet-beta -> wss://swift.velocity.exchange/ws, devnet -> wss://swift.master.velocity.exchange/ws
});
await swiftSubscriber.subscribe(
async (orderMessageRaw, signedMessage, isDelegateSigner) => {
// Inspect the incoming signed order
const orderParams = signedMessage.signedMsgOrderParams;
console.log("Market:", orderParams.marketIndex);
console.log("Direction:", orderParams.direction);
console.log("Size:", orderParams.baseAssetAmount);
console.log("Auction start:", orderParams.auctionStartPrice);
console.log("Auction end:", orderParams.auctionEndPrice);
// Decide if you want to fill it
if (shouldFill(orderParams)) {
await fillSwiftOrder(signedMessage);
}
}
);Class SwiftOrderSubscriberReference ↗
Class SwiftOrderSubscriberReference ↗| Property | Type | Required |
|---|---|---|
config | any | Yes |
heartbeatTimeout | any | Yes |
heartbeatIntervalMs | any | Yes |
ws | any | Yes |
velocityClient | any | Yes |
userAccountGetter | AccountGetter | No |
onOrder | (orderMessageRaw: SwiftOrderMessage, signedMessage: SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, isDelegateSigner?: boolean | undefined) => Promise<...> | No |
subscribed | boolean | Yes |
unsubscribe | () => void | Yes |
getSymbolForMarketIndex | (marketIndex: number) => string | Yes |
generateChallengeResponse | (nonce: string) => string | Yes |
handleAuthMessage | (message: any) => void | Yes |
subscribe | (onOrder: (orderMessageRaw: SwiftOrderMessage, signedMessage: SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, isDelegateSigner?: boolean | undefined) => Promise<...>, acceptSanitized?: boolean | undefined, acceptDepositTrade?: boolean | undefined) => Promise<...> | Yes |
getPlaceAndMakeSignedMsgOrderIxs | (orderMessageRaw: SwiftOrderMessage, signedMsgOrderParamsMessage: SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, makerOrderParams: OptionalOrderParams) => Promise<...> | Yes |
startHeartbeatTimer | any | Yes |
reconnect | any | Yes |
With UserAccount getter
Optionally provide a userAccountGetter to resolve taker UserAccount details. This lets you inspect the taker’s positions, collateral, and health before deciding to fill, which is useful for toxic flow filtering.
import { UserMap, loadKeypair } from "@velocity-exchange/sdk";
const userMap = new UserMap({
velocityClient,
connection,
subscriptionConfig: { type: "websocket" },
});
await userMap.subscribe();
const swiftSubscriber = new SwiftOrderSubscriber({
velocityClient,
velocityEnv: "mainnet-beta",
marketIndexes: [0, 1, 2],
keypair: loadKeypair("<KEYPAIR_PATH>"),
// userAccountGetter implements { mustGetUserAccount(publicKey: string): Promise<UserAccount> }
userAccountGetter: userMap,
});Class UserMapReference ↗
Class UserMapReference ↗In-memory cache of every `User` account on the program, keyed by the `User` account's own public key.
Sync/subscription filtering (see `getFilters`) is built from memcmp filters: the base `getUserFilter()` (matches the `User` account discriminator — required, or every account on chain would match) is always present; `getNonIdleUserFilter()` is added unless `includeIdle` is set (idle accounts are excluded by default to reduce subscription volume); `getUsersWithPoolId(filterByPoolId)` is added when `filterByPoolId` is set; and any `additionalFilters` from the config are appended last. The same filter set is used for the initial `getProgramAccounts` sync and for the live websocket/gRPC subscription, so what you sync is what you keep getting updates for.
Automatically does a full `sync()` whenever the program's `StateAccount.numberOfSubAccounts` changes (new/deleted user accounts), unless `disableSyncOnTotalAccountsChange` is set.
| Property | Type | Required |
|---|---|---|
userMap | any | Yes |
velocityClient | VelocityClient | Yes |
eventEmitter | StrictEventEmitter<EventEmitter, UserEvents> | Yes |
connection | any | Yes |
commitment | any | Yes |
includeIdle | any | Yes |
filterByPoolId | any | No |
additionalFilters | any | No |
disableSyncOnTotalAccountsChange | any | Yes |
lastNumberOfSubAccounts | any | No |
subscription | any | Yes |
stateAccountUpdateCallback | any | Yes |
decode | any | Yes |
mostRecentSlot | any | Yes |
syncConfig | any | Yes |
syncPromise | any | No |
syncPromiseResolver | any | Yes |
throwOnFailedSync | any | Yes |
subscribe | () => Promise<void>Populates the map with a full initial `sync()` (no-op if already
populated) and starts the configured live subscription
(`'websocket'`/`'polling'`/`'grpc'`), plus (unless
`disableSyncOnTotalAccountsChange`) a listener that triggers a full
re-sync whenever `StateAccount.numberOfSubAccounts` changes. | Yes |
addPubkey | (userAccountPublicKey: PublicKey, userAccount?: UserAccount | undefined, slot?: number | undefined, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<...>Adds `userAccountPublicKey` to the map, creating a `User` for it.
By default (`accountSubscription` omitted), subscribes it with a
`OneShotUserAccountSubscriber` seeded from `userAccount`/`slot` rather
than a live per-account websocket subscription — the map already gets
live updates in bulk via its own subscription (`WebsocketSubscription`/
`PollingSubscription`/`grpcSubscription`), so per-`User` subscriptions
here would needlessly multiply RPC load. | Yes |
has | (key: string) => booleanReturns true if a `User` account keyed by `key` (the `User` account pubkey, base58) is cached in the map. | Yes |
get | (key: string) => User | undefinedgets the User for a particular userAccountPublicKey, if no User exists, undefined is returned | Yes |
getWithSlot | (key: string) => DataAndSlot<User> | undefinedLike `get`, but also returns the slot at which the `User` account was last observed. | Yes |
mustGet | (key: string, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<User>gets the User for a particular userAccountPublicKey, if no User exists, new one is created | Yes |
mustGetWithSlot | (key: string, accountSubscription?: UserSubscriptionConfig | undefined) => Promise<DataAndSlot<User>>Like `mustGet`, but also returns the slot at which the `User` account was observed. | Yes |
mustGetUserAccount | (key: string) => Promise<UserAccount>Like `mustGet`, but returns the underlying `UserAccount` data directly (throws if the `User`'s account is not loaded). | Yes |
getUserAuthority | (key: string) => PublicKey | undefinedgets the Authority for a particular userAccountPublicKey, if no User exists, undefined is returned | Yes |
getDLOB | (slot: number) => Promise<DLOB>Implements the `DLOBSource` interface: builds a `DLOB` from every
subscribed user's open orders. | Yes |
updateWithOrderRecord | (record: OrderRecord) => Promise<void>Ensures an entry exists in the map for `record.user`, adding it via `addPubkey` if not already present. | Yes |
updateWithEventRecord | (record: any) => Promise<void>Incrementally updates the map in response to a single program event,
ensuring an entry exists for every `User` account the event references
(deposit/funding/liquidation/order/order-action/settle-pnl/new-user
records). Unrecognized event types are silently ignored. | Yes |
values | () => IterableIterator<User>Iterates all cached `User` instances. | Yes |
valuesWithSlot | () => IterableIterator<DataAndSlot<User>>Like `values`, but paired with the slot each `User` was last observed at. | Yes |
entries | () => IterableIterator<[string, User]>Iterates all `[userAccountPublicKey, User]` pairs in the map. | Yes |
entriesWithSlot | () => IterableIterator<[string, DataAndSlot<User>]>Like `entries`, but paired with the slot each `User` was last observed at. | Yes |
size | () => numberNumber of `User` accounts currently cached in the map. | Yes |
getUniqueAuthorities | (filterCriteria?: UserAccountFilterCriteria | undefined) => PublicKey[]Returns a unique list of authorities for all users in the UserMap that meet the filter criteria | Yes |
sync | () => Promise<void>Runs a full sync using the strategy configured in `UserMapConfig.syncConfig` (`'default'` or `'paginated'` — see `SyncConfig`). | Yes |
getFilters | anyBuilds the memcmp filter set for both the initial sync and the live
subscription: always the `User`-account discriminator filter; plus a
non-idle filter unless `includeIdle`; plus a pool-id filter if
`filterByPoolId` is set; plus any caller-supplied `additionalFilters`. | Yes |
defaultSync | anySyncs the UserMap using the default sync method (single getProgramAccounts call with filters).
This method may fail when velocity has too many users. (nodejs response size limits) | Yes |
paginatedSync | anySyncs the UserMap using the paginated sync method (multiple getMultipleAccounts calls with filters).
This method is more reliable when velocity has many users. | Yes |
unsubscribe | () => Promise<void>Tears down the live subscription, unsubscribes and removes every cached
`User`, and (if registered) removes the `stateAccountUpdate` listener
that triggers auto-resync on `numberOfSubAccounts` changes. | Yes |
updateUserAccount | (key: string, userAccount: UserAccount, slot: number) => Promise<void>Applies a fresh `userAccount` observation for `key` at `slot`. If the
user is already cached, updates in place only if `slot` is at least as
new as the cached slot (stale/out-of-order updates are dropped) and
emits `'userUpdate'`. If not cached yet, adds it via `addPubkey`.
Also advances `getSlot()`'s tracked most-recent slot. | Yes |
updateLatestSlot | (slot: number) => voidAdvances the map's tracked most-recent slot to `slot` if it's newer. | Yes |
getSlot | () => numberReturns the most recent slot at which any account update has been observed. | Yes |
Place-and-make with SWIFT
SWIFT fills use a special instruction (placeAndMakeSignedMsgPerpOrder) that includes an ed25519 signature verification, proving the taker actually signed this order offchain. The SDK handles building this instruction for you.
For the standard onchain place-and-make pattern, see JIT Auctions - Place-and-make. The SWIFT variant adds the signature verification step.
Using VelocityClient (recommended)
import {
getLimitOrderParams,
getUserAccountPublicKey,
getUserStatsAccountPublicKey,
isVariant,
PositionDirection,
PostOnlyParams,
} from "@velocity-exchange/sdk";
import { PublicKey } from "@solana/web3.js";
// From the SWIFT subscription callback
async function fillSwiftOrder(orderMessageRaw, signedMessage, isDelegateSigner) {
const takerAuthority = new PublicKey(orderMessageRaw.taker_authority);
const signingAuthority = new PublicKey(orderMessageRaw.signing_authority);
const subAccountId = signedMessage.subAccountId;
// Build the signed order params (message + signature from the raw order)
const signedOrderParams = {
orderParams: Buffer.from(orderMessageRaw.order_message, "hex"),
signature: Buffer.from(orderMessageRaw.order_signature, "base64"),
};
// Build UUID as Uint8Array
const uuidBytes = new TextEncoder().encode(orderMessageRaw.uuid);
// Resolve taker account addresses
const takerPubkey = await getUserAccountPublicKey(
velocityClient.program.programId, takerAuthority, subAccountId
);
const takerStatsPubkey = getUserStatsAccountPublicKey(
velocityClient.program.programId, takerAuthority
);
// Build your maker order params (opposite direction of taker)
const takerIsLong = isVariant(signedMessage.signedMsgOrderParams.direction, "long");
const makerOrderParams = getLimitOrderParams({
marketIndex: signedMessage.signedMsgOrderParams.marketIndex,
direction: takerIsLong ? PositionDirection.SHORT : PositionDirection.LONG,
baseAssetAmount: signedMessage.signedMsgOrderParams.baseAssetAmount,
price: myFillPrice,
postOnly: PostOnlyParams.MUST_POST_ONLY,
});
// Resolve taker user account (from UserMap or userAccountGetter)
const takerUserAccount = await userMap.mustGetUserAccount(takerPubkey.toString());
// Submit place-and-make (SDK handles ed25519 verification ix)
const txSig = await velocityClient.placeAndMakeSignedMsgPerpOrder(
signedOrderParams, // { orderParams: Buffer, signature: Buffer }
uuidBytes, // Uint8Array
{
taker: takerPubkey,
takerStats: takerStatsPubkey,
takerUserAccount,
signingAuthority,
},
makerOrderParams, // your maker order
);
console.log("Filled SWIFT order:", txSig);
}Method VelocityClient.placeAndMakeSignedMsgPerpOrderReference ↗
Method VelocityClient.placeAndMakeSignedMsgPerpOrderReference ↗| Parameter | Type | Required |
|---|---|---|
signedSignedMsgOrderParams | SignedMsgOrderParamsThe taker's signed order payload. | Yes |
signedMsgOrderUuid | Uint8Array<ArrayBufferLike>UUID identifying the signed-msg order, used by the program to
dedupe/match it against the recorded `SignedMsgUserOrders` entry. | Yes |
takerInfo | { taker: PublicKey; takerStats: PublicKey; takerUserAccount: UserAccount; signingAuthority: PublicKey; }Taker's account/authority info; see `placeSignedMsgTakerOrder`. | Yes |
orderParams | OptionalOrderParamsMaker order to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price`
is PRICE_PRECISION (1e6). Must have `orderType: LIMIT`, `postOnly` set, and be IOC. | Yes |
txParams | TxParamsOptional compute-unit/priority-fee overrides. | No |
subAccountId | numberSub-account placing the maker order; defaults to the active sub-account. | No |
precedingIxs | TransactionInstruction[]Instructions preceding these in the final transaction (used only to
compute the ed25519-verify instruction's sysvar index). | No |
overrideCustomIxIndex | numberExplicit sysvar-instructions index override. | No |
takerEscrow | RevenueShareEscrowAccountThe taker's decoded `RevenueShareEscrow`. Required to attach the escrow
when the taker is referred but the signed order carries no builder code. | No |
| Returns |
|---|
Promise<string> |
Build instructions manually
If you need more control over transaction construction (e.g., custom priority fees, specific ALTs, or bundling multiple instructions):
import { Transaction } from "@solana/web3.js";
// Same params as placeAndMakeSignedMsgPerpOrder, returns instruction array
const ixs = await velocityClient.getPlaceAndMakeSignedMsgPerpOrderIxs(
signedOrderParams, // { orderParams, signature }
uuidBytes, // Uint8Array
{ taker: takerPubkey, takerStats: takerStatsPubkey, takerUserAccount, signingAuthority },
makerOrderParams,
);
// Returns [ed25519VerifyIx, placeSignedMsgTakerOrderIx, placeAndMakeSignedMsgPerpOrderIx]:
// 1. ed25519 verification instruction (proves taker signature)
// 2. registers the taker's signed order onchain (same as placeSignedMsgTakerOrder)
// 3. places your maker order and fills the taker atomically
// Send transaction with your preferred method
const tx = new Transaction().add(...ixs);
const txSig = await connection.sendTransaction(tx, [wallet.payer]);Method VelocityClient.getPlaceAndMakeSignedMsgPerpOrderIxsReference ↗
Method VelocityClient.getPlaceAndMakeSignedMsgPerpOrderIxsReference ↗| Parameter | Type | Required |
|---|---|---|
signedSignedMsgOrderParams | SignedMsgOrderParamsThe taker's signed order payload. | Yes |
signedMsgOrderUuid | Uint8Array<ArrayBufferLike>UUID identifying the signed-msg order. | Yes |
takerInfo | { taker: PublicKey; takerStats: PublicKey; takerUserAccount: UserAccount; signingAuthority: PublicKey; }Taker's account/authority info. | Yes |
orderParams | OptionalOrderParamsMaker order to place; see `placeAndMakeSignedMsgPerpOrder` for field
precisions and required order shape. | Yes |
subAccountId | numberSub-account placing the maker order; defaults to the active sub-account. | No |
precedingIxs | TransactionInstruction[]Instructions preceding these in the final transaction (used only to
compute the ed25519-verify instruction's sysvar index). | No |
overrideCustomIxIndex | numberExplicit sysvar-instructions index override. | No |
takerEscrow | RevenueShareEscrowAccountSee `placeAndMakeSignedMsgPerpOrder`. | No |
| Returns |
|---|
Promise<TransactionInstruction[]> |
SWIFT vs onchain auction flow
SWIFT flow
Timeline:
Taker signs order offchain
Taker signs and prepares the order message.
Broadcast to SWIFT WebSocket
The signed message is broadcast to SWIFT (~50ms).
Maker receives order
Maker gets the order before it lands onchain (~100-500ms total).
Maker submits place-and-make
Maker submits a place-and-make transaction.
Taker order lands onchain
Taker transaction lands onchain (~1-2s total).
Maker fill lands
Maker fill transaction lands (ideally same slot).
Latency advantage:
- Makers see orders 100-500ms before onchain
- Faster reaction time → better auction slot
- Competitive edge for pricing and flow selection
Requirements:
- Subscribe to SWIFT WebSocket
- Handle ed25519 verification (SDK does this)
- Fast infrastructure to capitalize on latency advantage
- Dedicated RPC node recommended
Standard onchain auction
Timeline:
Taker submits tx to Solana
Order transaction is sent directly onchain.
Transaction lands onchain
The taker tx lands (~1-2s).
RPC/gRPC subscription fires
Makers receive the auction event from onchain subscriptions.
Maker computes fill
Maker processes pricing and risk checks.
Maker submits place-and-make
Maker sends the fill transaction.
Fill lands
Fill lands onchain (~1-2s later).
Total latency: 2-4 seconds from taker intent to maker fill
See JIT Auctions for full auction mechanics.
Filtering and risk management
Use the same filters as other JIT flows: oracle validation, position limits, and toxic-flow detection. See Bot Architecture - Risk and filtering for shared patterns and code.
Detecting SWIFT orders in onchain feeds
When subscribed to both SWIFT and onchain feeds, you may see the same order twice:
First via SWIFT WebSocket (offchain)
You receive the signed order from SWIFT before onchain landing.
Again when it lands onchain
The same order appears in your onchain subscription stream later.
Use isSignedMsgOrder() to identify SWIFT-origin orders and avoid double-handling:
import { isSignedMsgOrder } from "@velocity-exchange/sdk";
// In your AuctionSubscriber callback
auctionSubscriber.eventEmitter.on("onAccountUpdate", async (userAccount, pubkey, slot) => {
for (const order of userAccount.orders) {
if (order.baseAssetAmount.isZero()) continue;
if (isSignedMsgOrder(order)) {
// This came from SWIFT, you already saw it via WebSocket
// Skip to avoid submitting a duplicate fill
continue;
}
// Handle regular onchain auction
await handleAuction(order, userAccount, pubkey, slot);
}
});Function isSignedMsgOrderReference ↗
Function isSignedMsgOrderReference ↗True if the order was submitted via the signed-message (swift/off-chain relay) path (`OrderBitFlag.SignedMessage`).
| Parameter | Type | Required |
|---|---|---|
order | Order | Yes |
| Returns |
|---|
boolean |
Performance considerations
Latency optimization:
- Use dedicated RPC nodes for fastest transaction submission
- Pre-compute oracle prices and risk checks
- Keep WebSocket connection persistent (auto-reconnect on disconnect)
- Use
commitment: "processed"for fastest confirmations
Reliability:
- Handle WebSocket reconnections gracefully
- Have fallback to standard auction participation (via
AuctionSubscriber) - Monitor fill rates and adjust strategy
- Track SWIFT vs onchain fill success rates separately
Cost:
- SWIFT fills pay same maker rebates as regular fills
- No additional fees for SWIFT participation
- Transaction costs same as place-and-make
Gotchas
- SWIFT is not guaranteed flow: not all taker orders go through SWIFT. Takers using the standard SDK flow submit directly onchain. You should subscribe to both SWIFT and
AuctionSubscriberfor complete coverage, usingisSignedMsgOrder()to deduplicate. - ed25519 instruction ordering: the ed25519 verification instruction must be the first instruction in the transaction. The SDK handles this, but if building transactions manually, incorrect ordering causes silent verification failures.
- Latency advantage is perishable: the 100-500ms head start from SWIFT only helps if your fill transaction lands quickly. With a slow RPC node, the advantage is wasted. Use a dedicated RPC node with staked connections for fastest landing.
- UserAccount resolution: the
userAccountGettercallback should return quickly. If it requires an RPC call per order, you’ll negate the latency advantage. Pre-load user accounts viaUserMapsubscription. - Order expiry: SWIFT signed orders have a
maxTsfield. If your fill transaction lands after this timestamp, it will fail. Check remaining time before submitting. - Fallback strategy: if the SWIFT WebSocket disconnects, your bot should seamlessly fall back to standard
AuctionSubscriberflow. Don’t let a SWIFT outage stop you from filling auctions.
Related
- JIT Auctions - Understanding auction mechanics and pricing
- JIT-only MM - JIT market making strategy
- Orderbook & Matching - How orders are matched
- Bot Architecture - Reconnection, error handling, and production patterns
- Indicative Quotes - Signal liquidity offchain alongside SWIFT fills