@agentlayer.tech/wallet 0.1.13 → 0.1.14

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.
@@ -4,17 +4,25 @@
4
4
  These instructions apply to the entire `.openclaw/` tree.
5
5
 
6
6
  ## Purpose
7
- This tree contains local OpenClaw host-side workspace assets. In the current repo, its primary responsibility is the `agent-wallet` extension that bridges OpenClaw to the authoritative Python `agent-wallet` backend.
7
+ This tree contains local OpenClaw host-side workspace assets. In the current repo, its main responsibilities are:
8
+
9
+ - the `agent-wallet` extension that bridges OpenClaw to the authoritative Python `agent-wallet` backend
10
+ - the `pay-bridge` extension that bridges OpenClaw to the local `pay` CLI for paid API discovery and execution
8
11
 
9
12
  ## Current structure
10
13
  - `.openclaw/extensions/agent-wallet/index.ts` — TypeScript extension entrypoint registered by OpenClaw.
11
14
  - `.openclaw/extensions/agent-wallet/openclaw.plugin.json` — plugin manifest and config schema.
12
15
  - `.openclaw/extensions/agent-wallet/package.json` — extension package metadata.
13
16
  - `.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md` — user-facing operational wallet safety guidance.
17
+ - `.openclaw/extensions/pay-bridge/index.ts` — TypeScript entrypoint for the local `pay` CLI bridge.
18
+ - `.openclaw/extensions/pay-bridge/openclaw.plugin.json` — plugin manifest and config schema for pay tools.
19
+ - `.openclaw/extensions/pay-bridge/core.mjs` — local `pay` command execution and output shaping.
20
+ - `.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md` — user-facing operational guidance for paid API usage.
14
21
 
15
22
  ## Design intent
16
23
  - Keep the TypeScript extension thin and host-oriented.
17
24
  - Let Python own wallet logic, policy, approvals, signing rules, and Solana implementation details.
25
+ - Let `pay` remain the source of truth for paid API wallet/account behavior.
18
26
  - Let the extension focus on:
19
27
  - resolving config
20
28
  - locating the Python package
@@ -27,6 +35,7 @@ This tree contains local OpenClaw host-side workspace assets. In the current rep
27
35
 
28
36
  ### Keep bridge logic thin
29
37
  - Do not duplicate business logic from Python unless OpenClaw requires it at registration time.
38
+ - Do not duplicate payment protocol logic from `pay`; prefer invoking the local CLI and shaping its output.
30
39
  - Do not reimplement approval validation, transaction policy, wallet derivation, or Solana-specific rules in TypeScript.
31
40
  - Prefer forwarding config into the CLI bridge and letting Python decide runtime behavior.
32
41
  - Treat this layer as a transport and schema bridge, not an execution authority.
