Skip to main content

What Gets Signed

The signature is computed over a bincode serialization of:
action + account + signer
These three components are serialized together and then signed with your Ed25519 private key. Note: The signature field itself is NOT included in what gets signed.

Working Example

Important: Bincode serialization is complex and must match exactly. For production use, always use the official SDKs when available. This implementation is provided for reference only.

Install Dependencies

pnpm install tweetnacl bs58
# or
yarn add tweetnacl bs58

Expected Format

import * as nacl from 'tweetnacl';
import bs58 from 'bs58';

/**
 * Write a u64 little-endian
 */
function writeU64(value: number, buffer: Uint8Array, offset: number): number {
    const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 8);
    view.setBigUint64(0, BigInt(value), true); // little-endian
    return offset + 8;
}

/**
 * Write a u32 little-endian
 */
function writeU32(value: number, buffer: Uint8Array, offset: number): number {
    const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 4);
    view.setUint32(0, value, true); // little-endian
    return offset + 4;
}

/**
 * Write a f64 little-endian
 */
function writeF64(value: number, buffer: Uint8Array, offset: number): number {
    const view = new DataView(buffer.buffer, buffer.byteOffset + offset, 8);
    view.setFloat64(0, value, true); // little-endian
    return offset + 8;
}

/**
 * Write a string with length prefix (u64 LE)
 */
function writeString(str: string, buffer: Uint8Array, offset: number): number {
    const encoder = new TextEncoder();
    const strBytes = encoder.encode(str);
    offset = writeU64(strBytes.length, buffer, offset);
    buffer.set(strBytes, offset);
    return offset + strBytes.length;
}

/**
 * Serialize transaction using the exact bincode format
 */
