Reverse X lookup
curl --request GET \
--url https://walletlink.social/api/v1/reverse/twitter/{handle}import requests
url = "https://walletlink.social/api/v1/reverse/twitter/{handle}"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://walletlink.social/api/v1/reverse/twitter/{handle}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://walletlink.social/api/v1/reverse/twitter/{handle}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://walletlink.social/api/v1/reverse/twitter/{handle}"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://walletlink.social/api/v1/reverse/twitter/{handle}")
.asString();require 'uri'
require 'net/http'
url = URI("https://walletlink.social/api/v1/reverse/twitter/{handle}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body{
"data": [
{}
],
"meta": {
"handle": "<string>",
"total_count": 123,
"returned_count": 123,
"truncated": true,
"next_cursor": {}
}
}Reverse lookups
Reverse X lookup
Find every wallet attested to an X handle, for 1 match credit per wallet returned, with a key that belongs to an account.
GET
/
api
/
v1
/
reverse
/
twitter
/
{handle}
Reverse X lookup
curl --request GET \
--url https://walletlink.social/api/v1/reverse/twitter/{handle}import requests
url = "https://walletlink.social/api/v1/reverse/twitter/{handle}"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://walletlink.social/api/v1/reverse/twitter/{handle}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://walletlink.social/api/v1/reverse/twitter/{handle}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://walletlink.social/api/v1/reverse/twitter/{handle}"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://walletlink.social/api/v1/reverse/twitter/{handle}")
.asString();require 'uri'
require 'net/http'
url = URI("https://walletlink.social/api/v1/reverse/twitter/{handle}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body{
"data": [
{}
],
"meta": {
"handle": "<string>",
"total_count": 123,
"returned_count": 123,
"truncated": true,
"next_cursor": {}
}
}Finds every wallet attested to an X handle. Costs 1 match credit per wallet returned, 100 per page. A handle with no wallets attached costs nothing. Separately, each request weighs 2 units against your rate limit.
Only wallets whose recorded sources are all attested evidence (an onchain record, a Farcaster verification, an owner-signed link) are returned. Sources are recorded per wallet, not per handle, so a wallet whose attested link sits beside a correlated source is left out too: we cannot tell which of the two supplied the handle. A single-wallet lookup still shows it, with its labels. The total and every page follow the same rule, and an empty result means no such wallet, not that no address is attested anywhere.
This lookup needs a key that belongs to an account. A key bought with USDC and no account resolves addresses but answers this one with
403 ACCOUNT_REQUIRED: turning a handle into the wallets behind it is the direction that can find a person, so somebody has to answer for the search.
This runs the graph backwards, and it answers a question most wallet tooling cannot: which wallets has this account linked to itself? It is a one-time check, for example whether an account holds your token before a partnership, an allowlist or an airdrop. Following what a person holds over time is not what it is for, and the terms of service forbid it.
Path parameters
string
required
1 to 15 characters, letters, numbers and underscores. A leading
@ is
accepted and stripped. Case-insensitive.Query parameters
string
Continues a paginated result: pass the
next_cursor value from the previous
response, unmodified. Omit it for the first page. A value this API did not
produce returns INVALID_CURSOR.Request
curl https://walletlink.social/api/v1/reverse/twitter/example_user \
-H "Authorization: Bearer wts_live_YOUR_KEY"
Response
array
Matching wallets, at most 100 per page, ordered by Farcaster reach (highest
follower count first, wallets without one last, wallet address as the
tiebreak). Each entry carries
wallet, an always-present twitter object,
and whichever of ens_name, farcaster, lens, github and sources
apply, plus a flat quality_score. The twitter object carries
self_declared: true when the wallet’s owner named the handle in their own
record and nothing else backs it: the owner claims the handle, and the account
never confirmed it. See self-declared
handles.object
{
"data": [
{
"wallet": "0x00000000000000000000000000000000000000a1",
"ens_name": "example.eth",
"twitter": {
"handle": "example_user",
"url": "https://x.com/example_user",
"verified": true,
"self_declared": true,
"reachable": true,
"reachability": "live",
"reachability_checked_at": "2026-08-21T14:02:11.000Z"
},
"sources": ["onchain"],
"quality_score": 70
}
],
"meta": {
"handle": "example_user",
"total_count": 1,
"returned_count": 1,
"truncated": false,
"next_cursor": null
}
}
quality_score here is a flat number, unlike the nested quality object on
single lookup. Same scale, different shape.One handle, many wallets
Multiple results are normal, not a bug. People keep a hot wallet and a vault, and the same handle can be attested from several addresses.Second accounts are matched too
A wallet is returned when the handle is its primary X account or its second attested one. Some owners have attested two live X accounts from the same wallet through different evidence; a single lookup shows that astwitter.also, and searching either handle finds the wallet.
A wallet matched this way has a different handle in its own twitter.handle field, and names the handle you searched for under twitter.also. The same conditions apply as for twitter.also itself: both accounts must be live, and where the attestation carried an account id it must still match. Nothing is returned on the strength of a handle we cannot corroborate. A wallet matched this way needs only that second account’s own source to be attested, and a second account from a correlated source is not matched.
These wallets bill exactly as any other: 1 match credit per wallet returned.
The endpoint returns at most 100 wallets per page. When truncated is true, pass meta.next_cursor back as the cursor query parameter to fetch the next page; the last page carries next_cursor: null. Each page is its own request: it weighs 2 rate-limit units and bills 1 match credit per wallet it returns.
Pages walk the index in Farcaster-reach order, so the first page holds the most reachable wallets. The cursor is a position in a live index, not a snapshot: a wallet whose follower count changes mid-walk can appear twice or be skipped, and total_count can move between pages. Treat the cursor as opaque, use it promptly, and do not store it.
Errors
INVALID_HANDLE, INVALID_CURSOR, plus the standard errors.
No matches is not an error: data is [] with total_count: 0 at HTTP 200.