@@ -0,0 +1,32 @@
1
+ # pay-bridge
2
+
3
+ Thin OpenClaw bridge to the locally installed `pay` CLI.
4
+
5
+ This plugin is intentionally separate from `agent-wallet`:
6
+
7
+ - `agent-wallet` remains the execution wallet stack for Solana/EVM/BTC
8
+ - `pay-bridge` only discovers and calls paid APIs through `pay`
9
+ - the `pay` wallet stays separate from the AgentLayer wallet runtime
10
+
11
+ ## Exposed tools
12
+
13
+ - `pay_status`
14
+ - `pay_wallet_info`
15
+ - `pay_search_services`
16
+ - `pay_get_service_endpoints`
17
+ - `pay_api_request`
18
+
19
+ ## Intended workflow
20
+
21
+ 1. `pay_status`
22
+ 2. `pay_search_services`
23
+ 3. `pay_get_service_endpoints`
24
+ 4. `pay_api_request`
25
+
26
+ `pay_api_request` is deliberately narrow:
27
+
28
+ - it requires a `service_fqn`, `resource`, and `url`
29
+ - it validates the URL against `pay skills endpoints`
30
+ - it requires `purpose` and `user_confirmed=true`
31
+
32
+ This keeps the bridge thin and prevents it from becoming a generic arbitrary paid-curl launcher.
@@ -0,0 +1,287 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+ const ANSI_RE = /\u001b\[[0-9;]*m/g;
6
+
7
+ function stripAnsi(text) {
8
+ return String(text || "").replace(ANSI_RE, "");
9
+ }
10
+
11
+ function nonEmptyLines(text) {
12
+ return stripAnsi(text)
13
+ .split(/\r?\n/)
14
+ .map((line) => line.trim())
15
+ .filter(Boolean);
16
+ }
17
+
18
+ function extractLastJsonValue(text) {
19
+ const lines = nonEmptyLines(text);
20
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
21
+ const candidate = lines[i];
22
+ if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue;
23
+ try {
24
+ return JSON.parse(candidate);
25
+ } catch {
26
+ continue;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function collectStringLeaves(value, acc = new Set()) {
33
+ if (typeof value === "string") {
34
+ acc.add(value);
35
+ return acc;
36
+ }
37
+ if (Array.isArray(value)) {
38
+ for (const item of value) collectStringLeaves(item, acc);
39
+ return acc;
40
+ }
41
+ if (value && typeof value === "object") {
42
+ for (const item of Object.values(value)) collectStringLeaves(item, acc);
43
+ }
44
+ return acc;
45
+ }
46
+
47
+ function withAccountArgs(args, account) {
48
+ if (!account) return args;
49
+ return [...args, "--account", account];
50
+ }
51
+
52
+ export function resolvePayBinary(config = {}) {
53
+ return (
54
+ config.payBinary ||
55
+ process.env.OPENCLAW_PAY_BINARY ||
56
+ "pay"
57
+ );
58
+ }
59
+
60
+ export async function runPayCommand(payBinary, args, options = {}) {
61
+ const { cwd, input = null } = options;
62
+ let stdout = "";
63
+ let stderr = "";
64
+ try {
65
+ const result = await execFileAsync(payBinary, args, {
66
+ cwd,
67
+ env: { ...process.env },
68
+ input,
69
+ maxBuffer: 1024 * 1024 * 8,
70
+ });
71
+ stdout = result.stdout ?? "";
72
+ stderr = result.stderr ?? "";
73
+ } catch (error) {
74
+ stdout = typeof error?.stdout === "string" ? error.stdout : "";
75
+ stderr = typeof error?.stderr === "string" ? error.stderr : "";
76
+ const payload = extractLastJsonValue(stdout) || extractLastJsonValue(stderr);
77
+ const message =
78
+ payload?.error?.message ||
79
+ payload?.message ||
80
+ stripAnsi(stderr || stdout || error?.message || "pay command failed").trim() ||
81
+ "pay command failed";
82
+ const wrapped = new Error(message);
83
+ wrapped.stdout = stdout;
84
+ wrapped.stderr = stderr;
85
+ wrapped.details = payload && typeof payload === "object" ? payload : null;
86
+ throw wrapped;
87
+ }
88
+ return { stdout, stderr };
89
+ }
90
+
91
+ export function parseWhoamiOutput(stdout) {
92
+ const lines = nonEmptyLines(stdout);
93
+ const systemUser = lines[0] || null;
94
+ const noAccount = lines.some((line) => /no mainnet account/i.test(line));
95
+ return {
96
+ system_user: systemUser,
97
+ has_mainnet_account: !noAccount,
98
+ raw_lines: lines,
99
+ };
100
+ }
101
+
102
+ export function parseAccountListOutput(stdout) {
103
+ const lines = nonEmptyLines(stdout);
104
+ const noAccounts = lines.some((line) => /no accounts found/i.test(line));
105
+ return {
106
+ has_accounts: !noAccounts,
107
+ raw_lines: lines,
108
+ };
109
+ }
110
+
111
+ export async function getPayStatus(config = {}, options = {}) {
112
+ const payBinary = resolvePayBinary(config);
113
+ const versionResult = await runPayCommand(payBinary, ["--version"], options);
114
+ const whoamiResult = await runPayCommand(
115
+ payBinary,
116
+ withAccountArgs(["whoami"], config.defaultAccount),
117
+ options
118
+ );
119
+ const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
120
+ return {
121
+ installed: true,
122
+ pay_binary: payBinary,
123
+ version: stripAnsi(versionResult.stdout).trim() || null,
124
+ account_configured: parseWhoamiOutput(whoamiResult.stdout).has_mainnet_account,
125
+ has_any_accounts: parseAccountListOutput(accountListResult.stdout).has_accounts,
126
+ whoami: parseWhoamiOutput(whoamiResult.stdout),
127
+ accounts: parseAccountListOutput(accountListResult.stdout),
128
+ };
129
+ }
130
+
131
+ export async function getPayWalletInfo(config = {}, options = {}) {
132
+ const payBinary = resolvePayBinary(config);
133
+ const whoamiResult = await runPayCommand(
134
+ payBinary,
135
+ withAccountArgs(["whoami"], config.defaultAccount),
136
+ options
137
+ );
138
+ const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
139
+ return {
140
+ pay_binary: payBinary,
141
+ default_account: config.defaultAccount || null,
142
+ whoami: parseWhoamiOutput(whoamiResult.stdout),
143
+ accounts: parseAccountListOutput(accountListResult.stdout),
144
+ notes: [
145
+ "This wallet is managed by pay.sh and is separate from the AgentLayer execution wallet.",
146
+ ],
147
+ };
148
+ }
149
+
150
+ export async function searchPayServices(config = {}, params = {}, options = {}) {
151
+ const payBinary = resolvePayBinary(config);
152
+ const args = ["skills", "search"];
153
+ if (params.query) args.push(String(params.query));
154
+ if (params.category) args.push("--category", String(params.category));
155
+ args.push("--json");
156
+ const { stdout, stderr } = await runPayCommand(
157
+ payBinary,
158
+ withAccountArgs(args, params.account || config.defaultAccount),
159
+ options
160
+ );
161
+ const parsed = JSON.parse(stdout.trim() || "{}");
162
+ return {
163
+ query: params.query || "",
164
+ category: params.category || null,
165
+ results: parsed,
166
+ warnings: nonEmptyLines(stderr),
167
+ };
168
+ }
169
+
170
+ export async function getPayServiceEndpoints(config = {}, params = {}, options = {}) {
171
+ const payBinary = resolvePayBinary(config);
172
+ const args = [
173
+ "skills",
174
+ "endpoints",
175
+ String(params.service_fqn),
176
+ String(params.resource),
177
+ "--json",
178
+ ];
179
+ const { stdout, stderr } = await runPayCommand(
180
+ payBinary,
181
+ withAccountArgs(args, params.account || config.defaultAccount),
182
+ options
183
+ );
184
+ const parsed = JSON.parse(stdout.trim() || "{}");
185
+ return {
186
+ service_fqn: String(params.service_fqn),
187
+ resource: String(params.resource),
188
+ endpoints: parsed,
189
+ warnings: nonEmptyLines(stderr),
190
+ };
191
+ }
192
+
193
+ function ensureHttps(url, requireHttps = true) {
194
+ if (!requireHttps) return;
195
+ const parsed = new URL(url);
196
+ if (parsed.protocol !== "https:") {
197
+ throw new Error("pay_api_request only allows https URLs.");
198
+ }
199
+ }
200
+
201
+ function appendQuery(url, query) {
202
+ const parsed = new URL(url);
203
+ if (query && typeof query === "object") {
204
+ for (const [key, value] of Object.entries(query)) {
205
+ if (value === undefined || value === null) continue;
206
+ parsed.searchParams.set(key, String(value));
207
+ }
208
+ }
209
+ return parsed.toString();
210
+ }
211
+
212
+ export function endpointPayloadContainsUrl(endpointPayload, url) {
213
+ const strings = collectStringLeaves(endpointPayload);
214
+ return strings.has(url);
215
+ }
216
+
217
+ export async function executePayApiRequest(config = {}, params = {}, options = {}) {
218
+ if (params.user_confirmed !== true) {
219
+ throw new Error("pay_api_request requires user_confirmed=true.");
220
+ }
221
+ if (!params.purpose || !String(params.purpose).trim()) {
222
+ throw new Error("pay_api_request requires a non-empty purpose.");
223
+ }
224
+ if (!params.service_fqn || !params.resource || !params.url) {
225
+ throw new Error("pay_api_request requires service_fqn, resource, and url.");
226
+ }
227
+ if (params.json_body !== undefined && params.text_body !== undefined) {
228
+ throw new Error("Provide either json_body or text_body, not both.");
229
+ }
230
+
231
+ const endpointData = await getPayServiceEndpoints(config, {
232
+ account: params.account,
233
+ resource: params.resource,
234
+ service_fqn: params.service_fqn,
235
+ }, options);
236
+
237
+ const finalUrl = appendQuery(String(params.url), params.query);
238
+ ensureHttps(finalUrl, config.requireHttps !== false);
239
+ if (!endpointPayloadContainsUrl(endpointData.endpoints, String(params.url))) {
240
+ throw new Error("The requested URL is not present in pay_get_service_endpoints for this service/resource.");
241
+ }
242
+
243
+ const payBinary = resolvePayBinary(config);
244
+ const method = String(params.method || "GET").toUpperCase();
245
+ const args = ["curl"];
246
+ if (params.account || config.defaultAccount) {
247
+ args.push("--account", String(params.account || config.defaultAccount));
248
+ }
249
+ args.push("--request", method);
250
+
251
+ const headers = params.headers && typeof params.headers === "object" ? params.headers : {};
252
+ for (const [key, value] of Object.entries(headers)) {
253
+ args.push("--header", `${key}: ${String(value)}`);
254
+ }
255
+
256
+ if (params.json_body !== undefined) {
257
+ const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
258
+ if (!hasContentType) {
259
+ args.push("--header", "content-type: application/json");
260
+ }
261
+ args.push("--data", JSON.stringify(params.json_body));
262
+ } else if (params.text_body !== undefined) {
263
+ args.push("--data", String(params.text_body));
264
+ }
265
+
266
+ args.push(finalUrl);
267
+ const { stdout, stderr } = await runPayCommand(payBinary, args, options);
268
+ const trimmed = stdout.trim();
269
+ let responseBody = trimmed;
270
+ if (params.parse_json_response !== false) {
271
+ try {
272
+ responseBody = JSON.parse(trimmed);
273
+ } catch {
274
+ responseBody = trimmed;
275
+ }
276
+ }
277
+ return {
278
+ method,
279
+ purpose: String(params.purpose),
280
+ request_url: finalUrl,
281
+ service_fqn: String(params.service_fqn),
282
+ resource: String(params.resource),
283
+ response: responseBody,
284
+ raw_response_text: trimmed,
285
+ warnings: nonEmptyLines(stderr),
286
+ };
287
+ }
@@ -0,0 +1,196 @@
1
+ import {
2
+ executePayApiRequest,
3
+ getPayServiceEndpoints,
4
+ getPayStatus,
5
+ getPayWalletInfo,
6
+ searchPayServices,
7
+ } from "./core.mjs";
8
+
9
+ const PLUGIN_ID = "pay-bridge";
10
+
11
+ function asContent(data) {
12
+ return {
13
+ content: [
14
+ {
15
+ type: "text",
16
+ text: JSON.stringify(data, null, 2),
17
+ },
18
+ ],
19
+ };
20
+ }
21
+
22
+ function resolvePluginConfig(api) {
23
+ const globalConfig = api?.config ?? {};
24
+ const pluginEntry = globalConfig?.plugins?.entries?.[PLUGIN_ID];
25
+ return pluginEntry?.config ?? globalConfig?.config ?? {};
26
+ }
27
+
28
+ function registerTool(api, definition) {
29
+ api.registerTool({
30
+ name: definition.name,
31
+ description: definition.description,
32
+ parameters: definition.parameters,
33
+ returns: {
34
+ type: "object",
35
+ additionalProperties: true,
36
+ },
37
+ async execute(_id, params = {}) {
38
+ const config = resolvePluginConfig(api);
39
+ let result;
40
+ if (definition.name === "pay_status") {
41
+ result = await getPayStatus(config, { cwd: process.cwd() });
42
+ } else if (definition.name === "pay_wallet_info") {
43
+ result = await getPayWalletInfo(config, { cwd: process.cwd() });
44
+ } else if (definition.name === "pay_search_services") {
45
+ result = await searchPayServices(config, params, { cwd: process.cwd() });
46
+ } else if (definition.name === "pay_get_service_endpoints") {
47
+ result = await getPayServiceEndpoints(config, params, { cwd: process.cwd() });
48
+ } else if (definition.name === "pay_api_request") {
49
+ result = await executePayApiRequest(config, params, { cwd: process.cwd() });
50
+ } else {
51
+ throw new Error(`Unsupported pay-bridge tool: ${definition.name}`);
52
+ }
53
+ return asContent(result);
54
+ },
55
+ });
56
+ }
57
+
58
+ const toolDefinitions = [
59
+ {
60
+ name: "pay_status",
61
+ description:
62
+ "Check whether the local pay.sh CLI is installed and whether a pay wallet/account is configured. Use this before any paid API workflow.",
63
+ parameters: {
64
+ type: "object",
65
+ properties: {},
66
+ additionalProperties: false,
67
+ },
68
+ },
69
+ {
70
+ name: "pay_wallet_info",
71
+ description:
72
+ "Show pay wallet/account status. This wallet is separate from the AgentLayer execution wallet and is only for pay.sh API payments.",
73
+ parameters: {
74
+ type: "object",
75
+ properties: {},
76
+ additionalProperties: false,
77
+ },
78
+ },
79
+ {
80
+ name: "pay_search_services",
81
+ description:
82
+ "Search the pay.sh skills catalog for paid APIs. Prefer this instead of guessing URLs manually.",
83
+ parameters: {
84
+ type: "object",
85
+ properties: {
86
+ query: {
87
+ type: "string",
88
+ description: "Search text for service names, descriptions, or endpoint paths.",
89
+ },
90
+ category: {
91
+ type: "string",
92
+ description: "Optional category filter such as ai_ml, maps, data, compute, search, crypto_finance.",
93
+ },
94
+ account: {
95
+ type: "string",
96
+ description: "Optional pay account override.",
97
+ },
98
+ },
99
+ additionalProperties: false,
100
+ },
101
+ },
102
+ {
103
+ name: "pay_get_service_endpoints",
104
+ description:
105
+ "List the discoverable endpoints for a pay.sh service/resource pair and return their gateway URLs. Use the returned URL with pay_api_request.",
106
+ parameters: {
107
+ type: "object",
108
+ properties: {
109
+ service_fqn: {
110
+ type: "string",
111
+ description: "Fully qualified pay service name, for example solana-foundation/google/language.",
112
+ },
113
+ resource: {
114
+ type: "string",
115
+ description: "Resource name inside the service, for example entities or jobs.",
116
+ },
117
+ account: {
118
+ type: "string",
119
+ description: "Optional pay account override.",
120
+ },
121
+ },
122
+ required: ["service_fqn", "resource"],
123
+ additionalProperties: false,
124
+ },
125
+ },
126
+ {
127
+ name: "pay_api_request",
128
+ description:
129
+ "Call a paid API through the local pay.sh CLI using a URL returned by pay_get_service_endpoints. Requires explicit user_confirmed=true and keeps the pay wallet separate from AgentLayer execution wallets.",
130
+ optional: true,
131
+ parameters: {
132
+ type: "object",
133
+ properties: {
134
+ service_fqn: {
135
+ type: "string",
136
+ description: "Fully qualified pay service name used to validate the request URL.",
137
+ },
138
+ resource: {
139
+ type: "string",
140
+ description: "Resource name used to validate the request URL.",
141
+ },
142
+ url: {
143
+ type: "string",
144
+ description: "Exact HTTPS gateway URL returned by pay_get_service_endpoints.",
145
+ },
146
+ method: {
147
+ type: "string",
148
+ description: "HTTP method such as GET or POST.",
149
+ },
150
+ headers: {
151
+ type: "object",
152
+ additionalProperties: { type: "string" },
153
+ description: "Optional HTTP headers.",
154
+ },
155
+ query: {
156
+ type: "object",
157
+ additionalProperties: true,
158
+ description: "Optional query parameters to append to the URL.",
159
+ },
160
+ json_body: {
161
+ description: "Optional JSON request body.",
162
+ },
163
+ text_body: {
164
+ type: "string",
165
+ description: "Optional raw text request body. Do not provide with json_body.",
166
+ },
167
+ account: {
168
+ type: "string",
169
+ description: "Optional pay account override.",
170
+ },
171
+ parse_json_response: {
172
+ type: "boolean",
173
+ description: "If true, attempt to parse the response body as JSON.",
174
+ },
175
+ purpose: {
176
+ type: "string",
177
+ description: "Short user-facing reason for this paid API call.",
178
+ },
179
+ user_confirmed: {
180
+ type: "boolean",
181
+ description: "Must be true for paid API requests.",
182
+ },
183
+ },
184
+ required: ["service_fqn", "resource", "url", "method", "purpose", "user_confirmed"],
185
+ additionalProperties: false,
186
+ },
187
+ },
188
+ ];
189
+
190
+ export default function registerPayBridgePlugin(api) {
191
+ api?.logger?.info?.("[pay-bridge] registering pay.sh OpenClaw plugin");
192
+ for (const definition of toolDefinitions) {
193
+ registerTool(api, definition);
194
+ }
195
+ api?.logger?.info?.(`[pay-bridge] registered ${toolDefinitions.length} pay tools`);
196
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "id": "pay-bridge",
3
+ "name": "pay.sh Bridge",
4
+ "description": "Thin OpenClaw bridge to the local pay.sh CLI for paid API discovery and execution.",
5
+ "version": "0.1.0",
6
+ "contracts": {
7
+ "tools": [
8
+ "pay_status",
9
+ "pay_wallet_info",
10
+ "pay_search_services",
11
+ "pay_get_service_endpoints",
12
+ "pay_api_request"
13
+ ]
14
+ },
15
+ "skills": ["skills/pay-operator"],
16
+ "configSchema": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "properties": {
20
+ "payBinary": {
21
+ "type": "string",
22
+ "description": "Absolute path or executable name for the local pay.sh binary."
23
+ },
24
+ "defaultAccount": {
25
+ "type": "string",
26
+ "description": "Optional pay account name to use when a tool call does not specify one."
27
+ },
28
+ "requireHttps": {
29
+ "type": "boolean",
30
+ "description": "If true, pay_api_request refuses non-HTTPS URLs."
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "pay-bridge",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "openclaw": {
7
+ "extensions": [
8
+ "./index.ts"
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,20 @@
1
+ # pay-operator
2
+
3
+ Use this skill when the user wants to discover or call paid APIs through `pay`.
4
+
5
+ ## Rules
6
+
7
+ - Treat the `pay` wallet as separate from the AgentLayer execution wallet.
8
+ - Do not use `agent-wallet` tools for `pay` account management.
9
+ - Do not fall back to shell commands when the `pay-bridge` tools exist.
10
+ - Prefer this order:
11
+ 1. `pay_status`
12
+ 2. `pay_search_services`
13
+ 3. `pay_get_service_endpoints`
14
+ 4. `pay_api_request`
15
+
16
+ ## Notes
17
+
18
+ - `pay_api_request` requires `purpose` and `user_confirmed=true`.
19
+ - Use the exact gateway URL returned by `pay_get_service_endpoints`.
20
+ - If `pay_status` shows no configured account, stop and ask the user to finish `pay setup`.
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ import {
4
+ endpointPayloadContainsUrl,
5
+ parseAccountListOutput,
6
+ parseWhoamiOutput,
7
+ } from "./core.mjs";
8
+
9
+ const whoami = parseWhoamiOutput(`
10
+ yuriytsygankov
11
+ \u001b[2m(no mainnet account — run \`pay setup\`)\u001b[0m
12
+ `);
13
+ assert.equal(whoami.system_user, "yuriytsygankov");
14
+ assert.equal(whoami.has_mainnet_account, false);
15
+
16
+ const accounts = parseAccountListOutput(`
17
+ \u001b[2mNo accounts found. Run \`pay account new\` to create one.\u001b[0m
18
+ `);
19
+ assert.equal(accounts.has_accounts, false);
20
+
21
+ const endpointPayload = {
22
+ endpoints: [
23
+ {
24
+ method: "POST",
25
+ url: "https://api.example.com/v1/invoke",
26
+ },
27
+ ],
28
+ };
29
+ assert.equal(
30
+ endpointPayloadContainsUrl(endpointPayload, "https://api.example.com/v1/invoke"),
31
+ true
32
+ );
33
+ assert.equal(
34
+ endpointPayloadContainsUrl(endpointPayload, "https://api.example.com/v1/other"),
35
+ false
36
+ );
37
+
38
+ console.log("smoke_pay_bridge: ok");
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.14 - 2026-05-13
6
+
7
+ - Added a separate `.openclaw/extensions/pay-bridge/` plugin that keeps
8
+ `pay.sh` API payments outside the main AgentLayer execution wallet stack.
9
+ - Added OpenClaw tools for local `pay` discovery and execution:
10
+ `pay_status`, `pay_wallet_info`, `pay_search_services`,
11
+ `pay_get_service_endpoints`, and `pay_api_request`.
12
+ - Updated the local OpenClaw installer/runtime config flow to package and
13
+ enable the `pay-bridge` plugin alongside `agent-wallet`, including its
14
+ tool allowlist and absolute `pay` binary path when available.
5
15
  - Added an optional Hermes Agent bridge plugin under `hermes/plugins/agent_wallet`
6
16
  that forwards into the existing Python wallet CLI instead of duplicating
7
17
  OpenClaw wallet tools or policy.
package/README.md CHANGED
@@ -18,7 +18,7 @@ AgentLayer is a beta local-first wallet and finance stack for agents.
18
18
  The repository includes:
19
19
 
20
20
  - `agent-wallet/` - the main wallet backend for AgentLayer
21
- - `.openclaw/` - the local AgentLayer bridge layer
21
+ - `.openclaw/` - the local AgentLayer bridge layer, including the OpenClaw wallet bridge and the `pay.sh` API-payments bridge
22
22
  - `hermes/` - optional Hermes Agent plugin bridge for the same wallet backend
23
23
  - `wdk-btc-wallet/` - the local Bitcoin wallet service
24
24
  - `wdk-evm-wallet/` - the local EVM wallet service
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.13"
7
+ version = "0.1.14"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,10 +1,11 @@
1
- """Patch an OpenClaw config file for the agent-wallet plugin."""
1
+ """Patch an OpenClaw config file for the AgentLayer OpenClaw plugins."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import json
7
7
  import os
8
+ import shutil
8
9
  import sys
9
10
  from datetime import datetime, timezone
10
11
  from pathlib import Path
@@ -44,6 +45,15 @@ OPTIONAL_TOOLS = [
44
45
  "request_devnet_airdrop",
45
46
  ]
46
47
 
48
+ PAY_BRIDGE_PLUGIN_ID = "pay-bridge"
49
+ PAY_BRIDGE_TOOLS = [
50
+ "pay_status",
51
+ "pay_wallet_info",
52
+ "pay_search_services",
53
+ "pay_get_service_endpoints",
54
+ "pay_api_request",
55
+ ]
56
+
47
57
 
48
58
  def _default_config_path() -> Path:
49
59
  return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
@@ -83,6 +93,13 @@ def _default_extension_path() -> Path:
83
93
  return _repo_root() / ".openclaw" / "extensions" / "agent-wallet"
84
94
 
85
95
 
96
+ def _default_pay_bridge_extension_path() -> Path:
97
+ runtime_root = _trusted_runtime_root()
98
+ if runtime_root is not None:
99
+ return runtime_root / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
100
+ return _repo_root() / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
101
+
102
+
86
103
  def _default_package_root() -> Path:
87
104
  runtime_root = _trusted_runtime_root()
88
105
  if runtime_root is not None:
@@ -105,6 +122,14 @@ def _default_python_bin() -> str:
105
122
  return sys.executable
106
123
 
107
124
 
125
+ def _default_pay_binary() -> str:
126
+ explicit = os.getenv("OPENCLAW_PAY_BINARY", "").strip()
127
+ if explicit:
128
+ return explicit
129
+ resolved = shutil.which("pay")
130
+ return resolved or "pay"
131
+
132
+
108
133
  def _default_user_id() -> str:
109
134
  return f"{os.getenv('USER', 'openclaw-user')}-local"
110
135
 
@@ -142,8 +167,10 @@ def build_parser() -> argparse.ArgumentParser:
142
167
  default=True,
143
168
  )
144
169
  parser.add_argument("--extension-path", default=str(_default_extension_path()))
170
+ parser.add_argument("--pay-bridge-extension-path", default=str(_default_pay_bridge_extension_path()))
145
171
  parser.add_argument("--package-root", default=str(_default_package_root()))
146
172
  parser.add_argument("--python-bin", default=_default_python_bin())
173
+ parser.add_argument("--pay-binary", default=_default_pay_binary())
147
174
  parser.add_argument("--write-master-key", action=argparse.BooleanOptionalAction, default=False)
148
175
  return parser
149
176
 
@@ -214,12 +241,20 @@ def main() -> None:
214
241
 
215
242
  plugins = data.setdefault("plugins", {})
216
243
  plugins["enabled"] = True
244
+ allow = plugins.setdefault("allow", [])
245
+ if args.plugin_id not in allow:
246
+ allow.append(args.plugin_id)
247
+ if PAY_BRIDGE_PLUGIN_ID not in allow:
248
+ allow.append(PAY_BRIDGE_PLUGIN_ID)
217
249
 
218
250
  load = plugins.setdefault("load", {})
219
251
  paths = load.setdefault("paths", [])
220
252
  extension_path_text = str(Path(args.extension_path).expanduser().resolve())
221
253
  if extension_path_text not in paths:
222
254
  paths.append(extension_path_text)
255
+ pay_bridge_extension_path_text = str(Path(args.pay_bridge_extension_path).expanduser().resolve())
256
+ if pay_bridge_extension_path_text not in paths:
257
+ paths.append(pay_bridge_extension_path_text)
223
258
 
224
259
  entries = plugins.setdefault("entries", {})
225
260
  effective_network = _normalize_network(args.backend, args.network)
@@ -272,10 +307,25 @@ def main() -> None:
272
307
  "enabled": True,
273
308
  "config": plugin_config,
274
309
  }
310
+ existing_pay_entry = entries.get(PAY_BRIDGE_PLUGIN_ID) if isinstance(entries.get(PAY_BRIDGE_PLUGIN_ID), dict) else {}
311
+ existing_pay_config = (
312
+ dict(existing_pay_entry.get("config"))
313
+ if isinstance(existing_pay_entry.get("config"), dict)
314
+ else {}
315
+ )
316
+ pay_bridge_config = {
317
+ **existing_pay_config,
318
+ "payBinary": args.pay_binary.strip() or _default_pay_binary(),
319
+ "requireHttps": bool(existing_pay_config.get("requireHttps", True)),
320
+ }
321
+ entries[PAY_BRIDGE_PLUGIN_ID] = {
322
+ "enabled": True,
323
+ "config": pay_bridge_config,
324
+ }
275
325
 
276
326
  tools = data.setdefault("tools", {})
277
327
  also_allow = tools.setdefault("alsoAllow", [])
278
- for tool_name in OPTIONAL_TOOLS:
328
+ for tool_name in OPTIONAL_TOOLS + PAY_BRIDGE_TOOLS:
279
329
  if tool_name not in also_allow:
280
330
  also_allow.append(tool_name)
281
331
 
@@ -291,6 +341,7 @@ def main() -> None:
291
341
  "config_path": str(config_path),
292
342
  "backup_path": str(backup_path),
293
343
  "extension_path": extension_path_text,
344
+ "pay_bridge_extension_path": pay_bridge_extension_path_text,
294
345
  "python_bin": args.python_bin,
295
346
  "package_root": plugin_config["packageRoot"],
296
347
  "plugin_id": args.plugin_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -38,6 +38,7 @@
38
38
  "agent-wallet/pyproject.toml",
39
39
  ".openclaw/AGENTS.md",
40
40
  ".openclaw/extensions/agent-wallet/",
41
+ ".openclaw/extensions/pay-bridge/",
41
42
  "hermes/plugins/agent_wallet/",
42
43
  "wdk-btc-wallet/src/",
43
44
  "wdk-btc-wallet/bootstrap.sh",
@@ -69,4 +70,4 @@
69
70
  "evm"
70
71
  ],
71
72
  "license": "SEE LICENSE IN LICENSE"
72
- }
73
+ }