UBNet Browser Client API
API version: 6.4.1 · SDK version: 6.5.3 · Docs version: 6.5.3
frontend/public/ubnet.js is the canonical browser SDK entry point. It exposes window.UBNet for browser applications, static profile packages, and integrations.
Read installed versions from GET /api/version and SDK compatibility metadata from client.versions.
<script src="/ubnet.js"></script>
const UBNet = await window.UBNetReady;
const client = UBNet.createClient({
apiBase: window.location.origin,
walletProvider: window.ethereum,
profileName: ""
});
Public reads do not require a wallet. Ordinary browser signed operations use the configured EIP-1193 provider or window.ethereum. Private file sales use the separately configured agent signer described below.
The separate follow protocol documents profile-aware follow/unfollow events, projection and conflict rules, read pagination, and exports.
Public vs UBNet Feed reads
There is one public SDK, with two deliberately different read policies.
Generic/public data
Use these namespaces for third-party clients, profile pages, global rankings, and other public data that must not inherit UBNet Feed category policy:
await client.ubnet.posts.list({ limit: 25 });
await client.ubnet.posts.mine(walletAddress, options);
await client.ubnet.posts.forWallet(walletAddress, options);
await client.ubnet.posts.topByCategory({
category: "blockpower",
sort: "upvotes",
limit: 5
});
await client.ubnet.lists.list({ limit: 25 });
await client.ubnet.lists.mine(walletAddress, options);
await client.ubnet.lists.publicForWallet(walletAddress, options);
await client.ubnet.lists.topByCategory({
category: "music",
sort: "likes",
limit: 10
});
client.ubnet.posts.* does not apply Post Feed Allow. client.ubnet.lists.* does not apply List Feed Allow.
The generic ranking routes are:
GET /api/posts/top-by-categoryGET /api/collections/top-by-category
Block Powers are a global/public ranking. Use:
await client.ubnet.posts.blockPowers({ limit: 5 });
or equivalently:
await client.ubnet.posts.topByCategory({
category: "blockpower",
sort: "upvotes",
limit: 5
});
Do not use client.ubnet.feed.posts.topByCategory() for Block Powers.
The top-level generic SDK namespaces remain available for normal post/list operations such as client.posts.create(), client.posts.get(), client.lists.create(), and client.lists.items().
UBNet Feed-curated data
Use client.ubnet.feed.* only for UBNet frontend surfaces intentionally governed by Feed allowlists:
await client.ubnet.feed.posts.list({ limit: 25 });
await client.ubnet.feed.posts.topByCategory({
category: "news",
sort: "likes",
limit: 10
});
await client.ubnet.feed.lists.list({ limit: 25 });
await client.ubnet.feed.lists.topByCategory({
category: "music",
sort: "likes",
limit: 10
});
client.ubnet.feed.posts.* applies Post Feed Allow. client.ubnet.feed.lists.* applies List Feed Allow when that server toggle is enabled. The post and list policies are independent; there is no combined posts+lists Feed API.
The curated ranking routes are:
GET /api/ubnet/posts/top-by-categoryGET /api/ubnet/lists/top-by-category
There is no combined post-and-list ranking API. Callers must select posts or lists explicitly.
UBNet frontend routing guidance
- Main Feed:
client.ubnet.feed.posts.list() - Curated list Explore:
client.ubnet.feed.lists.list() - Global/public/profile post reads:
client.ubnet.posts.* - Global/public/profile list reads:
client.ubnet.lists.* - Block Powers:
client.ubnet.posts.topByCategory()orclient.ubnet.posts.blockPowers()
Generic posts
await client.posts.list({
limit: 50,
cursor,
author,
post_type,
keyword,
media_type,
has_cid,
schema_name,
schema_version,
source_instance_host,
profile
});
await client.posts.get(postId);
const history = await client.posts.history(postId, { signal });
await client.posts.replies(postId, { limit: 50 });
await client.posts.mine(walletAddress, { visibility: "all", limit: 50 });
Post history returns has_edits, edit_count and versions from the server. Show View History only when has_edits is true; visibility/publication-only events do not count. The original post is retained, and clearing text/media remains an edit. The API retains all signed events internally. This response contract requires API 6.4.1.
Create/edit/actions:
await client.posts.create(walletAddress, {
categories: ["general"],
title: "Hello UBNet",
content: "First post"
});
await client.posts.edit(walletAddress, postId, {
title: "Updated title",
content: "Updated body",
visibility: "public"
});
await client.posts.reply(walletAddress, postId, "Reply text");
await client.posts.react(walletAddress, postId, "like");
await client.posts.publication(walletAddress, postId, "published");
await client.posts.remove(walletAddress, postId);
await client.posts.deleteThread(walletAddress, threadRootId);
At least one category is required when creating a post. Categories are immutable after creation.
SDK post writes accept text, post, link, image, audio, video, and reply. Unsupported post types fail with invalid_post_type before signing.
Generic lists
await client.lists.list({ keyword: "music", limit: 25, cursor });
await client.lists.get(listId);
await client.lists.mine(walletAddress, { visibility: "all" });
await client.lists.publicForWallet(walletAddress, { visibility: "shown" });
List-item pagination uses offsets:
const page = await client.lists.items(listId, {
limit: 20,
offset: 0
});
console.log(page.items, page.has_more, page.next_offset);
Current authored list types are audio-list, video-list, image-list, post-list, link-list, and mixed-list. There is no authored any list type.
list_type belongs to the list root and cannot be changed. client.lists.update() removes list_type, listType, post_type, and postType from edit payloads. List items use target_type and media_type.
For CID list items, a blank gateway does not imply a hard-coded public gateway; playback uses the configured gateway registry.
Profiles and names
await client.profiles.get(walletAddress);
await client.profiles.names(walletAddress);
await client.profiles.ensBindings(walletAddress);
await client.profiles.verifyEnsControl(walletAddress, ensName);
await client.profiles.search("name", 50);
await client.profiles.followers(walletAddress, { limit: 50 });
await client.profiles.following(walletAddress, { limit: 50 });
await client.profiles.followExport(walletAddress);
await client.reservedSubdomains.check("example");
Post writes use the client's active profile. Set it with profileName when creating the client or with client.setProfileName().
IPFS discovery and playback
const result = await client.ipfs.discover("bafy...", {
expectedType: "audio"
});
discoverCid() is an alias. Discovery uses the server gateway registry and preserves path prefixes and signed/query parameters.
Playback helpers are:
client.ipfs.gateways(options?)client.ipfs.playbackUrls(value, options?)client.ipfs.playbackUrl(value, options?)client.ipfs.playback(value, options?)client.ipfs.markPlaybackSucceeded(url, options?)client.ipfs.markPlaybackFailed(url, options?)
Pass a fetched post/list item directly when possible. The SDK uses its root cid and canonical media_metadata; folder CIDs use media_metadata.media_path, while file CIDs use the root CID. A known folder without a media path is not treated as a playable directory root.
const playback = await client.ipfs.playback(item, { operation: "audio" });
audio.src = playback.url;
audio.onerror = () => {
const next = playback.fail(audio.currentSrc);
if (next) audio.src = next;
};
audio.onplaying = () => playback.succeed(audio.currentSrc);
For folder artwork use client.ipfs.playbackUrl(item, { asset: "thumbnail", operation: "image" }); this uses an explicit thumbnail URL first and otherwise media_metadata.thumbnail_path.
Playback uses only configured client_playback gateways, remembers successful gateways per operation, advances after failures, keeps a last-good registry cache, and never fabricates an ipfs.io fallback. Direct signed/query asset URLs and private relative unlock URLs are opaque and must be used unchanged.
Encrypted chats
client.chats is the high-level messaging API. UBNet chat list/history/state/activity reads use uncategorized messages only; categorized messages remain available through generic chat reads.
const identity = client.chats.getIdentity(wallet, {
profile: client.profileName
}).identity;
let status = await client.chats.getStatus(identity);
if (status.state === "setup_required") await client.chats.enable(identity);
if (status.state === "restore_required") await client.chats.restore(identity);
if (!client.chats.isUnlocked(identity)) await client.chats.unlock(identity);
const chats = await client.chats.list(identity, { limit: 50, cursor, search });
const messages = await client.chats.messages(chatId, identity, { limit: 50, cursor });
const sent = await client.chats.sendText(identity, chatId, "Hello");
Use status.can_read for readable-history gating. A successful wallet-recovery restore also establishes the normal short-lived unlocked session, so it does not require a second immediate unlock. Plaintext stays local to the browser SDK; only ciphertext is submitted.
sendText() and prepareMessage() may include categories. Categories are public event metadata and are not encrypted with message plaintext.
Use client.chats.sendMessage(identity, chatId, { message, file?, expiresAt?, viewSeconds? }) for unified messaging. Text and attachment are encrypted separately inside one signed event. Chat reads return only the small message payload; file ciphertext is fetched only when Download is requested. Normal direct and group messages retain sender history; an optional absolute expiry applies to all copies held by the server. Viewing timers select recipient-only direct delivery and cannot be used in groups. They start only when client.chats.openMessage(identity, messageId) is explicitly called. client.chats.inbox(identity) lists metadata without opening messages; client.chats.downloadAttachment(identity, openedOrHistoryMessage) produces a Blob only for the unlocked matching identity before expiry. Read the complete SDK reference for limits and retention details.
client.chats.sendAttachment(identity, chatId, file, { message? }) sends an encrypted file with an optional caption. Call await client.chats.limits() before file selection; attachmentLimits() is a synchronous cached snapshot, not a replacement for current server policy. History returns file metadata without decrypted bytes; explicit downloadAttachment(identity, chatId, message) performs the download decryption. New sends sign the encrypted transmission with signature_intent: "authorize_encrypted_message" and plaintext_attestation: "not_asserted".
See the messaging examples for explicit send, open, expiry and cleanup flows.
Uploads
Public Pinata uploads use a fresh wallet signature and a short-lived upload URL. Current helpers include:
client.uploads.avatar(wallet, file, options)— the same wallet-signed Spasm image-event flow asclient.profiles.uploadAvatar;options.walletProvidercan override the client's provider.client.uploads.pinataChallenge(data)client.uploads.pinataPresignedUrl(data)client.uploads.pinataSignedUrl(uploadUrl, file)client.uploads.pinata(data)
The full pinata() helper performs challenge, signature, presign, and direct upload. Public upload failures may include public_ipfs_uploads_disabled when the administrator-controlled upload flag is off.
Support and public events
await client.support.contact({ message: "Help" });
await client.events.list({ category: "example", latest: true });
Event discovery remains available through client.events.list(). Use post_type for post events and list_type for list events. Use the returned list_type for display. Category rankings are intentionally type-specific and are exposed only through the post or list ranking namespaces above.
Errors
Failed SDK requests use UBNet.UbnetError. Inspect:
error.statuserror.codeerror.payload
Do not branch on human-readable error text.
Private file sales
client.fileSales provides wallet-only autonomous seller operations through fileSales: {wallet, audience, signMessage} in createClient. It works without a browser provider, including in Node. Use prepareUpload, uploadToUrl, completeUpload, then create for a recoverable private transfer and paid link. The convenience upload accepts an async onPrepared journal hook and returns the completed file; never replay the whole helper after an uncertain transfer. See the full agent instructions for all 14 methods, inputs, response fields, authorization and recovery examples.
The feature defaults off. Require access token defaults on for existing policies and checks the configured token on every normal call; explicitly turning it off permits open signed access within allowances. Both inspection switches default off. Direct positive USDC payments go to the signing wallet at a fixed price; no platform fee or purchase-once entitlement is implemented. Fresh owner signatures permit disable and permanent deletion even when the token is lost or the service is off. Seller retention is outside the API. This namespace is distinct from public Pinata uploads and Spasm events.
Feed category discovery
client.categories.list({ signal, feedOnly: true }) requests only active, feed-enabled categories using GET /api/categories?feed_only=true, and filters the returned rows too. Omit feedOnly to keep all active categories. Use client.ubnet.feed.posts.list({ category, limit, cursor }) to narrow the feed; changing category starts a new cursor sequence and cannot widen server policy.
Production SDK commands (SDK 6.3)
client.productions wraps the existing production API and signs production commands through the same event pipeline as posts and lists. API compatibility remains 6.2. Reads do not request a signature. Creation, folder binding and removal each require one wallet signature and use the client's selected wallet profile.
const client = UBNet.createClient({ apiBase: location.origin, walletProvider: provider, profileName });
const page = await client.productions.list(wallet, { limit: 20, cursor, profile: profileName, signal });
const detail = await client.productions.get(productionId, { signal });
await client.productions.create(wallet);
await client.productions.bind(wallet, productionId, folderCid);
await client.productions.remove(wallet, productionId);
Commands return the normal SDK submission result. A successful submission confirms acceptance; the production projection may still be processing. Caller code must preserve its active identity across asynchronous work and use its normal pending-action controls.
Paid-file buyers (SDK 6.4)
Use client.fileSalesBuyer.links(recordOrText), quote(url, {signal}), pay(offer, {wallet, approve, onPayment, signal}), retry(payment, {signal}), exportPayment(payment), restorePayment(saved) and complete(payment). Buying is disabled unless the client is configured with fileSalesBuyer: {enabled:true}. Buyers use their own wallet and explicit payment approval, independently of the separately authorized seller API. See the complete buyer and browser checkout guide for supported payment terms, signer adapters, federation, safe proof journaling, download recovery, CORS and default-off UI settings. Never automatically sign another payment after an uncertain result.