@tab/tab-mcp
by slucifersz
README.md
# AllScale Tab
**Credit and settlement for the AI agent economy.** Put one middleware in front of any
MCP server and the agents calling it can run a tab: consume now, settle on terms.
The buyer sets a spending fence once — per-transaction cap, total cap, expiry. After that,
every paid tool call is a local ledger entry: no payment round-trip, no per-call latency, no
money moving. When the tab hits a threshold or the billing period closes, Tab invoices,
the buyer's kit pays, and the tab clears. Miss the settlement and service stops.
```
agent ──MCP──▶ [ tab middleware ] ──▶ your tools
│
├─ charge locally (no network, no money moves)
├─ over the limit → 402 PAYMENT_REQUIRED
└─ threshold hit → invoice ──▶ buyer kit ──▶ payout ──▶ tab cleared
│
every money move goes through one
SettlementAdapter → AllScale CLI
```
## Quickstart
```bash
npm install
npm test # unit + end-to-end tests (108)
npm run self-test # live checklist: accumulate → invoice → fund → claim → 402
./demo/run-demo.sh
```
With the AllScale CLI installed and logged in, these check it directly:
```bash
npm install -g @allscale/cli # then: allscale device-login
npm run capture:help # capture the CLI surface locally (git-ignored)
npm run verify:chains # re-derive the chain id table, fail on drift
node scripts/cli-probe.mjs # per-method: live-ok / argv-only / blocked
npm run test:cli # the suite with TAB_ADAPTER=cli
```
The demo builds everything, starts the live ledger page on <http://127.0.0.1:4790>, starts the
buyer's settlement daemon, then runs an agent that makes 60 paid calls. You will see the balance
climb, the cycle close itself with an `allscale invoice send`, the buyer answer with an
`allscale payout send` + `allscale claim-link claim`, the balance drop to zero, and — once the
tab is cut off — calls come back as `402 PAYMENT_REQUIRED` before service resumes.
Everything runs on the **stub adapter** by default: no credentials, no network, no money.
## Make your own MCP server paid
Three lines:
```ts
import { createRuntime, attachBilling } from '@tab/middleware';
import { startLedgerServer } from '@tab/core';
const { ledger, config } = createRuntime(); // reads tab.config.json, picks the adapter
attachBilling(server, { ledger, config }); // every tool on `server` is now billed
await startLedgerServer({ ledger }); // optional: GET /ledger for the UI
```
Or let an agent do it — Tab ships its own MCP server (`@tab/tab-mcp`) with `add_billing`,
`set_pricing` and `tab_status`:
```bash
claude mcp add tab -- node packages/tab-mcp/dist/src/bin/server.js
# then: "add billing to ./packages/example-server, one cent per call"
```
### tab.config.json
```json
{
"pricing": { "default": "0.01", "tools": { "fx_rate": "0.01", "fx_convert": "0.02" } },
"settleThreshold": "0.50",
"billingCycle": "threshold",
"creditLimit": "0.60"
}
```
| Field | Meaning |
| --- | --- |
| `pricing.default` | Charged for any tool without an override. `"0.00"` makes a tool free. |
| `pricing.tools` | Per-tool prices. |
| `settleThreshold` | Balance that closes a billing cycle. |
| `billingCycle` | `threshold` invoices automatically; `manual` waits for `POST /settle`. |
| `creditLimit` | How far a buyer may run up before calls are refused with a 402. |
All amounts are fixed-point decimal strings and are computed on as integer cents. No floats
touch money.
### The 402
A call that cannot be served is answered in an x402-shaped envelope, both as text and in the
result's `_meta['tab/payment_required']`:
```json
{
"code": 402,
"error": "PAYMENT_REQUIRED",
"tab": {
"balance": "0.50",
"creditLimit": "0.50",
"settleUrl": "http://127.0.0.1:4788/settle",
"status": "settling",
"reason": "CREDIT_EXCEEDED",
"referenceId": "tab-claude-2026w33"
}
}
```
This is the response *shape* only — protocol-level x402 signature verification is not
implemented (see Roadmap).
## Packages
| Package | What it is |
| --- | --- |
| `@tab/core` | Ledger engine, `SettlementAdapter` contract, stub + CLI adapters, read-only HTTP API |
| `@tab/middleware` | `attachBilling(server, …)` — makes any MCP server's tools paid |
| `@tab/example-server` | A paid MCP server (`fx_rate`, `fx_convert`) plus the demo consumption driver |
| `@tab/tab-mcp` | Tab's own MCP server: `add_billing`, `set_pricing`, `tab_status` |
| `@tab/buyer-kit` | Buyer daemon: validates invoices against the fence, pays them, echoes the real CLI command |
| `@tab/ledger-ui` | Single-page live ledger (vanilla JS, polls `GET /ledger`) |
### Ledger API
Owned by the MCP server process — the single writer.
| Route | Purpose |
| --- | --- |
| `GET /ledger` | Full snapshot: tabs, entries, invoices, CLI echo log |
| `GET /invoices?status=sent` | Bills awaiting payment |
| `POST /payments` | Report a funded **and claimed** payout (idempotent on `referenceId`) |
| `POST /settle` | Close the current cycle manually |
| `POST /cutoff` · `POST /reopen` | Suspend or restore service |
## Settlement
Every money movement — and only these — goes through `SettlementAdapter`:
| Adapter call | Real command |
| --- | --- |
| `enableFence` | *(no CLI command — granted in the dashboard)*, confirmed with `allscale payout status --json` |
| `fenceStatus` | `allscale payout status --json` |
| `sendPayout` | `allscale payout send --amount … --chain … --stable-coin … --reference-id … [--receiver-email …] --json` |
| `claimPayout` | `allscale claim-link claim --claim-token … --to-wallet --json` (a link can also be claimed by `--claim-url …`) |
| `sendInvoice` | `allscale invoice send --to-email … --amount … --line "desc\|qty\|total" --wallet-id … --payment-type 1 --memo … --json` |
| `listTransactions` | `allscale transaction list --json` |
**Settlement is two steps, not one.** `payout send` does not pay the seller: it funds a
**Claim Link** which the receiver must then claim, and the link expires within minutes
(~21 observed) — an unclaimed link is refunded to the buyer and the bill stays unpaid. So the
buyer kit funds and claims as one serialised step, and a bill is only marked paid once the
claim is proven. Authorization is granted per **chain × token pair** in the dashboard
(Store Settings → Payout Authorization); an undelegated pair is refused with
`FENCE_NOT_AUTHORIZED`.
If a claim is lost mid-flight — the process dies after funding, or the claim races the
on-chain deposit and exits `pending_deposit` — the link is still recoverable by id for as long
as it has not expired, because `claim-link get` will hand back the `claim_url`:
```bash
allscale claim-link get <claim-link-id> --select 'id status amount claim_url' --json
allscale claim-link status --claim-url <url> --json # wait for is_claimable: true
allscale claim-link claim --claim-url <url> --to-wallet --json
```
Defaults are `sepolia` / `USDT`, set with `TAB_CHAIN` and `TAB_STABLE_COIN`.
`TAB_ADAPTER=stub` (default) runs the local stub. `TAB_ADAPTER=cli` targets the real CLI.
**The CLI adapter requires the AllScale CLI** — `npm install -g @allscale/cli`. It is
implemented against the real command surface: every flag was read off `--help`, and every
difference from the original design assumptions is itemised in [`docs/DIFF.md`](docs/DIFF.md).
Every adapter method has now been exercised against the live API: `payout send` funded real
claim links on Sepolia and one was claimed on chain. The per-method evidence — and the one
failure mode still open, an unattended claim that can time out and let the link refund — is in
[`docs/cli-mode-report.md`](docs/cli-mode-report.md). Without store credentials the payout leg
fails closed (`CLI_AUTH_MISSING`) before any network call.
Ambiguous outcomes are never treated as failures: on an ambiguous exit or a timeout, Tab
checks `payout status` first, then retries the **same** `--reference-id`, which the backend
deduplicates.
Stub output follows the same discipline: fields whose names are not documented are not
invented — they live under `raw` with a TODO, and stub-generated values are prefixed `stub_`
so no demo output can be mistaken for a real settlement. The stub also models the two-step
claim flow, per-pair authorization, claim-link expiry and in-flight funding, so the failure
modes are reachable without touching money.
The claim token — and the claim URL, which embeds it — are bearer credentials. Neither is
logged, persisted to the ledger, or shown in the UI.
### Credentials
Read from the environment only, never from code or a config file, never logged:
| Variable | Used by |
| --- | --- |
| `ALLSCALE_STORE_API_KEY` | `payout send` (store-key HMAC — **not** your CLI login) |
| `ALLSCALE_STORE_API_SECRET` | `payout send` (prefer the env var; a flag value leaks via shell history) |
The other commands use the CLI's own session (`allscale device-login`), which needs the
`invoice:all`, `claim_link:all`, `wallet:read_only` and `transaction:read_only` scopes.
See [`.env.example`](.env.example) for the full list of settings (names only, no values).
`.env`, `credentials`, and `*.key` are git-ignored.
## Roadmap (not implemented)
Deliberately out of scope for this build:
- Usage-tier / volume pricing
- Multi-currency tabs (one stablecoin per tab today)
- Credit scoring and dynamic limits
- Dispute and arbitration flow
- Protocol-level x402 signature verification (only the response shape is implemented)
## Requirements
Node.js ≥ 20.10, npm. TypeScript is compiled to `dist/` — no runtime type-stripping needed.
## License
MIT — see [`LICENSE`](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues