UBNet Follow Protocol
API version: 6.4.1 · SDK version: 6.5.3 · Docs version: 6.5.3
This page defines how current UBNet clients publish, project, read, and export follow relationships. A follow is a public, signed statement made by one UBNet profile about another UBNet profile.
Relationship identity
The logical relationship key has four parts:
(follower_wallet, follower_profile_name, following_wallet, following_profile_name)
Wallet addresses and profile names are normalized before projection. The Default profile is stored as an empty profile name. Named profiles are independent identities, so one wallet can follow the same target from multiple profiles or follow multiple profiles belonging to the same target wallet.
Publishing with the browser SDK
const client = UBNet.createClient({
apiBase: window.location.origin,
walletProvider: window.ethereum,
profileName: "research"
});
await client.profiles.follow(
actorWallet,
targetWallet,
{ profile_name: "news" }
);
await client.profiles.unfollow(
actorWallet,
targetWallet,
{ profile_name: "news" }
);
The client's active profileName selects the follower profile. The option passed to follow() or unfollow() selects the target profile. The target may also be written as 0x…@news; an explicit profile_name option takes precedence.
Both methods require an EIP-1193 wallet provider and request a personal_sign signature. The target does not sign or approve the relationship.
Signed event contract
The current SDK creates a Spasm 2.0 event body using UBNet schema version 3.0.0. A follow body has this shape; values shown are illustrative:
{
"type": "SpasmEventBodyV2",
"timestamp": 1780000000000,
"authors": [{
"addresses": [{
"value": "0xfollower…",
"format": { "name": "ethereum-pubkey" }
}]
}],
"protocol": { "name": "spasm", "version": "2.0.0" },
"schema": { "name": "ubnet-follow", "version": "3.0.0" },
"action": "follow",
"profile_name": "research",
"target_wallet": "0xtarget…",
"target_profile_name": "news"
}
An unfollow uses action: "unfollow" and schema ubnet-unfollow; its identity fields are otherwise the same. The body is serialized and wrapped as:
{
"type": "SpasmEventBodySignedClosedV2",
"signedString": "{...serialized event body...}",
"signature": "0x…",
"signer": "0xfollower…"
}
The SDK submits that envelope to POST /api/events/submit. A follow is public metadata: do not place private information in profile names or additional event fields.
Projection and conflict rules
- The indexer extracts the author wallet, actor profile, target wallet, and target profile.
followsets the relationship's projectedactivevalue totrue.unfollowsetsactivetofalse. It does not delete the signed follow history.- The projection stores the latest event ID and signed event timestamp for the four-part relationship key.
- An event updates the projection only when its timestamp is equal to or newer than the stored timestamp. Older events cannot reverse newer projected state. If timestamps are equal, the event processed last wins.
Repeated follow events do not create duplicate projected relationships. Public follower/following reads return only rows whose projected state is active.
Reading followers and following
const followers = await client.profiles.followers(wallet, {
profile: "news",
limit: 50,
cursor: null
});
const following = await client.profiles.following(wallet, {
profile: "news",
limit: 50,
cursor: null
});
These call GET /api/users/:wallet/followers and GET /api/users/:wallet/following. The profile query accepts name, profile, or profile_name. The default page size is 50 and the maximum is 100.
{
"followers": [
{
"follower_wallet": "0x…",
"follower_profile_name": "research",
"following_wallet": "0x…",
"following_profile_name": "news",
"active": true,
"latest_event_id": "spasmid01…",
"updated_at": 1780000000000
}
],
"has_more": true,
"next_cursor": "opaque-base64url-cursor"
}
The following response uses the same row fields under following. Results are ordered by newest projected update, then by target wallet and target profile. Treat next_cursor as opaque and pass it unchanged to retrieve the next page.
Portable follow-list export
const exportData = await client.profiles.followExport(wallet, {
profile: "research"
});
GET /api/users/:wallet/follow-export returns all active relationships for the selected profile:
{
"type": "ubnet-follow-list-v2",
"wallet": "0x…",
"profile_name": "research",
"exported_at": "2026-09-27T00:00:00.000Z",
"following_profiles": [
{ "wallet": "0x…", "profile_name": "news" }
],
"follower_profiles": [
{ "wallet": "0x…", "profile_name": "reader" }
]
}
Use following_profiles and follower_profiles so each relationship retains its complete profile identity. Importing an export means publishing new signed follow events from the importing profile; the export itself is not an authorization token and does not recreate someone else's signatures.
Integration rules
- Publish
ubnet-followandubnet-unfollowversion 3.0.0 events. - Use the SDK write methods instead of constructing signed envelopes unless interoperability requires direct event production.
- Keep actor and target profile names explicit when an application supports named profiles.
- After a write, refresh or invalidate follower/following data; event submission and projection are separate steps.
- Do not infer a target's approval, endorsement, or reciprocal follow from this protocol.
- Use the profile-aware export arrays for backups and migrations.