Specification
The whole convention, on one page
The key
{"oa": {"v": 1, "object": "MicroPost"}}With attributes. They sit beside the envelope, not inside it, in the same flat object as app and tags:
{
"oa": { "v": 1, "object": "x-Recipe" },
"servings": 4,
"minutes": 35,
"ingredients": ["flour", "butter", "sea salt"]
}That placement is not cosmetic. A reader takes attributes from the top level, so an application that nests them under oa.attributes writes a post whose fields no reader can see. No error, no warning, just an empty object at the other end. It is also why the reserved names below exist at all: attributes share this level with keys that already have a meaning.
Written without the spacing above, and merged into the metadata your application already sends, that is 34 bytes more per post. Not a new operation, not a chain change, and nothing to migrate.
Nor is it an invention. Hive already does this for its other content. custom_json carries an id naming what the operation is. Across blocks 108,763,467 to 108,764,067, a 600-block window, all 5,298 of them carried one, spanning 110 different vocabularies. In the same window, 288 comment operations said nothing about what they were. The idea is Hive’s; it was simply never applied to posts.
They read the way you would expect, because the point of a name is to be read: sm_claim_daily, 3speak-publish, zingit_track_meta, hive_scrobble_ai. A game, a video platform, a music tracker. Nobody coordinated the shape and nobody had to, because each of them is only ever asking one question: what is this operation? Ours is oa-registry-dev, and it answers the same question about a registration.
Posts already travel with invented keys such as developer, signature and type, and they render everywhere, which is strong evidence that unrecognised keys are ignored. No frontend's parser has been formally tested against this one; if yours treats unknown metadata keys as an error, we would like to hear about it.
What goes in it
Nothing here is permission. Hive validates no metadata at all, so any of this can be written by anybody and the chain will carry it. What the two fields below decide is whether a reader understands what arrived.
| Field | Meaning |
|---|---|
v | Format version. Always 1 today. A reader that sees any other number ignores the envelope rather than guessing at it. |
object | What the post is. One of the five core types, or a name of your own such as x-Recipe. |
That is the whole envelope. Attributes are not a field inside it. They sit beside it, as shown above.
Core types
Six: Article, MicroPost, Comment, Container, Profile and Unknown. Any other value is an extension type: a reader reports the post as Unknown, keeps the name you gave it, and still hands over every attribute.
Profile is the odd one, and deliberately so. The others are kinds of content — each has a body, an author and a permlink — while this one names the account itself, so it never appears on a post and a classifier handed a post can never return it. It belongs in the account’s own metadata, which is the section below.
The x- prefix is a convention rather than a rule. Recipe and x-Recipe behave identically today, which we checked rather than assumed. Use it anyway: it is what keeps a name of yours from one day colliding with a core type, and it tells a reader at a glance which is which.
Posts and accounts
Everything above is about a post, which is where nearly all of this lives. There is a second document: the account itself. Hive keeps what an account says about itself in posting_json_metadata, the field a posting key may write, and an attribute goes there the same way it goes into a post — at the top level, beside the profile object every frontend already reads.
{
"profile": { "name": "3Speak", "profile_image": "…" },
"oa": { "v": 1, "object": "Profile" },
"threespeak.profile": { "interests": ["crypto-finance"] }
}Same rules throughout. The name is yours because you registered it, the shape is whatever you documented, and any reader may take it without asking. Only the envelope differs: Profile rather than Article, because what it describes is the account and not something the account published.
Merge, never replace. A post’s metadata is yours to write whole; an account’s is shared with profile and with every other application that has ever written there. Sending only your own key wipes the display name, the avatar, and on some accounts the login redirects. Read the field, add your key, write the whole object back — with account_update2, which is the operation that takes a posting key.
A registration says which of the two it means, so a reader knows where to look without guessing. Absent means a post, because that is what every registration made before this existed is about.
Reserved names
Reserved by this convention, never by Hive. The chain reserves nothing: json_metadata is a string to it and is not parsed by consensus. Nothing below is a rule you can break. These are names that already mean something to a reader, and the cost of using one is that your field does not arrive.
Some are the post’s own fields, the rest are keys frontends have written for years. Your field will not be there at the other end, and nobody gets an error saying so:
object, author, permlink, title, body, media, hints, attributes,
oa, app, tags, image, images, links, users, format, flow, community,
canonical_url, isPoll, pollOptions, appVersion, description,
thumbnails, videooa is on the list because it is the envelope itself. Three more are refused because they are instructions to the language rather than names: __proto__, constructor and prototype.
Whose name is it
Names start with the account that owns them, so frontend-x.recipe is @frontend-x’s. That is the whole convention: the name carries its own attribution, and no authority is needed to hand it out.
What follows is not part of the convention. Nothing on Hive enforces any of this. Anyone can broadcast any name, and any reader is free to believe whoever they like. The chain will carry all of it either way. The rule below is only how this site decides which definition to list. Your application does not need it to read a post, and is not bound by it.
A registration says which namespace it is for, and the chain says who signed it. When those agree the name is frontend-x.recipe. When they do not, the name begins with the signer anyway, so @frontend-y registering a definition of frontend-x.recipe is listed as frontend-y.frontend-x.recipe.
That is not a refusal and it is not an accusation. An application may well have something to say about a name that is not its own, and this is where it says it: in its own namespace, where it can be read and weighed. What it cannot do is produce a name beginning with somebody else, because the first segment is not its to write.
// A name is built from the signature, not taken from the payload.
// The chain decides the first segment; you decide the rest.
const nameOf = (signer, claimed, leaf) =>
claimed === signer ? `${signer}.${leaf}` : `${signer}.${claimed}.${leaf}`
nameOf('frontend-x', 'frontend-x', 'recipe') // frontend-x.recipe
nameOf('frontend-x', 'frontend-y', 'recipe') // frontend-x.frontend-y.recipe
nameOf('eve', 'frontend-x', 'recipe') // eve.frontend-x.recipeOne addition, for the case where an application’s name is not its account name: an entry can list other accounts that speak for it, and those count too. The entry vouches for them, not us.
This is about definitions, never about posts. All of the above decides whose definition of frontend-x.recipe gets listed. It says nothing about who may write that name into a post, and it must not: when somebody publishes a recipe through an application, the post is signed by them, not by the application. Anyone can write any attribute into anything they publish. That is the point of a shared vocabulary: a second application picking up frontend-x.recipe is adoption, which is the outcome this whole project is for.
Reading it back
Ordinary JSON, no library:
const meta = JSON.parse(post.json_metadata)
if (meta.oa?.v !== 1) return null // a version you know
if (meta.oa.object !== 'x-Recipe') return null
const { minutes, servings } = meta // ingredients ignored, deliberatelyA frontend renders the objects it supports and the attributes it wants, its own and other applications’, and ignores the rest. A post without the key needs no second path: it simply has no oa, and the comparison fails like any other mismatch.
Ignore an envelope whose version you do not know. That is the only rule in this section, and it is what lets a later version change the shape without silently misleading a reader written against this one.
One implementation note, because it costs people an afternoon. Whether json_metadata arrives as a string or already parsed depends on the API you fetched with, not on the post: condenser_api returns a string, bridge returns an object. Decide once where you fetch.
Posts that say nothing
Almost every post says nothing, and four years of them came before this page. So the other half of the convention is what a reader may infer from where a post sits and what shape it has. Six rules, first match wins:
// Every rule, in full. No import, no package, nothing to install.
// CONTAINERS is data, not part of the convention: see below for where it lives.
function objectOf(post, CONTAINERS) {
let meta = {}
try {
meta = typeof post.json_metadata === 'string'
? JSON.parse(post.json_metadata)
: post.json_metadata
} catch {}
// R0. What the post says about itself wins over anything below.
if (meta?.oa?.v === 1 && typeof meta.oa.object === 'string') return meta.oa.object
// Absent parent means top-level: bridge calls omit the field entirely.
const parent = post.parent_author ?? ''
if (CONTAINERS.includes(parent)) return 'MicroPost' // R1
if (CONTAINERS.includes(post.author) && !parent) return 'Container' // R2
if (parent !== '') return 'Comment' // R3
if ((post.title ?? '').trim() !== '') return 'Article' // R4
return 'Unknown' // R5
}This is the whole of it, and it is here to be copied rather than installed. Each rule carries a confidence when read through our packages, 1.00 for R0 down to 0 for R5; those are an ordering we chose, not a measured error rate.
Container accounts
R1 and R2 are the only rules that need something the post does not carry. A container account is one whose top-level posts exist to hold other people’s short posts: several applications independently publish a post a day and park every short post under it as a reply, so a reply to one of those is a micropost rather than a comment.
That definition is the convention. Which accounts they are is data, and it changes: whatever set is right today will be wrong later without this page being edited, so no list is printed here and the rules above take one as an argument.
You are not dependent on ours for it. The definition is checkable: a container account’s recent top-level posts are near-empty and each carries dozens of replies at depth one. Applied to the three accounts we track and to six ordinary busy accounts, that test matched all three and none of the six. Build your own list from the accounts your readers care about, or take ours from @openattribute/spec.
If your application parks short posts under a container of its own, tell us and it goes on ours. There is no approval to wait for and nothing to sign: an account either has that shape or it does not, and anybody can check.
An application that writes the key needs none of this. R0 fires and R1 never has to guess on its behalf.
What an edit can do
An edit is another comment operation, and its json_metadata replaces the previous one rather than merging with it. Whatever the editing application writes is what the post now carries, and whatever it omits is gone.
Both directions follow from that, and both are ordinary on chain:
An edit can erase the key. An edit made from an application that does not write oa drops it, silently and with no error anywhere. A declaration is not immutable, so write the key on every edit and not only on the first publish.
If you edit somebody else’s post, carry their keys with you. An application editing a post it did not publish should merge the metadata already on it and write its own fields over the top, rather than replacing the object. Keys it does not recognise then survive the edit instead of being destroyed by it.
This is the one part of the convention that asks something of an application on behalf of somebody else, and it is worth asking because the alternative cannot be arranged: the publisher is not present when their post is edited elsewhere, so no amount of care on their side reaches it. It costs an editor nothing, needs nobody else to agree first, and every application that does it makes every other application’s fields durable, including its own.
An edit can add it. A post published years before any of this can be declared by editing it, which is the only way to declare something already on chain. The author signs that edit, so nobody can declare on anybody else’s behalf.