The numbers
A principal is whoever authenticated: a given API key and a browser
session count separately, but all keys of one account share its account-level
protections.
The two Aro Mail claim endpoints have their own budgets
because a claim code is a bearer secret: the public preview is limited per IP,
and the claim itself is limited per principal and IP instead of the general
write budget, so a recipient’s few attempts never compete with an integration’s
other writes, and a code cannot be brute-forced through either door. The
operator-only
POST /transfer/letter/sweep is authenticated by a shared secret
and has no per-user limit.
When you exceed a limit
The API returns429 with the standard envelope:
Staying well under the limits
- Poll gas, not balances.
GET /gasis designed for polling;/balancesdoes FHE work. See Balances. - Poll transactions at 3–10 s intervals; faster adds no information (Ethereum blocks land every ~12 s) and burns read budget. See Track transactions.
- Batch-poll in-flight rows. If you have several transactions in flight,
one
GET /transactions?status=QUEUED(and likewisePENDING,SETTLING) per interval replaces oneGET /transactions/{id}per row; each list call costs a single read regardless ofpageSize. - Paginate deliberately.
pageSizegoes up to 100; one big page beats five small ones. - Money movement returns immediately with a
QUEUEDtransaction; a successful201is final for the write budget, so don’t retry it. Switch to polling the transaction (reads) until it reachesCONFIRMED,SETTLED, orFAILED. - Poll letters, not the claim preview.
GET /transfer/letter/{id}is the owner’s polling target for Aro Mail (10 s intervals are plenty; mailed letters refresh Lob status at most every 15 minutes anyway). The public preview is for the recipient’s claim page, not for tracking.
Lockout behavior
There are three locks: two for passwords, one for 2FA codes. Each answers423 account_locked.
Passwords at sign-in. Five consecutive wrong passwords at
POST /auth/login lock sign-in for 15 minutes; even correct credentials are
refused until the window passes. After a lock passes, one more password may
be tried, and a wrong one locks it again. A wrong password gets the generic
401 invalid_credentials, which never says whether the email exists.
Passwords from a session. Wrong passwords on a
step-up (creating an API key, adding a
passkey, exporting a wallet key, turning 2FA on or off) and a wrong
currentPassword when changing the password count toward a second lock with
the same rules: five in a row lock those actions for 15 minutes, with the
message “Too many wrong passwords. Try again later, or sign out and sign in
again with your password.” Sign-in still works while it is on.
The two password locks are kept apart so neither can be turned against you.
Anyone who knows your email can lock sign-in, but that never stops you,
while signed in, from stepping up or changing your password. A stolen
session can lock step-up, but never your sign-in, and signing in again with
your password lifts it (a passkey sign-in doesn’t). The price of keeping
them apart is a budget per lock: someone holding a stolen session can try
five passwords at step-up as well as five at sign-in, then one at each per
15 minutes, where a single shared lock would allow five in all.
A correct password at either one resets both counts. At step-up that
happens as soon as the password checks out, before any 2FA code is looked
at, so a wrong code never counts as a wrong password.
Each password counts before it is checked, so parallel guesses can’t slip
past the lock. When two arrive at once for the same lock, only one is
checked. At step-up the other gets 429 rate_limited with
retryAfterSeconds: 1. At sign-in it gets the usual
401 invalid_credentials after a hash check against nothing, so it matches
a wrong password in both body and timing. Retry it.
2FA codes. One per-account cap covers the sign-in code step
(POST /auth/login/totp) and the step-up codes on POST /keys,
POST /me/passkeys/options, POST /wallets/{id}/export and
POST /me/totp/disable. Each code tried
counts before it is checked. The 5th code tried without an accepted one locks
code entry for 15 minutes; after a lock passes, one more code may be tried,
and each further failure doubles the lock (30 minutes, 60 minutes, …) up to
24 hours. While locked, both the sign-in code step and step-up answer
423 account_locked, even for a valid code. A correct password does not
reset this counter, and restarting sign-in from the password step gives no
fresh attempts: only an accepted code, re-enrolling 2FA
(POST /me/totp/enable), or an admin Reset 2FA or Reset password
clears it. Changing your own password doesn’t: it needs only the password,
so it can’t be allowed to hand out fresh code guesses. A step-up sent
without a code on a 2FA account gets 401 totp_required and is not counted.
When two attempts arrive at once, only one is checked; the other gets
429 rate_limited with retryAfterSeconds: 1.