taximeter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +141 -0
- package/DECISIONS.md +54 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/SECURITY.md +69 -0
- package/SPEC-NOTES.md +369 -0
- package/SPEC.md +474 -0
- package/VERIFICATION.md +943 -0
- package/dist/chunk-I7YXGTRZ.js +1167 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +494 -0
- package/dist/index.d.ts +404 -0
- package/dist/index.js +182 -0
- package/dist/ui/assets/index-DOE5bD9P.js +101 -0
- package/dist/ui/assets/index-De4zKRDL.css +1 -0
- package/dist/ui/index.html +15 -0
- package/docs/RECORDING.md +75 -0
- package/docs/SDK.md +192 -0
- package/docs/demo.gif +0 -0
- package/docs/demo.mjs +145 -0
- package/docs/demo.tape +15 -0
- package/package.json +79 -0
- package/taximeter.config.example.json +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{color-scheme:light;--background: #f6f8f5;--surface: #ffffff;--surface-muted: #edf1ed;--text: #182b25;--muted: #60736a;--faint: #798b81;--border: #dce4dd;--border-strong: #becdc1;--accent: #237d5c;--accent-soft: #d9ece1;--warning: #8b6819;--warning-soft: #f7efd9;--danger: #a74335;--danger-soft: #fff0eb;--chart-grid: #e8ede8;--font-ui: "Inter", "Aptos", "Segoe UI", Arial, sans-serif;--font-mono: "JetBrains Mono", "Cascadia Code", "SFMono-Regular", Consolas, monospace;font-family:var(--font-ui);font-size:14px;color:var(--text);background:var(--background);font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}@media(prefers-color-scheme:dark){:root{color-scheme:dark;--background: #101b16;--surface: #16231c;--surface-muted: #1c2c23;--text: #e4ede5;--muted: #a0b4a6;--faint: #82998a;--border: #2d4033;--border-strong: #49604f;--accent: #74c6a0;--accent-soft: #243f32;--warning: #dfbe73;--warning-soft: #38301c;--danger: #f29782;--danger-soft: #35241f;--chart-grid: #24372b}}*{box-sizing:border-box}body{margin:0;background:var(--background)}button,input,select{font:inherit}button,select{color:var(--text)}button,a,select{-webkit-tap-highlight-color:transparent}button{cursor:pointer}a{color:inherit;text-decoration:none}a:hover{color:var(--accent)}button:focus-visible,a:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:4px}button,select{border-radius:3px}h1,h2,h3,p{margin:0}h1{font-size:24px;font-weight:550;letter-spacing:-.9px;line-height:1.2}h2{font-size:14px;font-weight:600;letter-spacing:-.15px}h3{font-size:14px;font-weight:550}strong{font-weight:600}code,.mono,.numeric{font-family:var(--font-mono);font-variant-numeric:tabular-nums}.mono{letter-spacing:-.4px}.numeric{text-align:right;white-space:nowrap}.muted,.secondary-label{color:var(--muted);font-weight:400}.eyebrow{display:block;font-size:10px;font-weight:650;letter-spacing:1.25px;color:var(--muted);line-height:1.5}.masthead{min-height:66px;padding:0 36px;display:flex;align-items:center;justify-content:space-between;gap:18px;background:var(--surface);border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:24px;min-width:0}.wordmark{display:inline-flex;align-items:center;gap:10px;font-size:22px;font-weight:650;letter-spacing:-1px;line-height:1;color:var(--text)}.mark{display:flex;align-items:flex-end;width:22px;height:21px;gap:3px}.mark i{display:block;width:5px;background:var(--accent)}.mark i:nth-child(1){height:9px}.mark i:nth-child(2){height:15px}.mark i:nth-child(3){height:21px}.brand-description{color:var(--muted);font-size:12px;padding-left:24px;border-left:1px solid var(--border)}.connection-state{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:11px;white-space:nowrap}.live-dot{width:6px;height:6px;border-radius:50%;background:var(--accent)}.connection-offline .live-dot{background:var(--danger)}.connection-connecting .live-dot{background:var(--warning)}.navigation{display:flex;align-items:stretch;gap:26px;padding:0 36px;min-height:45px;border-bottom:1px solid var(--border);background:var(--surface);overflow-x:auto}.navigation button{border:0;border-radius:0;border-bottom:2px solid transparent;background:transparent;color:var(--muted);padding:0 1px;font-size:12px;white-space:nowrap;flex-shrink:0}.navigation button:hover{color:var(--text)}.navigation button[aria-current=page]{border-bottom-color:var(--accent);color:var(--accent);font-weight:600}.nav-date{margin-left:auto;align-self:center;padding-left:18px;font-size:11px;color:var(--muted);white-space:nowrap}main{max-width:1600px;padding:23px 36px 0;margin:0 auto}.page-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin-bottom:22px}.page-heading .eyebrow{margin-bottom:6px}.asset-selector{display:flex;align-items:center;gap:10px;flex-shrink:0}.asset-selector label{font-size:11px;color:var(--muted)}.asset-selector select{padding:7px 28px 7px 10px;border:1px solid var(--border-strong);background:var(--surface);font-size:12px;max-width:340px}.connection-warning{color:var(--danger);background:var(--danger-soft);border-left:3px solid var(--danger);padding:11px 14px;margin-bottom:18px;font-size:12px}.budget-hero{display:grid;grid-template-columns:minmax(0,1.22fr) minmax(280px,.78fr);align-items:center;gap:55px;padding:0 0 19px;border-bottom:1px solid var(--border-strong)}.hero-value{min-width:0}.hero-number{font-size:clamp(27px,3.2vw,44px);line-height:1.2;letter-spacing:-2px;color:var(--accent);margin:7px 0 8px;overflow-x:auto;white-space:nowrap;padding-bottom:2px}.hero-unit{color:var(--muted);font-size:14px;letter-spacing:0;margin-left:13px}.hero-value p{font-size:10px;color:var(--muted);overflow-wrap:anywhere}.budget-detail{padding-top:10px}.budget-caption{display:flex;justify-content:space-between;align-items:baseline;gap:15px;font-size:11px}.budget-caption .mono{overflow-wrap:anywhere}.budget-track{height:7px;background:var(--surface-muted);border:1px solid var(--border);margin:12px 0 10px;overflow:hidden}.budget-track>span{display:block;height:100%;background:var(--accent)}.budget-caption.below{color:var(--muted);font-size:10px}:where(.budget-caption.below) strong{color:var(--text);font-weight:500}.scope-note{color:var(--faint);font-size:10px;margin-top:10px}.totals-strip{display:flex;align-items:center;gap:25px;font-size:10px;color:var(--muted);padding:10px 0 12px;border-bottom:1px solid var(--border);flex-wrap:wrap}.totals-strip>span{display:inline-flex;gap:8px;align-items:baseline}.totals-strip strong{color:var(--text);font-size:11px;font-weight:500}.totals-strip .blocked-count{margin-left:auto}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:16px 0 12px}.table-note{font-size:10px;color:var(--muted);white-space:nowrap}.table-scroll{overflow-x:auto;width:100%}table{width:100%;border-collapse:collapse;font-size:12px}th{text-align:left;font-size:10px;color:var(--muted);font-weight:550;border-top:1px solid var(--border-strong);border-bottom:1px solid var(--border-strong);padding:8px 10px;background:var(--surface-muted);white-space:nowrap}td{padding:8px 10px;border-bottom:1px solid var(--border);vertical-align:middle}th:first-child,td:first-child{padding-left:13px}th:last-child,td:last-child{padding-right:13px}.event-table{table-layout:fixed;min-width:850px;font-size:11px}.event-table th:nth-child(1){width:82px}.event-table th:nth-child(2){width:22%}.event-table th:nth-child(3){width:23%}.event-table th:nth-child(4){width:110px}.event-table th:nth-child(5){width:145px}.event-table th:nth-child(6){width:70px}.event-table th:nth-child(7){width:85px}.event-table td{padding-top:2px;padding-bottom:2px;line-height:16px;height:20px}.event-table tbody tr:hover{background:var(--surface)}.event-table .resource-cell,.event-table .attribution-cell,.event-table .asset-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.event-table .resource-cell a:hover{text-decoration:underline;text-underline-offset:3px}.network-cell,.time-cell{color:var(--muted)}.event-table .numeric{overflow:hidden;text-overflow:ellipsis}.status-cell{text-align:right}.event-status{font-size:10px;white-space:nowrap}.status-confirmed{color:var(--muted)}.status-unknown{color:var(--warning)}.status-failed{color:var(--muted)}.status-blocked{color:var(--danger);font-weight:600}.event-table .blocked-row{background:var(--danger-soft)}.blocked-row td:first-child{border-left:3px solid var(--danger);padding-left:10px}.ledger-footnote{font-size:10px;color:var(--muted);line-height:1.7;padding-top:11px}.empty-state{background:var(--surface);border:1px solid var(--border);margin:25px 0 22px;padding:34px 32px 0;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.95fr);column-gap:58px}.empty-heading h2{font-size:26px;font-weight:500;letter-spacing:-.8px;max-width:320px;margin:10px 0 13px}.empty-heading p{font-size:12px;color:var(--muted);line-height:1.8;max-width:430px}.connection-instructions{padding-left:28px;border-left:1px solid var(--border)}.connection-instructions>code{display:block;font-size:18px;color:var(--accent);margin:12px 0 16px;letter-spacing:-.5px;overflow-wrap:anywhere}.connection-instructions p{font-size:11px;line-height:1.8;color:var(--muted)}.connection-instructions strong{font-weight:500;overflow-wrap:anywhere}.connection-instructions .connection-limit{margin-top:12px}.empty-footer{grid-column:1 / -1;border-top:1px solid var(--border);margin-top:30px;padding:14px 0;display:flex;align-items:center;justify-content:space-between;gap:20px;color:var(--muted);font-size:10px}.group-section,.timeline-section,.export-section{border-top:1px solid var(--border-strong)}.group-table{min-width:650px}.group-table th button{border:0;padding:0;background:transparent;font:inherit;color:inherit}.group-table td{padding-top:17px;padding-bottom:17px}.group-name{max-width:420px;overflow-wrap:anywhere;font-size:13px}.group-bar-cell{width:28%}.group-bar{height:5px;background:var(--surface-muted);min-width:90px}.group-bar span{display:block;height:100%;background:var(--accent)}.quiet-empty{background:var(--surface);border:1px solid var(--border);padding:36px 25px;margin:14px 0}.quiet-empty h3{margin-bottom:8px;font-size:18px}.quiet-empty p{font-size:12px;color:var(--muted);line-height:1.7}.chart-heading{display:flex;align-items:baseline;justify-content:space-between;gap:20px;margin:18px 0 5px}.chart-heading h3{font-size:12px;color:var(--muted)}.chart-heading .mono{color:var(--accent);font-size:14px}.chart-scroll{overflow-x:auto}.chart{display:block;width:100%;min-width:660px}.chart text{fill:var(--muted);font:11px var(--font-mono);font-variant-numeric:tabular-nums}.chart-rule{stroke:var(--border-strong);stroke-width:1}.chart-grid{stroke:var(--chart-grid);stroke-width:1;stroke-dasharray:3 5}.sparkline{fill:none;stroke:var(--accent);stroke-width:2;vector-effect:non-scaling-stroke;stroke-linejoin:round}.chart-dot,.hour-bar{fill:var(--accent)}.hourly-heading{border-top:1px solid var(--border);padding-top:20px;margin-top:8px;font-size:11px}.hourly-chart{margin-top:8px}.section-intro{color:var(--muted);font-size:12px;line-height:1.8;max-width:700px;margin:2px 0 23px}.export-options{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:15px;margin-bottom:30px}.export-option{display:flex;flex-direction:column;gap:14px;padding:22px;border:1px solid var(--border-strong);background:var(--surface);min-height:192px}.export-option:hover{border-color:var(--accent);color:var(--text)}.export-option>strong{font-size:24px;letter-spacing:-.5px;font-weight:500;display:flex;justify-content:space-between}.download-arrow{color:var(--accent);font-size:23px}.export-option>span:not(.eyebrow){font-size:11px;color:var(--muted);line-height:1.6}.export-option>span.download-label{margin-top:auto;color:var(--accent);font-weight:600;padding-top:7px}.statement-preview{background:var(--surface);border:1px solid var(--border);padding:26px}.preview-heading{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:24px}.preview-heading>span{font-size:10px;color:var(--muted);letter-spacing:1px}.preview-summary{display:flex;gap:24px;flex-wrap:wrap;color:var(--muted);font-size:11px;margin-bottom:22px}.preview-summary strong{margin-left:8px;color:var(--text)}.statement-preview table{min-width:700px}.contract-address{display:block;font-family:var(--font-mono);color:var(--faint);font-size:9px;margin-top:5px}.diagnostics{border-top:1px solid var(--border);margin-top:22px;font-size:11px}.diagnostics summary{cursor:pointer;padding:13px 0}.diagnostics .mono{font-size:10px;margin-left:7px}.diagnostics ul{list-style:none;padding:0;margin:0 0 15px}.diagnostics li{display:grid;grid-template-columns:75px 145px 1fr;gap:14px;padding:8px 12px;border-bottom:1px solid var(--border);line-height:1.7}.diagnostics li>span{color:var(--muted)}.diagnostics li strong{font-weight:500;color:var(--warning)}.page-footer{display:flex;align-items:center;gap:15px;flex-wrap:wrap;margin-top:25px;padding:15px 0 18px;border-top:1px solid var(--border);color:var(--faint);font-size:9px;line-height:1.7}.page-footer>span:nth-child(2){margin-left:auto}.page-footer>.mono{margin-left:6px}.loading-shell{max-width:580px;margin:15vh auto 0;padding:30px}.loading-shell>.wordmark{margin-bottom:50px}.loading-shell h1{margin:10px 0 16px;font-size:30px}.loading-shell p{color:var(--muted);line-height:1.8;font-size:13px}.loading-shell>code{display:block;color:var(--accent);border:1px solid var(--border);padding:15px;margin-top:28px;background:var(--surface)}@media(min-width:1600px){.masthead,.navigation{padding-left:max(36px,calc((100vw - 1528px)/2));padding-right:max(36px,calc((100vw - 1528px)/2))}}@media(max-width:900px){.masthead,.navigation{padding-left:24px;padding-right:24px}main{padding-left:24px;padding-right:24px}.brand-description{display:none}.budget-hero{gap:28px;grid-template-columns:minmax(0,1fr) minmax(260px,1fr)}.totals-strip{gap:10px 20px}.empty-state{column-gap:26px;padding-left:24px;padding-right:24px}.empty-heading h2{font-size:22px}.empty-footer{align-items:flex-start}.export-option{padding:18px}}@media(max-width:600px){.masthead{padding:0 19px;min-height:61px}.wordmark{font-size:21px}.connection-state{font-size:10px}.navigation{gap:23px;padding:0 19px;min-height:43px}.navigation button{font-size:12px}.nav-date{display:none}main{padding:22px 19px 0}.page-heading{align-items:flex-start;flex-direction:column;gap:17px;margin-bottom:23px}h1{font-size:23px;letter-spacing:-.75px}.asset-selector{justify-content:space-between;width:100%}.asset-selector select{max-width:265px;flex:1}.budget-hero{grid-template-columns:minmax(0,1fr);gap:18px;padding-bottom:18px}.hero-number{font-size:34px;letter-spacing:-1.6px}.hero-unit{font-size:12px;margin-left:9px}.hero-value p{font-size:9px}.budget-detail{padding-top:0}.budget-caption{font-size:10px}.scope-note{margin-top:8px}.totals-strip{display:grid;grid-template-columns:1fr 1fr;gap:12px 18px;padding:16px 0}.totals-strip>span{flex-direction:column;gap:3px;min-width:0}.totals-strip strong{overflow-wrap:anywhere}.totals-strip .blocked-count{margin-left:0}.section-heading{gap:10px;padding-top:20px}.table-note{font-size:9px}.event-table{min-width:780px}.event-table td{padding-top:5px;padding-bottom:5px}.ledger-footnote{font-size:10px;line-height:1.8}.empty-state{grid-template-columns:minmax(0,1fr);padding:26px 22px 0;margin-top:23px}.empty-heading h2{font-size:24px;max-width:260px}.empty-heading p{font-size:12px;line-height:1.8}.connection-instructions{margin-top:24px;padding:23px 0 0;border-left:0;border-top:1px solid var(--border)}.connection-instructions>code{font-size:16px}.empty-footer{flex-direction:column;gap:6px;margin-top:25px;font-size:9px}.chart-heading{flex-direction:column;gap:8px}.chart{min-width:630px}.export-options{grid-template-columns:minmax(0,1fr);gap:12px}.export-option{min-height:176px;padding:21px}.section-intro{font-size:12px}.statement-preview{padding:19px}.preview-heading{flex-direction:column;align-items:flex-start}.preview-summary{gap:12px;flex-direction:column}.diagnostics li{grid-template-columns:66px 1fr;gap:6px 10px;padding-left:0;padding-right:0}.diagnostics li>span:last-child{grid-column:1 / -1}.page-footer{align-items:flex-start;gap:6px;flex-direction:column;margin-top:24px}.page-footer>span:nth-child(2),.page-footer>.mono{margin-left:0}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<meta name="description" content="A taximeter for your AI agents. Your local payment ledger." />
|
|
8
|
+
<title>Taximeter — Local ledger</title>
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DOE5bD9P.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-De4zKRDL.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Record the 20-second demo
|
|
2
|
+
|
|
3
|
+
`demo.gif` is a 20-second recording of the included simulation running on Windows,
|
|
4
|
+
captured on 2026-09-08. The simulation runs entirely against a local fixture and
|
|
5
|
+
an in-memory ledger. Its signatures and settlement reports are synthetic.
|
|
6
|
+
|
|
7
|
+
From a source checkout, prepare the built package:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm ci
|
|
11
|
+
npm run build
|
|
12
|
+
node docs/demo.mjs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The simulation prints 20 allowed payments, the documented 402 block for payment
|
|
16
|
+
21, and an exact total of `140` atomic units. It creates no wallet, key, or persistent
|
|
17
|
+
ledger and closes its listeners when finished.
|
|
18
|
+
|
|
19
|
+
## Captured Windows recording
|
|
20
|
+
|
|
21
|
+
The shipped GIF captures an actual `cmd.exe` session through Windows ConPTY using
|
|
22
|
+
[node-pty 1.1.0](https://github.com/microsoft/node-pty). The recorder types
|
|
23
|
+
`node docs/demo.mjs`, stores the terminal's output with its original timing in an
|
|
24
|
+
asciicast v2 file, and waits until 20 seconds have elapsed. It checks the block and
|
|
25
|
+
exact ledger total in the captured output and requires the command shell to exit
|
|
26
|
+
successfully. The terminal is 104 columns by 32 rows.
|
|
27
|
+
|
|
28
|
+
[agg 1.9.0](https://github.com/asciinema/agg/releases/tag/v1.9.0) renders that capture
|
|
29
|
+
using Consolas at 18 px, line height 1.1, the GitHub dark theme, and a 20 fps cap.
|
|
30
|
+
The final frame is held long enough for the complete GIF to last 20 seconds.
|
|
31
|
+
The resulting GIF is 1049×653 and 96,519 bytes. The final block response and ledger
|
|
32
|
+
total remain visible together. No demo output is fabricated or replaced.
|
|
33
|
+
|
|
34
|
+
The source capture and encoding tools are local development artifacts and are
|
|
35
|
+
excluded from the repository and npm package. FFmpeg converts the GIF to an H.264
|
|
36
|
+
MP4 with one padding row and column for even dimensions. The following recipes
|
|
37
|
+
can reproduce the demonstration from a source checkout.
|
|
38
|
+
|
|
39
|
+
## VHS
|
|
40
|
+
|
|
41
|
+
Install VHS and its ffmpeg/ttyd prerequisites from the
|
|
42
|
+
[official VHS instructions](https://github.com/charmbracelet/vhs#installation).
|
|
43
|
+
Run from the repository root on a system supported by VHS (WSL is suitable on Windows):
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
vhs docs/demo.tape
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The tape types exactly `node docs/demo.mjs`, then leaves the resulting block and
|
|
50
|
+
total visible. It writes `docs/demo.gif` at 960×660. Review the output before
|
|
51
|
+
committing the replacement; it must fit within the package's 2 MB tarball limit.
|
|
52
|
+
|
|
53
|
+
## asciinema alternative
|
|
54
|
+
|
|
55
|
+
With asciinema and agg installed from their official projects, record the same
|
|
56
|
+
script and convert it:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
mkdir -p tmp
|
|
60
|
+
asciinema rec --cols 104 --rows 32 --command "node docs/demo.mjs" tmp/demo.cast
|
|
61
|
+
agg tmp/demo.cast docs/demo.gif
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This quick alternative uses agg's default final pause and produces a shorter GIF.
|
|
65
|
+
For a 20-second result, increase `--last-frame-duration` by the difference between
|
|
66
|
+
20 seconds and the measured GIF duration, then render again. The verified Windows
|
|
67
|
+
rendering helper calculates that pause automatically.
|
|
68
|
+
|
|
69
|
+
Run these commands in a POSIX shell or WSL. Do not enter account data,
|
|
70
|
+
wallet material, or real payment authorizations while recording. The expected
|
|
71
|
+
terminal script is only the local demo command above.
|
|
72
|
+
|
|
73
|
+
The Windows ConPTY/agg recording above was executed and visually checked. The VHS
|
|
74
|
+
and asciinema alternatives remain documented recipes, not verified Windows runs.
|
|
75
|
+
Recording tools are optional and are not package dependencies.
|
package/docs/SDK.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Taximeter SDK
|
|
2
|
+
|
|
3
|
+
`withMeter` wraps a Node.js fetch transport with the same parser, SQLite ledger,
|
|
4
|
+
and policy engine used by the proxy. It supports Node 20 or later and ESM. It
|
|
5
|
+
does not start a proxy or dashboard, create a payment, or configure a wallet.
|
|
6
|
+
|
|
7
|
+
This project has not published its first npm release yet. From a source
|
|
8
|
+
checkout, run `npm ci` and `npm run build`, then save the examples at the
|
|
9
|
+
repository root. Their `import "taximeter"` statements resolve the package's own
|
|
10
|
+
built exports. To try it in another project, run `npm pack` after building,
|
|
11
|
+
copy the resulting tarball there, and use
|
|
12
|
+
`npm install ./taximeter-0.1.0.tgz`. After the first publication,
|
|
13
|
+
`npm install taximeter` will install the registry release.
|
|
14
|
+
|
|
15
|
+
## Try it locally
|
|
16
|
+
|
|
17
|
+
After choosing an installation method above, save this complete example as
|
|
18
|
+
`sdk-local.mjs`, then run `node sdk-local.mjs`.
|
|
19
|
+
It starts a temporary HTTP server on loopback, sends one ordinary request, and
|
|
20
|
+
closes both the server and an in-memory ledger. It makes no external requests.
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import { once } from "node:events";
|
|
24
|
+
import { createServer } from "node:http";
|
|
25
|
+
import { withMeter } from "taximeter";
|
|
26
|
+
|
|
27
|
+
const upstream = createServer((request, response) => {
|
|
28
|
+
response.writeHead(200, { "Content-Type": "application/json" });
|
|
29
|
+
response.end(JSON.stringify({
|
|
30
|
+
task: request.headers["taximeter-task"],
|
|
31
|
+
agent: request.headers["taximeter-agent"],
|
|
32
|
+
}));
|
|
33
|
+
});
|
|
34
|
+
upstream.listen(0, "127.0.0.1");
|
|
35
|
+
await once(upstream, "listening");
|
|
36
|
+
const address = upstream.address();
|
|
37
|
+
if (!address || typeof address === "string") {
|
|
38
|
+
throw new Error("Expected a local TCP listener");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const metered = withMeter(fetch, {
|
|
42
|
+
db: ":memory:",
|
|
43
|
+
taskId: "research",
|
|
44
|
+
agentId: "agent",
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const response = await metered(`http://127.0.0.1:${address.port}/example`, {
|
|
48
|
+
redirect: "error",
|
|
49
|
+
});
|
|
50
|
+
console.log(await response.text());
|
|
51
|
+
console.log(`Payments recorded: ${metered.ledger.events().length}`);
|
|
52
|
+
} finally {
|
|
53
|
+
metered.close();
|
|
54
|
+
upstream.closeAllConnections();
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
upstream.close((error) => error ? reject(error) : resolve());
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Expected output:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
{"task":"research","agent":"agent"}
|
|
65
|
+
Payments recorded: 0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
An ordinary request is not a payment. Attribution headers travel with the
|
|
69
|
+
request, but a ledger payment appears only when the transport sees a recognized
|
|
70
|
+
x402 authorization. A 402 challenge alone also contributes no spend.
|
|
71
|
+
|
|
72
|
+
## Place the meter inside the payment wrapper
|
|
73
|
+
|
|
74
|
+
Use `wrapFetchWithPayment(withMeter(fetch, options), client)`. This placement
|
|
75
|
+
lets Taximeter inspect the initial challenge and gate each signed replay before
|
|
76
|
+
the supplied transport forwards it. Wrapping a payment-enabled fetch on the
|
|
77
|
+
outside hides that wrapper's internal replay.
|
|
78
|
+
|
|
79
|
+
The following ESM factory accepts your application's existing, configured
|
|
80
|
+
`x402Client` or `x402HTTPClient`. It neither creates nor receives a private key.
|
|
81
|
+
Install the optional integration dependency with
|
|
82
|
+
`npm install @x402/fetch@2.25.0`, then save this module as `metered-payment.mjs`.
|
|
83
|
+
Taximeter itself does not depend on the official payment client.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import { wrapFetchWithPayment } from "@x402/fetch";
|
|
87
|
+
import { withMeter } from "taximeter";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {import("@x402/fetch").x402Client | import("@x402/fetch").x402HTTPClient} client
|
|
91
|
+
* @param {import("taximeter").MeterOptions} [options]
|
|
92
|
+
*/
|
|
93
|
+
export function createMeteredPaymentFetch(client, options = {}) {
|
|
94
|
+
const transport = withMeter(fetch, options);
|
|
95
|
+
return {
|
|
96
|
+
fetch: wrapFetchWithPayment(transport, client),
|
|
97
|
+
ledger: transport.ledger,
|
|
98
|
+
close: () => transport.close(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Pass your existing client and attribution options to
|
|
104
|
+
`createMeteredPaymentFetch(client, { taskId: "research", agentId: "agent" })`.
|
|
105
|
+
Use the returned `fetch` in place of your existing paid fetch, and call `close`
|
|
106
|
+
after all requests have finished. Client setup and authorization remain your
|
|
107
|
+
application's responsibility. Configuring the payment client to support a rail
|
|
108
|
+
does not add that rail to Taximeter's supported subset.
|
|
109
|
+
|
|
110
|
+
The factory's parameter types and composition match the published
|
|
111
|
+
[@x402/fetch 2.25.0 declarations](https://unpkg.com/@x402/fetch@2.25.0/dist/esm/index.d.mts)
|
|
112
|
+
and [transport implementation](https://unpkg.com/@x402/fetch@2.25.0/dist/esm/index.mjs).
|
|
113
|
+
The local example and factory were exercised with ordinary loopback traffic;
|
|
114
|
+
the integration tests use synthetic x402 authorizations. No wallet or genuine
|
|
115
|
+
payment was used to validate these examples.
|
|
116
|
+
|
|
117
|
+
## Options and lifecycle
|
|
118
|
+
|
|
119
|
+
`withMeter(fetchImpl, options)` returns a fetch-compatible function with a
|
|
120
|
+
`ledger` property and an idempotent `close()` method.
|
|
121
|
+
|
|
122
|
+
| Option | Default | Meaning |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `db` | Resolved configuration; normally `~/.taximeter/ledger.db` | SQLite path. Use `:memory:` for temporary state. Overrides `config.db` when the wrapper creates the ledger. |
|
|
125
|
+
| `config` | Resolved configuration | A partial Taximeter configuration, including budgets and policy. Amounts must be decimal integer strings in atomic units. |
|
|
126
|
+
| `ledger` | A new `Ledger` | An existing Taximeter ledger owned by the caller. Its storage path is used instead of `db`. |
|
|
127
|
+
| `taskId` | Existing `Taximeter-Task` header, otherwise unattributed | Overrides the task header on each request. Use an HTTP-header-compatible label of 1–256 characters. |
|
|
128
|
+
| `agentId` | Existing `Taximeter-Agent` header, otherwise unattributed | Overrides the agent header on each request. Use an HTTP-header-compatible label of 1–256 characters. |
|
|
129
|
+
|
|
130
|
+
Without an injected ledger, configuration resolves from home/cwd files and
|
|
131
|
+
environment, with explicit SDK options taking precedence. See the
|
|
132
|
+
[configuration reference](../README.md#configuration). With an injected
|
|
133
|
+
`ledger`, the SDK uses built-in defaults plus `config`; it does not load config
|
|
134
|
+
files or environment settings. Keep budgets consistent across writers sharing
|
|
135
|
+
the same database.
|
|
136
|
+
|
|
137
|
+
The wrapper closes only a ledger it created. For an injected ledger, close all
|
|
138
|
+
wrappers after their requests finish, then let the owner close the ledger.
|
|
139
|
+
Reusing a closed wrapper passes requests directly to the original transport
|
|
140
|
+
without metering. Do not use a closed wrapper for later payments.
|
|
141
|
+
|
|
142
|
+
## Fetch behavior and limits
|
|
143
|
+
|
|
144
|
+
- Use a Node fetch implementation that accepts standard `string`, `URL`, or
|
|
145
|
+
`Request` inputs. Header objects, header tuples, and `Headers` are supported.
|
|
146
|
+
Request bodies, abort signals, and transport options are preserved.
|
|
147
|
+
- The wrapper returns the original upstream `Response`. It does not consume
|
|
148
|
+
its body. It inspects a clone only for legacy 402 challenges, bounded to
|
|
149
|
+
64 KiB and 100 ms. Slow, oversized, malformed, or unsupported challenges pass
|
|
150
|
+
through with a diagnostic.
|
|
151
|
+
- Native fetch or a custom transport can follow redirects internally. Those
|
|
152
|
+
hops are invisible to Taximeter's preflight policy checks. A redirected
|
|
153
|
+
response produces a diagnostic, but its destination may already have received
|
|
154
|
+
a payment header. Use `redirect: "error"` on host-sensitive fetch calls, or a
|
|
155
|
+
transport that exposes each hop separately. Taximeter preserves your redirect
|
|
156
|
+
setting. Redirected v1 challenges are not cached against the initial URL.
|
|
157
|
+
- Internal retries hidden inside a custom transport have the same visibility
|
|
158
|
+
limit. Put payment retries outside the metered transport, as in the official
|
|
159
|
+
wrapper composition above.
|
|
160
|
+
- Recognized authorizations reserve capacity before forwarding. A policy denial
|
|
161
|
+
returns a 402 JSON response with `error: "blocked_by_taximeter"`, `reason`,
|
|
162
|
+
`budget`, `spent`, and `remaining`, and records a blocked event. The enclosing
|
|
163
|
+
payment client can apply its own response handling or throw its own error.
|
|
164
|
+
- If the transport throws after forwarding an authorization, the original
|
|
165
|
+
error is rethrown and the authorization remains reserved with unknown
|
|
166
|
+
settlement. Missing settlement evidence has the same conservative treatment.
|
|
167
|
+
Upstream-reported settlement is not independent on-chain verification.
|
|
168
|
+
- Storage failures pass traffic through and emit a local stderr warning.
|
|
169
|
+
Enforcement requires working storage. Unsupported payment formats also pass
|
|
170
|
+
through with a diagnostic; this is a cooperative meter rather than an
|
|
171
|
+
isolation boundary.
|
|
172
|
+
|
|
173
|
+
The initial adapter meters exact EVM EIP-3009 payments: legacy v1 on Base and
|
|
174
|
+
Base Sepolia, and v2 with EVM CAIP-2 networks. Permit2, ERC-7710, other schemes,
|
|
175
|
+
and other rails remain unmetered. Known Base/Base Sepolia USDC uses six decimal
|
|
176
|
+
places; unknown tokens remain exact atomic units. Budgets and totals are
|
|
177
|
+
separate for every network and asset. See [SPEC-NOTES](../SPEC-NOTES.md) for
|
|
178
|
+
wire formats, challenge correlation, idempotency, and settlement rules.
|
|
179
|
+
|
|
180
|
+
## Read the same ledger from the CLI
|
|
181
|
+
|
|
182
|
+
For persistent state, use `db: "./agent.db"` in your SDK options. Run
|
|
183
|
+
`npx taximeter report --db ./agent.db --json` to inspect exact totals, or
|
|
184
|
+
`npx taximeter export --db ./agent.db --csv ./agent.csv` to create a statement.
|
|
185
|
+
Export files must not already exist. Monetary CSV `amount` values are counted
|
|
186
|
+
contributions; blocked and known failed rows contribute zero. Sum them
|
|
187
|
+
separately by network and asset.
|
|
188
|
+
|
|
189
|
+
Run `npx taximeter start --db ./agent.db` to view that ledger in the local
|
|
190
|
+
dashboard. The SDK continues to call its own supplied transport; the CLI proxy
|
|
191
|
+
and `config.upstream` do not reroute SDK requests. Stop all SDK writers before
|
|
192
|
+
using `npx taximeter reset --db ./agent.db --yes`, which archives the ledger.
|
package/docs/demo.gif
ADDED
|
Binary file
|
package/docs/demo.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer, request } from "node:http";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { closeProxy, createProxy, Ledger, parseConfig, totals } from "../dist/index.js";
|
|
6
|
+
|
|
7
|
+
// Synthetic envelopes only: no signing, wallet, facilitator, or real payment.
|
|
8
|
+
const asset = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
9
|
+
const payTo = "0x209693bc6afc0c5328ba36faf03c514ef312287c";
|
|
10
|
+
const payer = "0x857b06519e91e3a54538791bdbb0e22373e36b66";
|
|
11
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64");
|
|
12
|
+
const accepted = {
|
|
13
|
+
scheme: "exact",
|
|
14
|
+
network: "eip155:8453",
|
|
15
|
+
asset,
|
|
16
|
+
payTo,
|
|
17
|
+
amount: "7",
|
|
18
|
+
maxTimeoutSeconds: 60,
|
|
19
|
+
extra: { name: "USD Coin", version: "2" },
|
|
20
|
+
};
|
|
21
|
+
const upstream = createServer((req, res) => {
|
|
22
|
+
const header = z.string().optional().parse(req.headers["payment-signature"]);
|
|
23
|
+
if (!header) {
|
|
24
|
+
const resource = z
|
|
25
|
+
.url()
|
|
26
|
+
.parse(`http://${z.string().parse(req.headers.host)}${z.string().parse(req.url)}`);
|
|
27
|
+
res.writeHead(402, {
|
|
28
|
+
"PAYMENT-REQUIRED": encode({
|
|
29
|
+
x402Version: 2,
|
|
30
|
+
resource: { url: resource },
|
|
31
|
+
accepts: [accepted],
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
res.end("Synthetic offer");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const payment = z
|
|
38
|
+
.object({ payload: z.object({ authorization: z.object({ nonce: z.string() }) }) })
|
|
39
|
+
.parse(JSON.parse(Buffer.from(header, "base64").toString("utf8")));
|
|
40
|
+
res.writeHead(200, {
|
|
41
|
+
"PAYMENT-RESPONSE": encode({
|
|
42
|
+
success: true,
|
|
43
|
+
network: "eip155:8453",
|
|
44
|
+
payer,
|
|
45
|
+
transaction: payment.payload.authorization.nonce,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
res.end("Synthetic settlement reported");
|
|
49
|
+
});
|
|
50
|
+
async function listen(server) {
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
server.once("error", reject);
|
|
53
|
+
server.listen(0, "127.0.0.1", () => {
|
|
54
|
+
server.off("error", reject);
|
|
55
|
+
resolve();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return z.object({ port: z.number() }).parse(server.address()).port;
|
|
59
|
+
}
|
|
60
|
+
const ledger = new Ledger(":memory:");
|
|
61
|
+
const proxy = createProxy({
|
|
62
|
+
ledger,
|
|
63
|
+
config: parseConfig({ budgets: { perTask: null, perAgent: null, global: { amount: "140" } } }),
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
const upstreamPort = await listen(upstream);
|
|
67
|
+
const proxyPort = await listen(proxy);
|
|
68
|
+
const resource = `http://127.0.0.1:${upstreamPort}/data`;
|
|
69
|
+
const offer = await fetch(resource);
|
|
70
|
+
assert.equal(offer.status, 402);
|
|
71
|
+
const challenge = z
|
|
72
|
+
.object({
|
|
73
|
+
x402Version: z.literal(2),
|
|
74
|
+
resource: z.object({ url: z.url() }),
|
|
75
|
+
accepts: z.array(z.object({ amount: z.literal("7") })),
|
|
76
|
+
})
|
|
77
|
+
.parse(
|
|
78
|
+
JSON.parse(
|
|
79
|
+
Buffer.from(z.string().parse(offer.headers.get("payment-required")), "base64").toString(
|
|
80
|
+
"utf8",
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
assert.equal(challenge.resource.url, resource);
|
|
85
|
+
await offer.arrayBuffer();
|
|
86
|
+
console.log("Taximeter | local simulation | no money moves");
|
|
87
|
+
console.log("Budget: 140 atomic units. Each payment: 7 atomic units.");
|
|
88
|
+
for (let index = 1; index <= 21; index++) {
|
|
89
|
+
const payload = {
|
|
90
|
+
signature: `0x${"12".repeat(65)}`,
|
|
91
|
+
authorization: {
|
|
92
|
+
from: payer,
|
|
93
|
+
to: payTo,
|
|
94
|
+
value: "7",
|
|
95
|
+
validAfter: "0",
|
|
96
|
+
validBefore: "9999999999",
|
|
97
|
+
nonce: `0x${index.toString(16).padStart(64, "0")}`,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const result = await new Promise((resolve, reject) => {
|
|
101
|
+
const req = request(
|
|
102
|
+
{
|
|
103
|
+
hostname: "127.0.0.1",
|
|
104
|
+
port: proxyPort,
|
|
105
|
+
path: resource,
|
|
106
|
+
headers: {
|
|
107
|
+
"PAYMENT-SIGNATURE": encode({
|
|
108
|
+
x402Version: 2,
|
|
109
|
+
accepted,
|
|
110
|
+
resource: { url: resource },
|
|
111
|
+
payload,
|
|
112
|
+
}),
|
|
113
|
+
"Taximeter-Task": "demo",
|
|
114
|
+
"Taximeter-Agent": "local-fixture",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
(res) => {
|
|
118
|
+
let body = "";
|
|
119
|
+
res.setEncoding("utf8");
|
|
120
|
+
res.on("data", (chunk) => {
|
|
121
|
+
body += z.string().parse(chunk);
|
|
122
|
+
});
|
|
123
|
+
res.on("error", reject);
|
|
124
|
+
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
req.on("error", reject);
|
|
128
|
+
req.end();
|
|
129
|
+
});
|
|
130
|
+
assert.equal(result.status, index <= 20 ? 200 : 402);
|
|
131
|
+
console.log(
|
|
132
|
+
`Payment ${index.toString().padStart(2)} | ${index <= 20 ? "allowed" : "BLOCKED"} | total ${totals(ledger.view())[0]?.amount} atomic units`,
|
|
133
|
+
);
|
|
134
|
+
if (index === 21) console.log(result.body);
|
|
135
|
+
await delay(80);
|
|
136
|
+
}
|
|
137
|
+
assert.equal(totals(ledger.view())[0]?.amount, "140");
|
|
138
|
+
assert.equal(ledger.events().filter((row) => row.status === "blocked").length, 1);
|
|
139
|
+
console.log("Exact ledger total: 140 atomic units. One blocked event recorded.");
|
|
140
|
+
} finally {
|
|
141
|
+
if (proxy.listening) await closeProxy(proxy);
|
|
142
|
+
upstream.closeAllConnections();
|
|
143
|
+
if (upstream.listening) await new Promise((resolve) => upstream.close(resolve));
|
|
144
|
+
ledger.close();
|
|
145
|
+
}
|
package/docs/demo.tape
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taximeter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A taximeter for your AI agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Ding808/taximeter.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"taximeter": "dist/cli/index.js",
|
|
16
|
+
"txm": "dist/cli/index.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"SECURITY.md",
|
|
31
|
+
"CONTRIBUTING.md",
|
|
32
|
+
"CODE_OF_CONDUCT.md",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"DECISIONS.md",
|
|
35
|
+
"VERIFICATION.md",
|
|
36
|
+
"SPEC.md",
|
|
37
|
+
"SPEC-NOTES.md",
|
|
38
|
+
"taximeter.config.example.json",
|
|
39
|
+
"docs"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"lint": "biome check --error-on-warnings .",
|
|
44
|
+
"format": "biome check --write .",
|
|
45
|
+
"test": "vitest run --coverage",
|
|
46
|
+
"build": "tsup && vite build",
|
|
47
|
+
"check:package": "node scripts/check-package.mjs",
|
|
48
|
+
"smoke:package": "node scripts/smoke-package.mjs",
|
|
49
|
+
"changeset": "changeset",
|
|
50
|
+
"version-packages": "changeset version",
|
|
51
|
+
"release": "npm run build && changeset publish"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"better-sqlite3": "12.8.0",
|
|
55
|
+
"commander": "13.1.0",
|
|
56
|
+
"uuid": "11.1.1",
|
|
57
|
+
"zod": "4.5.4"
|
|
58
|
+
},
|
|
59
|
+
"overrides": {
|
|
60
|
+
"tsup": {
|
|
61
|
+
"esbuild": "0.28.1"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@biomejs/biome": "2.5.12",
|
|
66
|
+
"@changesets/cli": "2.29.8",
|
|
67
|
+
"@types/better-sqlite3": "9.6.0",
|
|
68
|
+
"@types/node": "22.20.1",
|
|
69
|
+
"@types/react": "19.2.18",
|
|
70
|
+
"@types/react-dom": "19.2.7",
|
|
71
|
+
"@vitest/coverage-v8": "3.2.7",
|
|
72
|
+
"react": "19.2.8",
|
|
73
|
+
"react-dom": "19.2.8",
|
|
74
|
+
"tsup": "8.5.1",
|
|
75
|
+
"typescript": "5.9.3",
|
|
76
|
+
"vite": "6.4.3",
|
|
77
|
+
"vitest": "3.2.7"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"budgets": {
|
|
3
|
+
"perTask": { "amount": "5000000", "asset": "USDC" },
|
|
4
|
+
"perAgent": { "amount": "50000000", "asset": "USDC", "window": "24h" },
|
|
5
|
+
"global": { "amount": "100000000", "asset": "USDC", "window": "24h" }
|
|
6
|
+
},
|
|
7
|
+
"policy": {
|
|
8
|
+
"allowHosts": [],
|
|
9
|
+
"denyHosts": [],
|
|
10
|
+
"allowPayTo": [],
|
|
11
|
+
"maxSinglePayment": "1000000",
|
|
12
|
+
"maxSingleAsset": "USDC"
|
|
13
|
+
},
|
|
14
|
+
"ports": { "proxy": 8402, "dashboard": 8403 },
|
|
15
|
+
"db": "~/.taximeter/ledger.db"
|
|
16
|
+
}
|