Varnir
Scanner Playground Docs

Live on Ethereum Sepolia, Tron Nile & BNB Smart Chain testnets

A payment rail
AI agents can be
trusted with.

Software agents are starting to spend money. Varnir is the ledger underneath them — a chain in its own right, settling across the chains they already use. One agent proposes a payment, other agents verify it, and nothing moves until they have.

Testnet. Everything on this page runs on public test networks, with no real funds. Two-key approval — one key proposes, a separate key confirms — is built. Several independent verifiers on one proposal is the direction, not something you can use today. Each section says which is which.

01 — The problem

An agent that can pay is an agent that can be wrong expensively.

Give a model a key and it can move money. That is the whole security review. Nothing sits between the inference and the irreversible transaction: no second opinion, no policy check the model cannot talk its way past, no record of why the payment was thought correct.

  • i

    One model, one signature, no recourse

    One agent holding one private key is a single point of failure. Prompt injection, a hallucinated invoice, a poisoned tool response — each ends the same way: a valid signature over a payment nobody sanctioned.

  • ii

    Custody is the actual risk

    The usual fix is to hand the key to a platform. Now the platform can move your funds, and its breach is your breach. Swapping one unaccountable signer for another is not a control.

  • iii

    Nothing to audit afterwards

    "The agent decided to" is not an audit trail. Without a record of what was proposed, what checked it and why it passed, nobody can reason about an automated treasury — and nobody accountable will sign off on one.

02 — The approach

Put agents on both sides of the transaction.

An agent proposes a payment. It does not execute one. Independent verifying agents check the proposal against treasury policy, and only an approved proposal settles. The proposer never holds unilateral authority, because authority is the thing being split.

Direction — several verifiers is not built yet Proposing agent "pay 4,200 USDT" Verifier counterparty + limits Verifier policy + budget Verifier anomaly + duplicate Settlement on-ledger, final
Varnir enforces the split — who may propose, who may confirm, and that no key confirms its own proposal. What each verifier checks is your policy to write, not ours to prescribe. Every decision — approval or refusal — is a record you can go back and read. Today one confirmer releases a proposal; requiring several is not built.

That is the shape it is heading for. The first rung is built and running on public testnet today: two keys on one identity, a payment that cannot happen until the second one says so, and a proposal that dies on its own if nobody acts — voided by the approver, or expired after 24 hours.

Built — on testnet today send() polls its own identity confirmTransfer() Agent · spender key stake 0 · cannot send Proposal, not payment nothing has moved Approver · treasury key stake 80 · never its own Settlement on the stored terms
The split is enforced by key authority at consensus — not by a permission check an API could get wrong, and not by the agent behaving itself. The ledger refuses a compromised agent's payment. Nor can the approver rewrite what it approves: amount, destination and network are read back out of the stored proposal before anything executes. A stake-80 key has dual status — it can raise a transfer as well as confirm one — and the one thing it cannot do is confirm the transfer it raised: the proposal records which key created it, and that key's confirmation is refused. So two agents can check each other with nobody privileged above them, and one compromised key is not enough — even though that key can do both jobs. Refuse a proposal and it is voided; ignore it and it expires after 24 hours. Either way, nothing moves. How it works, in code →
Built

Your key never reaches our servers

Not a policy — a property of the code, everywhere in the stack. Keys are generated in your process and stay there. Transactions are signed locally and posted straight to a chain node, not relayed through an API that could have signed for you.

The one thing our API is asked during wallet setup is which wallet id to claim — a public, unauthenticated read. It hands out ids. It cannot assign one to you; that takes a transaction signed with your own key.

Built

Signed requests, not bearer tokens

An API credential is a keypair you generate and attach to your identity as a ledger authority — with a role, a spend policy and an expiry if you want them. Only the public half ever leaves your process. Each request carries a signature over the route, a timestamp and the exact body — so there is no secret in a header to leak through a log or a proxy, and a signature cannot be replayed against a different endpoint.

Built

Real infrastructure, not a diagram

Four ledger nodes, with onboarding, wallet assignment, transfer and swap written as on-ledger contracts. A public scanner over it. Wallets, an exchange, invoicing and deposit webhooks across three L1 testnets, and an SDK in seven languages. You can use it now.

Built

