meter402
by Risingtell
README.md
# meter402
**Per-second metered & streaming settlement for [x402](https://x402.org).**
x402 is a per-*request* payment standard: one call, one payment. But agents increasingly consume
things *continuously* — a price feed, a GPU, an inference stream, a live dataset. Billing that as
one-shot 402 calls is clumsy and over/under-charges.
meter402 adds the missing primitive: **pay-as-you-consume streaming settlement.** An agent opens a
session, then pays one tick per interval for exactly the wall-clock time it held the stream. Pay the
tick → get the next chunk. Stop paying → the gate shuts. Every tick is one real settlement, and a
built-in verifier can **re-derive every total straight from the chain** — so nobody has to trust the
server's numbers.
```
open session ──▶ quote tick ──▶ settle (x402) ──▶ commit + deliver next chunk
▲ │
└────────── agent decides to keep paying ┘ (stop = gate shuts)
```
- **Framework-free core**, with a one-line **Express** adapter and an **MCP server** so any AI agent
can stream and self-govern its spend.
- **Chain-agnostic.** The meter never assumes a chain; settlement + verification are pluggable. The
same core has run on Casper and can back any EIP-3009 / x402 facilitator (Base, Arc, …).
- **Proof-first.** A public `/impact` snapshot never over-claims, and the on-chain verifier is the
source of truth.
## Install
```bash
npm install meter402
# express is an optional peer dep, only needed for the Express adapter:
npm install express
```
## Quickstart — Express
```ts
import express from "express";
import { MemoryStore, MockSettlementProvider, StreamingMeter } from "meter402";
import { createMeterRouter } from "meter402/express";
const store = new MemoryStore([
{ id: "btc-feed", title: "BTC feed", ratePerSecond: "1000", asset: "USDC", payTo: "0xTreasury" },
]);
const meter = new StreamingMeter(store, { payTo: "0xTreasury", maxTickSeconds: 10, network: "mock" });
const app = express();
app.use("/meter", createMeterRouter({ store, meter, provider: new MockSettlementProvider() }));
app.listen(4021);
```
Runnable version: [`examples/server.ts`](./examples/server.ts) (`npm run example:server`).
## Quickstart — MCP (agent-native)
Expose streaming settlement as tools an autonomous agent can call — `open_session`, `tick`,
`close_session`, `impact`:
```ts
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { MemoryStore, MockSettlementProvider, StreamingMeter } from "meter402";
import { createMeterMcpServer } from "meter402/mcp";
const store = new MemoryStore([/* streams */]);
const meter = new StreamingMeter(store, { payTo: "0xTreasury", maxTickSeconds: 10 });
const server = createMeterMcpServer({ store, meter, provider: new MockSettlementProvider() });
await server.connect(new StdioServerTransport());
```
The agent decides, tick by tick, whether the next chunk is worth paying for — and records *why* it
closed the gate. That autonomous "not worth it → stop" decision is the interesting part.
## Batched settlement (defer gas, keep instant confirmation)
By default every tick is its own on-chain settlement — simple, but on a live chain each one pays
gas, and gas can dwarf a sub-cent tick. `BatchSettlementProvider` wraps any `SettlementProvider` so
ticks still confirm instantly (the agent never waits), but the real on-chain transfer only happens
once a threshold is crossed — same pattern Circle Nanopayments/Gateway proved out for USDC, just not
tied to Circle's ledger or any one chain.
```ts
import { BatchSettlementProvider, MemoryStore, MockSettlementProvider, StreamingMeter } from "meter402";
const store = new MemoryStore([/* streams */]);
const meter = new StreamingMeter(store, { payTo: "0xTreasury", maxTickSeconds: 10 });
const chain = new MockSettlementProvider(); // swap for a real x402/EVM facilitator when live
const batched = new BatchSettlementProvider(chain, store, {
maxTicks: 20, // flush after 20 ticks
maxAmount: "50000", // or once accrued value reaches this many smallest-units
maxWaitMs: 60_000, // or once the oldest unflushed tick is this old
});
// use `batched` wherever you'd pass `provider` to createMeterRouter / createMeterMcpServer
```
Grouping is per `(agent, payTo, asset)` — the only ticks that can legally collapse into one
transfer. Ticks settled before a flush get a `pending:<batchId>` placeholder txHash in the
`/impact` feed; once the batch flushes, every tick sharing that `batchId` is reconciled onto the
one real tx hash (needs `MeterStore.updateEventsByBatchId`, which `MemoryStore` implements). If the
underlying settlement call fails, nothing accrued is lost — the batch stays intact for the next
tick or an explicit `flushAll()` to retry. Call `flushAll()` on session close / shutdown so a
provider is never left holding delivered time that never got paid.
## Going live
Swap `MockSettlementProvider` for a real x402 facilitator that settles an EIP-3009 /
`transfer_with_authorization` transfer and returns the on-chain tx hash. The meter is unchanged —
`quoteTick()` feeds the x402 dynamic price *before* settlement; `commitTick()` records it *after*,
from the facilitator's after-settle hook, with the real hash.
## Proof, not trust
`meter.impact()` publishes totals that are always `≤` what actually settled on-chain. The included
**EVM verifier** reads the settlement token's `Transfer` ledger directly over JSON-RPC, counts only
real settlements (agent → provider transfers, excluding mint + agent-funding), re-derives the totals,
and fails loudly if the feed ever claims more than the chain shows:
```ts
import { createEvmVerifier, verifyAgainst } from "meter402/verify";
const verifier = createEvmVerifier({
network: "base-sepolia",
rpcUrl: "https://sepolia.base.org",
token: "0xUSDC",
agents: ["0xAgent1", "0xAgent2"], // the session payers
fromBlock: 12_345_678, // token deployment block
});
const report = await verifyAgainst(verifier, await fetchImpact());
if (!report.verified) throw new Error("feed over-claims");
```
Or run it as a CI gate: `RPC_URL=… TOKEN=… AGENTS=0xa1,0xa2 PROOF=./impact.json npm run verify:evm`
(exits non-zero if the feed over-claims). The RPC transport is injectable (`rpc`), so the same
adapter works with any provider — and the aggregation logic is unit-testable without a live chain.
A Casper reference verifier lives in the [`sluice`](https://github.com/Risingtell/sluice) project
this core was extracted from.
**Verified against live chains, not just fixtures:**
- **OKX X Layer mainnet** (`eip155:196`) — [`examples/verify-xlayer.ts`](./examples/verify-xlayer.ts)
re-derives the real USD₮0 settlement history of a live [OKX.AI](https://www.okx.ai) buyer agent:
34 settlements / $1.082 across two live services ([Argus](https://github.com/Risingtell/argus)
ASP #5246 and [VigilOK](https://github.com/Risingtell/vigilok) ASP #6032). Run it yourself and
check any row on [OKLink](https://www.oklink.com/x-layer).
- **Arc testnet** (`eip155:5042002`) — reconstructed a known x402 settlement from the token's
Transfer ledger; that live run also caught (and fixed, in v0.1.1) the busy-chain
max-results pitfall the `chunkSize` + agent-topic filtering now handle.
## Roadmap
- [x] Generic EVM verifier adapter (read ERC-20 transfers → re-derive totals)
- [ ] `bin/meter402-mcp` stdio entrypoint for drop-in agent configs
- [ ] Persistent store adapter (Postgres) alongside `MemoryStore`
- [ ] Budget/policy guardrails on the session (hard cap + rate ceiling)
- [x] Batched settlement mode (`BatchSettlementProvider`): instant per-tick confirmation, deferred
and batched on-chain settlement, chain-agnostic instead of locked to Circle's ledger — see
"Batched settlement" above.
- [ ] Per-chain gasless-authorization adapters to pair with batching (EIP-3009 on EVM; no
off-the-shelf equivalent confirmed yet for non-EVM chains like Casper — verify before building
rather than assume one exists).
## License
MIT © Oluwasogo "Israel" Ajala (Rising Technology)
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues