Skip to main content
Ondo uses a session-plus-API-key scheme. The user signs in once with their wallet, and from then on trading executes without a per-trade wallet popup. Three signing methods appear across Ondo’s actions:
  • siwe — the one-time Sign-In-With-Ethereum login. The wallet personal_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 via userWallet (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 (the registerApiKey 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, and withdrawal (which needs the transfer scope).
What the API key does not do:
  • 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.
Lifecycle:
  • The SDK creates the key lazily on first HMAC use, or eagerly when the user runs the registerApiKey setup step — both paths share the same creation logic, so whichever runs first satisfies the other.
  • The venue registers it under the name lifi-perps with scopes ['trade', 'transfer']. The transfer scope is required for the POST /v1/withdraw request that withdrawal relays.
  • The account holds one lifi-perps key 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 by createdAt, ties broken by the smaller keyId. If no lifi-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:
The SDK computes a per-request HMAC signature over the step from the API key and attaches it as a structured 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:
  1. The SDK reads serverTime from Ondo’s public GET /status endpoint.
  2. It sets driftMs = serverTime - Date.now() and stores driftMs in the provider storage for later orders.
  3. 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:
  • acceptProviderTerms and registerApiKey are bare markers — the backend step carries no request material at all. The SDK authors the venue call itself: acceptProviderTerms posts { termsVersion, privacyVersion } (both currently 1) to POST /v1/agreement; registerApiKey runs the same key-creation logic described above.
  • createDepositAddress carries 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 venue accountID, 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.
  • addWithdrawalAddress is 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 label LI.FI. This is the only session step that prompts the wallet after login.
  • setReferrer is not a bare marker — the backend authors a real request (POST /v1/account/referral with { 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 next checkSetup() 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 401 from 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.

Who signs what