npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@sage-protocol/sdk

v0.1.21

Published

Backend-agnostic SDK for interacting with the Sage Protocol (governance, SubDAOs, tokens).

Readme

Node.js Package

Sage Protocol SDK

Purpose

  • Backend-agnostic helpers for interacting with Sage Protocol contracts (governance, SubDAOs, libraries, prompts, tokens).
  • Shared foundation for the CLI, backend services, and upcoming web UI.

Design Principles

  • Pure data in/out; no console prompts or environment coupling.
  • Minimal ABI fragments maintained alongside the contracts (see src/abi).
  • Composable modules – import only what you need.

Quick Start

npm i --workspace @sage-protocol/sdk
import sdk from '@sage-protocol/sdk';

const provider = sdk.getProvider({ rpcUrl: 'https://base-sepolia.publicnode.com' });

// Discover SubDAOs deployed by the latest factory
const subdaos = await sdk.subdao.discoverSubDAOs({
  provider,
  factoryAddress: '0x396Ac71fa8145a89a38B02a7235798c1cD350966',
  fromBlock: 0,
});

// Fetch governance + profile metadata for a SubDAO
const info = await sdk.subdao.getSubDAOInfo({ provider, subdao: subdaos[0].subdao });
// info.profileCID points at an IPFS JSON profile/playbook document (name, description, avatar, social links, etc.)

CommonJS usage

const sdk = require('@sage-protocol/sdk');

What’s New in 0.0.8

  • 0.0.8 Highlights
    • Governance helpers (additive): resolveVotesToken, buildDelegateSelfPreferred, delegateSelfAndVerify, ensureProposeGates, readinessToPropose, listActiveProposals
    • Library helpers (additive): buildAuthorizeTimelockTx, executionReadiness
    • Subgraph normalization: listProposalsFiltered returns state (string) and stateNum (0–7)
    • Backward compatible: no removed/renamed exports; all additions are optional
    • IPFS worker helpers: discovery signal APIs (recordMcpUsage, recordLaunchEvent, recordDiscoveryEvent) and governance queue actions (submitGovernanceReport, listGovernanceReports, reviewGovernanceReport, batchGovernanceReports)

Module Overview

| Module | Highlights | Typical Use | | ------ | ---------- | ----------- | | getProvider() | Minimal ethers v6 RPC helper | Shared provider across mini app, CLI, agents | | governance | Governor metadata, proposal listings, tx builders | Voting dashboards, automation bots | | timelock | Min delay, queued operation inspection, schedule/cancel builders | Safe workflows, monitoring | | factory | Config reads, template enumeration, SubDAO create/fork builders | Launch tooling, analytics | | library | Manifest listings & scoped ownership checks | Prompt library browsers | | lineage | Library fork ancestry, per-library fork fees | Fork tracking, royalty analytics | | prompt | Prompt metadata, fork counts, usage counters | UI prompt catalogues, agent prompt selection | | ipfs | Upload helpers + discovery/governance worker APIs | CLI sandbox, dashboards, worker automation | | subdao | Discovery, staking helpers, per-user stats | SubDAO directories, onboarding | | token | SXXX balances/allowances, burner discovery, tx builders | Wallet gating, burner dashboards | | treasury | Reserve/POL snapshot, pending withdrawals, liquidity plans | Treasury analytics, Safe operators | | boost | Merkle/Direct boost readers + builders | Incentive distribution tooling | | subgraph | GraphQL helpers for proposals/libraries | Historical analytics | | services | High-level SubgraphService + IPFSService with retry/caching | Web app, CLI integration | | adapters | Normalized governance adapters (OZ) | UI layers needing a unified model | | errors | SageSDKError codes | Consistent downstream error handling |

Service Layer

The SDK provides high-level service classes with built-in retry logic, caching, and error handling for production applications.

SubgraphService - GraphQL queries with automatic retry and caching

import { services, serviceErrors } from '@sage-protocol/sdk';

// Initialize service
const subgraphService = new services.SubgraphService({
  url: 'https://api.studio.thegraph.com/query/your-subgraph',
  timeout: 10000,      // 10s timeout (default)
  retries: 3,          // 3 retry attempts with exponential backoff (default)
  cache: {
    enabled: true,     // Enable caching (default: true)
    ttl: 30000,        // 30s cache TTL (default)
    maxSize: 100,      // Max 100 cache entries (default)
  },
});

