Settlement
The settlement engine provides payment settlement for immediate settlements, escrow-based transactions, micropayment channels, and batch processing.
Settlement Types
Immediate Settlement
Funds transferred directly from payer to payee on-chain. Used for one-time payments and high-value transactions.
Escrow-Based Settlement
Funds locked in escrow until proof is provided. Used for AI inference payments where proof of computation is verified before release.
Micropayment Channels
Off-chain payment channels for per-token billing. Used for streaming AI inference where payment occurs per token generated.
Batch Settlement
Atomic processing of multiple settlements in a single transaction. Used for aggregating many small payments to reduce gas costs.
Immediate Settlement
let settlement = SettlementRequest {
settlement_id: "settle_abc123".to_string(),
payer: "tenzro1abc...xyz".to_string(),
payee: "tenzro1def...uvw".to_string(),
amount: 100.0,
asset: "TNZO".to_string(),
settlement_type: SettlementType::Immediate,
metadata: HashMap::from([
("service".to_string(), "inference".to_string()),
]),
proof: None,
};
let receipt = engine.settle(&settlement).await?;
// Receipt includes settlement_tx hash on Tenzro LedgerEscrow-Based Settlement
Escrow is a consensus-mediated on-chain primitive. Funds are locked at a deterministically-derived vault address by the Native VM; only the original payer can later release funds to the payee or refund them to themselves. Writes flow through three signed transaction types submitted via the standard transaction pipeline.
Native-VM selectors
| Selector | Operation | Gas |
|---|---|---|
| 0x01000010 | CreateEscrow | 75,000 |
| 0x01000011 | ReleaseEscrow | 60,000 |
| 0x01000012 | RefundEscrow | 50,000 |
Deterministic identifiers
- escrow_id —
SHA-256("tenzro/escrow/id/v1" || payer || nonce_le), derived by the VM and emitted in the receipt log ofCreateEscrow. - vault address —
Address(SHA-256("tenzro/escrow/vault/v1" || escrow_id)). Has no private key. Release/refund payouts go through a single auditable privileged-VM helper that callsstate.set_balancedirectly.
Authorization invariants (enforced by VM)
CreateEscrow.frommust equal the signing payer (verified at mempool admission). The VM never trusts apayerfield in the payload.ReleaseEscrowis rejected unlesstx.from == escrow.payer, the escrow is inFundedstate, not expired, and the proof verifies against the recordedrelease_conditions.RefundEscrowis rejected unlesstx.from == escrow.payerAND (the escrow is expired ORrelease_conditions ∈ {Timeout, Custom}).
Escrow Flow
- Build & sign — Payer builds a
CreateEscrowtransaction with payee, amount, asset, expiry, and release conditions. The node hybrid-signs with both the classical Ed25519 key and the ML-DSA-65 (FIPS 204) post-quantum key. - Submit — Submit via
tenzro_signAndSendTransaction(atomic server-side hybrid sign + submit) oreth_sendRawTransactionwith the full pre-signed payload (signature, public_key, pq_signature, pq_public_key, timestamp). - Lock — Native VM debits payer, credits derived vault, persists
EscrowAccount{Funded}toCF_SETTLEMENTS, emits log withescrow_id. - Service — Provider delivers AI inference or other service.
- Proof — Provider/payer assembles release proof matching the recorded
release_conditions. - Release / Refund — Payer signs
ReleaseEscrow(vault → payee) orRefundEscrow(vault → payer, after expiry) and submits.
// Submit a CreateEscrow transaction. Ambient auth: TENZRO_BEARER_JWT
// + TENZRO_DPOP_PROOF must be exported; the node resolves the signer
// from the DPoP-bound JWT's MPC wallet.
let tx_type = serde_json::json!({
"type": "CreateEscrow",
"data": {
"payee": payee_address,
"amount": amount.to_string(),
"asset_id": "TNZO",
"expires_at": expires_at_ms,
"release_conditions": { "type": "Timeout" },
},
});
let tx_hash: String = rpc.call("tenzro_signAndSendTransaction", json!({
"from": payer_address,
"to": payee_address,
"value": 0,
"gas_limit": 75_000,
"gas_price": 1_000_000_000,
"nonce": nonce,
"chain_id": chain_id,
"tx_type": tx_type,
})).await?;
// Read the escrow back once the tx finalizes
let escrow: EscrowAccount = rpc
.call("tenzro_getEscrow", json!([escrow_id_hex]))
.await?;Persistence + hydration
EscrowManager writes through to RocksDB CF_SETTLEMENTS under three prefixes:
escrow:<escrow_id>— fullEscrowAccountrecordescrow_payer:<address_hex>— Vec<escrow_id> indexescrow_payee:<address_hex>— Vec<escrow_id> index
On construction, the manager scans escrow: and rebuilds in-memory indices. All mutations use KvStore::write_batch_sync (fsync on commit). Restart-safe.
Micropayment Channels
Micropayment channels enable off-chain per-token billing for streaming AI inference, settling on-chain only when the channel closes.
Channel Lifecycle
1. Open Channel
let channel_id = manager.open_channel(
"tenzro1payer...abc".to_string(),
"tenzro1provider...xyz".to_string(),
1000.0, // Initial deposit (TNZO)
"TNZO".to_string(),
7200, // Timeout (2 hours)
).await?;2. Update State (Off-chain)
// For each token generated
manager.update_channel_state(
&channel_id,
0.001, // Cost per token
).await?;
// State updated locally, no on-chain transaction3. Close Channel (On-chain Settlement)
let settlement_tx = manager.close_channel(&channel_id).await?;
// Final state settled on Tenzro Ledger
// Remaining balance returned to payer
// Provider receives accumulated paymentsExample: Streaming Inference
// Open channel for 1000 TNZO
let channel = manager.open_channel(
payer, provider, 1000.0, "TNZO", 7200
).await?;
// Generate 50,000 tokens at $0.001 per token
for token in generated_tokens {
manager.update_channel_state(&channel.id, 0.001).await?;
}
// Total: 50 TNZO charged (off-chain)
// Close channel
let tx = manager.close_channel(&channel.id).await?;
// On-chain: 50 TNZO to provider, 950 TNZO refund to payerNanopayment Batching
NanopaymentBatcher aggregates many sub-cent payments into periodic on-chain settlements, saving gas by replacing N individual transactions with a single batch settlement.
- Configurable Window: Default 60-second batch window with configurable minimum and maximum batch sizes
- Cryptographic Signatures: Each nanopayment is individually signed and stored off-chain until settlement
- Threshold Triggers: Settlement fires when the batch window expires or the batch size threshold is reached
- Atomic Settlement: All payments in a batch are settled atomically — if any fails, the entire batch rolls back
- Gas Savings: Instead of N on-chain transactions, 1 batch settlement covers all accumulated payments
use tenzro_settlement::nanopayments::NanopaymentBatcher;
let batcher = NanopaymentBatcher::new(
settlement_engine,
BatchConfig {
window_secs: 60,
min_batch_size: 10,
max_batch_size: 1000,
},
);
// Add nanopayments (accumulated off-chain)
batcher.add_payment(payer, payee, 100_000, "inference-token-42")?;
batcher.add_payment(payer, payee, 150_000, "inference-token-43")?;
// Flush manually or wait for window expiry
let settlement = batcher.flush().await?;
println!("Settled {} payments, total: {}", settlement.count, settlement.total);Payment-Settlement Wiring
The PaymentSettlementCallback trait bridges the payment gateway to the settlement engine. When a payment is verified — whether via MPP credential, x402 payload, or AP2 authorization — the callback triggers on-chain settlement and returns the receipt to the payment layer for inclusion in the HTTP response.
// Payment gateway automatically settles via callback
impl PaymentSettlementCallback for TenzroSettlement {
async fn on_payment_verified(
&self,
payer: &Address,
payee: &Address,
amount: u128,
asset: &str,
receipt_id: &str,
) -> Result<SettlementReceipt> {
self.engine.settle_immediate(payer, payee, amount, asset).await
}
}Batch Settlement
Batch processing enables atomic settlement of multiple payments in a single transaction, reducing gas costs and ensuring atomicity.
let settlements = vec![
SettlementRequest {
settlement_id: "settle_1".to_string(),
payer: "tenzro1abc...".to_string(),
payee: "tenzro1provider1...".to_string(),
amount: 10.0,
asset: "TNZO".to_string(),
settlement_type: SettlementType::Immediate,
metadata: HashMap::new(),
proof: None,
},
SettlementRequest {
settlement_id: "settle_2".to_string(),
payer: "tenzro1abc...".to_string(),
payee: "tenzro1provider2...".to_string(),
amount: 15.0,
asset: "TNZO".to_string(),
settlement_type: SettlementType::Immediate,
metadata: HashMap::new(),
proof: None,
},
];
// Process atomically: all succeed or all fail
let receipts = batch_processor.process_batch(settlements).await?;Fee Collection
The FeeCollector manages network fees on all settlements:
- Network Fee: 0.5% on all settlements (configurable)
- Collection: Fees automatically deducted and routed to treasury
- Multi-Asset: Fees collected in the payment asset (TNZO, USDC, etc.)
- Transparency: All fees recorded on-chain in settlement receipts
// Example: 100 TNZO settlement with 0.5% fee
Amount: 100.0 TNZO
Network Fee: 0.5 TNZO (0.5%)
To Payee: 99.5 TNZO
To Treasury: 0.5 TNZO
// Receipt includes fee breakdown
{
"settlement_id": "settle_abc123",
"amount": "100.0",
"fee": "0.5",
"net_amount": "99.5",
"fee_recipient": "treasury"
}Settlement Receipt
Every settlement generates a cryptographically signed receipt:
{
"settlement_id": "settle_abc123",
"payer": "tenzro1abc...xyz",
"payee": "tenzro1def...uvw",
"amount": "100.0",
"asset": "TNZO",
"settlement_type": "Immediate",
"settlement_tx": "0x789abc...",
"fee": "0.5",
"net_amount": "99.5",
"timestamp": "2026-03-20T12:30:45Z",
"block_height": 1207,
"metadata": {
"service": "inference",
"request_id": "req_xyz789"
},
"signature": "0xdef456..."
}Verification
Settlement receipts can be verified via the Web Verification API:
# Verify settlement receipt
curl -X POST http://localhost:8080/verify/settlement \
-H "Content-Type: application/json" \
-d '{
"receipt": {
"settlement_id": "settle_abc123",
"amount": "100.0",
"proof": "0xabc..."
}
}'
# Response
{
"valid": true,
"settlement_confirmed": true
}Integration with Payments
Settlement is automatically triggered by payment protocol verification:
// MPP payment verification triggers settlement
let receipt = mpp_server.verify_credential(&credential).await?;
// Settlement happens automatically:
// 1. Funds transferred from payer to payee
// 2. Network fee collected
// 3. Settlement receipt generated
// 4. Receipt included in MPP receipt
// Payment receipt includes settlement_tx
{
"receipt_id": "rcpt_xyz789",
"amount": "0.05 TNZO",
"settlement_tx": "0x789abc...", // On-chain settlement
"session_token": "sess_mno345"
}Channel disputes
When two channel participants disagree on the off-chain state at close-time (e.g. the payer's view of the cumulative debit differs from the payee's claim), either party can open a dispute. The network records the challenger's evidence, runs a configurable timeout for the counterparty to respond, and finalizes the channel balance on-chain when the dispute resolves.
# Inspect a single dispute by id
tenzro dispute status <dispute_id>
# Every dispute ever opened against a given channel
tenzro dispute list-by-channel --channel-id <channel_id>Underlying RPCs: tenzro_getDispute { dispute_id } and tenzro_listDisputesByChannel { channel_id }. MicropaymentChannelManager persists every dispute under CF_CHANNELS / dispute:<dispute_id>; opens, responses, and resolutions are atomic write-batches that survive node restart.