Approval as a first-class record

A proposal is not an allow or a deny that vanishes once acted on. It is a durable, on-ledger record of exactly what was proposed and which authority confirmed it, readable on the public scanner afterwards. That is what lets someone accountable sign off on an automated treasury: they can read what it did, and on whose say-so.

Built

No bridge to hack

Varnir runs its own ledger with an L2 balance layer over it — and no cross-chain bridge. Nothing is locked in a contract on one chain so a wrapped copy can be minted on another. USDT stays on Tron, ether stays on Ethereum; Varnir mirrors what arrives and settles it internally.

Bridges are the largest single exploit class in crypto: value pooled behind one contract and one trust assumption. Not having one does not make Varnir unhackable. It does mean the bridge-hack surface — the one that comes with wrapped assets — is not there.

Built

Its own chain — and a layer over others

Varnir runs its own ledger with its own consensus: four nodes, its own state. By the usual test that makes it a layer-1, not a contract deployed on someone else's chain. It just doesn't try to be the chain your assets live on.

Value stays where it was deposited — USDT on Tron, ether on Ethereum. Varnir mirrors the deposit onto its own ledger, settles transfers and trades between identities internally, and withdraws back to the origin chain on the way out. So it is both: a sovereign ledger used as settlement tissue between the chains that already hold the value, with no token or gas economy of its own. Balances stay denominated in whatever you deposited.

03 — Live today

Testnet, and actually running.

Everything below is deployed on public test networks and reachable from this page. None of it handles real funds, and we would rather say so plainly than let a landing page imply otherwise.

Scanner ↗

The ledger and the three L1 testnets it watches, in public: transactions, identities, wallets and deposits.

scanner.varnir.site

Web wallet ↗

Onboard an identity, get a wallet assigned, send and receive — entirely in the browser. The private key is generated there and never leaves.

scanner.varnir.site/wallet

Exchange ↗

Make, take and cancel resting orders that settle atomically on the ledger — both legs in one transaction or neither, instant and gas-free, across assets and across chains. Fill part of an order and the rest stays on the book. An off-ledger matcher pairs orders; it cannot alter their terms or hold your funds. Sign in the browser or on your paired phone.

scanner.varnir.site/exchange

Invoicing & webhooks

Named, referenced invoices across one or more wallets. Payment is detected from observed deposits; an HMAC-signed callback fires at pending and again at confirmed.

USDT and native

Token registry

Which tokens the network credits is on-ledger state, not a list compiled into the code. Balances are keyed by the token’s L1 contract address, not its ticker — two tokens can share a symbol and never be confused for each other, and adding one is a ledger transaction, not a software release.

read it with listTokens()

Settlement layer

A custom Activeledger fork running as a four-node network, with onboarding, wallet assignment, transfer and swap implemented as on-ledger contracts.

Ethereum Sepolia, Tron Nile & BNB Smart Chain

SDK

One client, seven languages: TypeScript (@varnir/chain-client), Python, Kotlin/Java, C#/.NET, Go, PHP and Rust, at full parity and each tested end to end against a live network. Everything the web wallet does, from your own code, signed on your side of the wire. Go, PHP and Rust sign ML-DSA-65 and secp256k1 but not Falcon-512. All seven, compared →

Seven languages · one contract

Playground ↗

Edit and run the SDK against the live testnet, in the browser — onboard an identity, claim a wallet, raise an invoice, and watch each step's real result come back.

playground.varnir.site

Mobile wallet

A native wallet on the same SDK — onboard, send, receive, trade, raise invoices and manage signing keys from the phone. It also signs for the web scanner: pair by QR, and every action that needs a signature is approved on the phone, where the key stays. Works on-device today; not in the app stores yet.

On-device keys · QR remote signing

04 — Quantum-safe by design

The first quantum-safe order book.

The first we know of, anyway. Nobody can say when a computer running Shor's algorithm at scale arrives; when it does, it can derive a private key from any secp256k1 public key it can see, and most of crypto's public keys are already in view. Varnir is in an unusual position: it runs its own ledger, so it gets to choose its own signature scheme — and it settles on chains whose scheme it cannot choose. What follows is what that split lets us do, and where it stops.

Built — on testnet today

Post-quantum identities, on by default

