Skip to Content

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. SwiftOrderSubscriber defaults endpoint based on velocityEnv when you don’t pass one explicitly: wss://swift.velocity.exchange/ws for mainnet-beta, wss://swift.master.velocity.exchange/ws for devnet (both hardcoded in sdk/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 ↗
PropertyTypeRequired
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 ↗

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.

PropertyTypeRequired
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) => boolean
Returns true if a `User` account keyed by `key` (the `User` account pubkey, base58) is cached in the map.
Yes
get
(key: string) => User | undefined
gets the User for a particular userAccountPublicKey, if no User exists, undefined is returned
Yes
getWithSlot
(key: string) => DataAndSlot<User> | undefined
Like `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 | undefined
gets 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
() => number
Number 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
any
Builds 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
any
Syncs 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
any
Syncs 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) => void
Advances the map's tracked most-recent slot to `slot` if it's newer.
Yes
getSlot
() => number
Returns 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.

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 ↗
ParameterTypeRequired
signedSignedMsgOrderParams
SignedMsgOrderParams
The 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
OptionalOrderParams
Maker order to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price` is PRICE_PRECISION (1e6). Must have `orderType: LIMIT`, `postOnly` set, and be IOC.
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-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
number
Explicit sysvar-instructions index override.
No
takerEscrow
RevenueShareEscrowAccount
The 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 ↗
ParameterTypeRequired
signedSignedMsgOrderParams
SignedMsgOrderParams
The 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
OptionalOrderParams
Maker order to place; see `placeAndMakeSignedMsgPerpOrder` for field precisions and required order shape.
Yes
subAccountId
number
Sub-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
number
Explicit sysvar-instructions index override.
No
takerEscrow
RevenueShareEscrowAccount
See `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 ↗

True if the order was submitted via the signed-message (swift/off-chain relay) path (`OrderBitFlag.SignedMessage`).

ParameterTypeRequired
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 AuctionSubscriber for complete coverage, using isSignedMsgOrder() 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 userAccountGetter callback should return quickly. If it requires an RPC call per order, you’ll negate the latency advantage. Pre-load user accounts via UserMap subscription.
  • Order expiry: SWIFT signed orders have a maxTs field. 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 AuctionSubscriber flow. Don’t let a SWIFT outage stop you from filling auctions.
Last updated on