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 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 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)
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)
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 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 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 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. 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 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.