A Varnir identity is a key on our own ledger, so we decide what kind of key it is. A new identity signs with Falcon-512 by default, with ML-DSA-65 selectable — both NIST post-quantum schemes — and its 12-word recovery phrase derives that same post-quantum key. No classical key hides in the backup. A post-quantum key can sit beside a classical one on the same identity while everything around it catches up.

Everything that key authorises on the L2 — transfers, withdrawal authorisation, and every make, take and cancel on the exchange — is signed post-quantum. That is what the order-book claim rests on. It is the identity key and its recovery, not the whole account: the settlement gateway's custody-signing key is still classical secp256k1, and so are identities made before the release. The recovery phrase is Varnir-internal — there is no cross-wallet standard for post-quantum keys yet.

Coming — in design

QER: Quantum Exposure Reduction on L1

Ethereum, Tron and BSC sign with secp256k1, and no layer above them changes that. What Varnir can change is how much value sits behind any key that does get exposed. QER is our own settlement pattern for withdrawals: a fresh wallet for each one, no address used twice, and the change swept to a brand-new wallet in the same act. A key is exposed once, and by then it guards next to nothing.

On Ethereum the signed withdrawal will go out through a private relay rather than the public mempool, closing the window in which a transaction is visible but not yet included — the same route that protects against front-running. That piece is Ethereum-only; on Tron and BSC, QER is the fresh-wallet discipline alone.

Three things this is not. It is not your whole account: the identity key and its recovery are post-quantum; the gateway's custody key is not yet, and neither are identities made before the release. It is not L1 becoming quantum-proof: nothing Varnir does changes what Ethereum accepts as a signature, and QER — still a design, not a code path — lowers the value at exposure rather than removing it. And it is not mainnet: this is the public testnet, with no real funds. Each row gets marked built here when it is, and not before. What is covered, and what isn't →

05 — Where this is going

The convenience of an exchange, without the custody.

What makes a centralised venue pleasant is that a trade is a bookkeeping entry between two accounts on one system — instant, no gas, no waiting for a block. What makes it dangerous is separate: it holds the assets, so it can lose them, freeze them or spend them. The two come apart, and this rail is already on the right side of both.

Built

Balances already settle internally

A deposit credits an L2 balance on your own identity, written on-ledger as it confirms. Reading your balance is reading your own stream — not asking us what we think you have.

Built

And nobody here holds the keys

The keys that could move those funds on L1 are never whole at Varnir — split across independent node operators, or generated so the key is never assembled anywhere at all. There is no operator override to ask for, and none to steal.

Built

Gas-free transfer between identities

The settlement half of trading is written and reachable: one transaction debits an identity's L2 balance and credits another's, touching no L1 chain and paying no gas. It is an SDK call — transferOnLedger — and the swap contract built on the same move settles both legs of a trade in one transaction, or neither. That is what the exchange above runs on. The swap primitive, in code →

Not built

Treasury-grade trading, and fees

A treasury identity can't trade under propose-and-verify yet — an order from one fails closed rather than settling unchecked. And there is no fee in v1: that model is undecided, so nothing is charged.

Not built

Fewer chain transactions, on purpose

A rail paid per transaction wants more of them. Transfers between identities here already touch no chain and pay no gas, so this one gains nothing from volume — and the direction is to act on that: reach a chain only when funds must leave, and then as seldom as possible. That rail can't follow — its own revenue forbids it.

Built — on testnet today

Lose the key, not the account

Set up once: name people you trust, by email, and a threshold. If the key is ever lost or stolen, collect codes from enough of them, wait out a 48-hour window the owner can veto, and the ledger moves the account to a new identity — balances, limits and chosen keys included — freezing the old one so nothing can drain it, and sweeping anything that still arrives there to the new one. Optional, because it needs people you trust. And trusting them isn't trusting Varnir: the gateways email the codes, never enough to recover an account alone, and a recovery in flight is public.

Agents come first here on purpose. An agent trading against a bounded, approvable balance is a smaller promise than a person's savings, and it exercises the settlement layer for real while the amounts stay proportionate to a testnet. People's own funds are a higher bar, and the bars should be cleared in that order. The full roadmap, including what is missing →

06 — Connect your AI

Give an assistant a wallet, not your keys.

