Skip to content

For developers

What we built, and how to use it.

A Solana program, a TypeScript SDK, and this app. Every name below is a real export, and every claim names the file behind it.

Devnet only, with a test dollar. Not independently audited. Test counts come from the suites in the repository.

The program

Eight instructions, one channel, one receipt.

An Anchor 0.32 program in Rust. It holds the deposit in a vault, checks the agent’s signature through Solana’s Ed25519 precompile, and writes a receipt from measured balances.

  • open_channel

    Moves the deposit into a vault, a token account owned by the channel, and records the agent key, the ceiling, up to eight payees, the expiry, an optional operator and a label. A ceiling above the deposit, an expiry in the past or more than 366 days out, or a repeated payee is refused.

    • programs/dinara/src/instructions/open_channel.rs
    • 4 tests
  • top_up

    Adds funds, raises the ceiling or extends the expiry, and only ever widens the agent’s room. Lowering a ceiling or shortening an expiry would undercut payees who served against the old terms, so neither is possible.

    • programs/dinara/src/instructions/top_up.rs
    • 3 tests
  • request_close

    The owner brings expiry forward to five minutes from now, which leaves the agent and the operator that long to end the channel with the newest state.

    • programs/dinara/src/instructions/request_close.rs
  • settle

    The operator’s, and only the operator’s. Reads the agent’s newest signed state from the Ed25519 instruction just before it, pays every payee exactly what the agent signed, returns the rest to the owner, closes the vault and writes the receipt. Every amount on the receipt is measured from token balance changes, never copied from an argument.

    • programs/dinara/src/instructions/settle.rs
    • programs/dinara/src/meter.rs
    • 13 tests
  • propose_settlement

    Everyone else ends a channel by proposing an agent-signed state: the agent or operator before expiry, anyone after. Nothing moves yet. The proposal stays open for ten minutes.

    • programs/dinara/src/instructions/propose_settlement.rs
    • 8 tests for the window
  • challenge_settlement

    Anyone holding a later agent-signed state replaces the proposal while the window is open. A state is cumulative, so a later one never owes a payee less: an agent cannot end its channel with an old state that underpays.

    • programs/dinara/src/instructions/challenge_settlement.rs
  • finalize_settlement

    Once the window closes, anyone pays out the proposal that stood, with the same measured payout and receipt as settle.

    • programs/dinara/src/instructions/finalize_settlement.rs
  • reclaim

    If nobody settles within an hour of expiry, the owner takes everything back, and the channel still ends in one receipt that says so. Never while a proposal is under dispute.

    • programs/dinara/src/instructions/reclaim.rs
    • 4 tests with request_close

32 tests run the compiled program in LiteSVM, 10 cover the SDK on its own, and 4 Rust unit tests cover the state parser. Every rule that moves money has a test that it holds and a test that breaking it is refused with the program’s own error.

The SDK

@dinara/sdk, for each of the four parties.

TypeScript on @solana/web3.js. Amounts are bigint base units with six decimals, like USDC: 2_500n is a quarter of a cent.

The owner opens a channel

One transaction. openChannelInstruction derives the channel and its vault and returns both.

import {Transaction, sendAndConfirmTransaction} from "@solana/web3.js";
import {newChannelId, openChannelInstruction, usd} from "@dinara/sdk";

const {instruction, channel} = openChannelInstruction({
  owner: owner.publicKey,
  mint,                    // the test dollar
  ownerToken,              // the owner's token account
  id: newChannelId(),
  deposit: usd(100),
  ceiling: usd(40),        // never more than the deposit
  expiresAt: Math.floor(Date.now() / 1000) + 24 * 3600,
  agent: agentPublicKey,
  operator,                // optional: may settle at once; without one, settlement goes through the dispute window
  label: "Research agent",
  payees: [inference, search, transcribe],
});

const tx = new Transaction().add(instruction);
await sendAndConfirmTransaction(connection, tx, [owner]);

The agent pays per call

MeterSession never touches the chain. payingFetch answers a 402 with a signed voucher and refuses locally what settlement would refuse.

import {fetchChannel, MeterSession, payingFetch} from "@dinara/sdk";

const terms = await fetchChannel(connection, channel);
if (!terms) throw new Error("No channel at that address");
const session = new MeterSession(terms, agentSecretKey);

// A 402 is answered once, with a voucher for exactly the quoted price.
const url = "https://api.example.com/v1/complete";
const res = await payingFetch(session, url, {method: "POST", body}, {
  maxPrice: 50_000n, // refuse any quote above $0.05
});

session.latest; // the newest signed state: all a settlement needs

The payee checks a voucher

No call to Dinara at request time. verifyPayment checks the signature, channel, payee, ceiling, expiry, price, that the voucher is newer than the last, and that the channel’s operator, if any, is one this payee trusts to settle.

import {PublicKey} from "@solana/web3.js";
import {
  fetchChannel, HEADERS, paymentRequired, verifyPayment,
} from "@dinara/sdk";

const wallet = new PublicKey(PAYEE_WALLET);
const price = 2_500n; // $0.0025
let previous: {seq: bigint; owed: bigint} | null = null;
let newest: string | null = null;

