Intent-bound encrypted data for the agentic web.
Fangorn lets you publish data encrypted under programmable access conditions using gadgets. A gadget defines an NP-relation that must must be provably satisfied in order to recover the plaintext (decrypt). Data is organized by schemas, enabling agent-based discovery across any number of publishers.
Arbitrum Sepolia (Base Sepolia in progress).
npm i @fangorn-network/sdkInstall globally and initialize:
npm i -g @fangorn-network/sdk
fangorn initfangorn initprompts for a wallet private key, Pinata JWT, Pinata gateway URL, and default chain. Config is written to~/.fangorn/config.json.- You can also configure via environment variables instead of
fangorn init:
DELEGATOR_ETH_PRIVATE_KEY=0x...
# if you use pinata over storacha
PINATA_JWT=...
PINATA_GATEWAY=https://your-gateway.mypinata.cloud
# else email for storacha
STORACHA_EMAIL=...
CHAIN_NAME=arbitrumSepolia# Registers an ERC-8004 agent identity and a schema
fangorn schema register <name>
# Skip agent registration
fangorn schema register <name> --skip-erc
# Fetch a registered schema by name
fangorn schema get schema.name.v1# Encrypt and publish files under a schema, priced at 1 USDC unit
fangorn publish upload file.ext -s schema.name.v1 -p 1
# List your manifest entries for a schema
fangorn publish list -s schema.name.v1
# Inspect a specific entry
fangorn publish entry track1 -s schema.name.v1A price of
1equals the smallest USDC unit (0.000001 USDC).
The consumer flow is three phases: purchase => claim => decrypt.
# Phase 1: pay and join the sempaphore group group
fangorn consume purchase <owner> <tag> \
-s schema.name.v1 \
--burner-key 0x... \
--amount 1 \
--usdc 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
# Save the identity string printed by purchase — required for the next steps.
# Phase 2: prove membership and claim access (generates a Groth16 ZK proof)
fangorn consume claim <owner> <tag> \
-s schema.name.v1 \
--identity '<identity-string>' \
--stealth <stealth-address>
# Phase 3: decrypt a specific field
fangorn consume decrypt <owner> <tag> \
-s schema.name.v1 \
-f audio \
--nullifier <nullifier> \
--stealth-key 0x... \
-o output.mp3
# List a publisher's manifest
fangorn consume list -s schema.name.v1 --owner <address>
# Inspect a publisher's entry
fangorn consume entry track1 -s schema.name.v1 --owner <address>import { Fangorn, FangornConfig } from "@fangorn-network/sdk";
const fangorn = await Fangorn.create({
privateKey: "0x...",
storage: { pinata: { jwt: "...", gateway: "https://your-gateway.mypinata.cloud" } },
encryption: { lit: true },
config: FangornConfig.ArbitrumSepolia,
domain: "localhost",
});Storage options:
| Config | Mode |
|---|---|
{ pinata: { jwt, gateway } } |
Read + write |
| undefined | Read only |
A SchemaDefinition is a JSON object where each field declares its type. Fields marked @type: "encrypted" are automatically encrypted by the SDK at publish time. All other fields are stored in plaintext. The gadget hint on an encrypted field tells the SDK which access condition to apply.
// Define a schema
const definition: SchemaDefinition = {
title: { "@type": "string" },
artist: { "@type": "string" },
audio: { "@type": "encrypted", gadget: "settled" }, // field-level encryption
cover: { "@type": "file" }, // plaintext
};
// Register an ERC-8004 agent identity
const { agentId } = await fangorn.schema.registerAgent({
name: "schema.agent.name.v1",
description: "Music streaming data source agent",
});
// Register the schema on-chain
const { schemaId, schemaCid } = await fangorn.schema.register({
name: "schema.name.v1",
definition,
agentId,
});
// Fetch a schema by name
const schema = await fangorn.schema.get("schema.name.v1");When a record conforming to this schema is published, Fangorn encrypts each @type: "encrypted" field and replaces its value with a ciphertext handle and a gadget descriptor:
// Input record
{
"tag": "track1",
"fields": {
"title": "Cassini Division",
"artist": "Arca",
"audio": { "data": "<bytes>", "fileType": "audio/mp3" }
}
}
// Stored manifest entry (audio field encrypted)
{
"tag": "track1",
"fields": {
"title": "Cassini Division",
"artist": "Arca",
"audio": {
"@type": "encrypted",
"handle": {
"cid": "bafkrei...",
"gateway": "your-gateway.mypinata.cloud"
},
"gadgetDescriptor": {
"type": "settled",
"description": "Settlement-gated: SettlementRegistry.isSettled(resourceId, caller)",
"params": {
"resourceId": "0xce16c0...",
"settlementRegistryAddress": "0x4536881306ee355c2f18ae81658771c4488139a3",
"chainName": "arbitrumSepolia"
}
}
}
}
}The gadgetDescriptor is human- and agent-readable: it describes exactly what a consumer must do to unlock the field. Plaintext fields (title, artist) remain directly readable in the manifest without any purchase flow.
Each upload encrypts files via the gadget returned by gadgetFactory, pins the manifest to IPFS, and commits the new CID on-chain. Subsequent uploads merge with the existing manifest unless overwrite is set.
import { SettledGadget } from "@fangorn-network/sdk/gadgets";
import { SettlementRegistry } from "@fangorn-network/sdk/registries";
const owner = fangorn.getAddress();
// Default gadget = payment settled
await fangorn.publisher.upload(
{
records: [
{ tag: "track1", field: "audio", data: audioBytes, extension: ".mp3", fileType: "audio/mpeg" },
{ tag: "track1", field: "cover", data: imageBytes, extension: ".png", fileType: "image/png" },
],
schemaName: 'schema.name.v1'
gateway: "https://your-gateway.mypinata.cloud",
},
1n, // price in smallest USDC units
);import { Identity } from "@semaphore-protocol/identity";
const identity = new Identity();
// Sign ERC-3009 authorization with the burner wallet
const preparedRegister = await fangorn.consumer.prepareRegister({
walletClient,
paymentRecipient: ownerAddress,
amount: 1n,
usdcAddress: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
usdcDomainName: "USD Coin",
usdcDomainVersion: "2",
});
// Submit payment and join the Semaphore group
const { txHash } = await fangorn.consumer.register({
owner: ownerAddress,
schemaId,
tag: "track1",
identityCommitment: identity.commitment,
relayerPrivateKey: "0x...",
preparedRegister,
});
// Save identity.export(). Required for Phase 2 and 3// Generate Groth16 ZK proof of group membership
const preparedSettle = await fangorn.consumer.prepareSettle({
resourceId: SettlementRegistry.deriveResourceId(ownerAddress, schemaId, "track1"),
identity,
stealthAddress: "0x...",
});
// Submit the proof and trigger the hook call (if configured)
const { txHash, nullifier } = await fangorn.consumer.claim({
owner: ownerAddress,
schemaId,
tag: "track1",
relayerPrivateKey: "0x...",
preparedSettle,
});
// Store nullifier. required for Phase 3.import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrumSepolia } from "viem/chains";
const walletClient = createWalletClient({
account: privateKeyToAccount(stealthPrivateKey),
chain: arbitrumSepolia,
transport: http(rpcUrl),
});
const plaintext = await fangorn.consumer.decrypt({
owner: ownerAddress,
walletClient,
schemaId,
nullifierHash: nullifier,
tag: "track1",
field: "audio",
identity,
});Gadgets define the access control condition baked into encryption. Built-in gadgets:
| Gadget | Condition |
|---|---|
SettledGadget |
Caller must complete a USDC payment + ZK claim flow |
More coming soon ;)
See the gadgets docs for details on implementing your own gadgets.
| Contract | Address |
|---|---|
| DataSource Registry | 0xdb82c131a9d51f6e7695e744bb2bd7774cbb224c |
| Schema Registry | 0x35b67934f9c75bfef6ff3f4d61ff406d81420066 |
| Settlement Registry | 0x6aff8212e126ed3232958fd228bc58a202b8f590 |
pnpm testCopy the example env and fill in values:
cp env.example .env
pnpm test:e2eRequired variables:
| Variable | Description |
|---|---|
DELEGATOR_ETH_PRIVATE_KEY |
Publisher private key (needs testnet ETH) |
DELEGATEE_ETH_PRIVATE_KEY |
Consumer private key |
PINATA_JWT |
Pinata API JWT |
PINATA_GATEWAY |
Pinata gateway URL |
CHAIN_NAME |
arbitrumSepolia |
CAIP2 |
421614 |
CHAIN_RPC_URL |
RPC endpoint |
USDC_CONTRACT_ADDRESS |
USDC contract address |
DATA_SOURCE_REGISTRY_ADDRESS |
DataSourceRegistry address |
SCHEMA_REGISTRY_ADDRESS |
SchemaRegistry address |
SETTLEMENT_REGISTRY_ADDRESS |
SettlementTracker address |
Sample .env for Arbitrum Sepolia:
CHAIN_NAME=arbitrumSepolia
CAIP2=421614
CHAIN_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
USDC_CONTRACT_ADDRESS=0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
SETTLEMENT_REGISTRY_ADDRESS=0x6aff8212e126ed3232958fd228bc58a202b8f590
SCHEMA_REGISTRY_ADDRESS=0x35b67934f9c75bfef6ff3f4d61ff406d81420066
DATA_SOURCE_REGISTRY_ADDRESS=0xdb82c131a9d51f6e7695e744bb2bd7774cbb224cE2E tests deploy any contracts not defined in .env, register a test schema, publish manifests, and verify the full purchase → claim → decrypt cycle end-to-end.
- Schema validation is client-side only, meaning there is no on-chain validation at all, instead operating on a 'trust me' basis. This will be addressed in the future.
- Schema validation only works with Uint8Array inputs currently.
MIT