HCS-21 Adapter Registry
HCS-21 keeps Floras in sync by pairing a manifest (HCS-1) with on-chain declarations. This tutorial covers every step:
- Draft the manifest and capability metadata.
- Inscribe it via HCS-1.
- Wire the layered topics (registry-of-registries → version pointer → HCS-21 registry).
- Publish the declaration referencing your Flora/adapter config.
- Query/filter adapters based on capability tags.
HCS-21 spec primerDirect link to HCS-21 spec primer
- Manifest: stored via HCS-1/IPFS/Arweave/HTTPS (with integrity). Includes runtime requirements, capability metadata, and consensus schema (
consensus.entity_schema). - Declaration: small (<1 KB) JSON message on the HCS-21 topic referencing
manifest,config(Flora/appnet),state_model, and package integrity. - Layered registry: registry-of-registries entries now point to a version pointer topic (HCS-2, non-indexed) which, in turn, points to the active HCS-21 topic. Updating the registry only requires a new pointer message—no changes to the discovery list.
- Filtering: Capability metadata (
discovery_tags,communication_channels,extras) lets registries surface filters without downloading each manifest.
Tip: Declarations must fit within 1 KB, so keep description fields short and rely on the manifest for longer prose.
0. Understand the layered registryDirect link to 0. Understand the layered registry
Adapter discovery now uses three deterministic layers:
- Registry-of-registries topic (HCS-2 indexed) — the global list of adapter categories.
- Version pointer topic (HCS-2 non-indexed) — only the latest message matters; it points to the current HCS-21 registry topic.
- Adapter registry topic (HCS-21) — stores the declarations.
When rotating a registry, publish a new message to the version pointer topic with the new HCS-21 topic ID; consumers always read the pointer first, so the discovery list stays unchanged.
1. Draft the manifest (adapter.yml)Direct link to 1-draft-the-manifest-adapteryml
Build a self-describing manifest that matches the schema in src/hcs-21/types.ts. Example:
meta:
spec_version: '1.0'
adapter_version: '1.3.2'
generated: 2025-02-12T18:11:00Z
adapter:
id: npm/@hol-org/example-adapter@1.3.2
name: Example Adapter
maintainers:
- name: Hashgraph Online
contact: ops@hashgraph.online
license: Apache-2.0
package:
registry: npm
artifacts:
- url: npm://@hol-org/example-adapter@1.3.2
digest: sha384-demo-digest
runtime:
platforms: ['node>=20.10.0']
primary: node
entry: dist/index.js
capabilities:
discovery: true
discovery_tags: [agents, marketplaces]
communication: true
communication_channels: [text, x402]
protocols: [x402, uaid]
extras:
locales: [en, es]
rate_limit_hpm: 120
consensus:
entity_schema: hcs-21.entity-consensus@1
required_fields: [entity_id, registry, state_hash, epoch]
hashing: sha384
These discovery_tags, communication_channels, and extras fields make it easy for front-ends to filter adapters.
2. Inscribe the manifest (HCS-1)Direct link to 2. Inscribe the manifest (HCS-1)
import fs from 'node:fs';
import yaml from 'js-yaml';
import { HCS21Client } from '@hol-org/standards-sdk';
const hcs21 = new HCS21Client({
network: process.env.HEDERA_NETWORK || 'testnet',
operatorId: process.env.HEDERA_OPERATOR_ID!,
operatorKey: process.env.HEDERA_OPERATOR_KEY!,
});
async function inscribeManifest() {
const manifestYaml = fs.readFileSync('adapter.yml', 'utf8');
const manifest = yaml.load(manifestYaml) as object;
const pointer = await hcs21.inscribeMetadata({
document: manifest,
fileName: 'adapter.yml',
});
console.log('Manifest HRL:', pointer.pointer, 'sequence:', pointer.manifestSequence);
return pointer;
}
The pointer is usually hcs://1/<topicId>. Keep both the pointer and manifestSequence if you want to pin that exact message.
3. Wire the registry topicsDirect link to 3. Wire the registry topics
Create (or reuse) the layered topics before publishing adapters:
- Adapter registry topic (
type=0) — stores HCS-21 declarations. - Adapter category topic (
type=2) — registered inside the discovery topic so consumers can find this registry. - Version pointer topic (HCS-2 non-indexed) — exposes the latest declaration topic for each adapter slug.
const registryTopicId =
process.env.HCS21_REGISTRY_TOPIC_ID ||
(await hcs21.createRegistryTopic({
ttl: 86400,
indexed: 0,
type: HCS21TopicType.ADAPTER_REGISTRY,
transactionMemo: 'adapter-registry:price',
}));
const categoryTopicId =
process.env.HCS21_CATEGORY_TOPIC_ID ||
(await hcs21.createAdapterCategoryTopic({
ttl: 86400,
metaTopicId: process.env.HCS21_REGISTRY_METADATA_POINTER,
transactionMemo: 'adapter-registry:price:category',
}));
await hcs21.registerCategoryTopic({
discoveryTopicId: process.env.HCS21_DISCOVERY_TOPIC_ID!,
categoryTopicId,
metadata: process.env.HCS21_REGISTRY_METADATA_POINTER,
memo: 'adapter-registry:price',
});
const versionTopicId =
process.env.HCS21_VERSION_POINTER_TOPIC_ID ||
(await hcs21.createAdapterVersionPointerTopic({
ttl: 86400,
memoOverride: 'hcs-2:1:86400',
transactionMemo: 'adapter-registry:price:pointer',
}));
await hcs21.publishVersionPointer({
versionTopicId,
declarationTopicId: registryTopicId,
memo: 'adapter:npm/@hol-org/example-adapter',
});
await hcs21.publishCategoryEntry({
categoryTopicId,
adapterId: 'npm/@hol-org/example-adapter',
versionTopicId,
metadata: process.env.HCS21_REGISTRY_METADATA_POINTER,
});
- Rotating to a new registry topic only requires another
publishVersionPointercall with the newdeclarationTopicId. - Discovery entries remain stable; consumers always resolve the slug (
adapter:npm/@hol-org/example-adapter) to discover the pointer before streaming declarations.
4. Publish the HCS-21 declarationDirect link to 4. Publish the HCS-21 declaration
async function publishDeclaration() {
const manifestPointer = await inscribeManifest();
const pointer = await hcs21.resolveVersionPointer(
process.env.HCS21_VERSION_POINTER_TOPIC_ID!,
);
const registryTopicId =
process.env.HCS21_REGISTRY_TOPIC_ID || pointer.declarationTopicId;
await hcs21.publishDeclaration({
topicId: registryTopicId,
declaration: {
op: 'register',
adapterId: 'npm/@hol-org/example-adapter@1.3.2',
entity: 'agent',
adapterPackage: {
registry: 'npm',
name: '@hol-org/example-adapter',
version: '1.3.2',
integrity: 'sha384-demo-digest',
},
manifest: manifestPointer.pointer,
manifestSequence: manifestPointer.manifestSequence,
config: {
account: process.env.FLORA_ACCOUNT_ID!,
threshold: '2/3',
ctopic: process.env.FLORA_TOPIC_COMMUNICATION!,
ttopic: process.env.FLORA_TOPIC_TRANSACTION!,
stopic: process.env.FLORA_TOPIC_STATE!,
},
stateModel: 'hcs-21.entity-consensus@1',
},
});
console.log('Adapter declaration published');
}
publishDeclaration().catch(console.error);
If you’re updating an existing adapter, use op: 'update'. To deprecate it, set op: 'delete'.
5. Query & filter declarationsDirect link to 5. Query & filter declarations
Front-ends can use fetchDeclarations to pull all adapters for a registry and filter based on capability metadata:
async function listAdapters() {
const pointer = await hcs21.resolveVersionPointer(
process.env.HCS21_VERSION_POINTER_TOPIC_ID!,
);
const registryTopicId =
process.env.HCS21_REGISTRY_TOPIC_ID || pointer.declarationTopicId;
const messages = await hcs21.fetchDeclarations(
registryTopicId,
{ limit: 100, order: 'desc' },
);
return messages
.map(({ declaration }) => declaration)
.filter(declaration => {
const isNpm = declaration.package.registry === 'npm';
const matchesFlora =
declaration.config.account === process.env.FLORA_ACCOUNT_ID;
return isNpm && matchesFlora;
})
.map(declaration => ({
adapterId: declaration.adapter_id,
discoveryTags: declaration.capabilities?.discovery_tags ?? [],
protocols: declaration.capabilities?.protocols ?? [],
}));
}
Build UI filters directly from capabilities.discovery_tags, capabilities.communication_channels, capabilities.extras, etc.
6. ChecklistDirect link to 6. Checklist
- Manifest matches the schema and includes useful capability metadata.
- Inscribed via HCS-1; pointer stored alongside adapter code.
- Version pointer topic registered in the registry-of-registries list and kept up to date.
- Declaration references the correct Flora topics/threshold via
config. - Front-ends resolve the pointer topic before streaming and filter using capabilities.
-
flora.yaml(and HCS-11 profile) includeflora.config.uriso Petals know where to find adapter requirements.
Your adapter is now discoverable, verifiable, and filterable alongside other Flora-ready components. Next step: keep Petals in sync by running the HCS-17 state proof pipeline.
Notes from productionDirect link to Notes from production
- Manifest size: Keep YAML under 1 MiB. If you need to attach large schemas or docs, store them separately (HCS-1/IPFS) and reference via
extras. - Declaration cap: The 1 KB limit is strict. Encode only what Petals need to resolve the manifest and Flora context; descriptions belong in the manifest.
- Flora alignment: Petals ignore declarations if
config.account/threshold/topicsdon’t match theirflora.yaml. Double-check before publishing updates. - Filtering UX: Populate
capabilities.discovery_tags,communication_channels, andextrasconsistently; registry UIs rely on those fields to offer filters.