From 21bf233dd1f6dbe148b23e7d7a0760eece50ae53 Mon Sep 17 00:00:00 2001 From: Alex Sedighi Date: Mon, 22 Jun 2026 22:03:20 +1200 Subject: [PATCH 1/2] Add Nodle name service lookup runbook and script for AI agents. Document indexer GraphQL queries and provide ops/lookup_nodle_name.sh so owner addresses or names can be resolved to linked X handles quickly. --- ops/lookup_nodle_name.sh | 208 ++++++++++++++++++ .../doc/nodle-name-service-lookup.md | 190 ++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100755 ops/lookup_nodle_name.sh create mode 100644 src/nameservice/doc/nodle-name-service-lookup.md diff --git a/ops/lookup_nodle_name.sh b/ops/lookup_nodle_name.sh new file mode 100755 index 00000000..6d457b62 --- /dev/null +++ b/ops/lookup_nodle_name.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# Look up Nodle / Click name service records via the production indexer. +# +# Returns owned names, owners, and linked social handles (com.x / com.twitter). +# +# Usage: +# lookup_nodle_name.sh --address 0x98f3f23798deaa759ad1d5334a1c1d6acb87b717 +# lookup_nodle_name.sh --name girlnext.nodl.eth +# lookup_nodle_name.sh --name girlnext +# +# Environment (optional): +# N_INDEXER_URL default: https://indexer.nodleprotocol.io +# N_NODLE_NS_ADDR default: 0x9741565272C7B29574c88ed2eBDF15BFE9C04612 +# N_CLICK_NS_ADDR default: 0xF3271B61291C128F9dA5aB208311d8CF8E2Ba5A9 +# +# Requirements: curl, jq, python3 + +set -euo pipefail + +INDEXER_URL="${N_INDEXER_URL:-https://indexer.nodleprotocol.io}" +NODLE_NS_ADDR="${N_NODLE_NS_ADDR:-0x9741565272C7B29574c88ed2eBDF15BFE9C04612}" +CLICK_NS_ADDR="${N_CLICK_NS_ADDR:-0xF3271B61291C128F9dA5aB208311d8CF8E2Ba5A9}" + +usage() { + echo "Usage: $0 --address <0x...> | --name " >&2 + exit 1 +} + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Required command not found: $1" >&2 + exit 1 + fi +} + +query_indexer() { + local payload="$1" + curl -sS -X POST "$INDEXER_URL" \ + -H "Content-Type: application/json" \ + -d "$payload" +} + +normalize_address() { + python3 -c 'import sys; print(sys.argv[1].lower())' "$1" +} + +normalize_name() { + python3 -c 'import sys; print(sys.argv[1].lower())' "$1" +} + +social_handle() { + local records_json="$1" + local handle + handle="$(echo "$records_json" | jq -r ' + [.[] | select(.key == "com.x" or .key == "com.twitter") | .value] + | map(select(length > 0)) + | if length > 0 then .[0] else "" end + ')" + if [ -z "$handle" ]; then + echo "(none)" + else + echo "$handle" + fi +} + +print_ens_nodes() { + local nodes_json="$1" + local count + count="$(echo "$nodes_json" | jq 'length')" + + if [ "$count" -eq 0 ]; then + echo "No matching names found." + return 0 + fi + + echo "$nodes_json" | jq -c '.[]' | while read -r node; do + local complete_name owner contract records social + complete_name="$(echo "$node" | jq -r '.completeName')" + owner="$(echo "$node" | jq -r '.ownerId // empty')" + contract="$(echo "$node" | jq -r '.contract // empty')" + records="$(echo "$node" | jq -c '.textRecords.nodes // []')" + social="$(social_handle "$records")" + + echo "Name: $complete_name" + if [ -n "$owner" ]; then + echo "Owner: $owner" + fi + if [ -n "$contract" ]; then + echo "Contract: $contract" + fi + echo "X handle: $social" + echo + done +} + +lookup_by_address() { + local address + address="$(normalize_address "$1")" + + local query payload response nodes + query="$(cat </dev/null 2>&1; then + echo "Indexer error:" >&2 + echo "$response" | jq '.errors' >&2 + exit 1 + fi + + local account + account="$(echo "$response" | jq '.data.account')" + if [ "$account" = "null" ]; then + echo "No account found for address: $address" >&2 + echo "Tip: indexer account ids must be lowercase." >&2 + exit 1 + fi + + echo "Address: $address" + echo "Primary: $(echo "$account" | jq -r '.primaryName // "(none)"')" + echo + + nodes="$(echo "$account" | jq '.eNsByOwnerId.nodes')" + print_ens_nodes "$nodes" +} + +lookup_by_name() { + local raw_name complete_name bare_name domain_filter payload response nodes + + raw_name="$(normalize_name "$1")" + + if [[ "$raw_name" == *.* ]]; then + complete_name="$raw_name" + query="$(cat </dev/null 2>&1; then + echo "Indexer error:" >&2 + echo "$response" | jq '.errors' >&2 + exit 1 + fi + + nodes="$(echo "$response" | jq '.data.eNs.nodes')" + print_ens_nodes "$nodes" +} + +require_cmd curl +require_cmd jq +require_cmd python3 + +MODE="" +VALUE="" + +while [ $# -gt 0 ]; do + case "$1" in + --address|-a) + MODE="address" + VALUE="${2:-}" + shift 2 + ;; + --name|-n) + MODE="name" + VALUE="${2:-}" + shift 2 + ;; + -h|--help) + usage + ;; + *) + echo "Unknown argument: $1" >&2 + usage + ;; + esac +done + +if [ -z "$MODE" ] || [ -z "$VALUE" ]; then + usage +fi + +case "$MODE" in + address) + lookup_by_address "$VALUE" + ;; + name) + lookup_by_name "$VALUE" + ;; +esac diff --git a/src/nameservice/doc/nodle-name-service-lookup.md b/src/nameservice/doc/nodle-name-service-lookup.md new file mode 100644 index 00000000..29701077 --- /dev/null +++ b/src/nameservice/doc/nodle-name-service-lookup.md @@ -0,0 +1,190 @@ +# Nodle name service lookup (AI agent runbook) + +Use this guide to answer questions like: + +- Does name `girlnext` have an X account linked? +- Who owns `girlnext.nodl.eth`? +- What names does `0x98f3f23798deaa759ad1d5334a1c1d6acb87b717` hold? +- What X handle is linked to a given owner address? + +## Production contracts (ZkSync Era mainnet) + +| Service | Parent domain | Contract | +|---------|---------------|----------| +| Nodle name service | `nodl.eth` | `0x9741565272C7B29574c88ed2eBDF15BFE9C04612` | +| Click name service | `clk.eth` | `0xF3271B61291C128F9dA5aB208311d8CF8E2Ba5A9` | + +Indexer: `https://indexer.nodleprotocol.io` + +Social handles are stored as ENS-style text records: + +- `com.x` — primary X/Twitter handle key +- `com.twitter` — legacy alias (check both) + +A linked handle means the name owner signed and set the text record on-chain. It is **not** OAuth-verified with X. + +## Quick path: use the lookup script + +```bash +chmod +x ops/lookup_nodle_name.sh + +# By owner address +./ops/lookup_nodle_name.sh --address 0x98F3F23798deAa759AD1D5334A1c1D6ACb87b717 + +# By full name +./ops/lookup_nodle_name.sh --name girlnext.nodl.eth + +# By bare subdomain (searches all matching names) +./ops/lookup_nodle_name.sh --name girlnext +``` + +## Lookup by owner address + +**Important:** account ids in the indexer are **lowercase**. Checksummed addresses return `null`. + +```graphql +{ + account(id: "0x98f3f23798deaa759ad1d5334a1c1d6acb87b717") { + name + primaryName + eNsByOwnerId { + nodes { + name + completeName + contract + textRecords { + nodes { + key + value + } + } + } + } + } +} +``` + +Example `curl`: + +```bash +curl -s -X POST https://indexer.nodleprotocol.io \ + -H "Content-Type: application/json" \ + -d '{"query":"{ account(id: \"0x98f3f23798deaa759ad1d5334a1c1d6acb87b717\") { primaryName eNsByOwnerId { nodes { completeName contract textRecords { nodes { key value } } } } } }"}' \ + | jq . +``` + +Read `textRecords` for `com.x` or `com.twitter` to get the linked X handle. + +## Lookup by name + +### Full FQDN (`girlnext.nodl.eth`) + +```graphql +{ + eNs(filter: { completeName: { equalTo: "girlnext.nodl.eth" } }, first: 1) { + nodes { + name + completeName + ownerId + contract + textRecords { + nodes { + key + value + } + } + } + } +} +``` + +### Bare subdomain (`girlnext`) + +Use when only the label is known. May return multiple names if the same label exists on different parent domains. + +```graphql +{ + eNs(filter: { name: { equalTo: "girlnext" } }, first: 10) { + nodes { + name + completeName + ownerId + contract + textRecords { + nodes { + key + value + } + } + } + } +} +``` + +To scope to production Nodle names only, add a contract filter: + +```graphql +{ + eNs( + filter: { + name: { equalTo: "girlnext" } + contract: { equalTo: "0x9741565272C7B29574c88ed2eBDF15BFE9C04612" } + } + first: 1 + ) { + nodes { + completeName + ownerId + textRecords { + nodes { + key + value + } + } + } + } +} +``` + +## On-chain fallback (ZkSync Era) + +If the indexer is stale or unavailable, read text records directly: + +```bash +RPC=https://mainnet.era.zksync.io +NS=0x9741565272C7B29574c88ed2eBDF15BFE9C04612 + +cast call "$NS" "getTextRecord(string,string)(string)" "girlnext" "com.x" --rpc-url "$RPC" +cast call "$NS" "getTextRecord(string,string)(string)" "girlnext" "com.twitter" --rpc-url "$RPC" +``` + +Token id for `ownerOf` checks: `uint256(keccak256("girlnext"))`. + +## Decision flow + +```mermaid +flowchart TD + A[User question] --> B{Input type?} + B -->|Owner address| C[Lowercase address] + B -->|Name or FQDN| D[Normalize to lowercase] + C --> E[account.eNsByOwnerId query] + D --> F{Contains dot?} + F -->|Yes| G[eNs.completeName filter] + F -->|No| H[eNs.name filter] + E --> I[Read textRecords com.x / com.twitter] + G --> I + H --> I + I --> J{Handle found?} + J -->|Yes| K[Report linked X handle] + J -->|No| L[Report no X account linked] +``` + +## Verified example + +| Field | Value | +|-------|-------| +| Name | `girlnext.nodl.eth` | +| Contract | `0x9741565272C7B29574c88ed2eBDF15BFE9C04612` | +| Owner | `0x98f3f23798deaa759ad1d5334a1c1d6acb87b717` | +| X handle (`com.x`) | `@_GirlNextDoo_R` | +| Twitter (`com.twitter`) | not set | From 899ea70c6c2910fcc0a64fa088815c6f355865a8 Mon Sep 17 00:00:00 2001 From: Alex Sedighi Date: Mon, 22 Jun 2026 22:20:45 +1200 Subject: [PATCH 2/2] Fix spellcheck for name service lookup doc. Add runbook, girlnext, and checksummed to the cspell allowlist. --- .cspell.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index 84d43097..fdadbd4c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -133,6 +133,10 @@ "remy", "aabbcc", "mfas", - "reqs" + "reqs", + "runbook", + "girlnext", + "Checksummed", + "checksummed" ] }