Documentation
A name for a field, written where other applications can find it. Look up what exists, write it into your posts, read anybody else’s back, and register a name of your own when nothing fits. None of it needs anything installed.
What is already there
Hive gives you one content operation and a free-form metadata field, and applications have made full use of it. Every one of them has a working vocabulary of its own, already written into posts, already on chain, already public.
Read a few hundred posts and you find dozens of top-level keys in active use. A fitness application records measurements as chestUnit, waistUnit and thighsUnit. Another publishes image_ratios. Each is a considered answer to a real question, and each is sitting in the open where any application can pick it up.
Some of them already have. Over 1,200 blocks, seven names arrive from more than one application: canonical_url from four, developer and signature from three each, and dimensions, isPoll, original_author and original_permlink from two. Nobody arranged any of it: somebody read somebody else’s metadata, liked the name, and used it. That is this whole idea happening on its own, and making it ordinary is what OpenAttribute proposes.
Look first
Before inventing a name, see whether one exists. The register lists every attribute anybody has declared on chain: what it means, what shape it takes, who signed for it, and how often it has been written lately.
Using somebody else’s name is the point, not a liberty. A name is owned at registration and open at use. If a photography application registered exposure and it fits what you publish, write exposure. That is one more application whose content the same reader can render, which is the only way this is worth anything.
Write one
An attribute is one key in the json_metadata your application already sends, beside app and tags:
{
"app": "your-frontend/1.0",
"tags": ["recipes"],
"oa": { "v": 1, "object": "Article" },
"opnattr.pulse.micropost": { "url": "https://…" }
}That is the whole of publishing. The post is an ordinary comment operation with an ordinary title and body; the attribute rides along in metadata and changes nothing about how existing frontends render it. Unrecognised keys are ignored everywhere we have looked.
The oa key beside it is a separate, optional thing: it says what the post is rather than what it carries. Both are specified on the spec.
Your user signs the post, not your application. So a second application writing the same name is not taking anything from the first. There is nothing to permit and nobody to ask.
On an account
Not everything an application knows about somebody belongs in something they published. Interests, badges, the layout of a links page: that describes the account itself, and Hive already keeps it there, in posting_json_metadata. An attribute goes in the same place, 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 as a post. The name is yours because you registered it, the shape is whatever you documented, and other applications may read it without asking. The oa envelope says Profile here rather than Article, because what it describes is the account and not something the account wrote.
Merge, never replace. posting_json_metadata is one field shared with profile, so writing only your own key wipes the account’s display name, avatar and everything else living there — including, on some apps, the login redirects. Read the field, add your key, write the whole object back.
// Read what is there, add your key, write the whole thing back.
const meta = JSON.parse(account.posting_json_metadata || '{}')
meta['threespeak.profile'] = { interests }
broadcast('account_update2', {
account,
posting_json_metadata: JSON.stringify(meta), // everything, not just yours
})It is written with account_update2, which is the operation that takes a posting key. The older account_update needs the active key, and nobody should be asked for that to save a list of interests.
Say so when you register, so a reader knows which document to open: the form asks where does it live, and the answer travels with the registration. The register counts these separately from posts — a post is written once and counts forever, while an account holds the key until it takes it out again.
Read one back
Ordinary JSON. No library, no import, nothing installed:
const meta = typeof post.json_metadata === 'string'
? JSON.parse(post.json_metadata)
: post.json_metadata
const mine = meta['opnattr.pulse.micropost']
if (mine) render(mine.url)Read what you recognise and ignore the rest. A post carrying twenty attributes you have never heard of is normal, and none of them is an error.
One thing that 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.
Register your own
When nothing in the register fits. A registration is one signed custom_json, and that is the entire mechanism: no account with us, no approval, no queue.
{
"oa": 1,
"action": "register_attribute",
"attribute": {
"account": "your-account",
"leaf": "recipes",
"version": "1.0.0",
"category": "text",
"description": "Preparation and cooking time, in minutes.",
"schema": {
"type": "object",
"properties": {
"cookTime": { "type": "number" },
"prepTime": { "type": "number" }
},
"required": ["cookTime"]
}
}
}Broadcast it from wherever you already publish. Your own frontend, Keychain, a script, anything that speaks to Hive. It lands in the register exactly the same, because the search is for the custom_json id and not for who sent it or where from. There is a form at the registry if you would rather have one build the payload and show you the operation first, and it produces this and nothing else.
The name is not taken from what you wrote. It begins with the account the chain proved signed the operation, and you supply the rest: sign as @your-account and the name is your-account.recipes, whatever the payload says. Name a namespace that is not yours and it is still rooted in yours, so a registration by @you about frontend-x.video is listed as you.frontend-x.video.
Nothing is refused by that. An application may have something to say about a name it does not hold, and this is where it says it, in its own namespace where a reader can weigh it. What nobody can do is publish a name beginning with somebody else.
That is why nothing anywhere reads a name to decide who owns it. Hive account names may contain dots, so @openattribute.evil is an account anybody can make without holding @openattribute, and a reader stopping at the first dot would credit its registrations to somebody else.
What goes in a schema
Either your attribute is a marker, in which case it declares no schema at all and its presence is the whole statement, or it carries named values and the schema names them with their types.
Say what a unit is, in the name or in the description. cookTime as a number leaves a reader guessing between minutes and seconds, and the schema cannot tell them.
Verify your domain
The directory shows the website from your Hive profile beside your app. That link is a claim and proves nothing: any account can write any URL into its own profile. Verification is the other direction — the domain naming the account back — and two claims that match are a handshake nobody had to referee.
The domain says so in one DNS TXT record:
| Name | _openattribute | |
|---|---|---|
| Type | TXT | |
| Value | hive=your-account |
Your panel adds .your-frontend.com and the quotes itself. If it asks for the whole name instead, use _openattribute.your-frontend.com.
DNS rather than a file on your server, for the reason that decides it: a TXT record is readable from the visitor’s own browser over DNS-over-HTTPS, so the check runs in front of whoever is looking rather than on a server of ours they would have to trust.
If your profile names www.your-frontend.com, either host works: the check looks for the record on the hostname you gave and then on the bare domain beneath it. Both prove control of the same DNS, and the bare domain is where a panel opens.
“Domain verified”, never “verified”. The record proves control of the domain’s DNS at look-up time, no more. It does not say the app is good, the site is safe, or the operator is who they claim. A faker can enter any website, but can never finish the handshake for a domain they do not own — the only place the record can go is DNS you control.
Versions and retiring
Publish a new version by registering the same name again with a higher version. Nothing breaks: the earlier registration stays on chain exactly where its readers left it, and anybody who resolved the name last year still resolves it to what they read then.
Order decides, not the number. Your registrations are read in the order the chain accepted them and the last one stands, so a version that goes backwards is published rather than refused. Nothing compares the two strings. It is what you tell a reader, not a rule anything enforces.
Retiring is the same operation with a different action:
{
"oa": 1,
"action": "deprecate_attribute",
"attribute": { "account": "your-account", "leaf": "recipes" }
}Only the account that holds the namespace may retire a name. Registering beside somebody takes nothing away and is listed as its own thing; withdrawing an entry that already exists is not something a stranger gets to do.
Beside is literal. A registration signed by an account that does not hold the name it uses is kept as a line of its own, with that signature against it, and it never becomes the definition standing under the name. So two lines carrying one name is what you see when two accounts have claimed it, and which one is signed by the account it names is the difference the register prints. Nobody can overwrite your definition by registering over it, and you cannot make their claim vanish by retiring yours.
What nothing does
Worth knowing before you build, because every one of these has surprised somebody already.
- Nothing validates a value against its schema. Not the chain, not us, not any package here. A schema is a description a reading developer can act on, and
requiredis a statement of intent rather than a rule. - Nothing stops anybody writing any name. That is deliberate, and it is what makes adoption possible at all.
- An edit replaces metadata rather than merging it. An edit made from an application that does not write your key drops it, silently. Write it on every edit of your own, and when you edit a post you did not publish, merge what is already on it rather than replacing the object: you cannot be present when somebody edits your post elsewhere, so the only version of this that works is every editor carrying forward what it does not recognise. The same mechanism lets an edit add one, so a post from years ago can be given an attribute by its author.
- A reader may ignore you. Display is opt-in in every direction, and a registration is not a claim on anybody’s interface.
Posts from before this
Almost nothing on Hive carries any of this, and four years of posts came first. So there is a second, separate half: working out what a post is from where it sits and what shape it has. Six rules, fifteen lines, printed in full on the spec along with the account list two of them depend on.
It is not part of the register and it needs nothing registered. It is why a reader built today has anything to read at all.
The packages
Everything above runs with nothing installed, which was checked from an empty directory rather than assumed. The packages in this repository are a convenience, and this is what they save you: rotation between public nodes when one stops answering, a render model that pulls media out of the shapes different applications invented, attribute extraction with the reserved names already handled, and a classification that reports which rule decided and how confident it is.
Not on npm. npm install @openattribute/sdk fails today: nothing has been published under that name. The code is written and tested and has been installed into a bare project from a tarball, but the registry has nothing under it, and until that changes the packages are a thing you can read rather than a thing you can use.
Their reference documentation lives with them, in each package’s own README. This page used to be that reference, which put the manual for one unpublished library in front of the convention it is a convenience for.