export async function POST(request: Request) {
  const voucher = request.headers.get(HEADERS.voucher);
  const address = request.headers.get(HEADERS.channel);
  const channel = address
    ? await fetchChannel(connection, new PublicKey(address))
    : null;
  const check = channel
    ? verifyPayment({voucher, channel, payee: wallet, price, previous, trustedOperators: [DINARA_OPERATOR]})
    : null;
  if (!check?.ok) {
    return paymentRequired({payee: wallet, price, memo: "1k tokens"});
  }

  newest = voucher; // all you need to be paid at settlement
  previous = {seq: check.voucher.state.seq, owed: check.owed};
  return Response.json(await complete(request));
}

The operator settles once

settleInstructions returns the Ed25519 instruction and settle, which must stay adjacent and in that order. Without an operator, proposeSettlementInstructions, challengeSettlementInstructions and finalizeSettlementInstruction take the same state through the dispute window.

import {Transaction, sendAndConfirmTransaction} from "@solana/web3.js";
import {
  decodeVoucher, fetchReceipt, receiptAddress, settleInstructions,
} from "@dinara/sdk";

const signed = decodeVoucher(newestVoucher)!; // the agent's newest
const tx = new Transaction().add(
  ...settleInstructions({
    settler: operator.publicKey,
    channel,
    owner,
    mint,
    ownerToken,              // where the unused deposit returns
    agent,
    signed,
    payeeTokens,             // one token account per state line, in order
  }),
);
await sendAndConfirmTransaction(connection, tx, [operator]);

const receipt = await fetchReceipt(connection, receiptAddress(channel));

The signed state

89 + 9n bytes, signed by the agent.

What the agent signs for every charge, and what settlement reads back. A state is cumulative: each line is the total owed to one payee so far, so the newest state alone settles the channel and an older one can only pay less.

Little-endian. Mirrored in programs/dinara/src/meter.rs and packages/sdk/src/state.ts.
BytesFieldTypeWhat it holds
0–7magic[u8; 8]“DINARA01”, so a signature over anything else can never pass as a state
8–39channelPubkeyThe channel this state belongs to
40–47sequ64How many charges have been metered so far
48–55totalu64The sum of every line, in base units
56–87log_head[u8; 32]The head of the hash chain over every charge: head_k = sha256(head_k−1 ‖ event_k)
88nu8How many payee lines follow, at most 8
89…linesn × 9Each a payee index (u8) and the total owed to that payee so far (u64)

On the wire a voucher is dinara1.<state>.<signature>.<event> in base64url: short enough for an HTTP header, and complete enough to check without asking anyone.

On chain and off chain

Two transactions, any number of payments.

Opening and settling are the only transactions a channel needs. Everything between them is signed, checked and kept off-chain.

On chain

Solana devnet, through the program.

  • The channel account: owner, agent, operator, ceiling, expiry, payees, status and label
  • The vault: a token account owned by the channel, holding the deposit
  • The receipt: what each payee was paid, what was returned, how many charges were metered, and the hash-chain head
  • Two transactions per channel: open_channel and settle, or reclaim. A top-up or an early close adds one each.

Off chain

No transaction, no fee, no wallet pop-up.

  • Every charge: a new running total, signed by the agent’s ed25519 key
  • The 402 exchange: x-dinara-price, x-dinara-payee and x-dinara-voucher
  • Each voucher check, on the payee’s own server
  • The meter log: the vouchers this app’s demo APIs accepted, kept for the screen and for settlement

Deployed

On Solana devnet.

Both addresses open on the public Solana explorer.

The Dinara program

HDJy6t6uy5tAsiZrTqJU1HrEc6rnXptn2x4paydtgqd8View on the Solana explorer

Declared in programs/dinara/src/lib.rs and mirrored as DINARA_PROGRAM_ID in the SDK. Every channel and receipt is an account owned by it.

The test dollar

AnJMBHdvTq1dbBYYfub9BDmS5QQ9sgeShaeRsLxkowddView on the Solana explorer

An SPL mint with six decimals, like USDC. Only the faucet can mint it, and it has no value.

Not done

What this page will not claim.

A build page that lists only the wins is not evidence of anything.

  • Nothing here has been independently audited

    The program carries a test for each rule and for the case each rule refuses. That is not the same as review by somebody with no stake in the answer.

    • 0 external reviews
  • The dispute window is ten minutes

    Short enough to finish a demo, too short for a payee that checks in once a day. A production deployment would set hours. A payee also has to watch for proposals itself unless it reports its vouchers to an operator that will challenge for it.

    • programs/dinara/src/constants.rs
  • Devnet only, with a test dollar

    The setup script makes the test dollar, the faucet, the operator and the demo payees on devnet, and refuses to run against any other cluster. Nothing here touches mainnet or real USDC.

    • scripts/devnet-setup.mjs
  • The demo APIs are stand-ins

    Acme Inference API, Northwind Search and Relay Transcription are endpoints in this app. They quote real prices, check real vouchers and are paid real test dollars at settlement, but their answers are canned: the point is the payment, not the model.

    • apps/web/app/api/demo/[service]/route.ts
    • apps/web/lib/services.ts

Checking this yourself

  • Run pnpm program:build to build the program and sync its IDL, then pnpm program:test for the Rust unit tests and the SDK suite, which runs the compiled program in LiteSVM.
  • pnpm --filter @dinara/sdk smoke runs the whole lifecycle against the deployed program on devnet: open a channel, meter 30 charges offline, settle once through the operator, and read the receipt back.
  • programs/dinara/src/lib.rs lists the rules that move money in one place, and packages/sdk/src/state.ts documents the signed state byte by byte.
  • The program on the explorer: HDJy6t6uy5tAsiZrTqJU1HrEc6rnXptn2x4paydtgqd8