siwe— the one-time Sign-In-With-Ethereum login. The walletpersonal_signs a backend-issued ERC-4361 challenge.session— client-only setup steps the SDK runs directly against the venue with the session credential. No signed step returns to the backend.hmac— every trading action. The SDK signs each venue request with the API key and the backend relays the signed request to the venue.
The SIWE session
The user’s EVM-compatible signer — provided to the SDK viauserWallet (createPerpsClient) or setUserWallet() — signs one thing on Ondo: the SIWE login challenge.
createAction for siweLogin returns a SiweActionStep carrying the challenge ({ challengeId, message }). The wallet personal_signs the message, and the SDK completes the login directly with Ondo. No SiweSignedActionStep is produced and executeAction is skipped. The venue issues a session credential the SDK holds client-side — neither it nor the signature is sent to LI.FI.
The API key
The API key is a venue trading key the SDK creates during setup (theregisterApiKey step) using the session credential. It is stored client-side and is used to sign every trading request thereafter.
What the API key does:
- Signs every trading action —
placeOrder,placeTriggerOrder,cancelOrder,cancelAllOrders,placeTwapOrder,cancelTwapOrder,updateLeverage, andwithdrawal(which needs thetransferscope).
- It does not sign the SIWE login — that requires the user’s wallet.
- It is not transmitted to the LI.FI backend. Only the per-request HMAC material (
{ keyId, timestampMs, signature }) crosses the wire; the backend builds the venue’s transport headers from it at relay time.
- The SDK creates the key lazily on first HMAC use, or eagerly when the user runs the
registerApiKeysetup step — both paths share the same creation logic, so whichever runs first satisfies the other. - The venue registers it under the name
lifi-perpswith scopes['trade', 'transfer']. Thetransferscope is required for thePOST /v1/withdrawrequest thatwithdrawalrelays. - The account holds one
lifi-perpskey for each address. If the stored key lacks a required scope, the SDK revokes it when the venue still lists it, and creates a new key with both scopes. The SDK uses the stored session token for this step, so the user signs nothing. - Ondo caps an account at 10 API keys. When the account is already at the cap, the SDK reclaims the oldest key it owns (matched by name
lifi-perps) before creating a new one — oldest bycreatedAt, ties broken by the smallerkeyId. If nolifi-perps-named key is available to reclaim, key creation fails. - If the venue rejects an HMAC-signed request with
Unauthorized(the key was revoked or expired venue-side), the SDK evicts its local copy so the next trading action re-creates one automatically.
The HMAC-signed trading relay
Trading actions are HMAC-relayed.createAction returns one or more HmacActionSteps, each an unsigned venue request:
hmac field, yielding a HmacSignedActionStep:
request.body is a pre-serialized string that transits verbatim — it is the exact byte string the HMAC signature covers, and it is never re-serialized downstream.
Signature and relay: the SDK computes HMAC-SHA256(apiSecret, ${timestampMs}${METHOD}${pathWithQuery}${body}) and encodes the signature as hex. METHOD is uppercase, and the SDK concatenates the fields without separators. The SDK stamps hmac.timestampMs as Date.now() + driftMs immediately before executeAction. Without a stored correction, driftMs starts at 0.
Ondo accepts a timestamp at most 10 seconds behind or 1 second ahead of the Ondo clock. See Ondo’s API key authentication reference. The backend relays the signed request through the ONDO-KEY-ID, ONDO-TIMESTAMP, and ONDO-SIGN transport headers and returns the venue’s result. Ondo rejects a timestamp outside this window with timestamp_too_far, which the backend reports as NonceExpired (2042).
Clock drift correction: the SDK measures driftMs only after a failed NonceExpired result:
- The SDK reads
serverTimefrom Ondo’s publicGET /statusendpoint. - It sets
driftMs = serverTime - Date.now()and storesdriftMsin the provider storage for later orders. - If all action results failed, the SDK repeats create → sign → execute one time with a new timestamp and signature.
driftMs is negative when the device clock runs ahead of Ondo and positive when it runs behind. If the GET /status read fails, the SDK returns the original NonceExpired result without a retry.
Client-only session steps
createDepositAddress, acceptProviderTerms, registerApiKey, setReferrer, and addWithdrawalAddress are session steps. The SDK performs the venue call itself with the client-held session credential and skips executeAction, but the steps fall into different shapes:
acceptProviderTermsandregisterApiKeyare bare markers — the backend step carries no request material at all. The SDK authors the venue call itself:acceptProviderTermsposts{ termsVersion, privacyVersion }(both currently1) toPOST /v1/agreement;registerApiKeyruns the same key-creation logic described above.createDepositAddresscarries a fixed, backend-authored deposit policy ({ network: 'ethereum', symbol: 'USDC', depositDestination: { wallet: 'margin' } }) but no venue path or credentials. The SDK reads the account (GET /v1/account) to get the venueaccountID, provisions the address (POST /v1/provision_address) with the policy and that id, then reads the result back (POST /v1/wallet/deposit_address/list) to confirm the venue can serve it.addWithdrawalAddressis a bare marker (session: {}) that takes no params. The SDK requests a challenge from the venue (POST /v1/auth/erc-4361/address_book/get_challenge) and the user’s wallet signs the challenge message. The SDK then posts the signature (POST /v1/auth/erc-4361/address_book/complete_challenge) with the labelLI.FI. This is the onlysessionstep that prompts the wallet after login.setReferreris not a bare marker — the backend authors a real request (POST /v1/account/referralwith{ code }), and the SDK submits it as-is using the session credential rather than an HMAC signature.checkSetup()runs it without a wallet prompt once the account has no referrer attached.
The session credential’s lifecycle
The session credential has no refresh flow. The SDK evicts it in two cases and both fall back to a logged-out account state, which causes the nextcheckSetup() to re-stage siweLogin:
- Client-side expiry — the token store checks
expirationSecs * 1000 <= Date.now()on every read and drops an expired token before returning it. - Server-side revocation — a mid-call
401from the venue surfaces as a session-expired error; the SDK evicts the stored token immediately rather than letting a locally-valid-looking token soft-lock the UI.