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¶
- Generate an Ed25519 key pair encoded with Base64.
- 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 - Persist
PRIVATE_KEY_BASE64,PRIVATE_KEY_BASE64_SEED, andPUBLIC_KEY_BASE64securely (never commit them). - Send the public key to LiquidMesh at
support@liquidmesh.ioso it can be registered with your account. - 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¶
- Capture the exact request components:
timestampMs = Date.now()- Upper-case HTTP method (e.g.
POST) - Canonical request path (e.g.
/v1/bsc/swap, no scheme or host) - Raw request body string (empty for GET calls)
- Concatenate them:
preimage = `${timestampMs}${method}${path}${body}`. - Compute
message = sha256(preimage)and encode as lowercase hex. - Build the payload
{ tim: timestampMs, message, iss: API_KEY }plus optionaliat/exp. - Sign with your Ed25519 private key using the
EdDSAalgorithm.
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.