Varnir ships an MCP server you run yourself. Point Claude, ChatGPT or Gemini at it and the assistant gets thirteen tools — read balances, read the order book, place a trade, raise an invoice. What it cannot do is the part that matters.

ai agent · Varnir agent server niles testnet Simulated
what's in this wallet and what am I allowed to spend with this key
▸ get_identity {}
→ {
"identity": "a4c1f94d2b8e6f03a17c55e9dd2a4b8c0f3e1d9a2b4c6d8e0f1a2b3c4d567d9",
"network": "niles",
"mode": "writes"
}
▸ get_spend_policy {}
→ {
"identity": "a4c1f94d2b8e6f03a17c55e9dd2a4b8c0f3e1d9a2b4c6d8e0f1a2b3c4d567d9",
"keyFound": true,
"role": "agent",
"policy": {
"version": 1,
"rules": { "TRX-NILE:#native": { "perTxMax": "150000000" } }
},
"note": "Per-tx ceiling 150 TRX on niles native."
}
This is an agent key on niles with a 150 TRX per-transaction cap. The contract enforces that cap, not me, so there is no talking my way past it.
▸ get_balances {}
→ {
"TRX-NILE": [
" { "symbol": "TRX", "token": null, "decimals": 6, "raw": "12480770000", "reserved": "0", "available": "12480770000" },
" { "symbol": "USDT", "token": "9b28202c…bb06c", "decimals": 6, "raw": "0", "reserved": "0", "available": "0" }
]
}
You have 12,480.77 TRX and no USDT. None of it is tied up in open orders.
swap 600 TRX to USDT at market, keep slippage under 0.5%
▸ list_tokens {}
→ {
"TRX-NILE": [
" { "symbol": "TRX", "token": null, "decimals": 6, "chargeable": false },
" { "symbol": "USDT", "token": "9b28202c90af764ffa36279857a6d7cd928bb06c", "decimals": 6, "chargeable": true }
]
}
USDT on niles is contract 9b2820…b06c. Checking the book before I trade.
▸ get_order_book {}
→ [
" { "hash": "d9a12c…5b3c", "give": { "chain": "niles", "amount": "5000000000" }, "want": { "chain": "niles", "token": "9b28…bb06c", "amount": "208500000" } },
" { "hash": "3f7a9c…e6d1", "give": { "chain": "niles", "amount": "3000000000" }, "want": { "chain": "niles", "token": "9b28…bb06c", "amount": "125880000" } },
" { "hash": "b84e0a…9c02", "give": { "chain": "niles", "amount": "3500000000" }, "want": { "chain": "niles", "token": "9b28…bb06c", "amount": "147350000" } }
]
Best ask is 5,000 TRX for 208.50 USDT, about 0.0417 USDT per TRX. Placing a market order with slippage capped at 50 bps.
▸ market_order { "give": { "chain": "niles", "amount": "600" }, "wantAsset": { "network": "niles", "token": "0x9b28202c…bb06c" }, "maxSlippageBps": 50 }
→ {
"status": "error",
"error": {
"code": "POLICY_PER_TX_EXCEEDED",
"message": "600 TRX exceeds this key's per-transaction ceiling of 150 TRX. The contract refused the trade; nothing was attempted.",
"retryable": false,
"policy": true
}
}
600 TRX is too much for this key: its limit is 150 TRX per transaction, and the contract refused the trade before anything moved. I can't change that limit from here. You can, under Ledger Keys in your wallet: scanner.varnir.site/wallet/keys
done, bumped the per-transaction limit to 1,000 TRX. try the swap again
▸ get_spend_policy {}
→ {
"identity": "a4c1f94d2b8e6f03a17c55e9dd2a4b8c0f3e1d9a2b4c6d8e0f1a2b3c4d567d9",
"keyFound": true,
"role": "agent",
"policy": {
"version": 2,
"rules": { "TRX-NILE:#native": { "perTxMax": "1000000000" } }
},
"note": "Per-tx ceiling 1000 TRX on niles native."
}
The ledger shows it: 1,000 TRX per transaction now. Retrying the same order.
▸ market_order { "give": { "chain": "niles", "amount": "600" }, "wantAsset": { "network": "niles", "token": "0x9b28202c…bb06c" }, "maxSlippageBps": 50 }
→ {
"status": "partial",
"give": { "raw": "548200000", "decimals": 6, "amount": "548.20" },
"want": { "raw": "22859940", "decimals": 6, "amount": "22.86" },
"unfilledGive": { "raw": "51800000", "decimals": 6, "amount": "51.80" }
}
Partly filled: 548.20 TRX sold for 22.86 USDT at about 0.0417. The rest could not fill inside your 0.5%, so 51.80 TRX stays in your balance, free to spend.
Meanwhile the verifier agent checks my work from its own key. That key can confirm a transfer, but the contracts will not let it start one.
Re-reads the book and balances after the trade.
Matches: 548.20 TRX out, 22.86 USDT in, 51.80 TRX unfilled and still available. Nothing reserved.
put 800 TRX up as a limit sell for USDT at that rate or a touch above and leave it resting until it fills or I pull it
▸ create_order { "give": { "chain": "niles", "amount": "800" }, "want": { "chain": "niles", "token": "0x9b28202c…bb06c", "amount": "33.76" } }
→ {
"status": "ok",
"orderHash": "7a41f90e…c3d8",
"order": { "give": { "amount": "800" }, "want": { "amount": "33.76" } }
}
Listed: 800 TRX for 33.76 USDT, or 0.0422 per TRX, just above what we got. Those 800 TRX show as reserved until someone takes the order or you cancel it.
did that sell fill yet
▸ get_order_book {}
→ [
" { "hash": "3f7a9c…e6d1", "give": { "chain": "niles", "amount": "3000000000" }, "want": { "chain": "niles", "token": "9b28…bb06c", "amount": "125880000" } },
" { "hash": "b84e0a…9c02", "give": { "chain": "niles", "amount": "3500000000" }, "want": { "chain": "niles", "token": "9b28…bb06c", "amount": "147350000" } }
]
Yes. Order 7a41f90e…c3d8 is off the book and you did not cancel it, so a taker filled it at your price: 800 TRX out, 33.76 USDT in.
send 250 TRX to the ops identity for this week's payroll
▸ transfer_on_ledger { "to": "eb10c71f…399", "chain": "niles", "amount": "250" }
→ {
"status": "awaiting_cosign",
"umid": "c8417a5b…aa11",
"message": "The transfer was recorded but NO MONEY HAS MOVED. 250 TRX crosses the identity's co-sign threshold of 100 TRX, so it is held until a second key confirms it. Report this as 'not sent, approval pending'."
}
Not sent yet. 250 TRX is under the 1,000 TRX limit but over the 100 TRX co-sign threshold, so the ledger is holding it as proposal c8417a5b…aa11 until a second key confirms. I cannot approve it myself. No tool on this server does that.
Reads proposal c8417a5b…aa11: 250.00 TRX to the ops identity, inside the ceiling.
Confirms it. A verifier key can approve a transfer it did not raise, but it cannot send one.
The verifier has confirmed. Checking the transfer:
▸ get_transfer { "umid": "c8417a5b…aa11" }
→ {
"status": "settled",
"txid": "9f2e6a1c4d8b5f07a3c9e1d2b4e6f180c7a9d3e5b1f2c4a6d8e0b7d9f1a3c5e7",
"confirmed": { "success": true }
}
Settled. 250.00 TRX reached the ops identity. I proposed it, the verifier approved it, and neither key could have done both.
good, that went through. thanks
Run AI One scripted session - balances, a trade, a spending cap, a held transfer.
Built

