@sage-protocol/sdk
v0.1.21
Published
Backend-agnostic SDK for interacting with the Sage Protocol (governance, SubDAOs, tokens).
Readme
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/sdkimport 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 swrUsage:
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 msrevalidateOnFocus- Revalidate when window focusedrevalidateOnReconnect- Revalidate on network reconnectcache- Use service-level caching
Note: Hooks are optional and only available when react and swr peer dependencies are installed.
Examples
- Legacy base mini app hook – React-friendly context mirroring the former in-repo app (now maintained externally).
- Eliza agent recipe – exposes SDK helpers to agent toolchains.
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
resolveGovernanceContextis still exported for compatibility but now logs a deprecation warning. Use the richergovernance+subdaohelpers instead.- Prompt-level forking is deprecated. On-chain prompt forking via
SubDAO.forkPrompt()andSubDAO.forkPromptWithStable()is no longer supported. Useforked_frommetadata 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
lineagemodule 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=1New/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 EOAVotes 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 unknownSubgraph 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); // 0New 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 modeLibrary 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:
- Factory reads the parent library's fork fee from LibraryRegistry
- If fee > 0, SXXX is transferred from forker to parent's treasury
LibraryForkFeePaid(parentDAO, forker, amount)event is emitted- 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:
- Add
forked_from: <original-cid>to your prompt frontmatter - Upload the new prompt to IPFS
- 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 } | nullProposal 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.
