1247/orderly-api-authentication/SKILL.md
Complete authentication guide for Orderly Network - EIP-712 wallet signatures for EVM accounts, Ed25519 message signing for Solana accounts, and Ed25519 signatures for API requests
npx skillsauth add starchild-ai-agent/community-skills @1247/orderly-api-authenticationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill covers both authentication layers in Orderly Network: wallet signatures (EIP-712 for EVM, Ed25519 message signing for Solana) for account registration and key management, and Ed25519 signatures for API request authentication.
woofi_dex, or your own)Orderly Network uses a two-layer authentication system supporting both EVM and Solana wallets:
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Wallet Authentication │
│ ───────────────────────────── │
│ • Account registration │
│ • API key management (add/remove keys) │
│ • Privileged operations (withdrawals, admin) │
│ │
│ EVM: EIP-712 typed data signing │
│ Solana: Ed25519 message signing │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Layer 2: API Authentication (Ed25519) │
│ ───────────────────────────────────── │
│ • Trading operations (place/cancel orders) │
│ • Reading account data (positions, balances) │
│ • WebSocket connections │
│ │
│ Signed by: Ed25519 key pair │
│ Key type: Locally-generated Ed25519 key pair │
└─────────────────────────────────────────────────────────────┘
1. User connects wallet
2. Wallet signs EIP-712 message to register account
3. Account ID is created
4. User generates Ed25519 key pair
5. Wallet signs EIP-712 message to authorize the Ed25519 key
6. Ed25519 key is used for all subsequent API calls
| Environment | API Base URL | WebSocket URL |
| ----------- | --------------------------------- | ---------------------------------------- |
| Mainnet | https://api.orderly.org | wss://ws.orderly.org/ws/stream |
| Testnet | https://testnet-api.orderly.org | wss://testnet-ws.orderly.org/ws/stream |
Note: These API base URLs work for both EVM and Solana wallets. Orderly's API is omnichain - the same endpoints handle both chains.
Don't hardcode chain IDs. Fetch them dynamically for your broker:
// Get supported chains for your broker
const response = await fetch(`https://api.orderly.org/v1/public/chain_info?broker_id=${BROKER_ID}`);
const { data } = await response.json();
// data.chains contains supported chain_ids
// Use these chain IDs for EIP-712 domain configuration
Orderly uses two different EIP-712 domains depending on the operation:
| Domain Type | Use Case | Mainnet | Testnet |
| ------------- | ------------------------------------------- | -------------------------------------------- | -------------------------------------------- |
| Off-chain | Account registration, API key management | 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC | 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC |
| On-chain | Withdrawals, internal transfers, settle PnL | 0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203 | 0x1826B75e2ef249173FC735149AE4B8e9ea10abff |
Important: The on-chain
verifyingContractis the Ledger contract on Orderly L2. This is a single contract for all chains (not per-chain). Vault contracts exist on each supported EVM chain for deposits, but the Ledger is the source of truth for on-chain operations.
Used for operations that don't directly interact with smart contracts:
const OFFCHAIN_DOMAIN = {
name: 'Orderly',
version: '1',
chainId: 421614, // Connected chain ID (e.g., Arbitrum Sepolia)
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
};
Used for operations that interact with the Ledger contract on Orderly L2:
const ONCHAIN_DOMAIN = {
name: 'Orderly',
version: '1',
chainId: 42161, // Connected chain ID
verifyingContract: isTestnet
? '0x1826B75e2ef249173FC735149AE4B8e9ea10abff'
: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203',
};
Wallet authentication is required for account-level operations that need proof of ownership.
Before registration, verify if the wallet already has an account:
const BROKER_ID = 'woofi_dex'; // Your broker ID
const walletAddress = '0x...'; // User's wallet address
const response = await fetch(
`https://testnet-api.orderly.org/v1/get_account?broker_id=${BROKER_ID}&user_address=${walletAddress}`
);
const data = await response.json();
// If data.success is true, account already exists
// If not, proceed with registration
Retrieve a unique nonce required for registration (valid for 2 minutes):
const nonceResponse = await fetch('https://testnet-api.orderly.org/v1/registration_nonce');
const { data: nonce } = await nonceResponse.json();
console.log('Registration nonce:', nonce);
Create and sign an EIP-712 typed message:
// Registration Message Type
const REGISTRATION_TYPES = {
Registration: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'registrationNonce', type: 'uint256' },
],
};
// Create the message
const registerMessage = {
brokerId: BROKER_ID,
chainId: 421614,
timestamp: Date.now(),
registrationNonce: nonce,
};
// Sign with wallet (e.g., MetaMask) - Use OFFCHAIN_DOMAIN for registration
const signature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [
walletAddress,
{
types: REGISTRATION_TYPES,
domain: OFFCHAIN_DOMAIN,
message: registerMessage,
primaryType: 'Registration',
},
],
});
Send the signed payload to create the Orderly Account ID:
const registerResponse = await fetch('https://testnet-api.orderly.org/v1/register_account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: registerMessage,
signature: signature,
userAddress: walletAddress,
}),
});
const result = await registerResponse.json();
console.log('Account ID:', result.data.account_id);
// Store this account ID - you'll need it for API authentication
Once you have an account, you need to register Ed25519 keys for API access.
import { getPublicKeyAsync, utils } from '@noble/ed25519';
// Generate 32-byte private key (cryptographically secure)
const privateKey = utils.randomPrivateKey();
// Derive public key
const publicKey = await getPublicKeyAsync(privateKey);
// Encode public key as base58 (required by Orderly)
function encodeBase58(bytes: Uint8Array): string {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let result = '';
let num = 0n;
for (const byte of bytes) {
num = num * 256n + BigInt(byte);
}
while (num > 0n) {
result = ALPHABET[Number(num % 58n)] + result;
num = num / 58n;
}
return result;
}
const orderlyKey = `ed25519:${encodeBase58(publicKey)}`;
// Convert bytes to hex string for storage
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
console.log('Orderly Key:', orderlyKey);
console.log('Private Key (hex):', bytesToHex(privateKey));
// STORE PRIVATE KEY SECURELY - NEVER SHARE IT
Associate the Ed25519 key with your account via EIP-712:
const ADD_KEY_TYPES = {
AddOrderlyKey: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'orderlyKey', type: 'string' },
{ name: 'scope', type: 'string' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'expiration', type: 'uint64' },
],
};
const addKeyMessage = {
brokerId: BROKER_ID,
chainId: 421614,
orderlyKey: orderlyKey,
scope: 'read,trading', // Permissions: read, trading, asset (comma-separated)
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year from now
};
// Use OFFCHAIN_DOMAIN for API key management
const addKeySignature = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [
walletAddress,
{
types: ADD_KEY_TYPES,
domain: OFFCHAIN_DOMAIN,
message: addKeyMessage,
primaryType: 'AddOrderlyKey',
},
],
});
Register the API key:
const keyResponse = await fetch('https://testnet-api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: addKeyMessage,
signature: addKeySignature,
userAddress: walletAddress,
}),
});
const keyResult = await keyResponse.json();
console.log('Key registered:', keyResult.success);
When registering an API key, specify permissions:
| Scope | Permissions |
| --------- | ------------------------------------ |
| read | Read positions, orders, balance |
| trading | Place, cancel, modify orders |
| asset | Deposit, withdraw, internal transfer |
Multiple scopes can be combined comma-separated: 'read,trading,asset'
To remove a key (requires Ed25519 authentication with another valid key):
// POST /v1/client/remove_orderly_key
const removeResponse = await signAndSendRequest(
accountId,
privateKey, // Must be a different valid key
'https://api.orderly.org/v1/client/remove_orderly_key',
{
method: 'POST',
body: JSON.stringify({
orderly_key: 'ed25519:...', // Key to remove
}),
}
);
Solana wallets use native Ed25519 message signing (not EIP-712) for account operations. Solana wallets already use Ed25519 keys natively, making the signing process simpler but requiring different message formatting.
| Aspect | EVM Wallets | Solana Wallets |
| ------------------ | --------------------- | -------------------------------- |
| Signing Method | EIP-712 typed data | Plain message signing |
| Key Type | secp256k1 | Ed25519 (native) |
| Account Lookup | /v1/get_account | /v1/get_account?chain_type=SOL |
| Message Format | Structured JSON types | Raw bytes via adapter |
| Signature | Ethereum signature | Ed25519 signature |
Check if a Solana wallet already has an Orderly account:
import { PublicKey } from '@solana/web3.js';
const BROKER_ID = 'woofi_dex';
const solanaAddress = '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU'; // Base58 address
// Solana accounts require chain_type=SOL parameter
const response = await fetch(
`https://testnet-api.orderly.org/v1/get_account?` +
`address=${solanaAddress}&` +
`broker_id=${BROKER_ID}&` +
`chain_type=SOL`
);
const data = await response.json();
// data.data.account_id contains the Orderly account ID
// Account ID format is different from EVM (not a keccak256 hash)
Orderly provides a Solana adapter to generate properly formatted messages:
import { DefaultSolanaWalletAdapter } from '@orderly.network/default-solana-adapter';
import { Connection, clusterApiUrl, Keypair } from '@solana/web3.js';
import { signAsync } from '@noble/ed25519';
import bs58 from 'bs58';
// Setup wallet adapter
const walletAdapter = new DefaultSolanaWalletAdapter();
// Initialize with wallet details
walletAdapter.active({
address: solanaAddress,
provider: {
connection: new Connection(clusterApiUrl('devnet')), // or 'mainnet-beta'
signMessage: async (msg: Uint8Array) => {
// Sign with Solana wallet (Ed25519)
return await signAsync(msg, privateKeyBytes.slice(0, 32));
},
sendTransaction: async (tx, conn) => {
tx.sign([senderKeypair]);
return conn.sendTransaction(tx);
},
},
chain: {
id: network === 'mainnet' ? 900900900 : 901901901, // Solana chain IDs
},
});
const nonceResponse = await fetch('https://testnet-api.orderly.org/v1/registration_nonce');
const { data: nonce } = await nonceResponse.json();
// Generate registration message using adapter
const registerMessage = await walletAdapter.generateRegisterMessage({
brokerId: BROKER_ID,
timestamp: Date.now(),
registrationNonce: nonce,
});
// Sign with Solana wallet (raw message bytes, not EIP-712)
const signature = await wallet.signMessage(registerMessage.message);
const registerResponse = await fetch('https://testnet-api.orderly.org/v1/register_account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: registerMessage.message,
signature: signature,
userAddress: solanaAddress,
chainType: 'SOL', // Required for Solana
}),
});
const result = await registerResponse.json();
console.log('Account ID:', result.data.account_id);
Same as EVM - locally generate an Ed25519 key pair:
import { getPublicKeyAsync, utils } from '@noble/ed25519';
import bs58 from 'bs58';
// Generate key pair
const privateKey = utils.randomPrivateKey();
const publicKey = await getPublicKeyAsync(privateKey);
const orderlyKey = `ed25519:${bs58.encode(publicKey)}`;
// Generate add key message using adapter
const addKeyMessage = await walletAdapter.generateAddKeyMessage({
brokerId: BROKER_ID,
orderlyKey: orderlyKey,
scope: 'read,trading',
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year
});
// Sign with Solana wallet
const signature = await wallet.signMessage(addKeyMessage.message);
const keyResponse = await fetch('https://testnet-api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: addKeyMessage.message,
signature: signature,
userAddress: solanaAddress,
chainType: 'SOL',
}),
});
Withdrawals require wallet signature on both EVM and Solana:
// Fetch withdraw nonce
const nonceRes = await fetch(`${BASE_URL}/v1/withdraw_nonce`);
const {
data: { withdraw_nonce },
} = await nonceRes.json();
// Generate withdraw message
const withdrawMessage = await walletAdapter.generateWithdrawMessage({
brokerId: BROKER_ID,
receiver: solanaAddress,
token: 'USDC',
amount: '1000',
timestamp: Date.now(),
nonce: Number(withdraw_nonce),
});
// Sign with Solana wallet
const signature = await wallet.signMessage(withdrawMessage.message);
// Submit withdrawal request
const res = await fetch(`${BASE_URL}/v1/withdraw_request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: withdrawMessage.message,
signature: signature,
userAddress: solanaAddress,
verifyingContract: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203', // Mainnet
// verifyingContract: '0x1826B75e2ef249173FC735149AE4B8e9ea10abff', // Testnet
}),
});
// Fetch settle nonce
const nonceRes = await fetch(`${BASE_URL}/v1/settle_nonce`);
const {
data: { settle_nonce },
} = await nonceRes.json();
// Generate settle message
const settleMessage = await walletAdapter.generateSettleMessage({
brokerId: BROKER_ID,
timestamp: Date.now(),
settlePnlNonce: settle_nonce,
});
// Sign with Solana wallet
const signature = await wallet.signMessage(settleMessage.message);
// Submit settle request
const res = await fetch(`${BASE_URL}/v1/settle_pnl`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: settleMessage.message,
signature: signature,
userAddress: solanaAddress,
verifyingContract: '0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203',
}),
});
| Environment | Solana Chain ID | Solana Cluster | Orderly Vault Address | Verifying Contract |
| ----------- | --------------- | -------------- | ---------------------------------------------- | -------------------------------------------- |
| Mainnet | 900900900 | mainnet-beta | ErBmAD61mGFKvrFNaTJuxoPwqrS8GgtwtqJTJVjFWx9Q | 0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203 |
| Testnet | 901901901 | devnet | 9shwxWDUNhtwkHocsUAmrNAQfBH2DHh4njdAEdHZZkF2 | 0x1826B75e2ef249173FC735149AE4B8e9ea10abff |
Note: API base URLs are the same for EVM and Solana. See the Environment Configuration section at the top of this skill.
keccak256(address, keccak256(brokerId))/v1/get_account API (not a hash)eth_signTypedData_v4 with structured EIP-712 typesSolana doesn't use EIP-712 domain configuration:
// EVM - requires domain
domain: {
name: 'Orderly',
version: '1',
chainId: 42161,
verifyingContract: '0x...',
}
// Solana - no domain, just raw message
const message = await walletAdapter.generateRegisterMessage({...});
Once you have registered an Ed25519 key via wallet signing (EIP-712 for EVM or Ed25519 message signing for Solana), you use that key for all API operations.
| Header | Description |
| -------------------- | ---------------------------------------- |
| orderly-timestamp | Unix timestamp in milliseconds |
| orderly-account-id | Your Orderly account ID |
| orderly-key | Your public key prefixed with ed25519: |
| orderly-signature | Base64url-encoded Ed25519 signature |
import { getPublicKeyAsync, utils } from '@noble/ed25519';
// Generate private key (32 cryptographically secure random bytes)
const privateKey = utils.randomPrivateKey();
// Derive public key
const publicKey = await getPublicKeyAsync(privateKey);
// Encode public key as base58 (required by Orderly)
function encodeBase58(bytes: Uint8Array): string {
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const BASE = 58n;
let num = 0n;
for (const byte of bytes) {
num = num * 256n + BigInt(byte);
}
let result = '';
while (num > 0n) {
result = ALPHABET[Number(num % BASE)] + result;
num = num / BASE;
}
// Handle leading zeros
for (const byte of bytes) {
if (byte === 0) {
result = '1' + result;
} else {
break;
}
}
return result;
}
const orderlyKey = `ed25519:${encodeBase58(publicKey)}`;
// Convert bytes to hex string (browser & Node.js compatible)
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
console.log('Private Key (hex):', bytesToHex(privateKey));
console.log('Public Key (base58):', orderlyKey);
// STORE PRIVATE KEY SECURELY - NEVER SHARE IT
function buildSignMessage(timestamp: number, method: string, path: string, body?: string): string {
// Message format: timestamp + method + path + body
// Note: No spaces or separators between parts
return `${timestamp}${method}${path}${body || ''}`;
}
// Examples
const timestamp = Date.now();
// GET request (no body)
const getMessage = buildSignMessage(timestamp, 'GET', '/v1/positions');
// POST request (with body)
const body = JSON.stringify({
symbol: 'PERP_ETH_USDC',
side: 'BUY',
order_type: 'LIMIT',
order_price: '3000',
order_quantity: '0.1',
});
const postMessage = buildSignMessage(timestamp, 'POST', '/v1/order', body);
import { signAsync } from '@noble/ed25519';
async function signRequest(
timestamp: number,
method: string,
path: string,
body: string | undefined,
privateKey: Uint8Array
): Promise<string> {
const message = buildSignMessage(timestamp, method, path, body);
// Sign with Ed25519
const signatureBytes = await signAsync(new TextEncoder().encode(message), privateKey);
// Encode as base64url (NOT base64)
// Convert to base64, then make it URL-safe
const base64 = btoa(String.fromCharCode(...signatureBytes));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
For a simple, standalone authentication helper that always works correctly with query parameters and proper Content-Type headers:
import { getPublicKeyAsync, signAsync } from '@noble/ed25519';
import { encodeBase58 } from 'ethers';
export async function signAndSendRequest(
orderlyAccountId: string,
privateKey: Uint8Array | string,
input: URL | string,
init?: RequestInit | undefined
): Promise<Response> {
const timestamp = Date.now();
const encoder = new TextEncoder();
const url = new URL(input);
let message = `${String(timestamp)}${init?.method ?? 'GET'}${url.pathname}${url.search}`;
if (init?.body) {
message += init.body;
}
const orderlySignature = await signAsync(encoder.encode(message), privateKey);
return fetch(input, {
headers: {
'Content-Type':
init?.method !== 'GET' && init?.method !== 'DELETE'
? 'application/json'
: 'application/x-www-form-urlencoded',
'orderly-timestamp': String(timestamp),
'orderly-account-id': orderlyAccountId,
'orderly-key': `ed25519:${encodeBase58(await getPublicKeyAsync(privateKey))}`,
'orderly-signature': Buffer.from(orderlySignature).toString('base64url'),
...(init?.headers ?? {}),
},
...(init ?? {}),
});
}
This helper function:
application/x-www-form-urlencoded, others use application/json)const baseUrl = 'https://api.orderly.org';
const accountId = '0x123...';
const privateKey = new Uint8Array(32); // Your private key
// GET request with query parameters
const positions = await signAndSendRequest(accountId, privateKey, `${baseUrl}/v1/positions`);
const positionsData = await positions.json();
// GET request with query params
const orders = await signAndSendRequest(
accountId,
privateKey,
`${baseUrl}/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE`
);
const ordersData = await orders.json();
// POST request with body
const order = await signAndSendRequest(accountId, privateKey, `${baseUrl}/v1/order`, {
method: 'POST',
body: JSON.stringify({
symbol: 'PERP_ETH_USDC',
side: 'BUY',
order_type: 'LIMIT',
order_price: '3000',
order_quantity: '0.1',
}),
});
const orderData = await order.json();
// DELETE request
const cancel = await signAndSendRequest(
accountId,
privateKey,
`${baseUrl}/v1/order?order_id=123&symbol=PERP_ETH_USDC`,
{ method: 'DELETE' }
);
class OrderlyApiError extends Error {
code: number;
details: any;
constructor(response: any) {
super(response.message || 'API Error');
this.code = response.code;
this.details = response;
}
}
// Usage with error handling
async function apiRequest(
accountId: string,
privateKey: Uint8Array,
url: string,
init?: RequestInit
) {
const response = await signAndSendRequest(accountId, privateKey, url, init);
const result = await response.json();
if (!result.success) {
throw new OrderlyApiError(result);
}
return result.data;
}
Query parameters must be included in the signature message. The URL is parsed to extract both pathname and search parameters:
// Correct - query params are parsed from the URL
const url = new URL('/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE', baseUrl);
// Message: timestamp + method + pathname + search
// Result: "1234567890123GET/v1/orders?symbol=PERP_ETH_USDC&status=INCOMPLETE"
// Wrong - query params added separately after signing
const path = '/v1/orders';
const signature = await sign(timestamp, 'GET', path);
const url = `${path}?symbol=PERP_ETH_USDC`; // Signature mismatch!
Cause: Signature doesn't match expected value
Check:
1. Message format: timestamp + method + path + body (no spaces)
2. Method is uppercase: GET, POST, DELETE, PUT
3. Path includes query parameters
4. Body is exact JSON string (same whitespace)
5. Signature is base64url encoded (not base64)
Cause: Timestamp is too old or too far in the future
Solution:
- Ensure server clock is synchronized
- Timestamp must be within ±30 seconds
- Generate timestamp immediately before signing
Cause: Public key format incorrect
Solution:
- Must be prefixed with 'ed25519:'
- Public key must be base58 encoded
- Key must be registered to account
When registering an API key, specify permissions:
| Scope | Permissions |
| --------- | ------------------------------------ |
| read | Read positions, orders, balance |
| trading | Place, cancel, modify orders |
| asset | Deposit, withdraw, internal transfer |
// When adding key via EIP-712 signing
const addKeyMessage = {
brokerId: 'woofi_dex',
chainId: 42161,
orderlyKey: 'ed25519:...',
scope: 'read,trading', // Multiple scopes comma-separated
timestamp: Date.now(),
expiration: Date.now() + 31536000000, // 1 year
};
// NEVER hardcode private keys
// BAD:
const privateKey = new Uint8Array([1, 2, 3, ...]);
// GOOD: Load from environment
const privateKeyHex = process.env.ORDERLY_PRIVATE_KEY;
// Convert hex string to Uint8Array (browser & Node.js compatible)
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
const privateKey = hexToBytes(privateKeyHex);
// BETTER: Use secure key management
// AWS KMS, HashiCorp Vault, etc.
Rotate your API keys periodically for security:
// Generate new key pair
const newPrivateKey = utils.randomPrivateKey();
const newPublicKey = await getPublicKeyAsync(newPrivateKey);
// Register new key (requires wallet signature via EIP-712)
// POST /v1/orderly_key - No Ed25519 auth required
const orderlyKey = `ed25519:${encodeBase58(newPublicKey)}`;
const timestamp = Date.now();
const expiration = timestamp + 31536000000; // 1 year
const addKeyMessage = {
brokerId: 'your_broker_id',
chainId: 42161, // Arbitrum mainnet
orderlyKey: orderlyKey,
scope: 'read,trading', // Comma-separated scopes
timestamp: timestamp,
expiration: expiration,
};
// Sign with wallet (EIP-712)
const addKeySignature = await wallet.signTypedData({
domain: {
name: 'Orderly',
version: '1',
chainId: 42161,
verifyingContract: '0x...', // Contract address
},
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
AddOrderlyKey: [
{ name: 'brokerId', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'orderlyKey', type: 'string' },
{ name: 'scope', type: 'string' },
{ name: 'timestamp', type: 'uint256' },
{ name: 'expiration', type: 'uint256' },
],
},
primaryType: 'AddOrderlyKey',
message: addKeyMessage,
});
const registerResponse = await fetch('https://api.orderly.org/v1/orderly_key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: addKeyMessage,
signature: addKeySignature,
userAddress: walletAddress,
}),
});
const registerResult = await registerResponse.json();
if (!registerResult.success) {
throw new Error(`Failed to register key: ${registerResult.message}`);
}
// Update your application config
config.privateKey = newPrivateKey;
config.orderlyKey = orderlyKey;
// Remove old key using authenticated request
// POST /v1/client/remove_orderly_key - Requires Ed25519 auth
const oldOrderlyKey = `ed25519:${encodeBase58(oldPublicKey)}`;
const removeResponse = await signAndSendRequest(
accountId,
newPrivateKey, // Use the NEW key to authenticate
'https://api.orderly.org/v1/client/remove_orderly_key',
{
method: 'POST',
body: JSON.stringify({
orderly_key: oldOrderlyKey,
}),
}
);
const removeResult = await removeResponse.json();
if (!removeResult.success) {
throw new Error(`Failed to remove old key: ${removeResult.message}`);
}
// Set IP restriction for key
POST /v1/client/set_orderly_key_ip_restriction
Body: {
orderly_key: 'ed25519:...',
ip_list: ['1.2.3.4', '5.6.7.8'],
}
// Get current restrictions
GET /v1/client/orderly_key_ip_restriction?orderly_key={key}
// Reset (remove) restrictions
POST /v1/client/reset_orderly_key_ip_restriction
Body: { orderly_key: 'ed25519:...' }
WebSocket also requires Ed25519 authentication:
const ws = new WebSocket(`wss://ws-private-evm.orderly.org/v2/ws/private/stream/${accountId}`);
ws.onopen = async () => {
const timestamp = Date.now();
const message = timestamp.toString();
const signature = await signAsync(new TextEncoder().encode(message), privateKey);
// Convert to base64url (browser & Node.js compatible)
const base64 = btoa(String.fromCharCode(...signature));
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
ws.send(
JSON.stringify({
id: 'auth_1',
event: 'auth',
params: {
orderly_key: orderlyKey,
sign: base64url,
timestamp: timestamp,
},
})
);
};
// Verify key is valid
GET /v1/get_orderly_key?orderly_key={key}
// Response
{
"success": true,
"data": {
"account_id": "0x...",
"valid": true,
"scope": "read,trading",
"expires_at": 1735689600000
}
}
| Chain | Chain ID | Mainnet | Testnet | | -------- | --------------------- | ------- | ------- | | Arbitrum | 42161 / 421614 | ✅ | ✅ | | Optimism | 10 / 11155420 | ✅ | ✅ | | Base | 8453 / 84532 | ✅ | ✅ | | Ethereum | 1 / 11155111 | ✅ | ✅ | | Solana | 900900900 / 901901901 | ✅ | ✅ | | Mantle | 5000 / 5003 | ✅ | ✅ |
"Nonce expired" error
"Account already exists" error
/v1/get_account to retrieve existing account info"Invalid signature" error
eth_signTypedData_v4 not eth_signTypedDataSignature Mismatch (Code 10016)
Cause: Signature doesn't match expected value
Check:
1. Message format: timestamp + method + path + body (no spaces)
2. Method is uppercase: GET, POST, DELETE, PUT
3. Path includes query parameters
4. Body is exact JSON string (same whitespace)
5. Signature is base64url encoded (not base64)
Timestamp Expired (Code 10017)
Cause: Timestamp is too old or too far in the future
Solution:
- Ensure server clock is synchronized
- Timestamp must be within ±30 seconds
- Generate timestamp immediately before signing
Invalid Orderly Key (Code 10019)
Cause: Public key format incorrect
Solution:
- Must be prefixed with 'ed25519:'
- Public key must be base58 encoded
- Key must be registered to account
| Aspect | EIP-712 Wallet Auth | Ed25519 API Auth |
| ------------------ | ----------------------------------------- | ----------------------------- |
| Purpose | Account operations, key management | Trading, reading data |
| Signer | User's Web3 wallet | Locally-generated Ed25519 key |
| Key type | Ethereum private key | Ed25519 key pair |
| Endpoints | /v1/register_account, /v1/orderly_key | All other endpoints |
| Signature type | EIP-712 typed data | Raw Ed25519 + base64url |
| Scope | Create/manage API keys | Use API keys for trading |
development
OpenSea API integration for NFT and token discovery, marketplace intelligence, and order/transaction workflows. Use when working with OpenSea data or trading flows (e.g. collection stats, trending collections/tokens, NFT metadata, listings/offers, swap quotes, transaction receipt polling).
development
Generate a warm, healing parallel-universe fairy tale (~1000 words) plus 3 cohesive storybook illustrations, themed "if this person had never been born, what would the world miss." Output is a polished HTML storybook that can be previewed and published. Use when the user wants a personalized "if I had never been born" / "如果我没出生" tale for a real person — input is a name, age, and 3 key life events. Great for birthdays, memorials, encouragement gifts, or healing keepsakes.
development
Onboard a user to Phala Cloud and deploy a verifiable Starchild TEE agent — a minimal FastAPI runtime running inside an Intel TDX confidential VM, plus a published chat dashboard with attestation verification. Use when the user wants to "try TEE", "run an agent in a confidential VM", "deploy to Phala", or replicate the internal Starchild TEE test setup.
tools
Trade on Polymarket prediction markets (CLOB V2) from a Privy EOA wallet. Search markets, place/cancel orders, manage positions. No private key handling. Use when the user wants to bet on event outcomes (e.g. "buy YES at 0.65 on the ceasefire market", "what are my open positions", "close my Trump bet").