Most exchange actions include a signature and nonce:
{
"action": { /* action details */ },
"nonce": 1234567890,
"expiresAfter": 1234569999,
"signature": {
"r": "0x1234...",
"s": "0x5678...",
"v": 27
}
}
- Unsigned actions:
reportDeposit
- Action-scoped EIP-712 signatures:
acceptTerms, generateReferralCode (signature is a hex string inside action)
Nonce
The nonce is a timestamp in milliseconds used to prevent replay attacks:
const nonce = Date.now();
Each nonce can only be used once. Requests with duplicate nonces will be rejected.
Signature Components
The signature consists of three components (standard ECDSA):
r - First 32 bytes of signature (hex string)
s - Second 32 bytes of signature (hex string)
v - Recovery ID (27 or 28)
Creating Signatures
1. Construct the Action Hash
Hash the action payload using EIP-712 structured data signing:
import { ethers } from 'ethers';
const domain = {
name: 'Notional',
version: '1',
chainId: 42161
};
const types = {
Order: [
{ name: 'asset', type: 'uint32' },
{ name: 'isBuy', type: 'bool' },
{ name: 'limitPx', type: 'uint64' },
{ name: 'sz', type: 'uint64' },
{ name: 'reduceOnly', type: 'bool' },
{ name: 'orderType', type: 'uint8' }
]
};
const action = {
asset: 0,
isBuy: true,
limitPx: '50000.0',
sz: '1.0',
reduceOnly: false,
orderType: 1
};
const signature = await signer._signTypedData(domain, types, action);
Use the Notional SDK for exact typed-data definitions and action normalization.
2. Split the Signature
const sig = ethers.utils.splitSignature(signature);
const request = {
action: action,
nonce: Date.now(),
signature: {
r: sig.r,
s: sig.s,
v: sig.v
}
};
Agent Approval
Instead of signing each request with your main wallet, you can authorize an API wallet (agent) to sign on your behalf.
Benefits
- Security: Keep your main wallet offline
- Convenience: No manual signature approval for each trade
- Automation: Enable trading bots and automated strategies
Approving an Agent
Send an approveAgent action signed by your main wallet:
{
"action": {
"type": "approveAgent",
"agentAddress": "0x...",
"agentName": "My Trading Bot",
"nonce": 1234567890
},
"nonce": 1234567890,
"signature": {
"r": "0x...",
"s": "0x...",
"v": 27
}
}
Request to:
Response:
{
"status": "ok",
"response": {
"type": "approveAgent"
}
}
Using an Approved Agent
Once approved, the agent can sign requests on behalf of the user:
- Agent creates the action payload
- Agent signs the action with its own private key
- Backend validates:
- Signature is valid for agent address
- Agent is approved for the user
- Action is authorized
The userAddress is recovered from the approval record, not the signature.
Validation Process
When a signed request arrives, the backend:
- Validates signature structure - Checks r, s, v format
- Recovers signer address - Uses ECDSA recovery
- Checks authorization:
- If signer = user → Direct mode (approved)
- If signer = agent → Agent mode (checks approval table)
- Verifies nonce - Ensures uniqueness
- Validates action - Checks order parameters, margin, etc.
Error Responses
Invalid Signature
{
"error": "Invalid signature"
}
HTTP Status: 401 Unauthorized
Agent Not Approved
{
"error": "Agent not approved for user"
}
HTTP Status: 401 Unauthorized
Duplicate Nonce
{
"error": "Nonce already used"
}
HTTP Status: 400 Bad Request
Security Best Practices
- Never share your private key - Use hardware wallets for main account
- Limit agent permissions - Only approve agents you trust
- Monitor agent activity - Review orders placed by agents
- Rotate agents - Revoke and re-approve agents periodically
- Use unique nonces - Ensure timestamp-based nonces are unique
Example: Complete Order Flow
import { ethers } from 'ethers';
// 1. Connect wallet
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// 2. Create order action
const action = {
type: 'order',
orders: [{
a: 0, // BTC
b: true, // Buy
p: '50000.0', // Limit price
s: '0.1', // Size
r: false, // Not reduce-only
t: { limit: { tif: 'Gtc' } } // Good-til-cancel
}],
grouping: 'na'
};
// 3. Sign the action
const domain = { /* EIP-712 domain */ };
const types = { /* EIP-712 types */ };
const signature = await signer._signTypedData(domain, types, action);
const sig = ethers.utils.splitSignature(signature);
// 4. Submit to API
const response = await fetch('https://api.notional.xyz/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action,
nonce: Date.now(),
signature: { r: sig.r, s: sig.s, v: sig.v }
})
});
const result = await response.json();
console.log('Order placed:', result);
Next Steps