The key never reaches us, or the model

The server runs on your host, with your key in your environment. Varnir never holds it, and neither does the assistant — it calls a tool, your process signs. The model sees the result, never the secret.

Built

The limit is on the ledger, not in the server

You mint a scoped key with a spend policy attached — a per-transaction ceiling, a period cap, an allowlist, an expiry — and the contracts enforce it. A compromised server, a jailbroken prompt or a patched binary does not widen it, because nothing in the process is what is holding the line.

Built

It refuses to start with the wrong key

At boot it reads its own key back off the ledger and checks what it really is. Your identity's owner key, a key with no policy, a read-only key: it declines to write rather than run in a shape where the policy argument does not hold.

Built

Over the threshold, nothing moves

A payment above the key's co-sign threshold becomes a held proposal instead of a transfer. The assistant gets a transaction id and no money has moved — and because "it didn't error" is the easiest thing to misread, the tools say so in those words.

Built

It cannot approve its own spending

There is deliberately no tool to confirm or void a held proposal, and none to add a key or move a threshold. A treasury key that did not raise a proposal can still release one — at its amount, past the caps that bound the assistant. So that door is not in the building.

Off by default

A REST API, if you want one

The same operations are reachable over plain HTTP for things that do not speak MCP. It ships disabled, because an open port that can spend is a different risk from a local tool your assistant calls. Set it up →