// Fetch SubDAOs with caching
const subdaos = await subgraphService.getSubDAOs({ limit: 50, skip: 0 });

// Fetch proposals with filters
const proposals = await subgraphService.getProposals({
  governor: '0xGovernor',
  states: ['ACTIVE', 'PENDING'],
  fromTimestamp: 1640000000,
  limit: 20,
  cache: true,  // Use cache (default: true)
});

// Get single proposal by ID
const proposal = await subgraphService.getProposalById('0x123...', { cache: true });

// Fetch libraries
const libraries = await subgraphService.getLibraries({
  subdao: '0xSubDAO',
  limit: 50,
});

// Fetch prompts by tag
const prompts = await subgraphService.getPromptsByTag({
  tagsHash: '0xabc123...',
  registry: '0xRegistry',
  limit: 50,
});

// Cache management
subgraphService.clearCache();
const stats = subgraphService.getCacheStats();
// → { enabled: true, size: 12, maxSize: 100 }

IPFSService - Parallel gateway fetching with caching

import { services, serviceErrors } from '@sage-protocol/sdk';
import { ethers } from 'ethers';

// Initialize service
const ipfsService = new services.IPFSService({
  workerBaseUrl: 'https://api.sageprotocol.io',
  gateway: 'https://ipfs.io',
  signer: ethers.Wallet.fromPhrase('...'), // Optional: for worker auth
  timeout: 15000,      // 15s timeout (default)
  retries: 2,          // 2 retry attempts (default)
  cache: {
    enabled: true,     // Enable caching (default: true)
    ttl: 300000,       // 5min cache TTL for immutable CIDs (default)
    maxSize: 50,       // Max 50 cache entries (default)
  },
});

// Fetch content by CID (parallel gateway race)
// Tries multiple gateways in parallel, returns first success
const content = await ipfsService.fetchByCID('QmTest123...', {
  cache: true,
  timeout: 5000,       // 5s timeout per gateway
  extraGateways: ['https://cloudflare-ipfs.com', 'https://gateway.pinata.cloud'],
});

// Upload content to IPFS worker
const cid = await ipfsService.upload(
  { title: 'My Prompt', content: '...' },
  { name: 'prompt.json', warm: true }
);
// → 'QmNewContent123...'

// Pin CIDs to worker
await ipfsService.pin(['QmTest1...', 'QmTest2...'], { warm: false });

// Warm gateways (prefetch)
await ipfsService.warm('QmTest123...', {
  gateways: ['https://ipfs.io', 'https://cloudflare-ipfs.com'],
});

// Cache management
ipfsService.clearCache();
const stats = ipfsService.getCacheStats();

Error Handling

import { services, serviceErrors } from '@sage-protocol/sdk';

try {
  const subdaos = await subgraphService.getSubDAOs({ limit: 50 });
} catch (error) {
  if (error instanceof serviceErrors.SubgraphError) {
    console.error(`Subgraph error [${error.code}]:`, error.message);
    console.log('Retryable:', error.retryable);
    // Codes: TIMEOUT, NETWORK, INVALID_RESPONSE, NOT_FOUND, QUERY_FAILED
  }
}

try {
  const content = await ipfsService.fetchByCID('QmTest...');
} catch (error) {
  if (error instanceof serviceErrors.IPFSError) {
    console.error(`IPFS error [${error.code}]:`, error.message);
    // Codes: TIMEOUT, PIN_FAILED, INVALID_CID, NOT_FOUND, GATEWAY_FAILED, UPLOAD_FAILED
  }
}

// Format user-friendly error messages
const friendlyMsg = serviceErrors.formatErrorMessage(error);

Utility Exports

import { serviceUtils } from '@sage-protocol/sdk';

// Simple in-memory cache with TTL
const cache = new serviceUtils.SimpleCache({
  enabled: true,
  ttl: 60000,      // 1 minute
  maxSize: 200,
});
cache.set('key', { data: 'value' });
const val = cache.get('key');

// Exponential backoff retry
const result = await serviceUtils.retryWithBackoff(
  async () => {
    // Your async operation
    return await fetchData();
  },
  {
    attempts: 3,
    baseDelay: 1000,   // Start at 1s
    maxDelay: 10000,   // Cap at 10s
    onRetry: ({ attempt, totalAttempts, delay, error }) => {
      console.log(`Retry ${attempt}/${totalAttempts} after ${delay}ms:`, error.message);
    },
  }
);

