Block 282559
success
137 requests from 3 sessions in one Ethereum transaction: assets.Transferred, revive.ContractEmitted, contracts.Called and utility.BatchCompleted inside one extrinsic.
Live on the Portaldot V3 testnet
If you run an API, Portal Meter lets AI agents pay you a cent per call on Portaldot, and you settle more than a hundred calls in one transaction instead of paying a fee on each one.
No wallet needed to try it. Solidity and ink! in one transaction. 123 tests, MIT licensed.
Replay of a real batch, sped up. The order of the calls is illustrative.
On Portaldot every new storage slot locks a deposit. One paid request left 7.53 tPOT locked against a price of 0.025 test dollars: fine for a call worth a few cents, fatal for one worth a fraction of a cent. So we built prepaid sessions.
Storage deposit locked for good, per request tPOT, Portaldot V3 testnet, measured with our own keys
Per request: 7.70 locked at pay, 0.17 back at settle (blocks 276657 to 276660). Session of 100 requests: 11.55 locked at open (block 282490), 3.68 for its ink! anchor (block 282559), 7.67 back at close (block 297818). Data. Test tokens have no value; the ratio is the point. Designed from these measured numbers, and published for other builders as Portaldot V3 notes; for calls worth more, per-request escrow is there too.
No transaction per request, and no trust in the seller's word: every batch it redeems carries the buyer's own signature over the requests made and the responses received.
The payer locks a budget in MeterSession. It can name a fresh session key, so an agent holds a key that can spend this budget and nothing else. Every pallet-assets token is reachable from Solidity at a precompile address: the hosted seller is priced in pUSD, asset 7777, Portaldot's own test dollar.
MeterSessioncloseHTTP 402 with a price, a voucher signed by the client, HTTP 200 with the data. Each voucher commits to the hash of the previous response, so the vouchers form a chain the seller cannot rewrite.
One Ethereum transaction carries utility.batchAll: the money moves in Solidity and the public record lands in ink!, together or not at all, for many sessions at once. An Ethereum-signed transaction to the runtime pallets address dispatches revive.call and contracts.call together; a single-VM chain would need a bridge or a second transaction.
Open any block on the Portaldot explorer, or replay the public session log against the chain from the live page.
success
137 requests from 3 sessions in one Ethereum transaction: assets.Transferred, revive.ContractEmitted, contracts.Called and utility.BatchCompleted inside one extrinsic.
Sent by the seller's own cron: 67 requests from 2 sessions.
The five-line quickstart below, settled: redeem and ink! anchor in one transaction.
Endpoint 3 registered in Solidity and listed in ink!, one Ethereum transaction.
Paid into escrow, served once, settled with its ink! anchor in the same transaction.
A refunded request, recorded in ink! in the same transaction.
The contracts behind every receipt above, and the checks anyone can run against them.
Redeemed event and the ink! anchor. Run it yourself.{{HEALTH}}
Prepaid sessions (MeterSession, hash-chained vouchers, the meter and the session client), the ink! registry, cross-VM settlement in one Ethereum transaction and its SCALE encoder, the Cloudflare Worker seller, the settler, the verifier and the paid-indexer example. All {{COUNT}} commits of this repository, the first on 1 Nov 2026: commit history.
From HSK Meter (Oct 2026, HashKey Chain, MIT), as patterns rewritten here, no file copied: the x402-style payment headers, a request id bound to its payer, the provider refund and single-use request id, the endpoint record, and the escrow seller's quote, pay and retry flow.
"Contract" means the deployed MeterSession, MeterSettlement and MeterRegistry. Off-chain parts are named as off-chain.
redeem checks the signature, caps the total at the budget and accepts only newer vouchers.close.Refunds: close after expiry returns the unspent budget plus most of the storage deposit (7.67 tPOT measured). A failed request is not charged and a lost response is sent again for free, by seller code. Full model: Sessions: protocol, guarantees and limits and Security.
You need a testnet EVM key with about 12 tPOT and 0.25 pUSD (asset 7777, from the native exchange) in buyer.env. What we got on 5 Nov 2026, as printed; JSON bodies condensed to one line and long hashes shortened.
$ npm i github:swiftbotlabs/portal-meter npm WARN deprecated node-domexception@1.0.0: Use your platform's native DOMException instead npm WARN deprecated @substrate/connect@0.8.11: versions below 1.x are no longer maintained added 79 packages in 13s $ curl -s https://portal-meter.swiftbotlab.workers.dev/v1/asset/7777 { "error": "payment required", "accepts": [ { … "provider": "0xE13A4d566A1f6492C1511EF100DC0417ccE18E59", "endpointId": 3, "tokenSymbol": "pUSD", "price": "10000", … } ] } $ npx portal-meter session-open 3 --keys buyer.env --budget 0.25 --days 1 --out s.json session 0xbf59f966153d082007c9a1b36c6361dc1365adc1522c71badc1c6d0f7f1778ab: budget 0.25, tx 0xc7dcc4a5…06c6, block 329592, state in s.json $ npx portal-meter session-buy https://portal-meter.swiftbotlab.workers.dev/v1/asset/7777 --session s.json · HTTP 200, request 1, spent 10000, head 0xebcfba2e…b065 { "at": { "number": 329593, … }, "assetId": 7777, "name": "Portaldot USD (test)", "symbol": "pUSD", "decimals": 6, … } $ npx portal-meter session-verify https://portal-meter.swiftbotlab.workers.dev --session s.json NOT YET 1 request in the log, no batch on-chain yet: the seller settles on its own schedule, run this again later # the seller's batch landed in block 329697; line 5 again: ACCEPT 1 request in the log, 1 batch on-chain
npx portal-meter session-verify https://portal-meter.swiftbotlab.workers.dev \ --id 0xbf59f966153d082007c9a1b36c6361dc1365adc1522c71badc1c6d0f7f1778ab \ --provider 0xE13A4d566A1f6492C1511EF100DC0417ccE18E59 --endpoint 3
The same package is mirrored on the hosted seller: npm i https://portal-meter.swiftbotlab.workers.dev/source/{{TGZ}}
The metering core works on Web standard Request and Response, so it runs on Node.js, Cloudflare Workers, Bun and Deno, with an adapter for Express. Register an endpoint on both VMs with one command, wrap a handler, done. The hosted seller is itself built on it.
Reuse, tested: a second seller in a folder of its own, built on the published entry points only, live as endpoint 4 at paid-indexer.swiftbotlab.workers.dev.
import { createMeter, metered } from 'portal-meter'; const meter = createMeter({ chainId, sessionContract, endpointId, wallet, token, tokenSymbol, decimals, readSession }); // Express: the handler returns the body, the meter hashes and signs it app.get('/v1/weather/:city', metered(meter, { price: 10_000n }, async req => forecast(req.params.city))); // Cloudflare Workers, Bun, Deno, Hono: Web standard Request and Response export default { fetch: req => meter.charge(req, { price: 10_000n }, () => forecast('oslo')) };
All of it is served by the hosted seller itself, as plain text and files.
Full git history as a bundle, in parts: {{PARTS}}. Join them with cat portal-meter.bundle.part* > portal-meter.bundle, then git clone portal-meter.bundle.