Full lesson
ContentLedger
Pay-per-request content licensing for AI agents on Solana: on-chain registry of publishers' rates, escrow vouchers, and receipts anchored in Merkle batches.
What it does
From the project README:
A royalty rail for AI data. An agent asks the gateway for one specific work, learns its price, pays in the same exchange, and gets the content back together with a receipt. The publisher's share lands in the publisher's wallet without a person on either side, and anyone can later check that receipt against a root anchored on Solana — without trusting the gateway that issued it.
- Original
- ContentLedger-crypto/ContentLedger
- SuperStack fork
- Blockchains/ContentLedger @
9b12fdb - Stars
- 0
- Last push (original)
- 2026-10-25
- Languages
- TypeScript, Rust, HTML, Shell, CSS, JavaScript
- Detected stack
- Anchorweb3.js
- Categories
- AI & AgentsPayments
- Licence
- Apache-2.0
- Homepage
- https://contentledger-crypto.github.io/ContentLedger/
Architecture
223 source files. By extension: .ts ×191, .rs ×16, .tsx ×14, .js ×2.
| Top-level directory | Source files |
|---|---|
apps | 103 |
packages | 58 |
tools | 39 |
tests | 23 |
On-chain programs: packages/program/programs/contentledger
Dapp: devnet deploy pending
⏳ Devnet deploy pending — goes live when the devnet wallet is topped up. This fork's own dapp is not deployed yet, so there is no program address or transaction to show. It is queued in dapp-queue.txt; once devnet SOL is available it is deployed, exercised with real devnet transactions, and this section switches to the live dapp at /dapps/contentledger/. Until then, the lesson and step-by-step guide below are complete, and you can run it locally with the commands below.
Key code, explained
Excerpts chosen automatically where the code uses the most Solana concepts, pinned to the fork's commit.
packages/program/programs/contentledger/src/instructions/escrow.rs L169–192 @ 9b12fdb · Apache-2.0 · original/// Забирає **весь** залишок: FR-008d говорить про невитрачений залишок, а не/// про довільну суму, і часткові виводи давали б стан, у якому вікно вже/// витрачене, а гроші ще в сховищі.pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> { let withdraw_after = ctx.accounts.escrow.withdraw_after; require!( withdraw_after != 0, ContentLedgerError::WithdrawNotRequested ); require!( Clock::get()?.unix_timestamp >= withdraw_after, ContentLedgerError::WithdrawTooEarly ); let consumer = ctx.accounts.consumer.key(); let seeds: &[&[u8]] = &[ESCROW_SEED, consumer.as_ref(), &[ctx.accounts.escrow.bump]]; token::transfer( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.escrow.to_account_info(),- Line 174
require!(: An explicit validation that aborts the instruction with an error. - Line 179
Clock::get()?.unix_timestamp >= withdraw_after,: Reads on-chain time from the Clock sysvar. - Line 184
let seeds: &[&[u8]] = &[ESCROW_SEED, consumer.as_ref(), &[ctx.accounts.escrow.bump]];: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 186
token::transfer(: An SPL token transfer through the Token Program. - Line 187
CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
packages/program/programs/contentledger/src/instructions/settle.rs L157–180 @ 9b12fdb · Apache-2.0 · original/// wider entry would push 120 of them past the CPI account-size ceiling.#[event]pub struct BatchSettled { pub escrow: Pubkey, pub seq_end: u64, pub protocol_fee_bps: u16, pub node_share_bps: u16,} #[derive(Accounts)]pub struct SettleBatch<'info> { /// The settler. The agent's consent lives in the verification instruction, /// not in this transaction — it is signed while the gateway serves content, /// hours before anyone settles. Also the payer of every account this /// settlement creates (FR-010a): neither the agent nor a publisher is. #[account(mut)] pub authority: Signer<'info>, #[account( seeds = [CONFIG_SEED], bump = config.bump, has_one = authority @ ContentLedgerError::Unauthorized, )] pub config: Account<'info, Config>,- Line 173
pub authority: Signer<'info>,: Requires this account to have signed the transaction. - Line 176
seeds = [CONFIG_SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 177
bump = config.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 178
has_one = authority @ ContentLedgerError::Unauthorized,: Checks that a stored pubkey field matches the passed account.
packages/program/programs/contentledger/src/instructions/registry.rs L49–72 @ 9b12fdb · Apache-2.0 · original labels >= 2} #[derive(Accounts)]#[instruction(host_hash: [u8; 32])]pub struct RegisterDomain<'info> { /// Платник ренти. До M4 це або оператор (посів реєстру, T022), або сам /// власник домену — атестації, яку вимагає FR-005a, ще не існує. #[account(mut)] pub payer: Signer<'info>, #[account(seeds = [CONFIG_SEED], bump = config.bump)] pub config: Account<'info, Config>, #[account( init, payer = payer, space = 8 + Domain::INIT_SPACE, // Хеш приходить аргументом, а не рахується тут: під фічею `idl-build` // anchor не вміє представити виклик функції у виразі сідів, і аргумент // там просто не в області видимості. Довіри це не додає — обробник // перехешовує `host` і звіряє. seeds = [DOMAIN_SEED, host_hash.as_ref()], bump,- Line 58
pub payer: Signer<'info>,: Requires this account to have signed the transaction. - Line 60
#[account(seeds = [CONFIG_SEED], bump = config.bump)]: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 60
#[account(seeds = [CONFIG_SEED], bump = config.bump)]: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 64
init,: Creates and pays for a new account. Space and payer must be specified.
packages/chain/src/confirm.ts L61–84 @ 9b12fdb · Apache-2.0 · original if (recent) return decided(recent, commitment) if ((await connection.getBlockHeight(commitment)) <= lastValidBlockHeight) return null // The recent-status cache spans only the last ~150 blocks; a transaction that landed // before that is in history alone, and calling it expired would forget money that moved. const [past] = ( await connection.getSignatureStatuses([signature], { searchTransactionHistory: true }) ).value return past ? decided(past, commitment) : { status: 'expired' }} function decided(status: SignatureStatus, commitment: Watched['commitment']): Settled | null { const reached = status.confirmationStatus ?? 'processed' if (RANK[reached] < RANK[commitment]) return null return status.err === null ? { status: 'landed' } : { status: 'failed', err: status.err }} /** In place of web3.js `sendAndConfirmTransaction`, for the same reason as `awaitSignature`. */export async function sendAndAwait( connection: SendConnection, instructions: readonly TransactionInstruction[], [payer, ...cosigners]: readonly [Keypair, ...Keypair[]], { commitment, ...watch }: WatchOptions & { commitment: Watched['commitment'] },): Promise<string> { const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment)- Line 77
/** In place of web3.js `sendAndConfirmTransaction`, for the same reason as `awaitSignatur: Submits the signed transaction to the cluster. - Line 84
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment: Fetches a recent blockhash. Transactions expire if it gets too old.
How to run, build and test
These commands come from the repo's own manifests (package.json scripts, Anchor.toml, Cargo, go.mod, Makefile). Check the README for prerequisites and environment variables.
| Directory | Command | Runs |
|---|---|---|
. | npm run dev | pnpm -r --parallel dev |
. | npm run build | pnpm -r build |
. | npm run test | pnpm -r --parallel test |
. | npm run lint | biome check . |
packages/db | npm run test | vitest run |
packages/chain | npm run test | vitest run |
packages/shared | npm run test | vitest run |
apps/web | npm run dev | vite |
apps/web | npm run build | vite build |
apps/web | npm run test | vitest run |
apps/settler | npm run start | node --env-file-if-exists=../../.env --import tsx src/main.ts |
apps/settler | npm run test | vitest run |
apps/gateway | npm run dev | node --env-file-if-exists=../../.env --import tsx --watch src/server.ts |
apps/gateway | npm run start | node --env-file-if-exists=../../.env --import tsx src/server.ts |
apps/gateway | npm run test | vitest run |
tests | npm run test | vitest run |
What you could build from it
- Fork the SuperStack copy and extend it within the AI & Agents gaps listed on the idea page.
- Learn the pattern first: AI agents on Solana
- Learn the pattern first: Building and sending transactions from a client