07 — Build on it

Five steps from nothing to a live invoice.

Generate a key, onboard it, claim a wallet, register a scoped signing key, raise an invoice. Every signature happens in your process, in whichever of the seven languages you already write.

wallet-and-invoice-flow.ts
// 1. A key you generate, and keep. It never leaves this process.
const phrase = generateRecoveryPhrase();
const { privateKeyHex } = derivePrivateKeyFromPhrase(phrase);

// 2. Onboard it. Signed here, posted straight to a chain node.
const client = await VarnirClient.onboard({ privateKeyHex });

// 3. Claim a wallet, then prove ownership on-chain.
const wallet = await client.getWallet('niles');

// 4. Attach a scoped key — a real authority on your ledger identity.
const scoped = VarnirClient.generateKey();
await client.addKey({
  publicKeyHex: scoped.publicKeyHex,
  role: 'offledger',
  scopes: ['invoices:create'],
});

// 5. Raise an invoice — signed request, no bearer token.
const invoice = await client.createInvoice({
  wallets: [wallet.id],
  amount: '10',
  currency: 'native',
  name: 'Order 4021',
});
# 1. A key you generate, and keep. It never leaves this process.
key = generate_key()              # falcon-512, the onboarding default

# 2. Onboard it. Signed here, posted straight to a chain node.
client = VarnirClient.onboard(key.private_key)

# 3. Claim a wallet, then prove ownership on-chain.
wallet = client.get_wallet('niles')

# 4. Attach a scoped key — a real authority on your ledger identity.
scoped = generate_key(KeyType.SECP256K1)
client.add_key(AddKeyOptions(
    public_key=scoped.public_key,
    role=KeyRole.OFFLEDGER,
    scopes=['invoices:create'],
))

# 5. Raise an invoice — signed request, no bearer token.
invoice = client.create_invoice(
    wallet_ids=[wallet.id],
    amount='10',
    currency='native',
    name='Order 4021',
)
// 1. A key you generate, and keep. It never leaves this process.
var key = Signing.GenerateKey();   // falcon-512, the onboarding default

// 2. Onboard it. Signed here, posted straight to a chain node.
using var client = VarnirClient.ForKeyPair(key);
await client.OnboardAsync();

// 3. Claim a wallet, then prove ownership on-chain.
var wallet = await client.GetWalletAsync("niles");

// 4. Attach a scoped key — a real authority on your ledger identity.
var scoped = Signing.GenerateKey(Keys.Secp256k1);
await client.AddKeyAsync(new AddKeyOptions {
    PublicKey = scoped.PublicKey,
    Role      = KeyRole.OffLedger,
    Scopes    = new[] { "invoices:create" },
});

// 5. Raise an invoice — signed request, no bearer token.
var invoice = await client.CreateInvoiceAsync(
    walletIds: new[] { wallet.Id },
    amount: "10",
    currency: "native",
    name: "Order 4021");
// 1. A key you generate, and keep. It never leaves this process.
val key = Signing.generateKey()   // falcon-512, the onboarding default

// 2. Onboard it. Signed here, posted straight to a chain node.
val client = VarnirClient.forKeyPair(key)
client.onboard()

// 3. Claim a wallet, then prove ownership on-chain.
val wallet = client.getWallet("niles")

// 4. Attach a scoped key — a real authority on your ledger identity.
val scoped = Signing.generateKey(Keys.SECP256K1)
client.addKey(AddKeyOptions(
    publicKey = scoped.publicKey,
    role      = KeyRole.OFFLEDGER,
    scopes    = listOf("invoices:create"),
))