Performance Notes

  • Parallel Gateway Fetching: IPFSService fetches from multiple IPFS gateways simultaneously (Promise.race pattern), reducing typical fetch times from 7-28s to 1-2s (10-14x improvement)
  • In-Memory Caching: Both services cache results in memory with configurable TTL to reduce redundant network calls
  • Exponential Backoff: Automatic retry with exponential backoff (1s → 2s → 4s delays) for transient failures
  • Edge Runtime Compatible: Uses fetch() instead of axios, compatible with Cloudflare Pages and Vercel Edge

React Integration

The SDK provides React hooks built on SWR for seamless integration in React applications:

Prerequisites:

# Install peer dependencies
npm install react swr
# or
yarn add react swr

Usage:

import { services, hooks } from '@sage-protocol/sdk';

// Initialize services (once, typically in a context provider)
const subgraphService = new services.SubgraphService({
  url: 'https://api.studio.thegraph.com/query/your-subgraph',
});

const ipfsService = new services.IPFSService({
  workerBaseUrl: 'https://api.sageprotocol.io',
  gateway: 'https://ipfs.io',
  signer: yourSigner, // Optional for uploads
});

// Use hooks in components
function SubDAOList() {
  const { data: subdaos, error, isLoading } = hooks.useSubDAOs(subgraphService, {
    limit: 50,
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {subdaos.map(subdao => (
        <li key={subdao.id}>{subdao.name}</li>
      ))}
    </ul>
  );
}

function ProposalList({ governorAddress }) {
  const { data: proposals } = hooks.useProposals(subgraphService, {
    governor: governorAddress,
    states: ['ACTIVE', 'PENDING'],
    limit: 20,
    refreshInterval: 30000, // Auto-refresh every 30s
  });

  return (
    <ul>
      {proposals?.map(proposal => (
        <li key={proposal.id}>{proposal.description}</li>
      ))}
    </ul>
  );
}

function PromptViewer({ cid }) {
  const { data: content, isLoading } = hooks.useFetchCID(ipfsService, cid, {
    extraGateways: ['https://cloudflare-ipfs.com'],
  });

  if (isLoading) return <div>Loading prompt...</div>;

  return <pre>{JSON.stringify(content, null, 2)}</pre>;
}

function UploadForm() {
  const { upload, isUploading, error, data: cid } = hooks.useUpload(ipfsService);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const content = { title: 'My Prompt', content: '...' };
    try {
      const newCid = await upload(content, { name: 'prompt.json' });
      console.log('Uploaded:', newCid);
    } catch (err) {
      console.error('Upload failed:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={isUploading}>
        {isUploading ? 'Uploading...' : 'Upload'}
      </button>
      {error && <div>Error: {error.message}</div>}
      {cid && <div>Uploaded to: {cid}</div>}
    </form>
  );
}

Available Hooks:

| Hook | Description | Returns | |------|-------------|---------| | useSubDAOs(service, options) | Fetch SubDAOs from subgraph | { data, error, isLoading, mutate } | | useProposals(service, options) | Fetch proposals from subgraph | { data, error, isLoading, mutate } | | useFetchCID(service, cid, options) | Fetch IPFS content by CID | { data, error, isLoading, mutate } | | useUpload(service) | Upload content to IPFS | { upload, isUploading, error, data, reset } |

Hook Options:

All data-fetching hooks support SWR options:

  • refreshInterval - Auto-refresh interval in ms
  • revalidateOnFocus - Revalidate when window focused
  • revalidateOnReconnect - Revalidate on network reconnect
  • cache - Use service-level caching

Note: Hooks are optional and only available when react and swr peer dependencies are installed.

Examples

Important

  • For all write calls to the factory (create/fork/stable-fee flows), pass the SubDAOFactory diamond proxy address as factoryAddress. Legacy monolithic factory routes have been removed.

Deprecations

  • resolveGovernanceContext is still exported for compatibility but now logs a deprecation warning. Use the richer governance + subdao helpers instead.
  • Prompt-level forking is deprecated. On-chain prompt forking via SubDAO.forkPrompt() and SubDAO.forkPromptWithStable() is no longer supported. Use forked_from metadata in prompt frontmatter instead. Library-level forks (entire SubDAOs) remain fully supported via the factory.
  • Prompt fork fees (stable fee configuration for prompt forks) have been removed. Per-library SXXX fork fees are now the standard model for monetizing library forks. See the lineage module documentation above.

Next Phases

Phase 6 focuses on integration polish and packaging. Track progress in the SDK Improvement Specification.

New governance/factory/library helpers (2025‑10)

Proposal ID and preflight

import sdk from '@sage-protocol/sdk';
const idHex = sdk.governance.computeProposalIdHex({ targets, values, calldatas, description });
const pre = await sdk.governance.simulatePropose({ provider, governor, targets, values, calldatas, description, sender });
if (!pre.ok) throw new Error(`preflight failed: ${pre.error?.message}`);

Votes at latest‑1 (ERC20Votes)

// Token path
const votes1 = await sdk.governance.getVotesLatestMinusOne({ provider, token: sxxxToken, account: user });
// Governor path (auto‑resolves token)
const votes2 = await sdk.governance.getVotesLatestMinusOne({ provider, governor, account: user });

Factory‑mapped registry

const mapped = await sdk.factory.getSubDAORegistry({ provider, factory, subdao });

Registry preflight as timelock

const { to, data } = sdk.library.buildUpdateLibraryForSubDAOTx({ registry, subdao, manifestCID, promptCount, libraryId: 'main' });
const sim = await sdk.library.simulateAsTimelock({ provider, registry, to, data, timelock });
if (!sim.ok) throw new Error(`registry preflight failed: ${sim.error?.message}`);

Propose by hash (arrays + bytes32)

// For governors that prefer descriptionHash (or deterministic IDs)
const dh = sdk.governance.hashDescription(description);
const tx = sdk.governance.buildProposeTxByHash({ governor, targets, values, calldatas, descriptionHash: dh });

API Notes and Examples

Salted Proposal Descriptions

import sdk from '@sage-protocol/sdk';
// buildProposeTx() auto‑salts descriptions unless you pass a bytes32 description hash
const tx = sdk.governance.buildProposeTx({
  governor: '0xGov',
  targets: ['0xTarget'],
  values: [0n],
  calldatas: ['0x...'],
  descriptionOrHash: 'My Title', // will append \n\n[SALT:0x...] automatically
});

Private Transaction Submission

import sdk from '@sage-protocol/sdk';
// Submit signed private transaction via builder/relay RPC (eth_sendPrivateTransaction)
const { hash } = await sdk.utils.privateTx.sendTransaction({
  signer, // ethers v6 signer
  tx: { to: '0xGov', data: '0x...', value: 0n },
  privateRpcUrl: process.env.SAGE_PRIVATE_RPC,
});

Subgraph‑First Prompt Reads (with tag filters)

import sdk from '@sage-protocol/sdk';

// Bulk listing by registry (subgraph)
const items = await sdk.subgraph.listRegistryPrompts({
  url: process.env.SAGE_SUBGRAPH_URL,
  registry: '0xRegistry',
  first: 50,
});

// Filter by tagsHash (bytes32), optionally scope to a registry
const tagged = await sdk.subgraph.listPromptsByTag({
  url: process.env.SAGE_SUBGRAPH_URL,
  tagsHash: '0xabc123...'
});

// On‑chain bounded fallback (IDs): latest prompts
const onchainLatest = await sdk.prompt.listPrompts({ provider, registry: '0xRegistry', limit: 50 });

// On‑chain bounded tag page
const onchainPage = await sdk.prompt.listByTagPage({ provider, registry: '0xRegistry', tagHash: '0xabc123...', offset: 0, limit: 25 });

Environment Controls

# Private txs
export SAGE_PRIVATE_RPC=https://builder.your-relay.example/rpc

# Deterministic proposal salt (for reproducible ids)
export SAGE_GOV_SALT=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

# Disable auto‑salt (not recommended)
export SAGE_GOV_SALT_DISABLE=1

New/Updated Helpers (2025-10)

Factory enumeration (CLI parity)

// Full list of SubDAO addresses from factory storage
const addrs = await sdk.factory.listSubDAOs({ provider, factory: FACTORY });

// Indexed range with indices returned
const indexed = await sdk.factory.listSubDAOsIndexed({ provider, factory: FACTORY, from: 0n, to: 99n });
// → [{ index: 0, address: '0x...' }, ...]

Governance overloads and quorum

// Build queue/execute using arrays+descriptionHash when supported; else fall back to id-based
const ol = await sdk.governance.detectGovernorOverloads({ provider, governor });
if (ol.hasQueueArrays) {
  const q = sdk.governance.buildQueueTx({ governor, targets, values, calldatas, descriptionOrHash: descHash });
} else if (ol.hasQueueById) {
  const q = sdk.governance.buildQueueByIdTx({ governor, proposalId });
}

// Quorum at a snapshot
const quorum = await sdk.governance.getQuorumAt({ provider, governor, blockTag: 12_345n });
// Convenience: quorum at latest-1
const qLatest = await sdk.governance.getQuorumLatestMinusOne({ provider, governor });

Self‑delegate convenience (ERC20Votes)

// Build a delegate(self) tx from a Governor (reads sxxxToken address)
const tx = await sdk.governance.buildDelegateSelfTx({ provider, governor, account: user });
// send via userOp (CDP) or EOA

Votes token resolution and gates

// Resolve the IVotes token used for proposals
const votesToken = await sdk.governance.resolveVotesToken({ provider, governor });

// Build preferred self-delegate using resolved token
const del = await sdk.governance.buildDelegateSelfPreferred({ provider, governor, account: user });

// Delegate and verify votes (no throw)
const res = await sdk.governance.delegateSelfAndVerify({ provider, governor, account: user, signer, minVotes: 1n });
// → { txHash, ok, votes, payload }

// Readiness to propose (preflight)
const gates = await sdk.governance.ensureProposeGates({ provider, governor, proposer: user });
// Optionally include execution check (registry update as timelock)
const ready = await sdk.governance.readinessToPropose({
  provider,
  governor,
  proposer: user,
  execution: { registry, timelock, subdao, libraryId: 'main', manifestCID, promptCount: 12 },
});

Subgraph normalization

// listProposalsFiltered now returns both state (string) and stateNum (0–7)
const items = await sdk.subgraph.listProposalsFiltered({ url: SUBGRAPH_URL, governor });
// item.state → 'PENDING' | 'ACTIVE' | ... ; item.stateNum → 0..7 or null when unknown

Subgraph State Normalization

  • State Mapping
    • PENDING → 0
    • ACTIVE → 1
    • CANCELED/CANCELLED → 2
    • DEFEATED → 3
    • SUCCEEDED → 4
    • QUEUED → 5
    • EXPIRED → 6
    • EXECUTED → 7
  • Example
const items = await sdk.subgraph.listProposalsFiltered({ url: SUBGRAPH_URL, governor });
console.log(items[0].state);    // 'PENDING'
console.log(items[0].stateNum); // 0

New Helper Examples

  • Votes token + self‑delegate
const votesToken = await sdk.governance.resolveVotesToken({ provider, governor });
const tx = await sdk.governance.buildDelegateSelfPreferred({ provider, governor, account: user });
const res = await sdk.governance.delegateSelfAndVerify({ provider, governor, account: user, signer, minVotes: 1n });
console.log(res.ok, res.votes, res.txHash);
  • Propose gates and execution readiness (one‑shot)
const gates = await sdk.governance.ensureProposeGates({ provider, governor, proposer: user });
const ready = await sdk.governance.readinessToPropose({
  provider, governor, proposer: user,
  execution: { registry, timelock, subdao, libraryId: 'main', manifestCID, promptCount: 12 }
});
console.log({ threshold: gates.threshold, votesOk: gates.votesOk, execReady: ready.executionReady });
  • List active proposals (subgraph)
const active = await sdk.governance.listActiveProposals({ url: SUBGRAPH_URL, governor });
// active[i].state (string), active[i].stateNum (0–7)
  • Library authorize + readiness
const auth = sdk.library.buildAuthorizeTimelockTx({ registry, timelock, subdao });
const exec = await sdk.library.executionReadiness({ provider, registry, timelock, subdao, libraryId: 'main', manifestCID, promptCount });
console.log(exec.ok, exec.error);

Proposal timeline (subgraph)

const t = await sdk.subgraph.getProposalTimeline({ url: SUBGRAPH_URL, id: idHexOrDecimal });
// { id, createdAt, queuedAt, executedAt, canceledAt, eta, state }

Compatibility

  • 0.0.8 Compatibility
    • All changes are additive; no removed or renamed exports. Existing callers can continue to rely on string state. stateNum is optional.

Troubleshooting

  • JSON + BigInt
    • Use a replacer when printing SDK results:
JSON.stringify(obj, (_, v) => (typeof v === 'bigint' ? v.toString() : v));
  • Governor filter casing
    • The SDK normalizes governor for Bytes filters; if you issue manual GraphQL, use lowercase governor addresses to avoid case sensitivity pitfalls.
  • Votes snapshot timing
    • getVotesLatestMinusOne reads at latest‑1 to avoid same‑block edge cases on ERC20Votes.

Prompt pagination helpers

const totalByTag = await sdk.prompt.getByTagCount({ provider, registry, tagHash });
const page = await sdk.prompt.listByTagPage({ provider, registry, tagHash, offset: 0, limit: 25 });

Team SubDAO helpers

// One-click create + operatorize
const res = await sdk.subdao.createOperatorSubDAO({
  signer,
  factoryAddress: FACTORY,
  operator: '0xSafeOrEOA',
  name: 'My SubDAO',
  description: 'Team controlled (Timelock + Safe/EOA executor)',
  accessModel: 0,
  minStakeAmount: 0n,
  burnAmount: 1500n * 10n**18n,
  sxxx: SXXX,
});

// Convert existing SubDAO to operator mode
await sdk.subdao.makeOperator({ signer, subdao: res.subdao, operator: '0xSafeOrEOA', grantAdmin: false });

// Ensure SXXX approval for factory burn if needed
await sdk.subdao.ensureSxxxBurnAllowance({ signer, sxxx: SXXX, spender: FACTORY, amount: 1500n * 10n**18n });

// (Optional) Build a setProfileCid tx for newer SubDAO implementations that expose setProfileCid(string)
// Use this in a Governor/Timelock proposal or Safe Transaction Builder, not as a direct EOA call.
const profileCid = 'bafy...'; // CID of dao-profile.json on IPFS
const { to, data, value } = sdk.subdao.buildSetProfileCidTx({ subdao: res.subdao, profileCid });
// Example: schedule via timelock or propose via Governor, depending on your governance mode

Library Lineage and Fork Fees

The lineage module provides helpers for tracking library fork relationships and per-library SXXX fork fees.

Fork Ancestry

Libraries can be forked from other libraries. The lineage module tracks this ancestry:

import sdk from '@sage-protocol/sdk';

const provider = sdk.getProvider({ rpcUrl: process.env.RPC_URL });
const registry = '0xLibraryRegistry';
const subdao = '0xSubDAO';

// Check if a library was forked from another
const isFork = await sdk.lineage.isFork({ provider, registry, subdao });
// → true if this library has a parent

// Get the parent library (null if original)
const parent = await sdk.lineage.getParentLibrary({ provider, registry, subdao });
// → '0xParentSubDAO' or null

// Get full ancestry chain from root to current
const chain = await sdk.lineage.getLineageChain({ provider, registry, subdao });
// → { chain: ['0xRoot', '0xChild', '0xGrandchild'], depth: 2, root: '0xRoot' }

Per-Library Fork Fees

Library owners can set an SXXX fee for forking their library. This fee is paid by the forker to the parent library's treasury when creating a forked SubDAO.

// Get the SXXX fork fee for a library (0n = free fork)
const fee = await sdk.lineage.getLibraryForkFee({ provider, registry, subdao });
// → 1000000000000000000n (1 SXXX in wei)

// Get comprehensive library info including fork fee
const info = await sdk.lineage.getLibraryInfo({ provider, registry, subdao });
// → {
//     parentDAO: '0xParent' | null,
//     forkFee: 1000000000000000000n,
//     manifestCID: 'Qm...',
//     version: '1.0.0'
//   }

Setting Fork Fees (via Governance)

Fork fees can only be set by the library's timelock. This is typically done through a governance proposal:

import { ethers } from 'ethers';

// Build the setLibraryForkFee transaction (for use in a proposal)
const registryIface = new ethers.Interface([
  'function setLibraryForkFee(address dao, uint256 fee)'
]);

const feeInWei = ethers.parseUnits('10', 18); // 10 SXXX
const calldata = registryIface.encodeFunctionData('setLibraryForkFee', [subdao, feeInWei]);

// Include in a governance proposal
const proposeTx = sdk.governance.buildProposeTx({
  governor,
  targets: [registry],
  values: [0n],
  calldatas: [calldata],
  descriptionOrHash: 'Set library fork fee to 10 SXXX'
});

Fork Fee Flow

When a SubDAO is forked via the factory:

  1. Factory reads the parent library's fork fee from LibraryRegistry
  2. If fee > 0, SXXX is transferred from forker to parent's treasury
  3. LibraryForkFeePaid(parentDAO, forker, amount) event is emitted
  4. Fork proceeds with standard SubDAO creation

Note: Forkers must approve SXXX to the factory before forking a library with a fee set.

Deprecation: Prompt-Level Forks

On-chain prompt forking is deprecated. Prompt forks are now tracked via metadata only:

  1. Add forked_from: <original-cid> to your prompt frontmatter
  2. Upload the new prompt to IPFS
  3. Register via governance proposal

Library-level forks (entire SubDAOs) continue to work and are tracked via LibraryRegistry.registerForkedDAO().

Governance Adapter (OpenZeppelin)

Normalize proposals/timelines across OZ governors with a compact adapter, inspired by Boardroom’s pattern.

import sdk from '@sage-protocol/sdk';
import { getProvider } from '@sage-protocol/sdk';

const provider = getProvider({ rpcUrl: process.env.RPC_URL });
const tr = sdk.adapters.transports.createTransports({ provider, signer: null, subgraph: process.env.SAGE_SUBGRAPH_URL });

// List proposals (paged by block ranges)
const page = await sdk.adapters.governance.openzeppelin.getProposals({
  provider,
  governor: '0xGovernor',
  fromBlock: 0,
  toBlock: 'latest',
  pageSize: 10_000,
  // Optional: improve signature resolution
  // ChainId is auto-detected from provider; you can override via chainId if needed.
  // The adapter will try Sourcify → Etherscan/BaseScan automatically using NEXT_PUBLIC_ETHERSCAN_API_KEY.
  // You may override with your own abiResolver as needed.
  abiResolver: async ({ address, chainId }) => null,
  selectorResolver: async (selector) => null,        // plug in 4byte or your mapping
});

// On‑chain timeline fallback
const t = await sdk.adapters.governance.openzeppelin.getTimelineOnchain({
  provider,
  governor: '0xGovernor',
  id: page.items[0].id,
});

// Signature list (best effort; uses Sourcify → Etherscan/BaseScan (with NEXT_PUBLIC_ETHERSCAN_API_KEY) → 4byte)
const sigs = await sdk.adapters.governance.openzeppelin.getSignatureList({
  provider,
  targets: page.items[0].targets,
  calldatas: page.items[0].calldatas,
  // optional resolvers (provide only if you have your own fetchers)
  abiResolver: async ({ address, chainId }) => null,
  selectorResolver: async (selector) => null,
});

// Proposal events page (Created/Queued/Executed/Canceled) with pagination cursor
const evPage = await sdk.adapters.governance.openzeppelin.getProposalEventsPage({
  provider,
  governor: '0xGovernor',
  fromBlock: 0,
  toBlock: 'latest',
  pageSize: 20_000,
});
// evPage.items: [{ type:'created'|'queued'|'executed'|'canceled', blockNumber, timestamp, txHash, id }]
// evPage.nextCursor: { fromBlock } | null

Proposal model returned by adapter:

  • id (bigint), proposer (address), createdAt (seconds), startBlock, endBlock, quorum (bigint|null), txHash, targets[], values[], calldatas[], signatures[]

Explorer API keys

Set one or more of the following to improve ABI resolution in adapters:

NEXT_PUBLIC_ETHERSCAN_API_KEY=YourEtherscanOrBaseScanKey
NEXT_PUBLIC_ALCHEMY_API_KEY=YourAlchemyKey   # optional (not used for ABI directly)
NEXT_PUBLIC_TENDERLY_ACCESS_TOKEN=YourTenderlyToken  # optional (not used by default)

Notes:

  • For Base Sepolia (chainId 84532) we prefer BaseScan V2 (chainid=84532) automatically.
  • For Base mainnet (8453) we use BaseScan v1 endpoint by default.
  • For Ethereum mainnet (1) we use Etherscan v1.
  • If Sourcify/Etherscan fail, the adapter falls back to 4byte signatures; unknown selectors appear as their 4‑byte hex.