function serializeTransaction(action: any, account: string, signer: string): Uint8Array {
    const parts: Uint8Array[] = [];
    
    // 1. Transaction type
    if (action.type === 'order') {
        const typeBytes = new TextEncoder().encode('order');
        const typePart = new Uint8Array(8 + typeBytes.length);
        writeU64(typeBytes.length, typePart, 0);
        typePart.set(typeBytes, 8);
        parts.push(typePart);
        
        // 2. Quantity of orders
        const orders = action.orders || [];
        const countPart = new Uint8Array(8);
        writeU64(orders.length, countPart, 0);
        parts.push(countPart);
        
        // 3. For each order
        for (const order of orders) {
            const assetBytes = new TextEncoder().encode(order.c);
            const assetSize = 8 + assetBytes.length;
            const orderSize = assetSize + 1 + 8 + 8 + 1 + 4; // asset + is_buy + price + size + reduce_only + order_type
            let orderTypeSize = 0;
            
            if (order.t.limit) {
                orderTypeSize = 4; // TIF u32
            } else if (order.t.trigger) {
                orderTypeSize = 1 + 8; // is_market + triggerPx
            }
            
            const totalOrderSize = orderSize + orderTypeSize;
            const orderPart = new Uint8Array(totalOrderSize);
            let offset = 0;
            
            // Asset
            offset = writeString(order.c, orderPart, offset);
            
            // is_buy
            orderPart[offset] = order.b ? 1 : 0;
            offset += 1;
            
            // price
            offset = writeF64(order.px, orderPart, offset);
            
            // size
            offset = writeF64(order.sz, orderPart, offset);
            
            // reduce_only
            orderPart[offset] = order.r ? 1 : 0;
            offset += 1;
            
            // order_type
            if (order.t.limit) {
                writeU32(0, orderPart, offset); // limit = 0
                offset += 4;
                // TIF: 0 = GTC, 1 = IOC, 2 = ALO
                const tifMap: { [key: string]: number } = { 'GTC': 0, 'IOC': 1, 'ALO': 2 };
                writeU32(tifMap[order.t.limit.tif] || 0, orderPart, offset);
                offset += 4;
            } else if (order.t.trigger) {
                writeU32(1, orderPart, offset); // trigger = 1
                offset += 4;
                // is_market
                orderPart[offset] = order.t.trigger.is_market ? 1 : 0;
                offset += 1;
                // triggerPx
                offset = writeF64(order.t.trigger.triggerPx, orderPart, offset);
            }
            
            parts.push(orderPart);
        }
    } else if (action.type === 'cancel') {
        const typeBytes = new TextEncoder().encode('cancel');
        const typePart = new Uint8Array(8 + typeBytes.length);
        writeU64(typeBytes.length, typePart, 0);
        typePart.set(typeBytes, 8);
        parts.push(typePart);
        
        const cancels = action.cancels || [];
        const countPart = new Uint8Array(8);
        writeU64(cancels.length, countPart, 0);
        parts.push(countPart);
        
        for (const cancel of cancels) {
            const cancelSize = 8 + new TextEncoder().encode(cancel.c).length + 8 + new TextEncoder().encode(cancel.oid).length;
            const cancelPart = new Uint8Array(cancelSize);
            let offset = 0;
            
            offset = writeString(cancel.c, cancelPart, offset);
            offset = writeString(cancel.oid, cancelPart, offset);
            
            parts.push(cancelPart);
        }
    } else if (action.type === 'cancelall') {
        const typeBytes = new TextEncoder().encode('cancelall');
        const typePart = new Uint8Array(8 + typeBytes.length);
        writeU64(typeBytes.length, typePart, 0);
        typePart.set(typeBytes, 8);
        parts.push(typePart);
        
        const cancels = action.cancels || [];
        const countPart = new Uint8Array(8);
        writeU64(cancels.length, countPart, 0);
        parts.push(countPart);
        
        for (const cancel of cancels) {
            const cancelPart = new Uint8Array(8 + new TextEncoder().encode(cancel.c).length);
            let offset = 0;
            offset = writeString(cancel.c, cancelPart, offset);
            parts.push(cancelPart);
        }
    } else if (action.type === 'updateusersettings') {
        const typeBytes = new TextEncoder().encode('updateusersettings');
        const typePart = new Uint8Array(8 + typeBytes.length);
        writeU64(typeBytes.length, typePart, 0);
        typePart.set(typeBytes, 8);
        parts.push(typePart);
        
        if (action.settings && action.settings.m) {
            const leverageCount = action.settings.m.length;
            const countPart = new Uint8Array(8);
            writeU64(leverageCount, countPart, 0);
            parts.push(countPart);
            
            for (const [symbol, leverage] of action.settings.m) {
                const symbolBytes = new TextEncoder().encode(symbol);
                const leverageSize = 8 + symbolBytes.length + 8;
                const leveragePart = new Uint8Array(leverageSize);
                let offset = 0;
                
                offset = writeString(symbol, leveragePart, offset);
                offset = writeF64(leverage, leveragePart, offset);
                
                parts.push(leveragePart);
            }
        } else {
            const countPart = new Uint8Array(8);
            writeU64(0, countPart, 0);
            parts.push(countPart);
        }
    } else if (action.type === 'agentwalletcreation') {
        const typeBytes = new TextEncoder().encode('agentwalletcreation');
        const typePart = new Uint8Array(8 + typeBytes.length);
        writeU64(typeBytes.length, typePart, 0);
        typePart.set(typeBytes, 8);
        parts.push(typePart);
        
        if (action.agent) {
            const agentBytes = new TextEncoder().encode(action.agent.a);
            const agentPart = new Uint8Array(8 + agentBytes.length + 1);
            let offset = 0;
            offset = writeString(action.agent.a, agentPart, offset);
            agentPart[offset] = action.agent.d ? 1 : 0;
            parts.push(agentPart);
        }
    } else if (action.type === 'faucet') {
        const typeBytes = new TextEncoder().encode('faucet');
        const typePart = new Uint8Array(8 + typeBytes.length);
        writeU64(typeBytes.length, typePart, 0);
        typePart.set(typeBytes, 8);
        parts.push(typePart);
        
        if (action.faucet) {
            const userBytes = new TextEncoder().encode(action.faucet.u);
            const userPart = new Uint8Array(8 + userBytes.length);
            writeU64(userBytes.length, userPart, 0);
            userPart.set(userBytes, 8);
            parts.push(userPart);
        }
    }
    
    // 4. Account (32 bytes, decoded from base58)
    const accountBytes = bs58.decode(account);
    if (accountBytes.length !== 32) {
        throw new Error(`Account must be 32 bytes, got ${accountBytes.length}`);
    }
    parts.push(accountBytes);
    
    // 5. Signer (32 bytes, decoded from base58)
    const signerBytes = bs58.decode(signer);
    if (signerBytes.length !== 32) {
        throw new Error(`Signer must be 32 bytes, got ${signerBytes.length}`);
    }
    parts.push(signerBytes);
    
    // Concatenate all parts
    const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
    const result = new Uint8Array(totalLength);
    let resultOffset = 0;
    for (const part of parts) {
        result.set(part, resultOffset);
        resultOffset += part.length;
    }
    
    return result;
}

/**
 * Sign a transaction action for the exchange
 */
function signTransaction(
    secretKey: Uint8Array,
    action: any,
    account: string,
    signer: string
): string {
    // Serialize the transaction
    const message = serializeTransaction(action, account, signer);
    
    // Sign with Ed25519
    const signature = nacl.sign.detached(message, secretKey);
    
    // Encode as base58
    return bs58.encode(signature);
}

Account vs Signer

The transaction includes two separate public key fields:
  • account: The account being traded (whose positions/orders are affected)
  • signer: Who’s signing the transaction (usually same as account, or authorized agent)

Same Account and Signer

Most common case: you’re trading your own account.
const transaction = {
  action: { type: "order", orders: [order] },
  account: myPublicKey,    // Your account
  signer: myPublicKey      // You're signing
};

Agent Wallet (Different Signer)

Agent wallet trading on behalf of user:
const transaction = {
  action: { type: "order", orders: [order] },
  account: userPublicKey,   // User's account being traded
  signer: agentPublicKey    // Agent signing (must be pre-authorized)
};
Agent Wallets: If signer != account, the signer must be pre-authorized via the /agent-wallet endpoint.

Implementation Notes

Official SDKs Available: Official SDKs for TypeScript, Python, Rust, and other languages handle all bincode serialization correctly and provide a simple, type-safe API.For production use, always prefer SDKs over manual implementation to ensure correctness and maintainability.
Bincode serialization requires:
  • Little-endian encoding for all numeric types
  • Length-prefixed strings (u64 length prefix)
  • Exact format matching for all action types
If implementing manually, ensure your bincode serialization matches the exchange’s expected format exactly.