Projects
An NFT that points at content. Its owner publishes versions and moves tags; nobody else can.
A project is an ABX NFT. ownerOf is the entire
authorization model — publishing rights follow the token, with no migration
step and no separate registry.
Identity
| Field | Mutable | Purpose |
|---|---|---|
(chainId, tokenId) | no | Canonical identity. The token id is the project id. |
owner | yes | ownerOf(tokenId). The only address that can publish. |
bamfs.name | yes | A display label. Not unique, not an identifier. |
| root kind | locked at first publish | Directory or single file. |
The name is display metadata only, so "My Cool Art" is fine. The limits are
128 bytes and no control characters. Always store (chainId, tokenId) as the
handle — never the name.
Creating one
Minting and publishing are separate acts, and upload does both:
bamfs upload ./build --name "My Cool Art" --tag v1.0.0Keeping them apart is what lets you publish into a token you already hold, or bought from someone else:
bamfs upload ./build --project-id 17With neither flag, the upload is still complete — the root is addressable at
/g/cid/<root>/ whether or not a token ever points at it.
The mint is open and free with zero royalty. Nothing gatekeeps it.
Versions
Publishing writes the bamfs.cid param on the token, and BamfsProjectHook
appends to an on-chain history as it does:
await client.publishVersion(tokenId, rootCid);Reading that history back is a single eth_call against hook storage. There
is no indexer, and nothing here depends on event logs — providers prune those,
and an index that expires is not an index.
Version numbers start at 1 and never change meaning. The first publish also
locks the root kind: publish a file root and every later version must be a file
root, or the hook reverts with RootKindLocked.
Tags
Mutable named pointers from (tokenId, tag) → version, authorized against
ownerOf:
bamfs tag set 17 stable 2
bamfs tag get 17 stable
bamfs tag list 17
bamfs tag rm 17 stable- Charset is
^[a-z0-9][a-z0-9._-]{0,63}$. Dots are allowed so semver shapes work. Uppercase is rejected, so case-folding cannot produce two tags that look identical but address differently. latestis reserved and virtual. It always resolves to the head, has no storage, and writes to it revert — so every project has a resolvablelatestand no off-chain resolver needs a special case.- A tag pins its version. Moving the head does not drag a tag forward. That
is the point: a contract reading
tag/stable/render.jsis undisturbed by an unrelated publish.
Semver is a convention, not a protocol feature. The on-chain primitive is a flat
tag → version map; the SDK ships isSemverTag and pickLatestSemverTag.
A tagged publish is two transactions — the CID lands on the collection and the tag on the hook, so no single-contract multicall spans them.
Transferring
One transaction, immediate and irreversible:
bamfs transfer 17 0xNewOwnerThere is no window in which to reconsider. Transferring the token transfers the sole right to publish and to move tags. Both the CLI and the web UI confirm before sending.
The upside is that projects are sellable on any marketplace.
A preferred gateway
bamfs.gateway is an efficiency hint. The gateway tries that prefix first, then
falls back to rebuilding from chain — so an unset, stale, or dead value costs
latency and never correctness. Set it if you pin; ignore it otherwise.
Resolving
/g/p/<chainId>/<tokenId>/latest/<path> the head
/g/p/<chainId>/<tokenId>/v/<n>/<path> a fixed version
/g/p/<chainId>/<tokenId>/tag/<tag>/<path> a tag
/g/cid/<rootCid>/<path> raw content, no project neededA directory root with no sub-path serves index.html when present, and a
listing otherwise — which is what makes a project URL a website.
Caching follows whether a URL can ever address different bytes: /cid/… and
/v/N/… are immutable and cached for a year; latest and tags get short
freshness with a long stale-while-revalidate. Responses carry
X-BAMFS-Source: ipfs|chain.
