> ## Documentation Index
> Fetch the complete documentation index at: https://docs.walletlink.social/llms.txt
> Use this file to discover all available pages before exploring further.

# Batch lookup

> POST /v1/batch

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](/api-reference/jobs) 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](/api-reference/estimate) first.

## Headers

<ParamField header="Idempotency-Key" type="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](#retries-and-the-idempotency-key) below.
</ParamField>

## Body

<ParamField body="wallets" type="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](/api-reference/introduction#plans)). The whole body is capped at 1 MB.
</ParamField>

## Request

```bash theme={null}
curl -X POST https://walletlink.social/api/v1/batch \
  -H "Authorization: Bearer wts_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "wallets": [
      "0x00000000000000000000000000000000000000a1",
      "0x0000000000000000000000000000000000000001"
    ]
  }'
```

## Response

<ResponseField name="data" type="array">
  One entry per **unique** address, in submission order. Entries are the same
  object as [single lookup](/api-reference/wallet), or `null` where nothing was
  found. That includes `twitter.also`, [a second X
  account](/api-reference/wallet#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.
</ResponseField>

<ResponseField name="meta" type="object">
  <Expandable title="properties">
    <ResponseField name="requested" type="number">
      Unique addresses after deduplication.
    </ResponseField>

    <ResponseField name="found" type="number">
      How many resolved to at least one identity, including ENS, Lens and
      GitHub.
    </ResponseField>

    <ResponseField name="matched" type="number">
      How many resolved to an X handle or a Farcaster account. This is the
      number billed, and it is never more than `found`.
    </ResponseField>

    <ResponseField name="not_found" type="number">
      Unique addresses with no identity at all: `requested` minus `found`.
    </ResponseField>

    <ResponseField name="previously_checked" type="object">
      Negative knowledge for the misses: wallet to ISO timestamp, for each
      `null` entry we have checked before and found bare. A miss absent from
      this map has never been examined. The whole field is absent when no miss
      was previously checked; absent is not false. The single lookup makes the
      same distinction with `meta.checked_at`.
    </ResponseField>
  </Expandable>
</ResponseField>

```json theme={null}
{
  "data": [
    {
      "wallet": "0x00000000000000000000000000000000000000a1",
      "ens_name": "example.eth",
      "farcaster": {
        "username": "example",
        "url": "https://warpcast.com/example",
        "followers": 123456,
        "fid": 900001
      },
      "sources": ["onchain", "farcaster"],
      "last_updated": "2026-08-14T02:11:09.000Z",
      "stale": false
    },
    null
  ],
  "meta": {
    "requested": 2,
    "found": 1,
    "matched": 1,
    "not_found": 1,
    "previously_checked": {
      "0x0000000000000000000000000000000000000001": "2026-08-20T05:40:00.000Z"
    }
  }
}
```

<Warning>
  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.
</Warning>

## Differences from single lookup

Batch returns a deliberately lighter record. One thing present on
[`/v1/wallet/{address}`](/api-reference/wallet) 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.

<Note>
  **`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.
</Note>

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](/api-reference/errors).

A single malformed address rejects the entire batch. The error message names the
first few offenders.
