The Dinara program
HDJy6t6uy5tAsiZrTqJU1HrEc6rnXptn2x4paydtgqd8View on the Solana explorerDeclared 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.
For developers
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
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.
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.
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.
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.
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.
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.
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.
Once the window closes, anyone pays out the proposal that stood, with the same measured payout and receipt as settle.
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.
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
TypeScript on @solana/web3.js. Amounts are bigint base units with six decimals, like USDC: 2_500n is a quarter of a cent.
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]);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 needsNo 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));
}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
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.
| Bytes | Field | Type | What it holds |
|---|---|---|---|
| 0–7 | magic | [u8; 8] | “DINARA01”, so a signature over anything else can never pass as a state |
| 8–39 | channel | Pubkey | The channel this state belongs to |
| 40–47 | seq | u64 | How many charges have been metered so far |
| 48–55 | total | u64 | The sum of every line, in base units |
| 56–87 | log_head | [u8; 32] | The head of the hash chain over every charge: head_k = sha256(head_k−1 ‖ event_k) |
| 88 | n | u8 | How many payee lines follow, at most 8 |
| 89… | lines | n × 9 | Each 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
Opening and settling are the only transactions a channel needs. Everything between them is signed, checked and kept off-chain.
Solana devnet, through the program.
open_channel and settle, or reclaim. A top-up or an early close adds one each.No transaction, no fee, no wallet pop-up.
x-dinara-price, x-dinara-payee and x-dinara-voucherDeployed
Both addresses open on the public 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.
An SPL mint with six decimals, like USDC. Only the faucet can mint it, and it has no value.
Not done
A build page that lists only the wins is not evidence of anything.
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.
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.
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.
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.
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.