APIPerps Auth EVM Example

StandX Perps Authentication - EVM Example

This example demonstrates how to authenticate with the StandX Perps API using an EVM-compatible wallet (e.g., BSC).

Prerequisites

  • Node.js environment with TypeScript support
  • EVM wallet with private key
  • Required packages:
    npm install @noble/curves @scure/base ethers

Complete Implementation

import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base";
import { ethers } from "ethers";
 
// Types
export type Chain = "bsc" | "solana";
 
export interface SignedData {
  domain: string;
  uri: string;
  statement: string;
  version: string;
  chainId: number;
  nonce: string;
  address: string;
  requestId: string;
  issuedAt: string;
  message: string;
  exp: number;
  iat: number;
}
 
export interface LoginResponse {
  token: string;
  address: string;
  alias: string;
  chain: string;
  perpsAlpha: boolean;
}
 
export interface RequestSignatureHeaders {
  "x-request-sign-version": string;
  "x-request-id": string;
  "x-request-timestamp": string;
  "x-request-signature": string;
}
 
// Authentication Class
export class StandXAuth {
  private ed25519PrivateKey: Uint8Array;
  private ed25519PublicKey: Uint8Array;
  private requestId: string;
  private baseUrl = "https://api.standx.com";
 
  constructor() {
    const privateKey = ed25519.utils.randomSecretKey();
    this.ed25519PrivateKey = privateKey;
    this.ed25519PublicKey = ed25519.getPublicKey(privateKey);
    this.requestId = base58.encode(this.ed25519PublicKey);
  }
 
  async authenticate(
    chain: Chain,
    walletAddress: string,
    signMessage: (msg: string) => Promise<string>
  ): Promise<LoginResponse> {
    const signedDataJwt = await this.prepareSignIn(chain, walletAddress);
    const payload = this.parseJwt<SignedData>(signedDataJwt);
    const signature = await signMessage(payload.message);
    return this.login(chain, signature, signedDataJwt);
  }
 
  private async prepareSignIn(chain: Chain, address: string): Promise<string> {
    const res = await fetch(
      `${this.baseUrl}/v1/offchain/prepare-signin?chain=${chain}`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ address, requestId: this.requestId }),
      }
    );
    const data = await res.json();
    if (!data.success) throw new Error("Failed to prepare sign-in");
    return data.signedData;
  }
 
  private async login(
    chain: Chain,
    signature: string,
    signedData: string,
    expiresSeconds: number = 604800 // default: 7 days
  ): Promise<LoginResponse> {
    const res = await fetch(
      `${this.baseUrl}/v1/offchain/login?chain=${chain}`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ signature, signedData, expiresSeconds }),
      }
    );
    return res.json();
  }
 
  signRequest(
    payload: string,
    requestId: string,
    timestamp: number
  ): RequestSignatureHeaders {
    const version = "v1";
    const message = `${version},${requestId},${timestamp},${payload}`;
    const signature = ed25519.sign(
      Buffer.from(message, "utf-8"),
      this.ed25519PrivateKey
    );
 
    return {
      "x-request-sign-version": version,
      "x-request-id": requestId,
      "x-request-timestamp": timestamp.toString(),
      "x-request-signature": Buffer.from(signature).toString("base64"),
    };
  }
 
  private parseJwt<T>(token: string): T {
    const base64Url = token.split(".")[1];
    const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
    return JSON.parse(Buffer.from(base64, "base64").toString("utf-8"));
  }
}
 
// Usage Example
async function main() {
  // Initialize auth
  const auth = new StandXAuth();
 
  // Setup wallet
  const provider = new ethers.JsonRpcProvider(
    "https://bsc-dataseed.binance.org/"
  );
  const privateKey = process.env.WALLET_PRIVATE_KEY!;
  const wallet = new ethers.Wallet(privateKey, provider);
 
  // Authenticate
  const loginResponse = await auth.authenticate(
    "bsc",
    wallet.address,
    async (message) => wallet.signMessage(message)
  );
 
  console.log("Access Token:", loginResponse.token);
 
  // Sign a request
  const payload = JSON.stringify({
    symbol: "BTC-USD",
    side: "buy",
    order_type: "limit",
    qty: "0.1",
    price: "50000",
    time_in_force: "gtc",
    reduce_only: false,
  });
 
  const headers = auth.signRequest(payload, crypto.randomUUID(), Date.now());
 
  // Make authenticated request
  await fetch("https://perps.standx.com/api/new_order", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${loginResponse.token}`,
      ...headers,
    },
    body: payload,
  });
}
 
main().catch(console.error);

Key Points

  1. Wallet Setup: Uses ethers.js to create a wallet from a private key
  2. Message Signing: EVM wallets sign the message directly using wallet.signMessage()
  3. Signature Format: The signature is returned as-is from the wallet (hex string)

Environment Variables

Create a .env file with:

WALLET_PRIVATE_KEY=your_private_key_here

Security Note: Never commit private keys to version control. Use environment variables or secure key management solutions.