// 5. Raise an invoice — signed request, no bearer token.
val invoice = client.createInvoice(
    walletIds = listOf(wallet.id),
    amount    = "10",
    currency  = "native",
    name      = "Order 4021",
)
// 1. A key you generate, and keep. No phrase — nothing upstream mints one,
//    so hang onto the private string. Falcon-512 isn't signable by the Go
//    SDK, so the default here is ml-dsa-65, the post-quantum type it is.
key, _ := varnir.GenerateKey("") // "ml-dsa-65" (default) or "secp256k1"

// 2. Onboard it — the identity is a pure function of the key, computed
//    here and posted straight to a chain node.
client, _ := varnir.Onboard(ctx, varnir.Options{
    PrivateKey:       key.PrivateKey,
    Nodes:            []string{gateway},
    ApiBaseUrl:       apiBase,
    TransferContract: transferContract,
    KeysContract:     keysContract,
    OnboardContract:  onboardContract,
})

// 3. Claim a wallet, then prove ownership on-chain.
wallet, _ := client.CreateWallet(ctx, "niles", "standard")

// 4. Attach a scoped key — a real authority on your ledger identity.
scoped, _ := varnir.GenerateKey(varnir.Secp256k1)
_, _ = client.AddKey(ctx, scoped.PublicKey, varnir.Secp256k1)

// 5. Raise an invoice — signed request, no bearer token.
invoice, _ := client.CreateInvoice(ctx, varnir.CreateInvoiceOptions{
    Wallets: []string{wallet.ID},
    Name:    "Order 4021",
    Amount:  "10",
})
// 1. A key you generate, and keep. No phrase — nothing upstream mints one.
//    Falcon-512 isn't signable by PHP's pqcrypto_compat, so the default is
//    ml-dsa-65, the post-quantum type PHP can use.
$key = Keys::generateKey(); // ml-dsa-65 (default) or Keys::SECP256K1

// 2. Onboard it — the identity is a pure function of the key, computed
//    here and posted straight to a chain node.
$client = Client::onboard(
    privateKey:       $key->privateKey,
    nodes:            [$gateway],
    onboardContract:  $onboardContract,
    apiBaseUrl:       $apiBase,
    transferContract: $transferContract,
    keysContract:     $keysContract,
);

// 3. Claim a wallet, then prove ownership on-chain.
$wallet = $client->createWallet('niles');

// 4. Attach a scoped key — a real authority on your ledger identity.
$scoped = Keys::generateKey(Keys::SECP256K1);
$client->addKey($scoped->publicKey, 'secp256k1');

// 5. Raise an invoice — signed request, no bearer token.
$invoice = $client->createInvoice(
    [$wallet->id],
    'Order 4021',
    '10',
);
// 1. A key you generate, and keep. No phrase — nothing upstream mints one.
//    Falcon-512 isn't signable by the Rust SDK, so None defaults to
//    ml-dsa-65, the post-quantum type Rust can use.
let key = generate_key(None)?; // None = ml-dsa-65; Some(SECP256K1) for classical

// 2. Onboard it — the identity is a pure function of the key, computed
//    here and posted straight to a chain node.
let mut options = ClientOptions::new("", key.private_key);
options.gateways = vec![gateway];
options.api_base_url = Some(api_base);
options.transfer_contract = Some(transfer_contract);
options.keys_contract = Some(keys_contract);
options.onboard_contract = Some(onboard_contract);
let client = Client::onboard(options)?;

// 3. Claim a wallet, then prove ownership on-chain.
let wallet = client.create_wallet("niles", "standard")?;

// 4. Attach a scoped key — a real authority on your ledger identity.
let scoped = generate_key(Some(SECP256K1))?;
client.add_key(&scoped.public_key, "secp256k1")?;

// 5. Raise an invoice — signed request, no bearer token.
let invoice = client.create_invoice(&NewInvoice {
    wallet_ids: vec![wallet.id],
    name: "Order 4021".into(),
    amount: "10".into(),
    ..Default::default()
})?;

08 — Pricing

Pay for the chain, not the ledger.

Transfers and swaps between Varnir identities settle on our ledger and cost no gas and no credits. You spend credits only when funds go out to a real blockchain.

Free

Enough to try it.

$0/mo*

