StandX Perps Authentication
⚠️ This document is under construction.
This document explains how to obtain JWT access tokens for the StandX Perps API through wallet signatures.
Prerequisites
- Valid wallet address and corresponding private key
- Development environment with
ed25519algorithm support
Authentication Flow
1. Prepare Wallet and Temporary ed25519 Key Pair
- Prepare Wallet: Ensure you have a blockchain wallet with its address and private key.
- Generate Temporary ed25519 Key Pair and
requestId
2. Get Signature Data
Request signature data from the server:
Note: Code examples provided below are for reference purposes only and demonstrate the general implementation approach. Adapt them to your specific production environment.
Using curl
curl 'https://api.standx.com/v1/offchain/prepare-signin?chain=<chain>' \
-H 'Content-Type: application/json' \
--data-raw '{
"address": "<your_wallet_address>",
"requestId": "<base58_encoded_public_key>"
}'TypeScript/ES6 Implementation Reference
import axios from "axios";
const chain = "bsc"; // or "solana"
const walletAddress = "<your_wallet_address>";
const url = `https://api.standx.com/v1/offchain/prepare-signin?chain=${chain}`;
const data = {
address: walletAddress,
requestId: requestId, // requestId from previous step
};
try {
const response = await axios.post(url, data, {
headers: { "Content-Type": "application/json" },
});
if (response.data.success) {
const signedData = response.data.signedData;
// Use signedData for next step
}
} catch (error) {
console.error("Request failed:", error.message);
}Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| chain | string | Yes | Blockchain network: bsc or solana |
| address | string | Yes | Wallet address |
| requestId | string | Yes | Base58-encoded ed25519 public key from step 1 |
Success Response
{
"success": true,
"signedData": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}3. Parse and Verify Signature Data
signedData is a JWT string that must be verified using StandX’s public key.
Get Verification Public Key
# Using curl
curl 'https://api.standx.com/v1/offchain/certs'Example signedData Payload
{
"domain": "standx.com",
"uri": "https://standx.com",
"statement": "Sign in with Ethereum to access more StandX features...",
"version": "1",
"chainId": 56,
"nonce": "74Gd7Plf3a1TMVElc",
"address": "0x...",
"requestId": "<requestId>",
"issuedAt": "2025-10-12T17:46:44.731Z",
"message": "standx.com wants you to sign in with your Ethereum account:\n...",
"exp": 1760291384,
"iat": 1760291204
}4. Sign the Message
Sign payload.message with your wallet private key to generate the signature.
TypeScript/ES6 Implementation Reference
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(
"https://bsc-dataseed.binance.org/"
);
const privateKey = "<your_wallet_private_key>"; // Keep secure; use environment variables
const wallet = new ethers.Wallet(privateKey, provider);
// Sign using the message from the parsed payload
const signature = await wallet.signMessage(payload.message);5. Get Access Token
Submit the signature and original signedData to the login endpoint.
Optional Parameter:
expiresSeconds(number): Token expiration time in seconds. Defaults to604800(7 days) if not specified. This controls how long the JWT access token remains valid before requiring re-authentication.
Security Note: For security best practices, avoid setting excessively long expiration times. Shorter token lifetimes reduce the risk of unauthorized access if a token is compromised. Consider your security requirements when configuring this value.
Using curl
curl 'https://api.standx.com/v1/offchain/login?chain=<chain>' \
-H 'Content-Type: application/json' \
--data-raw '{
"signature": "0x...",
"signedData": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresSeconds": 604800
}'TypeScript/ES6 Implementation Reference
const loginUrl = `https://api.standx.com/v1/offchain/login?chain=${chain}`;
try {
const loginResponse = await axios.post(
loginUrl,
{
signature,
signedData,
expiresSeconds, // default: seconds for 7days
},
{
headers: { "Content-Type": "application/json" },
}
);
const { token, address, chain } = loginResponse.data;
// Store token for subsequent API requests
} catch (error) {
console.error("Login failed:", error.message);
}Success Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"address": "0x...",
"alias": "user123",
"chain": "bsc",
"perpsAlpha": true
}6. Use Access Token
Use the obtained token for subsequent API requests by adding Authorization: Bearer <token> to the request headers.
Body Signature Flow
Basic Flow
- Prepare a key pair
- Build message:
{version},{id},{timestamp},{payload} - Sign with private key
- Base64 encode signature
- Attach signature to request headers
{
...
"authorization": "Bearer <token>",
"x-request-sign-version": "v1",
"x-request-id": "uuid",
"x-request-timestamp": "timestamp",
"x-request-signature": "signature",
...
}Code example (only for reference):
import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base";
import { v4 as uuidv4 } from "uuid";
/**
* Sign request and return Base64-encoded signature.
*/
function encodeRequestSignature(
xRequestVersion: string,
xRequestId: string,
xRequestTimestamp: number,
payload: string,
signingKey: Uint8Array
): string {
// Build message to sign: "{version},{id},{timestamp},{payload}"
const signMsg = `${xRequestVersion},${xRequestId},${xRequestTimestamp},${payload}`;
// Sign message with Ed25519 private key
const messageBytes = Buffer.from(signMsg, "utf-8");
const signature = ed25519.sign(messageBytes, signingKey);
// Base64 encode the signature
return Buffer.from(signature).toString("base64");
}
// --- Example Usage ---
// Generate Ed25519 key pair
const privateKey = ed25519.utils.randomSecretKey();
const publicKey = ed25519.getPublicKey(privateKey);
// Generate requestId (base58-encoded public key)
const requestId = base58.encode(publicKey);
// Prepare request parameters
const xRequestVersion = "v1";
const xRequestId = uuidv4();
const xRequestTimestamp = Date.now();
const payloadDict = {
user_id: 12345,
data: "some important information",
};
const payloadStr = JSON.stringify(payloadDict);
// Generate signature
const signature = encodeRequestSignature(
xRequestVersion,
xRequestId,
xRequestTimestamp,
payloadStr,
privateKey
);
// Verify signature (optional)
try {
const verifyMsg = `v1,${xRequestId},${xRequestTimestamp},${payloadStr}`;
const signatureBytes = Buffer.from(signature, "base64");
const messageBytes = Buffer.from(verifyMsg, "utf-8");
const isValid = ed25519.verify(signatureBytes, messageBytes, publicKey);
if (!isValid) throw new Error("Verification failed");
} catch (error) {
console.error("Signature verification error:", error.message);
}
// Send Request with Body Signature
fetch("/api/request_need_body_signature", {
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${token}`,
"x-request-sign-version": "v1",
"x-request-id": xRequestId,
"x-request-timestamp": xRequestTimestamp.toString(),
"x-request-signature": signature,
},
body: payloadStr,
});Complete Authentication Class Example
Here’s a complete implementation using a class-based approach:
import { ed25519 } from "@noble/curves/ed25519";
import { base58 } from "@scure/base";
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;
}
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
import { ethers } from "ethers";
async function example() {
// 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,
});
}