Sol.College

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 directorySource files
apps103
packages58
tools39
tests23

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.

ContentLedger-crypto/ContentLedger · 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(),
  1. Line 174 require!(: An explicit validation that aborts the instruction with an error.
  2. Line 179 Clock::get()?.unix_timestamp >= withdraw_after,: Reads on-chain time from the Clock sysvar.
  3. 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.
  4. Line 186 token::transfer(: An SPL token transfer through the Token Program.
  5. Line 187 CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
ContentLedger-crypto/ContentLedger · 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>,
  1. Line 173 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 176 seeds = [CONFIG_SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  3. Line 177 bump = config.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  4. Line 178 has_one = authority @ ContentLedgerError::Unauthorized,: Checks that a stored pubkey field matches the passed account.
ContentLedger-crypto/ContentLedger · 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,
  1. Line 58 pub payer: Signer<'info>,: Requires this account to have signed the transaction.
  2. 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.
  3. 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.
  4. Line 64 init,: Creates and pays for a new account. Space and payer must be specified.
ContentLedger-crypto/ContentLedger · 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)
  1. Line 77 /** In place of web3.js `sendAndConfirmTransaction`, for the same reason as `awaitSignatur: Submits the signed transaction to the cluster.
  2. 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.

DirectoryCommandRuns
.npm run devpnpm -r --parallel dev
.npm run buildpnpm -r build
.npm run testpnpm -r --parallel test
.npm run lintbiome check .
packages/dbnpm run testvitest run
packages/chainnpm run testvitest run
packages/sharednpm run testvitest run
apps/webnpm run devvite
apps/webnpm run buildvite build
apps/webnpm run testvitest run
apps/settlernpm run startnode --env-file-if-exists=../../.env --import tsx src/main.ts
apps/settlernpm run testvitest run
apps/gatewaynpm run devnode --env-file-if-exists=../../.env --import tsx --watch src/server.ts
apps/gatewaynpm run startnode --env-file-if-exists=../../.env --import tsx src/server.ts
apps/gatewaynpm run testvitest run
testsnpm run testvitest run

What you could build from it

Used in lessons