Skip to content

fangorn-network/fangorn

Repository files navigation

Fangorn SDK

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.

Supported Networks

Arbitrum Sepolia (Base Sepolia in progress).


Installation

npm i @fangorn-network/sdk

CLI Quickstart

Install globally and initialize:

npm i -g @fangorn-network/sdk
fangorn init
  • fangorn init prompts 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

Register a Schema

# 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

Publish Data

# 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.v1

A price of 1 equals the smallest USDC unit (0.000001 USDC).

Consume Data

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>

SDK Usage

Initialization

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

Schemas

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.

Publishing

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

Consuming

Phase 1: Purchase

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

Phase 2: Claim

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

Phase 3: Decrypt

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

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.


Contracts

Arbitrum Sepolia

Contract Address
DataSource Registry 0xdb82c131a9d51f6e7695e744bb2bd7774cbb224c
Schema Registry 0x35b67934f9c75bfef6ff3f4d61ff406d81420066
Settlement Registry 0x6aff8212e126ed3232958fd228bc58a202b8f590

Testing

Unit Tests

pnpm test

E2E Tests

Copy the example env and fill in values:

cp env.example .env
pnpm test:e2e

Required 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=0xdb82c131a9d51f6e7695e744bb2bd7774cbb224c

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


Limitations/Future Work

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

License

MIT

About

Intent-Bound Data

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors