Skip to main content
POST
Batch lookup
Resolves many addresses in one request. Costs 1 match credit per address that resolves to an X handle or a Farcaster account, after duplicates are removed. Addresses that match nothing cost nothing. This is the endpoint to build on. Fifty single lookups and one fifty-wallet batch cost the same match credits, but the batch is one request instead of fifty and returns in a fraction of the time. Batch answers from the index, which is why it returns in milliseconds. For a list larger than a batch, or when the misses are worth re-checking against live sources, submit an async job instead: same billing rule, no per-request address ceiling, and live resolution on wallets the index has not checked. To know what a list would cost before sending it, run the free estimate first.

Headers

string
Optional retry protection. An opaque value up to 200 characters, such as a UUID. Resending the identical request with the same key inside 24 hours replays the stored response, marked Idempotency-Replayed: true, and bills nothing. See retries below.

Body

string[]
required
Addresses to resolve, up to your plan’s max_batch_size: 50 on the default plan, 200 with a live Scale pack, 1,000 with a live Index pack (see plans). The whole body is capped at 1 MB.

Request

Response

array
One entry per unique address, in submission order. Entries are the same object as single lookup, or null where nothing was found. That includes twitter.also, a second X account attested for the same wallet, on the few records that have one. Each found entry also carries last_updated and stale, the same freshness pair single lookup reports in its meta, here per row because a batch has one meta for many rows.
object
Positions map to the deduplicated list, not to the array you sent. Submit [A, B, A] and you get two entries back, not three. Deduplicate before you send, then index by the wallet field rather than by position.

Differences from single lookup

Batch returns a deliberately lighter record. One thing present on /v1/wallet/{address} is absent here:
  • No quality object.
stale and last_updated are present as of 2026-09-01, per row rather than in meta, and misses carry their checked-at time through meta.previously_checked. Only the staleness response headers stay single-lookup-only, since one header cannot describe fifty rows. farcaster.verified is present as of 2026-08-30, on the same terms as single lookup, and so is twitter.verified. It was previously omitted here alone, which meant a multi-address caller received an account without the class of evidence behind it. Every match carries its evidence, and that has to hold on the route that resolves the most of them.
twitter.verified widened on 20 September 2026. An X handle that came from a Farcaster account’s verified accounts now reports true. It already did when our Farcaster sweep wrote the row, and reported false when a live lookup did, so the same evidence gave two answers depending on which path reached the address last. If you treat verified as a filter, expect more rows to pass it; none of them are new evidence, only evidence that was already there being reported consistently.
The reachability fields and twitter.also are present here on the same terms as on single lookup. If you are filtering on confidence, resolve in bulk with batch and then re-fetch the shortlist through single lookup. It is usually a small number of addresses and it is the only way to see the quality metadata.

Cost

Match credits are charged on the addresses that resolve to an X handle or a Farcaster account, after deduplication. meta.matched is exactly that count. An address that carries only an ENS name, a Lens profile or a GitHub account is returned and counted in found, but not billed. Submitting [A, B, A] where A matches and B does not costs 1 credit and returns 2 records. Deduplicating still helps, because duplicates count against your rate limit even though they are billed once. A batch that exceeds your batch limit is rejected whole with BATCH_SIZE_EXCEEDED. Nothing is charged and nothing is partially processed, so split oversized batches client-side and send the pieces.

Retries and the Idempotency-Key

A plain retry bills again, because it resolves again. Send an Idempotency-Key header on any batch you might have to retry, and a resend of the identical request replays the stored response instead:
  • The dedup is scoped to your API key and pinned to the exact request body, and lasts 24 hours. A replay returns the original body and status with an Idempotency-Replayed: true header, bills no matches, and resolves nothing.
  • Rate limits still apply to a replay: the request weight is spent at the gate, before the store is consulted.
  • Reusing a key with a different body is refused with 422 IDEMPOTENCY_KEY_REUSED rather than silently answered with the older list.
  • A response too large to store (over 256 KB, far beyond a full 50-address batch) is recorded without a body; replaying it answers 409 IDEMPOTENCY_NOT_REPLAYABLE, and the fix is to resend under a fresh key, knowingly billed as a new request.
  • Only a 200 consumes a key. A request that failed can be retried under the same key into a success.
  • The window protects sequential retries, not concurrent duplicates: two copies of the same request racing each other can both execute and both bill. Serialize retries.
  • The balance gate runs before the replay store is consulted, so a key at zero balance cannot replay even a stored response. If the batch that spent your last credits lost its response in transit, read your usage rather than retrying: the matches were billed and recorded.

Errors

INVALID_REQUEST, INVALID_ADDRESS, BATCH_SIZE_EXCEEDED, IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_NOT_REPLAYABLE, plus the standard errors. A single malformed address rejects the entire batch. The error message names the first few offenders.