API Authorization
Overview
This endpoint enables Web3 users to authenticate by connecting their wallet and signing a message. Upon successful verification, a JWT token is issued for authorizing subsequent API requests.
Authentication Flow
1. Frontend generates message with wallet address, chain ID, timestamp, and nonce
2. User signs the message using their wallet (personal_sign)
3. Frontend sends signed message to this endpoint
4. Backend verifies signature and wallet ownership
5. Backend issues JWT token
6. Frontend includes JWT token in Authorization header for all subsequent API callsEndpoint Information
Endpoint: POST /api/v1/makers/connect
Description: Authenticate Web3 wallet connection and obtain JWT token for API authorization
Authentication Required: No (this is the authentication endpoint)
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| wallet_address | string | Yes | Ethereum-compatible wallet address (checksummed or lowercase) |
| chain_id | string | Yes | Blockchain network chain ID (e.g., "1" for Ethereum mainnet, "97" for BSC testnet) |
| message | string | Yes | Plain text message that was signed by the wallet |
| signature | string | Yes | Hex-encoded signature generated by personal_sign method |
Message Format Specification
Template
Wallet address: {wallet_address}
ChainId: {chain_id}
Timestamp: {utc_timestamp}
Nonce: {unique_nonce}Required Fields
| Field | Format | Description | Example |
|---|---|---|---|
| wallet_address | Ethereum address | Must match the signing wallet address | 0x1234567890123456789012345678901234567890 |
| chain_id | Numeric string | Must match the current blockchain network | 97 |
| utc_timestamp | ISO 8601 UTC | Current UTC time, prevents replay attacks | 2025-11-12T01:59:44Z |
| nonce | UUID or random string | Unique identifier, prevents replay attacks | ce7e949b-2018-4cb7-bac0-246e1c146c60 |
Format Rules
- Each field must be on a separate line
- Field name and value separated by
:(colon + space) - Field order can be customized
- Additional custom fields are allowed but will be ignored
- Timestamp should be within reasonable time window (e.g., ±5 minutes)
- Nonce must be unique per request
Message Example
Wallet address: 0x1234567890123456789012345678901234567890
ChainId: 97
Timestamp: 2025-11-12T01:59:44Z
Nonce: ce7e949b-2018-4cb7-bac0-246e1c146c60Signature Generation
import { ethers } from 'ethers';
async function signMessage(walletAddress, chainId) {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const timestamp = new Date().toISOString().replace(/\.\d{3}/, '');
const nonce = crypto.randomUUID();
const message = `Wallet address: ${walletAddress}
ChainId: ${chainId}
Timestamp: ${timestamp}
Nonce: ${nonce}`;
const signature = await signer.signMessage(message);
return { message, signature };
}Response Parameters
Success Response
| Field | Type | Description |
|---|---|---|
| code | integer | Status code (0 = success) |
| data | object | Response data object |
| data.jwt | string | JWT token for API authorization |
JWT Token Structure
The JWT token contains:
- Header: Algorithm and token type
- Payload: Wallet address, chain ID, expiration time
- Signature: Server-side signature for verification
Token expiration: Typically 24 hours (configurable by server)
Request Example
cURL
curl -X POST "{API_BASE_URL}/api/v1/makers/connect" \
-H "Content-Type: application/json" \
-d '{
"wallet_address": "0x1234567890123456789012345678901234567890",
"chain_id": "97",
"message": "Wallet address: 0x1234567890123456789012345678901234567890\nChainId: 97\nTimestamp: 2025-11-12T01:59:44Z\nNonce: ce7e949b-2018-4cb7-bac0-246e1c146c60",
"signature": "0x8f3d2e1c4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e2d3c4b5a6f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e01"
}'JavaScript (Fetch API)
const API_BASE_URL = process.env.API_BASE_URL; // Set based on environment
async function authenticate(walletAddress, chainId, message, signature) {
const response = await fetch(`${API_BASE_URL}/api/v1/makers/connect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
wallet_address: walletAddress,
chain_id: chainId,
message: message,
signature: signature,
}),
});
const data = await response.json();
if (data.code === 0) {
// Store JWT token
localStorage.setItem('jwt_token', data.data.jwt);
return data.data.jwt;
} else {
throw new Error(data.message || 'Authentication failed');
}
}Response Example
Success Response
{
"code": 0,
"data": {
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3YWxsZXRfYWRkcmVzcyI6IjB4MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MCIsImNoYWluX2lkIjoiOTciLCJleHAiOjE3MzE0NTU5ODR9.K7Xz9Ym3Qw5Rt8Pn2Lv6Jh4Fg1Ds0Cx9Bw7Au5Nt3Km"
}
}Error Response
{
"code": 400,
"message": "Invalid signature"
}Using JWT Token for API Authorization
After obtaining the JWT token, include it in the Authorization header for all subsequent API requests:
Header Format
Authorization: Bearer <jwt_token>Example Request
curl -X GET "{API_BASE_URL}/api/v1/orders" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."JavaScript Example
async function fetchOrders(jwtToken) {
const response = await fetch(`${API_BASE_URL}/api/v1/orders`, {
method: 'GET',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
});
return await response.json();
}Error Codes
| Code | Message | Description | Solution |
|---|---|---|---|
| 400 | Invalid signature | Signature verification failed | Ensure message and signature match, check wallet connection |
| 400 | Invalid message format | Message doesn't contain required fields | Follow message format specification |
| 400 | Timestamp expired | Timestamp is too old or too far in future | Generate new message with current timestamp |
| 400 | Nonce already used | Nonce has been used before | Generate new unique nonce |
| 400 | Invalid wallet address | Wallet address format is invalid | Provide valid Ethereum address |
| 400 | Chain ID mismatch | Chain ID doesn't match expected network | Verify correct chain ID for target network |
| 401 | Token expired | JWT token has expired | Re-authenticate to obtain new token |
| 401 | Invalid token | JWT token is invalid or malformed | Re-authenticate to obtain new token |
| 429 | Too many requests | Rate limit exceeded | Wait before retrying |
| 500 | Internal server error | Server-side error occurred | Contact support if persists |
Security Best Practices
For Frontend Developers
Secure Token Storage
- Store JWT token in memory or httpOnly cookies
- Avoid localStorage for sensitive applications
- Clear token on logout
Nonce Generation
- Use cryptographically secure random values
- Never reuse nonces
- Use UUID v4 or similar
Timestamp Validation
- Use server time if available
- Account for clock skew
- Implement reasonable time windows
Signature Verification
- Always use personal_sign (not eth_sign)
- Verify message content before signing
- Display message to user clearly
For Backend Developers
Signature Verification
- Verify signature matches wallet address
- Validate all message fields
- Check timestamp within acceptable window
Replay Attack Prevention
- Store used nonces (with expiration)
- Validate timestamp freshness
- Implement rate limiting
Token Management
- Set appropriate expiration times
- Use secure signing algorithms (HS256 or RS256)
- Implement token refresh mechanism
Rate Limiting
- Limit authentication attempts per IP
- Limit attempts per wallet address
- Implement exponential backoff
Token Refresh
When JWT token expires, re-authenticate using this endpoint to obtain a new token. Consider implementing automatic token refresh before expiration:
async function refreshTokenIfNeeded(jwtToken) {
// Decode JWT to check expiration
const payload = JSON.parse(atob(jwtToken.split('.')[1]));
const expiresAt = payload.exp * 1000; // Convert to milliseconds
const now = Date.now();
// Refresh if token expires in less than 1 hour
if (expiresAt - now < 3600000) {
// Re-authenticate to get new token
const { message, signature } = await signMessage(walletAddress, chainId);
return await authenticate(walletAddress, chainId, message, signature);
}
return jwtToken;
}Complete Integration Example
import { ethers } from 'ethers';
class StockProtocolAuth {
constructor(apiBaseUrl) {
this.apiBaseUrl = apiBaseUrl;
this.jwtToken = null;
}
async connect() {
// Request wallet connection
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const walletAddress = await signer.getAddress();
const network = await provider.getNetwork();
const chainId = network.chainId.toString();
// Generate message
const timestamp = new Date().toISOString().replace(/\.\d{3}/, '');
const nonce = crypto.randomUUID();
const message = `Wallet address: ${walletAddress}
ChainId: ${chainId}
Timestamp: ${timestamp}
Nonce: ${nonce}`;
// Sign message
const signature = await signer.signMessage(message);
// Authenticate
const response = await fetch(`${this.apiBaseUrl}/api/v1/makers/connect`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet_address: walletAddress,
chain_id: chainId,
message: message,
signature: signature,
}),
});
const data = await response.json();
if (data.code === 0) {
this.jwtToken = data.data.jwt;
return this.jwtToken;
} else {
throw new Error(data.message || 'Authentication failed');
}
}
async apiCall(endpoint, options = {}) {
if (!this.jwtToken) {
throw new Error('Not authenticated. Call connect() first.');
}
const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${this.jwtToken}`,
'Content-Type': 'application/json',
},
});
return await response.json();
}
}
// Usage
const API_BASE_URL = process.env.API_BASE_URL; // e.g., 'https://uat-api.example.com' or 'https://api.example.com'
const auth = new StockProtocolAuth(API_BASE_URL);
await auth.connect();
const orders = await auth.apiCall('/api/v1/orders');