Ethereum Follow Protocol (EFP)
The Ethereum Follow Protocol (EFP) is an onchain social graph for Ethereum accounts — a decentralized “who follows whom” primitive. Because EFP composes directly with ENS identity, the ENS Omnigraph API exposes it alongside ENS data, so you can read an account’s follow graph and resolve each followed account’s ENS name in a single query.
Enabling the plugin
Section titled “Enabling the plugin”EFP is indexed by the efp plugin. The Omnigraph’s Query.efp and Account.efp fields return data only when the connected ENSIndexer has this plugin enabled; otherwise they resolve to null — treat that like any other unsupported-instance case, not a query error.
To enable it on your own instance, add efp to the ENSIndexer PLUGINS environment variable (a comma-separated list), for example:
PLUGINS=unigraph,efpThe efp plugin’s datasources are defined for the mainnet namespace (the EFP contracts on Base, OP Mainnet, and Ethereum Mainnet) and the local ens-test-env devnet.
The hosted ENSNode ‘Alpha’ instance has the efp plugin enabled, so you can run every query on this page against it without hosting anything yourself.
What it indexes
Section titled “What it indexes”The plugin indexes the full EFP onchain state into ENSDb:
- Lists — the ERC-721 NFTs that hold follow relationships, with their
owner/manager/userroles and storage location. - List records — the individual follows (address records), each with its tags (
block,mute,top8, and custom tags). - Account metadata — the onchain
(address, key) → valuestore, including theprimary-listkey used to validate an account’s primary list.
The API surface
Section titled “The API surface”EFP is exposed through two entry points:
Account.efp— an account’s EFP presence: validated, block/mute-filteredfollowing/followersconnections (whose edges are fullAccounts you can resolve straight into ENS names), the validatedprimaryList, and raw accountmetadata.Query.efp— list-centric queries:list(by: { tokenId }),lists(where:)by role address, andlistRecords(where:)(the raw record set, includingblock/mute).
Prefer Account.efp.following / followers for the social-graph answer — they apply EFP’s primary-list validation and block/mute filtering for you. Drop to the raw Query.efp.listRecords / EfpList.records only when you specifically need tags, storage location, or non-primary lists.
Example
Section titled “Example”This query loads an account's validated EFP social graph — its primary list, following, and followers — and resolves each followed account's ENS primary name. Requires the efp plugin.
query EfpFollowGraph($address: Address!) { account(by: { address: $address }) { efp { # The validated primary list, or null if unset/unvalidated. primaryList { tokenId } # following/followers are validated and block/mute-filtered, so the # edges are full Accounts you can walk straight into ENS names. following(first: 10) { totalCount edges { node { address resolve { primaryName(by: { chainName: ETHEREUM }) { name { beautified } } } } } } followers(first: 10) { totalCount edges { node { address } } } } }}{ "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"}{ "data": { "account": { "efp": { "primaryList": { "tokenId": "6509" }, "following": { "totalCount": 10, "edges": [ { "node": { "address": "0x1b63142628311395ceafeea5667e7c9026c862ca", "resolve": { "primaryName": { "name": { "beautified": "tgerring.eth" } } } } }, { "node": { "address": "0x2b888954421b424c5d3d9ce9bb67c9bd47537d12", "resolve": { "primaryName": { "name": { "beautified": "lefteris.eth" } } } } }, { "node": { "address": "0x50ec05ade8280758e2077fcbc08d878d4aef79c3", "resolve": { "primaryName": { "name": { "beautified": "hayden.eth" } } } } }, { "node": { "address": "0x54becc7560a7be76d72ed76a1f5fee6c5a2a7ab6", "resolve": { "primaryName": { "name": { "beautified": "simona.eth" } } } } }, { "node": { "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "resolve": { "primaryName": { "name": { "beautified": "barmstrong.eth" } } } } }, { "node": { "address": "0x648aa14e4424e0825a5ce739c8c68610e143fb79", "resolve": { "primaryName": { "name": { "beautified": "sassal.eth" } } } } }, { "node": { "address": "0x983110309620d911731ac0932219af06091b6744", "resolve": { "primaryName": { "name": { "beautified": "brantly.eth" } } } } }, { "node": { "address": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", "resolve": { "primaryName": { "name": { "beautified": "nick.eth" } } } } }, { "node": { "address": "0xd7029bdea1c17493893aafe29aad69ef892b8ff2", "resolve": { "primaryName": { "name": null } } } }, { "node": { "address": "0xf56345338cb4cddaf915ebef3bfde63e70fe3053", "resolve": { "primaryName": { "name": { "beautified": "bored.eth" } } } } } ] }, "followers": { "edges": [ { "node": { "address": "0x00000066ced4c62a0f740de74eb08bc2a1552764" } }, { "node": { "address": "0x000000dcf1190af44f7149b85299f18ce7221024" } }, { "node": { "address": "0x000066b5a8e1e35b7e2f64e24715829647f80000" } }, { "node": { "address": "0x00076993122809055293d8210a60d8265a7e17f3" } }, { "node": { "address": "0x000f16ca1e73a0b6b10e1ed4cc5f8232336c1482" } }, { "node": { "address": "0x0011e9582ac18d9b80204817eaa2e3b544e727aa" } }, { "node": { "address": "0x00173c4cb23e6d876fcb036ba954a2f9cfcafa19" } }, { "node": { "address": "0x00197ff7abdac8ad42152ff9d5bd9e521701a944" } }, { "node": { "address": "0x002153708f11f2651215059eea30820ee4d49ff3" } }, { "node": { "address": "0x003e7b75ff09615ad4a6f763dc7d28e48ed513d9" } } ], "totalCount": 5338 } } } }}Output matches a point in time snapshot GraphQL response from our alpha ENSNode instance. Live output depends on the configuration of your ENSNode instance and ENS state updates.
import { createEnsNodeClient } from "enssdk/core";import { graphql, omnigraph } from "enssdk/omnigraph";
const client = createEnsNodeClient({ url: process.env.ENSNODE_URL || "https://api.alpha.ensnode.io"}).extend(omnigraph);
const EfpFollowGraphQuery = graphql(` query EfpFollowGraph($address: Address!) { account(by: { address: $address }) { efp { # The validated primary list, or null if unset/unvalidated. primaryList { tokenId } # following/followers are validated and block/mute-filtered, so the # edges are full Accounts you can walk straight into ENS names. following(first: 10) { totalCount edges { node { address resolve { primaryName(by: { chainName: ETHEREUM }) { name { beautified } } } } } } followers(first: 10) { totalCount edges { node { address } } } } } }`);
const result = await client.omnigraph.query({ query: EfpFollowGraphQuery, variables: { address: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", },});
if (result.errors) throw new Error(JSON.stringify(result.errors));console.log(JSON.stringify(result.data, null, 2));{ "data": { "account": { "efp": { "primaryList": { "tokenId": "6509" }, "following": { "totalCount": 10, "edges": [ { "node": { "address": "0x1b63142628311395ceafeea5667e7c9026c862ca", "resolve": { "primaryName": { "name": { "beautified": "tgerring.eth" } } } } }, { "node": { "address": "0x2b888954421b424c5d3d9ce9bb67c9bd47537d12", "resolve": { "primaryName": { "name": { "beautified": "lefteris.eth" } } } } }, { "node": { "address": "0x50ec05ade8280758e2077fcbc08d878d4aef79c3", "resolve": { "primaryName": { "name": { "beautified": "hayden.eth" } } } } }, { "node": { "address": "0x54becc7560a7be76d72ed76a1f5fee6c5a2a7ab6", "resolve": { "primaryName": { "name": { "beautified": "simona.eth" } } } } }, { "node": { "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "resolve": { "primaryName": { "name": { "beautified": "barmstrong.eth" } } } } }, { "node": { "address": "0x648aa14e4424e0825a5ce739c8c68610e143fb79", "resolve": { "primaryName": { "name": { "beautified": "sassal.eth" } } } } }, { "node": { "address": "0x983110309620d911731ac0932219af06091b6744", "resolve": { "primaryName": { "name": { "beautified": "brantly.eth" } } } } }, { "node": { "address": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", "resolve": { "primaryName": { "name": { "beautified": "nick.eth" } } } } }, { "node": { "address": "0xd7029bdea1c17493893aafe29aad69ef892b8ff2", "resolve": { "primaryName": { "name": null } } } }, { "node": { "address": "0xf56345338cb4cddaf915ebef3bfde63e70fe3053", "resolve": { "primaryName": { "name": { "beautified": "bored.eth" } } } } } ] }, "followers": { "edges": [ { "node": { "address": "0x00000066ced4c62a0f740de74eb08bc2a1552764" } }, { "node": { "address": "0x000000dcf1190af44f7149b85299f18ce7221024" } }, { "node": { "address": "0x000066b5a8e1e35b7e2f64e24715829647f80000" } }, { "node": { "address": "0x00076993122809055293d8210a60d8265a7e17f3" } }, { "node": { "address": "0x000f16ca1e73a0b6b10e1ed4cc5f8232336c1482" } }, { "node": { "address": "0x0011e9582ac18d9b80204817eaa2e3b544e727aa" } }, { "node": { "address": "0x00173c4cb23e6d876fcb036ba954a2f9cfcafa19" } }, { "node": { "address": "0x00197ff7abdac8ad42152ff9d5bd9e521701a944" } }, { "node": { "address": "0x002153708f11f2651215059eea30820ee4d49ff3" } }, { "node": { "address": "0x003e7b75ff09615ad4a6f763dc7d28e48ed513d9" } } ], "totalCount": 5338 } } } }}Output matches a point in time snapshot GraphQL response from our alpha ENSNode instance. Live output depends on the configuration of your ENSNode instance and ENS state updates.
enssdk package manager setup
# 1. Create projectmkdir -p my-ens-script/src && cd my-ens-scriptnpm init -y && touch src/index.tsnpm pkg set type=module scripts.start="tsx src/index.ts"# 2. Install dependenciesnpm install enssdk@1.16.0 && npm install -D tsx typescript @types/node# 3. Paste the TypeScript snippet above into src/index.ts# 4. RunENSNODE_URL=https://api.alpha.ensnode.io npm startSee the enssdk docs for gql.tada plugin and tsconfig setup.
import { OmnigraphProvider, useOmnigraphQuery, graphql } from "enskit/react/omnigraph";import { createEnsNodeClient } from "enssdk/core";import { omnigraph } from "enssdk/omnigraph";
const client = createEnsNodeClient({ url: import.meta.env.VITE_ENSNODE_URL || "https://api.alpha.ensnode.io"}).extend(omnigraph);
const EfpFollowGraphQuery = graphql(` query EfpFollowGraph($address: Address!) { account(by: { address: $address }) { efp { # The validated primary list, or null if unset/unvalidated. primaryList { tokenId } # following/followers are validated and block/mute-filtered, so the # edges are full Accounts you can walk straight into ENS names. following(first: 10) { totalCount edges { node { address resolve { primaryName(by: { chainName: ETHEREUM }) { name { beautified } } } } } } followers(first: 10) { totalCount edges { node { address } } } } } }`);
function EfpFollowGraphResult() { const [result] = useOmnigraphQuery({ query: EfpFollowGraphQuery, variables: { address: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", }, }); const { data, fetching, error } = result; if (!data && fetching) return <p>Loading…</p>; if (error) return <p>Error: {error.message}</p>; if (!data) return <p>No data returned.</p>; const formatted = JSON.stringify( data, (_, value) => (typeof value === "bigint" ? value.toString() : value), 2, ); return <code>{formatted}</code>;}
export default function App() { return ( <OmnigraphProvider client={client}> <EfpFollowGraphResult /> </OmnigraphProvider> );}{ "data": { "account": { "efp": { "primaryList": { "tokenId": "6509" }, "following": { "totalCount": 10, "edges": [ { "node": { "address": "0x1b63142628311395ceafeea5667e7c9026c862ca", "resolve": { "primaryName": { "name": { "beautified": "tgerring.eth" } } } } }, { "node": { "address": "0x2b888954421b424c5d3d9ce9bb67c9bd47537d12", "resolve": { "primaryName": { "name": { "beautified": "lefteris.eth" } } } } }, { "node": { "address": "0x50ec05ade8280758e2077fcbc08d878d4aef79c3", "resolve": { "primaryName": { "name": { "beautified": "hayden.eth" } } } } }, { "node": { "address": "0x54becc7560a7be76d72ed76a1f5fee6c5a2a7ab6", "resolve": { "primaryName": { "name": { "beautified": "simona.eth" } } } } }, { "node": { "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "resolve": { "primaryName": { "name": { "beautified": "barmstrong.eth" } } } } }, { "node": { "address": "0x648aa14e4424e0825a5ce739c8c68610e143fb79", "resolve": { "primaryName": { "name": { "beautified": "sassal.eth" } } } } }, { "node": { "address": "0x983110309620d911731ac0932219af06091b6744", "resolve": { "primaryName": { "name": { "beautified": "brantly.eth" } } } } }, { "node": { "address": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", "resolve": { "primaryName": { "name": { "beautified": "nick.eth" } } } } }, { "node": { "address": "0xd7029bdea1c17493893aafe29aad69ef892b8ff2", "resolve": { "primaryName": { "name": null } } } }, { "node": { "address": "0xf56345338cb4cddaf915ebef3bfde63e70fe3053", "resolve": { "primaryName": { "name": { "beautified": "bored.eth" } } } } } ] }, "followers": { "edges": [ { "node": { "address": "0x00000066ced4c62a0f740de74eb08bc2a1552764" } }, { "node": { "address": "0x000000dcf1190af44f7149b85299f18ce7221024" } }, { "node": { "address": "0x000066b5a8e1e35b7e2f64e24715829647f80000" } }, { "node": { "address": "0x00076993122809055293d8210a60d8265a7e17f3" } }, { "node": { "address": "0x000f16ca1e73a0b6b10e1ed4cc5f8232336c1482" } }, { "node": { "address": "0x0011e9582ac18d9b80204817eaa2e3b544e727aa" } }, { "node": { "address": "0x00173c4cb23e6d876fcb036ba954a2f9cfcafa19" } }, { "node": { "address": "0x00197ff7abdac8ad42152ff9d5bd9e521701a944" } }, { "node": { "address": "0x002153708f11f2651215059eea30820ee4d49ff3" } }, { "node": { "address": "0x003e7b75ff09615ad4a6f763dc7d28e48ed513d9" } } ], "totalCount": 5338 } } } }}Output matches a point in time snapshot GraphQL response from our alpha ENSNode instance. Live output depends on the configuration of your ENSNode instance and ENS state updates.
enskit package manager setup
# 1. Create projectnpm create vite@latest my-ens-app -- --template react-ts --no-interactive --no-immediatecd my-ens-app# 2. Install dependenciesnpm installnpm install enskit@1.16.0 enssdk@1.16.0# 3. Copy the TSX snippet above into src/App.tsx# 4. RunVITE_ENSNODE_URL=https://api.alpha.ensnode.io npm run devSee the enskit docs for gql.tada plugin and provider setup.
# POST JSON to your ENSNode Omnigraph endpoint (same path enssdk uses).curl -sS -X POST "https://api.alpha.ensnode.io/api/omnigraph" \ -H "Content-Type: application/json" \ -d '{ "query": "query EfpFollowGraph($address: Address!) { account(by: { address: $address }) { efp { primaryList { tokenId } following(first: 10) { totalCount edges { node { address resolve { primaryName(by: { chainName: ETHEREUM }) { name { beautified } } } } } } followers(first: 10) { totalCount edges { node { address } } } } } }", "variables": {"address":"0xd8da6bf26964af9d7eed9e03e53415d37aa96045"}}'{ "data": { "account": { "efp": { "primaryList": { "tokenId": "6509" }, "following": { "totalCount": 10, "edges": [ { "node": { "address": "0x1b63142628311395ceafeea5667e7c9026c862ca", "resolve": { "primaryName": { "name": { "beautified": "tgerring.eth" } } } } }, { "node": { "address": "0x2b888954421b424c5d3d9ce9bb67c9bd47537d12", "resolve": { "primaryName": { "name": { "beautified": "lefteris.eth" } } } } }, { "node": { "address": "0x50ec05ade8280758e2077fcbc08d878d4aef79c3", "resolve": { "primaryName": { "name": { "beautified": "hayden.eth" } } } } }, { "node": { "address": "0x54becc7560a7be76d72ed76a1f5fee6c5a2a7ab6", "resolve": { "primaryName": { "name": { "beautified": "simona.eth" } } } } }, { "node": { "address": "0x5b76f5b8fc9d700624f78208132f91ad4e61a1f0", "resolve": { "primaryName": { "name": { "beautified": "barmstrong.eth" } } } } }, { "node": { "address": "0x648aa14e4424e0825a5ce739c8c68610e143fb79", "resolve": { "primaryName": { "name": { "beautified": "sassal.eth" } } } } }, { "node": { "address": "0x983110309620d911731ac0932219af06091b6744", "resolve": { "primaryName": { "name": { "beautified": "brantly.eth" } } } } }, { "node": { "address": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", "resolve": { "primaryName": { "name": { "beautified": "nick.eth" } } } } }, { "node": { "address": "0xd7029bdea1c17493893aafe29aad69ef892b8ff2", "resolve": { "primaryName": { "name": null } } } }, { "node": { "address": "0xf56345338cb4cddaf915ebef3bfde63e70fe3053", "resolve": { "primaryName": { "name": { "beautified": "bored.eth" } } } } } ] }, "followers": { "edges": [ { "node": { "address": "0x00000066ced4c62a0f740de74eb08bc2a1552764" } }, { "node": { "address": "0x000000dcf1190af44f7149b85299f18ce7221024" } }, { "node": { "address": "0x000066b5a8e1e35b7e2f64e24715829647f80000" } }, { "node": { "address": "0x00076993122809055293d8210a60d8265a7e17f3" } }, { "node": { "address": "0x000f16ca1e73a0b6b10e1ed4cc5f8232336c1482" } }, { "node": { "address": "0x0011e9582ac18d9b80204817eaa2e3b544e727aa" } }, { "node": { "address": "0x00173c4cb23e6d876fcb036ba954a2f9cfcafa19" } }, { "node": { "address": "0x00197ff7abdac8ad42152ff9d5bd9e521701a944" } }, { "node": { "address": "0x002153708f11f2651215059eea30820ee4d49ff3" } }, { "node": { "address": "0x003e7b75ff09615ad4a6f763dc7d28e48ed513d9" } } ], "totalCount": 5338 } } } }}Output matches a point in time snapshot GraphQL response from our alpha ENSNode instance. Live output depends on the configuration of your ENSNode instance and ENS state updates.