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
GET /v1/reverse/twitter/
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 linked 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.
This runs the graph backwards, and it answers a question most wallet tooling cannot: given a person, what do they hold?
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.object
{
"data": [
{
"wallet": "0x00000000000000000000000000000000000000a1",
"ens_name": "example.eth",
"twitter": {
"handle": "example_user",
"url": "https://x.com/example_user",
"verified": 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.
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.