Start on the testnet
  • 100 credits a month, hard cap
  • 1 network, 1 standard wallet
  • 5 pending invoices
  • 3 open orders on the book
  • 5 pending approvals at a time
  • Standard notifications from Varnir
  • CSV export of the last 30 days
  • MCP server for 1 AI agent (proposer + verifier)
  • Ledger keys for your AI: spend limits, co-sign, propose-and-verify
  • Community support

Hobbyist

For side projects and early builds.

$49/mo*

Start on the testnet

Everything in Free, plus:

  • 5,000 credits a month, then 1.2¢ per credit
  • Up to 10% of unused credits roll over a month
  • All live networks
  • 25 standard wallets per network
  • 1 advanced (MPC-TSS) wallet per network
  • 25 custom tokens
  • 5 AI agents (proposer + verifier)
  • 30 pending invoices
  • 25 open orders on the book
  • 50 pending approvals at a time
  • 3 deposit callbacks
  • CSV export of the last 12 months
  • Email support

Enterprise

Net across your business. Scoped and built with you.

Custom

Contact ushello@varnir.site

Everything in Trader, plus:

  • Atomic nettingOffsetting flows settle together. Only the net reaches L1.
  • Gross-to-net recordEvery obligation behind a net settlement, kept and traceable.
  • Settlement on your scheduleNet positions go to L1 daily or per batch.
  • Private counterparty networkTrade and settle only with counterparties you approve.
  • Signed proof of reservesA signed feed attesting your identities' balances are held.
Four more, below

Enterprise, in detail

Atomic netting

Offsetting flows settle together in one atomic step, and only the net difference goes to L1. It starts across your own identities and accounts; the direction is across the businesses you trade with, so only the minimum needed settles on-chain. Every L1 transaction avoided is gas not paid, and activity that never leaves the ledger is activity nobody sees on-chain.

Gross-to-net record

Netting settles the difference, but your books need the whole. Enterprise is built to keep every underlying obligation that went into a net settlement on the ledger, traceable back from the amount that reached L1. You can show what each counterparty owed and was paid, and hand exact figures to your accountants and tax advisers.

Settlement on your schedule

Net positions settle to L1 in windows you choose, daily or per batch, rather than once per transaction. Fewer settlements means fewer on-chain transactions and a rhythm that fits how you operate.

Private counterparty network

Trade and settle only with counterparties you approve. That can be a private order book or a request-for-quote flow between you, with the same instant, gas-free settlement on the ledger.

Signed proof of reserves

A signed attestation feed showing that the balances behind your identities are held. It attests to balances at a point in time. It is not an audit and not a guarantee, and we scope it with you.

Organisations and teams

Enterprise is for firms where many people act under one name. Identities sit under one organisation with single sign-on, roles set what each person can do, and one consolidated view shows the whole book.

Accounting feeds and durable webhooks

Exports run on a schedule and land in the accounting tools you already use. Events are delivered with retries, and any window can be replayed if a system on your side missed it.

Dedicated capacity

Capacity is reserved for you rather than shared with everyone else. Onboarding for the networks and tokens you need is queued ahead of general requests.

Dedicated support and custom limits

Credits, advanced wallets and AI agents are set to what your operation needs, not to a default tier. Support runs on terms agreed with you.

A credit is about 1¢, spent only on a withdrawal out to a blockchain. By default a withdrawal goes out within 24 hours (Economy): a Tron USDT withdrawal is 240 credits†, a BSC token 10, and a native coin 10; Ethereum tokens follow live gas, from 120. Need it sooner? Within the hour (Standard) or straight away (Instant) cost more. Payout speeds are coming; until then every withdrawal goes out straight away at the Standard rate. Transfers and swaps on the ledger cost none.

†Credit weights per withdrawal may be adjusted every 6 hours to match the average network cost over the preceding hour; changes are normally minor, and a withdrawal is charged the weight in effect when it is sent.

*Example plans and prices. Varnir is a free testnet during this phase, nothing is charged, and Enterprise features are scoped with each customer, with much of that still being built.

We are early, and we would rather say so.

Varnir is a working testnet with a clear thesis, not a finished product. If AI agents paying on their own behalf is a problem you are thinking about — something to build on, integrate, or argue with — the code is the honest version of the pitch.