zetrix-agentic-wallet 0.3.4
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/README.md +177 -0
- package/USER_GUIDE.md +233 -0
- package/dist/index.js +201 -0
- package/dist/runtime/VERSION +2 -0
- package/dist/runtime/server-bundle.cjs +103232 -0
- package/openclaw.plugin.json +49 -0
- package/package.json +42 -0
- package/skills/zetrix-agentic-wallet/SKILL.md +176 -0
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Zetrix Agentic Wallet — OpenClaw plugin
|
|
2
|
+
|
|
3
|
+
**Using the wallet?** See the
|
|
4
|
+
**[user guide](https://github.com/Zetrix-Chain/zetrix-agentic-wallet/blob/main/openclaw-plugin/USER_GUIDE.md)**
|
|
5
|
+
— install, first steps, spending limits and backup, written for subscribers. It also ships inside this
|
|
6
|
+
package as `USER_GUIDE.md`. This file is the engineering detail: why the plugin is built the way it is,
|
|
7
|
+
and what to know before changing it.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Installs the Zetrix Agentic Wallet into an OpenClaw gateway, so a subscriber gets the wallet by
|
|
12
|
+
installing one package instead of hand-writing an `mcp.servers` entry — which they cannot do at all on
|
|
13
|
+
a hosted gateway they do not own.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
openclaw plugins install clawhub:zetrix-agentic-wallet@<version>
|
|
17
|
+
openclaw plugins enable zetrix-agentic-wallet
|
|
18
|
+
openclaw gateway restart
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Nothing else is required. No `npx`, no registry access at runtime, no MCP configuration, and **no
|
|
22
|
+
password**: the wallet generates and stores its own credentials.
|
|
23
|
+
|
|
24
|
+
## Build
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run build # in the repo root — produces the wallet bundle
|
|
28
|
+
npm run build # in openclaw-plugin — bundles the plugin and vendors the wallet
|
|
29
|
+
npm test # 56 tests: registration logic, paths, and the package contract
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Set by the subscriber, through the gateway's plugin config UI or `plugins.entries`:
|
|
35
|
+
|
|
36
|
+
| Field | Default | Meaning |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `network` | `zetrix:testnet` | Mainnet spends real funds |
|
|
39
|
+
| `maxPaymentAmount` | `{"*":"0"}` | Per-asset ceiling in raw units. **Refuses every payment until set** |
|
|
40
|
+
| `zetrixAddress` | *(unset)* | Pin an existing holder account instead of creating one |
|
|
41
|
+
|
|
42
|
+
These defaults apply with no action from the subscriber, so a fresh install is safe but **cannot pay**
|
|
43
|
+
until a cap is set. Changing config takes effect without a gateway restart:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openclaw config set plugins.entries.zetrix-agentic-wallet.config.maxPaymentAmount \
|
|
47
|
+
'{"ZTX":"1000000000","*":"0"}' --json
|
|
48
|
+
openclaw mcp reload
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
⚠️ **Configure through `plugins.entries`, never by editing `mcp.servers` directly.** The plugin owns
|
|
52
|
+
that entry and rewrites it from plugin config, so a hand-edit is the wrong layer in two ways. It is
|
|
53
|
+
overwritten the next time the plugin registers — and if it is *not* overwritten, that is worse: the
|
|
54
|
+
plugin fingerprints the entry it wrote and disowns anything that no longer matches, so a hand-edited
|
|
55
|
+
entry is left permanently unmanaged and stops tracking config changes. Neither failure announces itself.
|
|
56
|
+
|
|
57
|
+
Setting plugin config re-registers the entry immediately, because the CLI loads the plugin in-process
|
|
58
|
+
(see [If the MCP entry goes missing](#if-the-mcp-entry-goes-missing)). Editing `openclaw.json` by hand
|
|
59
|
+
does not.
|
|
60
|
+
|
|
61
|
+
## Why it works the way it does
|
|
62
|
+
|
|
63
|
+
Three things were tested against a live gateway (OpenClaw **2026.7.1-2**) and shaped this design. Each
|
|
64
|
+
is stated here with what was observed, because the design looks odd without it — and because if any of
|
|
65
|
+
them changes upstream, the corresponding workaround should be deleted rather than carried forever.
|
|
66
|
+
|
|
67
|
+
**The manifest declares no `mcpServers`, and that is deliberate.** It is the documented way for a plugin
|
|
68
|
+
to contribute an MCP server, but **it is not a field this OpenClaw release implements** — the ClawHub
|
|
69
|
+
Plugin Inspector lists every supported `PluginManifest` field for 2026.7.1-2 and `mcpServers` is not
|
|
70
|
+
among them. So a declared server is silently dropped: never spawned, no diagnostic. Native `api.registerTool` tools do register, but never
|
|
71
|
+
reach a `claude-cli` harness, so an agent cannot call them. Writing the entry into `mcp.servers` from
|
|
72
|
+
the registration hook is the only route that works end to end. *If the manifest route is ever fixed
|
|
73
|
+
upstream, `src/index.ts` is what should be deleted.*
|
|
74
|
+
|
|
75
|
+
**The wallet is vendored into the plugin, as a single CJS bundle.** `openclaw plugins install` is a
|
|
76
|
+
directory copy, not an npm install: a declared dependency produced no `node_modules` in the installed
|
|
77
|
+
copy, and `files` was ignored. So the wallet must ship inside the plugin.
|
|
78
|
+
|
|
79
|
+
Vendoring a real `npm install` tree was the first attempt, and it is the more faithful one — the wallet
|
|
80
|
+
publishes with `zetrix-sdk-nodejs` external, so that is the configuration it is tested in. It had to be
|
|
81
|
+
abandoned: ~8,250 files produced a tarball **OpenClaw cannot install**.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
failed to extract archive: Error: extract tar timed out after 120000ms
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A tarball is how ClawHub ships, so an unextractable package is fatal rather than merely slow. One
|
|
88
|
+
bundled file is 3.4 MB, packs to a 644 KB / 7-file tarball, and installs instantly.
|
|
89
|
+
|
|
90
|
+
⚠️ **The bundle must be CJS.** The SDK and its dependencies use dynamic `require`, which esbuild cannot
|
|
91
|
+
express in ESM output — an ESM bundle loads and then dies at first use with *"Dynamic require of
|
|
92
|
+
`buffer` is not supported"*. In CJS `require` stays native, and both `account.getInfo` and the
|
|
93
|
+
protobufjs-heavy `contract.call` were verified working through the bundle against a live node. A test
|
|
94
|
+
asserts the format, because this fails at *call* time, not at load.
|
|
95
|
+
|
|
96
|
+
**The runtime is then copied *out* of the plugin, and so is the ownership marker.** OpenClaw exposes no
|
|
97
|
+
uninstall hook a plugin can use — `api.lifecycle` offers only `registerRuntimeLifecycle`, and a
|
|
98
|
+
disabled plugin is never loaded, so no cleanup code of ours can ever run. Two consequences:
|
|
99
|
+
|
|
100
|
+
- Pointing the config entry at the plugin's own directory would leave a **broken** server behind on
|
|
101
|
+
uninstall. Pointing it at `<gatewayStateDir>/zetrix-agentic-wallet/runtime` leaves a **working** one.
|
|
102
|
+
- The ownership marker lived inside the plugin at first, and `plugins install --force` wiped it — after
|
|
103
|
+
which the plugin treated its own entry as subscriber-owned and refused to manage it, orphaning the
|
|
104
|
+
entry on every update. It now lives beside the runtime.
|
|
105
|
+
|
|
106
|
+
## If the MCP entry goes missing
|
|
107
|
+
|
|
108
|
+
Removing `mcp.servers["zetrix-agentic-wallet"]` by hand **stops the wallet working immediately** — the
|
|
109
|
+
tools disappear, because there is no server for the agent to call. The plugin still reports as enabled;
|
|
110
|
+
it just provides nothing.
|
|
111
|
+
|
|
112
|
+
The entry is self-healing, but not on the trigger you would expect. Tested on 2026.7.1-2:
|
|
113
|
+
|
|
114
|
+
| Action | Entry restored? |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `openclaw gateway restart` | **No** — waited two minutes, no registration in the logs |
|
|
117
|
+
| `openclaw plugins install` / `enable` | Yes |
|
|
118
|
+
| `openclaw plugins inspect <id> --runtime` | Yes |
|
|
119
|
+
| `openclaw agent …` | Yes |
|
|
120
|
+
|
|
121
|
+
The registration hook runs when the **CLI** loads the plugin in-process, not on a gateway restart. So if
|
|
122
|
+
the entry is ever lost, the fix is any plugin-loading command:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
openclaw plugins inspect zetrix-agentic-wallet --runtime
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Worth knowing for two reasons: a restart is the natural thing to reach for and it will not help, and a
|
|
129
|
+
subscriber who deletes the entry to disable the wallet will find it back after the next plugin
|
|
130
|
+
operation. To disable the wallet properly, disable the plugin.
|
|
131
|
+
|
|
132
|
+
## Uninstalling completely
|
|
133
|
+
|
|
134
|
+
Because there is no uninstall hook, removal is two steps. The wallet keeps working after step 1, which
|
|
135
|
+
is intentional — it is a wallet holding an account, and silently breaking it would be worse.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
openclaw plugins uninstall zetrix-agentic-wallet --force
|
|
139
|
+
openclaw mcp unset zetrix-agentic-wallet
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
⚠️ **Before deleting `<gatewayStateDir>/zetrix-agentic-wallet/`, back up the wallet.** It holds the
|
|
143
|
+
holder identity and the generated HSM password, and that password is the only thing that can authorize
|
|
144
|
+
signing for the account. Run `npx agentic-wallet-mcp export-credentials` in an interactive terminal
|
|
145
|
+
first. There is no recovery once it is gone.
|
|
146
|
+
|
|
147
|
+
Two things to know if `mcp unset` fails: OpenClaw rejects config writes that shrink the file sharply
|
|
148
|
+
(`Config write rejected … size-drop`), which a legitimate removal can trigger, and
|
|
149
|
+
`plugins uninstall --force` may leave the plugin directory behind — OpenClaw auto-loads anything under
|
|
150
|
+
`extensions/`, so the plugin keeps reporting as enabled until that directory is deleted.
|
|
151
|
+
|
|
152
|
+
## Caveats for review
|
|
153
|
+
|
|
154
|
+
- **This plugin writes to `mcp.servers`, which is documented as the operator's authoritative override
|
|
155
|
+
surface.** It never overwrites an entry it did not create, but the inversion is real and was a
|
|
156
|
+
deliberate choice made only after the two supported routes were shown not to work.
|
|
157
|
+
- **Writing config directly bypasses OpenClaw's own write validation**, including the size-drop guard
|
|
158
|
+
above. Our writes only add keys, so they cannot shrink the file, but the bypass is worth knowing.
|
|
159
|
+
- **`plugins.allow` is a whitelist, not additive trust.** If you set it, enumerate every required
|
|
160
|
+
plugin — setting it to a single id excluded the agent harness during testing and left the agent
|
|
161
|
+
unable to run at all.
|
|
162
|
+
|
|
163
|
+
## Validating before publish
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm run build # from the repo root
|
|
167
|
+
node scripts/build.mjs # from openclaw-plugin
|
|
168
|
+
clawhub package validate . --openclaw-version <target>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Run this before treating any plugin change as done. It extracts the real `PluginManifest` type from the
|
|
172
|
+
target OpenClaw release and checks the manifest against it, which catches a class of mistake nothing else
|
|
173
|
+
does: a field that is silently ignored rather than rejected. It found two in this plugin — `uiHints`
|
|
174
|
+
instead of `configUiHints`, and confirmed `mcpServers` is not implemented in 2026.7.1-2 at all.
|
|
175
|
+
|
|
176
|
+
Reports land in `reports/` (gitignored). `--runtime --allow-execute` additionally imports the plugin code
|
|
177
|
+
in an isolated workspace.
|
package/USER_GUIDE.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Zetrix Agentic Wallet — user guide
|
|
2
|
+
|
|
3
|
+
Gives your OpenClaw agent a Zetrix wallet. It can prove who you are, collect verifiable credentials,
|
|
4
|
+
and pay for pay-per-use resources — without you creating an account, choosing a password, or editing
|
|
5
|
+
any configuration.
|
|
6
|
+
|
|
7
|
+
**Two things to know before you start:**
|
|
8
|
+
|
|
9
|
+
- The wallet **creates its own account and password**. You never type either. That also means the
|
|
10
|
+
credentials exist only on this machine — [back them up](#backing-up-your-wallet).
|
|
11
|
+
- It **will not pay for anything** until you [set a spending limit](#setting-your-spending-limit).
|
|
12
|
+
That is deliberate, not a fault.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
openclaw plugins install clawhub:zetrix-agentic-wallet
|
|
20
|
+
openclaw plugins enable zetrix-agentic-wallet
|
|
21
|
+
openclaw gateway restart
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Check it worked:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
openclaw plugins list # zetrix-agentic-wallet, enabled
|
|
28
|
+
openclaw mcp doctor # zetrix-agentic-wallet: ok
|
|
29
|
+
openclaw mcp probe zetrix-agentic-wallet # 10 tools
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Nothing else is needed. No `npx`, no account signup, no password, no MCP configuration.
|
|
33
|
+
|
|
34
|
+
The first time the wallet runs it creates a Zetrix account on **testnet** and stores it under
|
|
35
|
+
`~/.openclaw/zetrix-agentic-wallet/state/`.
|
|
36
|
+
|
|
37
|
+
## 2. Try it
|
|
38
|
+
|
|
39
|
+
Open the dashboard and ask in plain language — you do not need to name tools:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
openclaw dashboard
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **What is my Zetrix wallet address?**
|
|
46
|
+
|
|
47
|
+
You should get an address starting `ZTX3…`, a holder DID, and the network (`zetrix:testnet`).
|
|
48
|
+
|
|
49
|
+
Other things that work straight away, all free:
|
|
50
|
+
|
|
51
|
+
> Do I hold any verifiable credentials?
|
|
52
|
+
> What is my ZTX balance?
|
|
53
|
+
> What attributes does credential template X require?
|
|
54
|
+
|
|
55
|
+
## 3. What it can do
|
|
56
|
+
|
|
57
|
+
| Ask for | Costs money? |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Wallet address, DID, network, balance | no |
|
|
60
|
+
| Which credentials you hold | no |
|
|
61
|
+
| What a credential template requires | no |
|
|
62
|
+
| Reading on-chain contract data | no |
|
|
63
|
+
| Proving your identity to a service | no |
|
|
64
|
+
| **Fetching a resource that charges per use** | **yes** |
|
|
65
|
+
| **Obtaining a verifiable credential** | **yes** |
|
|
66
|
+
| **Starting a Verified AI Birthcert check (owner identity verification via MyDigital ID)** | **yes** |
|
|
67
|
+
|
|
68
|
+
The three paid actions are refused until you set a limit.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Setting your spending limit
|
|
73
|
+
|
|
74
|
+
The wallet starts at **zero** — every payment is declined. This is on purpose: a wallet that could
|
|
75
|
+
spend whatever a website asked for, straight out of the box, is not a safe default.
|
|
76
|
+
|
|
77
|
+
### Through the dashboard
|
|
78
|
+
|
|
79
|
+
Open the plugin's settings and set **Maximum payment per call**. That is the whole change.
|
|
80
|
+
|
|
81
|
+
### From the command line
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
openclaw config set plugins.entries.zetrix-agentic-wallet.config.maxPaymentAmount \
|
|
85
|
+
'{"ZTX":"1000000000","*":"0"}' --json
|
|
86
|
+
openclaw mcp reload
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
It takes effect immediately — no gateway restart.
|
|
90
|
+
|
|
91
|
+
### Reading the limit
|
|
92
|
+
|
|
93
|
+
The value is a map of **token → maximum per payment**, and `"*"` is the fallback for any token you have
|
|
94
|
+
not listed.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "ZTX": "1000000000", "*": "0" }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **`"ZTX": "1000000000"`** — up to 1,000,000,000 raw units of ZTX per payment
|
|
101
|
+
- **`"*": "0"`** — everything else refused
|
|
102
|
+
|
|
103
|
+
Amounts are in **raw units**, not whole tokens. ZTX has 6 decimals, so `1000000000` is **1,000 ZTX**.
|
|
104
|
+
|
|
105
|
+
### One limit for everything, or a limit per token?
|
|
106
|
+
|
|
107
|
+
Both work. Pick based on how much you care about what you are paying *in*.
|
|
108
|
+
|
|
109
|
+
**One limit for everything** — simplest, and fine for testing:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{ "*": "1000000000" }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Any token, up to 1,000,000,000 raw units per payment.
|
|
116
|
+
|
|
117
|
+
**A limit per token** — safer, and what we recommend once real money is involved:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{ "ZTX": "1000000000", "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b": "5000000", "*": "0" }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Only those two are payable, each with its own ceiling; everything else is refused.
|
|
124
|
+
|
|
125
|
+
**A ticker like `"JMYR"` works too**, for tokens the wallet knows. A payment request identifies a
|
|
126
|
+
token by its contract address, so the wallet resolves the ticker to that address for you. If you
|
|
127
|
+
write **both**, the contract address wins — it is the more specific of the two.
|
|
128
|
+
|
|
129
|
+
A ticker the wallet does not recognise still matches nothing, and the payment falls through to `"*"`.
|
|
130
|
+
If a refusal mentions the `"*"` fallback, that is what happened: no limit was set for that asset, so
|
|
131
|
+
use its contract address instead.
|
|
132
|
+
|
|
133
|
+
To find a token's address, ask your agent:
|
|
134
|
+
|
|
135
|
+
> Look up the JMYR token contract address on this network
|
|
136
|
+
|
|
137
|
+
**Why per-token is safer**, despite being more work:
|
|
138
|
+
|
|
139
|
+
- **The same number is not the same value.** 1,000 ZTX and 1,000 JMYR are different amounts of money. A
|
|
140
|
+
single universal limit treats them as interchangeable, because it only counts units.
|
|
141
|
+
- **Decimals vary by token.** ZTX and JMYR both use 6, but nothing guarantees the next one does. On a
|
|
142
|
+
token with 2 decimals, `1000000000` raw units would be ten million tokens.
|
|
143
|
+
- **It auto-approves tokens you have never heard of.** With `{"*": "1000000000"}`, a service can quote
|
|
144
|
+
an obscure token and be paid, as long as the unit count fits. Listing tokens explicitly means you
|
|
145
|
+
decide what is acceptable currency, not the service asking for money.
|
|
146
|
+
|
|
147
|
+
**Once you list any token, the limit becomes an allowlist.** A token with no entry and no `"*"` fallback
|
|
148
|
+
is **denied**, not passed through. Keeping `"*": "0"` means "only the tokens I have listed".
|
|
149
|
+
|
|
150
|
+
### It is a hard limit, not a prompt
|
|
151
|
+
|
|
152
|
+
The limit is enforced by the wallet itself, before any payment is signed. Your agent cannot talk its way
|
|
153
|
+
past it, and neither can a website. If a payment exceeds the limit it is refused, and the agent should
|
|
154
|
+
tell you so rather than retrying.
|
|
155
|
+
|
|
156
|
+
> ⚠️ **Set this through the plugin settings, not by editing `openclaw.json` directly.** The plugin
|
|
157
|
+
> manages its own entry in that file and will either overwrite your edit or stop managing the entry
|
|
158
|
+
> altogether. Neither is obvious when it happens.
|
|
159
|
+
|
|
160
|
+
## Adding funds
|
|
161
|
+
|
|
162
|
+
A brand-new wallet has no funds, so even with a limit set it cannot pay yet.
|
|
163
|
+
|
|
164
|
+
1. Ask your agent for your wallet address
|
|
165
|
+
2. Send testnet ZTX to it from another Zetrix wallet
|
|
166
|
+
3. Ask **"what is my ZTX balance?"** to confirm it arrived
|
|
167
|
+
|
|
168
|
+
Until the address has received anything, the wallet reports it is **not yet activated on chain** — that
|
|
169
|
+
means "send it some ZTX", not "something is broken".
|
|
170
|
+
|
|
171
|
+
## Backing up your wallet
|
|
172
|
+
|
|
173
|
+
**Do this once, now.** The wallet generated its own password and keeps it on this machine. It is the
|
|
174
|
+
only thing that can authorise payments from your account. If you lose this machine without a backup, the
|
|
175
|
+
account and anything in it are **gone permanently** — nobody can recover it, including us.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npx agentic-wallet-mcp export-credentials
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Run it in your own terminal window. It prints your address, DID and password. Store them somewhere
|
|
182
|
+
private, such as a password manager.
|
|
183
|
+
|
|
184
|
+
It only works in an interactive terminal, and it is deliberately **not** something your agent can do —
|
|
185
|
+
so your password never appears in a conversation.
|
|
186
|
+
|
|
187
|
+
## Moving to mainnet
|
|
188
|
+
|
|
189
|
+
Testnet tokens are not real. To use real funds:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
openclaw config set plugins.entries.zetrix-agentic-wallet.config.network zetrix:mainnet
|
|
193
|
+
openclaw mcp reload
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Before you do:
|
|
197
|
+
|
|
198
|
+
- **Back up your wallet** (above) if you have not already
|
|
199
|
+
- **Review your spending limit** — the same number now means real money
|
|
200
|
+
- **The Verified AI Birthcert check is unavailable on mainnet through this plugin.** Its endpoint
|
|
201
|
+
was never confirmed reachable on mainnet, so the wallet refuses to use it there unless an
|
|
202
|
+
operator sets it explicitly — something this plugin does not currently expose a setting for.
|
|
203
|
+
Every other tool works normally.
|
|
204
|
+
- Note that mainnet uses a **different account** from your testnet one
|
|
205
|
+
|
|
206
|
+
## If something is wrong
|
|
207
|
+
|
|
208
|
+
| What you see | What it means |
|
|
209
|
+
|---|---|
|
|
210
|
+
| Agent says the wallet tools are unavailable | Plugin not enabled, or gateway needs restarting. Check `openclaw plugins list` |
|
|
211
|
+
| *"payment blocked … exceeds configured MAX_PAYMENT_AMOUNT"* | Working as intended — raise the limit if the amount is right |
|
|
212
|
+
| *"not activated"* | The address has no funds yet. Send it ZTX |
|
|
213
|
+
| Balance lookup fails but the wallet otherwise works | Usually a network problem reaching the Zetrix node |
|
|
214
|
+
| Everything fails after connecting to a corporate VPN | Some VPNs block the Zetrix endpoints. Try disconnecting |
|
|
215
|
+
|
|
216
|
+
Useful commands:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
openclaw mcp doctor # is the wallet configured correctly?
|
|
220
|
+
openclaw mcp probe zetrix-agentic-wallet # can it start, and what tools does it offer?
|
|
221
|
+
openclaw logs --limit 200 # what did it actually say?
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Removing the wallet
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
openclaw plugins uninstall zetrix-agentic-wallet --force
|
|
228
|
+
openclaw mcp unset zetrix-agentic-wallet
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
⚠️ **Back up first.** Deleting `~/.openclaw/zetrix-agentic-wallet/` destroys the account permanently.
|
|
232
|
+
The wallet keeps working between those two commands — that is intentional, so an uninstall does not
|
|
233
|
+
silently strand an account holding funds.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { join as join2 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/paths.ts
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
function resolvePluginPaths(rootDir) {
|
|
9
|
+
const gatewayStateDir = join(rootDir, "..", "..");
|
|
10
|
+
const homeDir = join(gatewayStateDir, "zetrix-agentic-wallet");
|
|
11
|
+
const runtimeDir = join(homeDir, "runtime");
|
|
12
|
+
return {
|
|
13
|
+
gatewayStateDir,
|
|
14
|
+
configPath: join(gatewayStateDir, "openclaw.json"),
|
|
15
|
+
ownershipPath: join(homeDir, ".mcp-registration.json"),
|
|
16
|
+
homeDir,
|
|
17
|
+
runtimeDir,
|
|
18
|
+
shippedRuntimeDir: join(rootDir, "dist", "runtime"),
|
|
19
|
+
walletStateDir: join(homeDir, "state"),
|
|
20
|
+
walletBundlePath: join(runtimeDir, "server-bundle.cjs")
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/mcp-registration.ts
|
|
25
|
+
var SERVER_NAME = "zetrix-agentic-wallet";
|
|
26
|
+
function buildServerEntry(walletBundlePath, config, stateDir) {
|
|
27
|
+
return {
|
|
28
|
+
command: "node",
|
|
29
|
+
args: [walletBundlePath],
|
|
30
|
+
env: {
|
|
31
|
+
ZETRIX_NETWORK: config.network ?? "zetrix:testnet",
|
|
32
|
+
// Deliberately absent when unconfigured, rather than defaulted here.
|
|
33
|
+
//
|
|
34
|
+
// The wallet's own default is network-aware — testnet allows exactly the credential fee,
|
|
35
|
+
// mainnet refuses everything, because the cap is per call and nothing limits how many calls
|
|
36
|
+
// are made. This layer cannot know the network at config time, so anything sent from here
|
|
37
|
+
// would shadow that and pin every subscriber to one behaviour.
|
|
38
|
+
//
|
|
39
|
+
// The manifest declares no `default` for the same reason: OpenClaw materialises configSchema
|
|
40
|
+
// defaults into plugin config with no operator action (SPIKE-0.4-FINDINGS.md §2.1), so a
|
|
41
|
+
// declared default would arrive here as a real value and never reach this branch.
|
|
42
|
+
...config.maxPaymentAmount ? { MAX_PAYMENT_AMOUNT: JSON.stringify(config.maxPaymentAmount) } : {},
|
|
43
|
+
ZETRIX_WALLET_STATE_DIR: stateDir,
|
|
44
|
+
...config.zetrixAddress ? { ZETRIX_ADDRESS: config.zetrixAddress } : {}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function readConfig(deps) {
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = deps.readFile(deps.configPath);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
deps.log(`could not read ${deps.configPath}: ${e.message} \u2014 skipping MCP registration`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
59
|
+
deps.log(`could not use ${deps.configPath}: not a JSON object \u2014 skipping MCP registration`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
deps.log(`could not parse ${deps.configPath}: ${e.message} \u2014 skipping MCP registration`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function readOwnership(deps) {
|
|
69
|
+
if (!deps.exists(deps.ownershipPath)) return null;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(deps.readFile(deps.ownershipPath));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function weOwnIt(deps, existing) {
|
|
77
|
+
const record = readOwnership(deps);
|
|
78
|
+
if (!record) return false;
|
|
79
|
+
if (!existing) return true;
|
|
80
|
+
if (!record.entry) return true;
|
|
81
|
+
return JSON.stringify(record.entry) === JSON.stringify(existing);
|
|
82
|
+
}
|
|
83
|
+
function writeOwnership(deps, entry) {
|
|
84
|
+
const record = {
|
|
85
|
+
serverName: SERVER_NAME,
|
|
86
|
+
entry,
|
|
87
|
+
note: "Created by the Zetrix Agentic Wallet plugin. The entry above is a fingerprint: the plugin only manages mcp.servers while the live entry still matches it, so a hand-edited entry is never overwritten. Deleting this file makes the plugin treat the entry as subscriber-owned."
|
|
88
|
+
};
|
|
89
|
+
deps.writeFile(deps.ownershipPath, `${JSON.stringify(record, null, 2)}
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
function writeConfigAtomically(deps, config) {
|
|
93
|
+
const tmp = `${deps.configPath}.zetrix-tmp`;
|
|
94
|
+
deps.writeFile(tmp, `${JSON.stringify(config, null, 2)}
|
|
95
|
+
`);
|
|
96
|
+
deps.renameFile(tmp, deps.configPath);
|
|
97
|
+
}
|
|
98
|
+
function registerServer(deps, entry) {
|
|
99
|
+
const config = readConfig(deps);
|
|
100
|
+
if (!config) return;
|
|
101
|
+
const existing = config.mcp?.servers?.[SERVER_NAME];
|
|
102
|
+
if (existing && !weOwnIt(deps, existing)) {
|
|
103
|
+
deps.log(
|
|
104
|
+
`mcp.servers["${SERVER_NAME}"] is already present and was not created by this plugin (or was changed since) \u2014 leaving it untouched. Remove it if you want the plugin to manage the server.`
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (existing && JSON.stringify(existing) === JSON.stringify(entry)) {
|
|
109
|
+
writeOwnership(deps, entry);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
config.mcp = config.mcp ?? {};
|
|
113
|
+
config.mcp.servers = config.mcp.servers ?? {};
|
|
114
|
+
config.mcp.servers[SERVER_NAME] = entry;
|
|
115
|
+
writeConfigAtomically(deps, config);
|
|
116
|
+
writeOwnership(deps, entry);
|
|
117
|
+
deps.log(existing ? `refreshed mcp.servers["${SERVER_NAME}"]` : `registered mcp.servers["${SERVER_NAME}"]`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/index.ts
|
|
121
|
+
function makeDeps(paths, log) {
|
|
122
|
+
return {
|
|
123
|
+
configPath: paths.configPath,
|
|
124
|
+
ownershipPath: paths.ownershipPath,
|
|
125
|
+
readFile: (p) => readFileSync(p, "utf8"),
|
|
126
|
+
writeFile: (p, c) => writeFileSync(p, c, { encoding: "utf8", mode: 384 }),
|
|
127
|
+
renameFile: (from, to) => renameSync(from, to),
|
|
128
|
+
exists: (p) => existsSync(p),
|
|
129
|
+
removeFile: (p) => rmSync(p, { force: true }),
|
|
130
|
+
log
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function pathIsExpected(paths) {
|
|
134
|
+
const expectedSuffix = join2("zetrix-agentic-wallet", "runtime");
|
|
135
|
+
return paths.runtimeDir.endsWith(expectedSuffix) && paths.runtimeDir.startsWith(paths.homeDir) && paths.homeDir.length > expectedSuffix.length;
|
|
136
|
+
}
|
|
137
|
+
function shippedBundleMatchesDigest(paths, shippedVersion, log) {
|
|
138
|
+
const recorded = /sha256:([0-9a-f]{64})/.exec(shippedVersion)?.[1];
|
|
139
|
+
if (!recorded) return true;
|
|
140
|
+
const shippedBundle = join2(paths.shippedRuntimeDir, "server-bundle.cjs");
|
|
141
|
+
const actual = createHash("sha256").update(readFileSync(shippedBundle)).digest("hex");
|
|
142
|
+
if (actual === recorded) return true;
|
|
143
|
+
log(
|
|
144
|
+
`refusing to install the wallet runtime: the shipped bundle does not match the digest recorded in VERSION (expected ${recorded.slice(0, 12)}, got ${actual.slice(0, 12)}). The package may be corrupt or modified \u2014 reinstall it from a trusted source.`
|
|
145
|
+
);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
function syncRuntime(paths, log) {
|
|
149
|
+
const shippedVersionFile = join2(paths.shippedRuntimeDir, "VERSION");
|
|
150
|
+
if (!existsSync(shippedVersionFile)) {
|
|
151
|
+
log(`the plugin is missing its wallet runtime at ${paths.shippedRuntimeDir} \u2014 reinstall the plugin`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const shipped = readFileSync(shippedVersionFile, "utf8").trim();
|
|
155
|
+
const installedVersionFile = join2(paths.runtimeDir, "VERSION");
|
|
156
|
+
const installed = existsSync(installedVersionFile) ? readFileSync(installedVersionFile, "utf8").trim() : null;
|
|
157
|
+
if (installed === shipped && existsSync(paths.walletBundlePath)) return true;
|
|
158
|
+
if (!pathIsExpected(paths)) {
|
|
159
|
+
log(`refusing to install the wallet runtime: unexpected target path ${paths.runtimeDir}`);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (!shippedBundleMatchesDigest(paths, shipped, log)) return false;
|
|
163
|
+
const describe = (v) => {
|
|
164
|
+
if (!v) return "none";
|
|
165
|
+
const [nameVersion, hashLine = ""] = v.split("\n");
|
|
166
|
+
const short = hashLine.replace("sha256:", "").slice(0, 12);
|
|
167
|
+
return short ? `${nameVersion} (${short})` : nameVersion;
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
mkdirSync(paths.homeDir, { recursive: true });
|
|
171
|
+
rmSync(paths.runtimeDir, { recursive: true, force: true });
|
|
172
|
+
cpSync(paths.shippedRuntimeDir, paths.runtimeDir, { recursive: true });
|
|
173
|
+
log(
|
|
174
|
+
installed ? `updated the wallet runtime: ${describe(installed)} -> ${describe(shipped)}` : `installed the wallet runtime ${describe(shipped)}`
|
|
175
|
+
);
|
|
176
|
+
return true;
|
|
177
|
+
} catch (e) {
|
|
178
|
+
log(`could not install the wallet runtime into ${paths.runtimeDir}: ${e.message}`);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function register(api) {
|
|
183
|
+
const paths = resolvePluginPaths(api.rootDir);
|
|
184
|
+
const log = (m) => {
|
|
185
|
+
const line = `zetrix-agentic-wallet: ${m}`;
|
|
186
|
+
if (api.logger?.info) api.logger.info(line);
|
|
187
|
+
else process.stderr.write(`${line}
|
|
188
|
+
`);
|
|
189
|
+
};
|
|
190
|
+
const deps = makeDeps(paths, log);
|
|
191
|
+
if (!syncRuntime(paths, log)) {
|
|
192
|
+
log("the plugin is installed but cannot provide wallet tools");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
registerServer(deps, buildServerEntry(paths.walletBundlePath, api.pluginConfig ?? {}, paths.walletStateDir));
|
|
196
|
+
}
|
|
197
|
+
var index_default = { register };
|
|
198
|
+
export {
|
|
199
|
+
index_default as default,
|
|
200
|
+
register
|
|
201
|
+
};
|