Skip to content

Using JWTs for LiquidMesh API Requests

All LiquidMesh Gateway integrations use Ed25519-signed JWTs going forward. Every call must include both the LM-API-KEY header and a short-lived Bearer token to prove request integrity.


Prerequisites

  1. Generate an Ed25519 key pair encoded with Base64.
  2. Run the Node.js snippet below (Node 18+):
    node - <<'NODE'
    import { generateKeyPairSync } from 'crypto';
    
    const { publicKey, privateKey } = generateKeyPairSync('ed25519');
    
    const seed = privateKey.export({ type: 'pkcs8', format: 'der' }).subarray(16, 48);
    const pubKeyRaw = publicKey.export({ type: 'spki', format: 'der' }).subarray(-32);
    const fullPrivateKey = Buffer.concat([seed, pubKeyRaw]);
    
    console.log('PUBLIC_KEY_BASE64:', pubKeyRaw.toString('base64'));
    console.log('PRIVATE_KEY_BASE64 (seed+public):', fullPrivateKey.toString('base64'));
    console.log('PRIVATE_KEY_BASE64_SEED (Base64URL for JWT d):', seed.toString('base64url'));
    NODE
    
  3. Persist PRIVATE_KEY_BASE64, PRIVATE_KEY_BASE64_SEED, and PUBLIC_KEY_BASE64 securely (never commit them).
  4. Send the public key to LiquidMesh at support@liquidmesh.io so it can be registered with your account.
  5. Receive the API key bound to your public key along with your gateway base URL (e.g. https://api.liquidmesh.io). Store both secrets in a dedicated key vault or secrets manager and load them securely at runtime.

JWT structure

Field Location Description
typ Header Always JWT.
alg Header Always EdDSA (Ed25519).
tim Payload Millisecond timestamp when the token is created. Used for replay protection.
message Payload Hex-encoded SHA-256 hash of the preimage {timestamp}{METHOD}{PATH}{BODY}.
iss Payload Your API key (issuer).
iat / exp Payload Issued-at and expiration timestamps in seconds. Recommended TTL ≤ 2 seconds.

Authorization: Bearer <jwt> must accompany every request together with LM-API-KEY.


Signing workflow

  1. Capture the exact request components:
  2. timestampMs = Date.now()
  3. Upper-case HTTP method (e.g. POST)
  4. Canonical request path (e.g. /v1/bsc/swap, no scheme or host)
  5. Raw request body string (empty for GET calls)
  6. Concatenate them: preimage = `${timestampMs}${method}${path}${body}`.
  7. Compute message = sha256(preimage) and encode as lowercase hex.
  8. Build the payload { tim: timestampMs, message, iss: API_KEY } plus optional iat/exp.
  9. Sign with your Ed25519 private key using the EdDSA algorithm.

Examples (Node.js)

import { SignJWT, importJWK } from 'jose';
import { createHash } from 'crypto';

const apiKey = process.env.API_KEY;
const path = '/v1/bsc/quote';
const body = '';
const timestamp = Date.now();
const preimage = `${timestamp}GET${path}${body}`;
const message = createHash('sha256').update(preimage).digest('hex');

const privateKey = await importJWK({
  kty: 'OKP',
  crv: 'Ed25519',
  d: process.env.PRIVATE_KEY_BASE64_SEED,
  x: process.env.PUBLIC_KEY_BASE64
}, 'EdDSA');

const token = await new SignJWT({ tim: timestamp, message, iss: apiKey })
  .setProtectedHeader({ typ: 'JWT', alg: 'EdDSA' })
  .setIssuedAt()
  .setExpirationTime('2s')
  .sign(privateKey);

Quote request (GET)

const baseUrl = process.env.BASE_URL;
const query = new URLSearchParams({
  amount: '10000000000',
  chainId: '56',
  inputToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
  outputToken: '0x55d398326f99059fF775485246999027B3197955'
}).toString();

const quotePath = `/v1/bsc/quote?${query}`;
const quoteTimestamp = Date.now();
const quotePreimage = `${quoteTimestamp}GET${quotePath}`;
const quoteMessage = createHash('sha256').update(quotePreimage).digest('hex');

const quoteToken = await new SignJWT({ tim: quoteTimestamp, message: quoteMessage, iss: apiKey })
  .setProtectedHeader({ typ: 'JWT', alg: 'EdDSA' })
  .setIssuedAt()
  .setExpirationTime('2s')
  .sign(privateKey);

const quoteResponse = await fetch(`${baseUrl}${quotePath}`, {
  method: 'GET',
  headers: {
    'LM-API-KEY': apiKey,
    'Authorization': `Bearer ${quoteToken}`,
    'Content-Type': 'application/json'
  }
});

Swap request (POST)

const swapPath = '/v1/bsc/swap';
const swapBody = JSON.stringify({
  userAddress: '0x5EA0E65751c95bA3CEdaeC5BcD95606094160ce1',
  slippageBps: 10000,
  swapInfo: {
    chainId: '56',
    inputToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
    inputAmount: '10000000000',
    outputToken: '0x55d398326f99059fF775485246999027B3197955',
    slippageBps: 5000,
    outputAmount: '1',
    routePlans: [/* ... */]
  }
});

const swapTimestamp = Date.now();
const swapPreimage = `${swapTimestamp}POST${swapPath}${swapBody}`;
const swapMessage = createHash('sha256').update(swapPreimage).digest('hex');

const swapToken = await new SignJWT({ tim: swapTimestamp, message: swapMessage, iss: apiKey })
  .setProtectedHeader({ typ: 'JWT', alg: 'EdDSA' })
  .setIssuedAt()
  .setExpirationTime('2s')
  .sign(privateKey);

const swapResponse = await fetch(`${baseUrl}${swapPath}`, {
  method: 'POST',
  headers: {
    'LM-API-KEY': apiKey,
    'Authorization': `Bearer ${swapToken}`,
    'Content-Type': 'application/json'
  },
  body: swapBody
});

Best practices

  • Keep private keys in a dedicated secrets manager and never commit them to version control.

Need help? Contact support@liquidmesh.io.