Official contract:
0x7e054F3c74a544bCc5BE222AC3B43548C87AA842
Architecture
Contract: Single Solidity contract (pragma ^0.8.0) implementing users, posts, comments, reactions, and follows. Mutating functions are gated by onlyEOA (EOA‑only) to reduce automated abuse.
UI: A minimal web client that reads/writes directly to the contract using a standard provider. It reconstructs timelines via counters and contiguous IDs, batch‑loads in small slices on scroll, supports deep links (#post{ID} and #post{ID}/#comment{ID}), and refreshes the home feed on an interval.
Resilience: No backend/indexer; as long as Ethereum is reachable, the app renders truthfully.
Determinism: Global post IDs and per‑user jump tables guarantee predictable pagination.
UX: Instant UI toggles by reading on‑chain booleans/enums (follow, like, etc.).
Why this design is smart (and fast)
- Single source of truth: Ethereum state → zero drift, zero “event lag”.
- Zero ops: No indexer/DB/cron to run or maintain. Fewer moving parts; fewer outages.
- Symmetric clients: Any UI makes the same calls and gets the same answers. No privileged backend.
- Latency bounded by RPC: Optional Multicall compresses round‑trips without changing the model.
- Forkable by design: Anyone can build a new client without permission; the chain is the API.
Data Model
// Users
struct UserBasic { uint256 userId; string username; uint256 accountCreationTime; uint256 accountCreationBlock; bool isRegistered; }
struct UserProfile { string nickname; string about; string website; string location; string profilePicture; string coverPicture; uint256 pinnedPost; }
struct UserStats { uint256 postCount; uint256 commentCount; uint256 followerCount; uint256 followingCount; }
// Content
enum Reaction { NONE, LIKE, DISLIKE }
struct Post {
uint256 globalPostId; address author; uint256 authorPostId; uint256 postTime;
string content; uint256 commentCount; uint256 likeCount; uint256 dislikeCount; uint256 repostCount;
bool isHidden; bool isRepost; uint256 originalPostId; string reposterContent;
mapping(address => Reaction) reactions;
}
struct Comment {
uint256 commentId; address author; uint256 commentTime; string comment;
uint256 likeCount; uint256 dislikeCount; bool isHidden; mapping(address => Reaction) reactions;
}
// Key indices
uint256 private totalUsers; uint256 private globalPostCount;
mapping(string => address) usernameToAddress; // case-insensitive lookup
mapping(uint256 => address) userAddressById; // userId → address
mapping(address => mapping(uint256 => uint256)) userPostId; // per-user idx → global post id
mapping(uint256 => Post) allPosts; // global posts
mapping(uint256 => mapping(uint256 => Comment)) postComments; // per-post contiguous comments
// UI booleans
mapping(uint256 => mapping(address => bool)) hasCommentedOnPost;
mapping(uint256 => mapping(address => bool)) hasRepostedOnPost;
// Social graph
mapping(address => mapping(address => bool)) isFollowing;
The key: contiguous, monotonic IDs and authoritative counters make reads trivial and deterministic.
Identity & Usernames
- Registration: createAccount(nickname, username) saves UserBasic, UserProfile, and UserStats, and assigns a monotonic userId.
- Case‑insensitive uniqueness: Usernames are validated (≥5 chars, lowercase letters/digits). The lowercase form indexes usernameToAddress; display casing is preserved in UserBasic.username.
- Lookups: Resolve by username (getUserAddressByUsername) or by userId (getUserAddressById).
Posts & Reposts
- Creation: createPost(content) increments globalPostCount and the author’s postCount; assigns globalPostId and authorPostId. Text must be non‑empty.
- Editing & Hiding: Authors can editPost or hidePost. Hidden posts keep IDs/counters; getters return empty text when hidden.
- Reposts: createRepost(originalId, reposterContent) links to the original and increments its repostCount. editRepost updates reposter note.
- Pinning: pinPost(globalPostId) (or 0 to unpin) is enforced on‑chain for the author only.
Reactions & Social
- Reactions: Per‑item mapping(address => Reaction) supports constant‑time checks. The API accepts "like"/"dislike" strings and adjusts counters on change.
- Follow graph: followUser(target, bool) flips isFollowing and maintains follower/following counters with underflow guards.
- Interaction flags: hasCommentedOnPost and hasRepostedOnPost provide O(1) UI gating.
Visibility & Integrity
- Soft hide: isHidden flags avoid breaking links or pagination; getters return empty text when hidden.
- Repost integrity: Reposts record originalPostId and bump the original’s repostCount.
- EOA guard: onlyEOA limits mutations to externally‑owned accounts.
Query Patterns (Indexer‑Free)
Global/Home Feed
// 1) Latest post id
N = getGlobalPostCount()
// 2) Fetch descending in batches
for id in [N, N-1, ..., N-k]: render(getPost(id))
User Feed
// 1) Per-user total
K = getUserStats(user).postCount
// 2) Jump table per-user idx → global id
for i in [K, K-1, ..., K-k]: id = getGlobalPostId(user, i); render(getPost(id))
Comments
// 1) Count
C = getPostCommentCount(postId)
// 2) Contiguous 1..C
for i in [1 .. C]: render(getComment(postId, i))
Instant UI Toggles
liked = getUserReactionOnPost(postId, me)
following = getIsFollowing(me, user)
reposted = getHasRepostedAPost(postId, me)
Tip: Batch read‑only calls with Multicall to reduce latency on mobile.
Why no indexer is needed: Every query here is a direct lookup or a bounded loop over contiguous IDs. There is no search, no off‑chain sorting, and no background job. The blockchain is the index.
Contract API (Selected)
Accounts
createAccount(string nickname, string username)
changeUsername(string newUsername)
isUserRegistered(address user) → bool
getUserAddressById(uint256 id) → address
getUserAddressByUsername(string username) → address
getUserBasic(address) → (userId, username, createdAt, createdBlock, isRegistered)
getUserProfile(address) → (nickname, about, website, location, profilePicture, coverPicture, pinnedPost)
getUserStats(address) → (postCount, commentCount, followerCount, followingCount)
Posts
createPost(string content)
,editPost(uint256 id, string newContent)
,hidePost(uint256 id, bool)
createRepost(uint256 originalId, string reposterContent)
,editRepost(uint256 id, string newReposterContent)
getGlobalPostCount() → uint256
,getGlobalPostId(address, uint256) → uint256
,getPost(uint256) → (id, author, authorPostId, time, text, commentCount, likeCount, dislikeCount, hidden, isRepost, originalPostId, repostCount)
Comments
createComment(uint256 postId, string text)
,editComment(uint256 postId, uint256 commentId, string newText)
,hideComment(uint256 postId, uint256 commentId, bool)
getPostCommentCount(uint256 postId) → uint256
,getComment(uint256 postId, uint256 commentId) → (...)
getUserComment(address user, uint256 userCommentId) → (postId, commentId)
,getUserCommentCount(address) → uint256
Social & Reactions
followUser(address target, bool follow)
,getIsFollowing(address follower, address followed) → bool
reactToPost(uint256 postId, string reaction)
,reactToComment(uint256 postId, uint256 commentId, string reaction)
getUserReactionOnPost(uint256 postId, address user) → string
,getUserReactionOnComment(uint256 postId, uint256 commentId, address user) → string
getHasCommentedOnPost(uint256 postId, address user) → bool
,getHasRepostedAPost(uint256 postId, address user) → bool
UI Implementation Notes
- Routing & Deep Links: The UI supports
#post{ID}
and#post{ID}/#comment{ID}
routes for single‑post and single‑comment views; other hashes map to usernames or sections. - Feeds: Home feed walks backward from
getGlobalPostCount()
in batches onwindow.onscroll
. User feed maps per‑user indices viagetGlobalPostId(user,i)
. Comments load contiguously usinggetPostCommentCount()
. - Profile/Stats Widgets: “Recently Joined” reads
getTotalUsers()
then iterates downwards to fetch the latest addresses viagetUserAddressById()
. “Platform Stats” readsgetTotalUsers()
andgetGlobalPostCount()
. - Reactions: Buttons call
reactToPost
/reactToComment
. Highlight state is re‑derived viagetUserReactionOnPost
/...OnComment
. - Markdown: Post/comment text is parsed with
marked
;@mentions
are auto‑linked to#username
. (If building your own client, add an HTML sanitizer after parsing.) - Caching: The UI caches user display names/profile pics per address to reduce repeated calls.
- Auto Refresh: Home feed can auto‑reload every 60s when on the home route.
Links
App: etherfeed.org
FAQ: etherfeed.org/faq
Roadmap: etherfeed.org/roadmap
Twitter/X: @0xEtherFeed
Telegram (Channel): t.me/EtherFeedOrg
Telegram (Chat): t.me/EtherFeedChat
Comments