@agentlayer.tech/wallet 0.1.17 → 0.1.19

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.
Files changed (34) hide show
  1. package/.openclaw/AGENTS.md +0 -7
  2. package/.openclaw/extensions/agent-wallet/README.md +3 -2
  3. package/.openclaw/extensions/agent-wallet/dist/index.js +105 -7
  4. package/.openclaw/extensions/agent-wallet/index.ts +105 -7
  5. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +5 -1
  6. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  7. package/CHANGELOG.md +24 -0
  8. package/README.md +1 -3
  9. package/RELEASING.md +5 -15
  10. package/agent-wallet/README.md +7 -0
  11. package/agent-wallet/agent_wallet/config.py +11 -0
  12. package/agent-wallet/agent_wallet/evm_user_wallets.py +310 -2
  13. package/agent-wallet/agent_wallet/openclaw_adapter.py +303 -1
  14. package/agent-wallet/agent_wallet/openclaw_runtime.py +10 -41
  15. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +52 -0
  16. package/agent-wallet/agent_wallet/providers/x402.py +1323 -0
  17. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +30 -0
  18. package/agent-wallet/pyproject.toml +2 -1
  19. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  20. package/agent-wallet/scripts/install_agent_wallet.py +3 -0
  21. package/agent-wallet/scripts/install_openclaw_local_config.py +25 -49
  22. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +9 -1
  23. package/package.json +1 -2
  24. package/wdk-evm-wallet/src/server.js +6 -0
  25. package/wdk-evm-wallet/src/wdk_evm_wallet.js +108 -0
  26. package/.openclaw/extensions/pay-bridge/README.md +0 -38
  27. package/.openclaw/extensions/pay-bridge/core.mjs +0 -287
  28. package/.openclaw/extensions/pay-bridge/dist/core.mjs +0 -287
  29. package/.openclaw/extensions/pay-bridge/dist/index.js +0 -196
  30. package/.openclaw/extensions/pay-bridge/index.ts +0 -196
  31. package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +0 -34
  32. package/.openclaw/extensions/pay-bridge/package.json +0 -49
  33. package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +0 -20
  34. package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +0 -38
@@ -440,6 +440,36 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
440
440
  self.address = address
441
441
  return address
442
442
 
443
+ def sign_x402_evm_exact_typed_data(
444
+ self,
445
+ *,
446
+ domain: dict[str, Any],
447
+ types: dict[str, Any],
448
+ primary_type: str,
449
+ message: dict[str, Any],
450
+ ) -> bytes:
451
+ data = self.client.post_sync(
452
+ "/v1/evm/x402/exact/sign",
453
+ {
454
+ "walletId": self.wallet_id,
455
+ "accountIndex": self.account_index,
456
+ "network": self.network,
457
+ "domain": domain,
458
+ "types": types,
459
+ "primaryType": primary_type,
460
+ "message": message,
461
+ },
462
+ )
463
+ signature = str(data.get("signature") or "").strip()
464
+ if not signature:
465
+ raise WalletBackendError("wdk-evm-wallet did not return an x402 EVM signature.")
466
+ if signature.startswith("0x"):
467
+ signature = signature[2:]
468
+ try:
469
+ return bytes.fromhex(signature)
470
+ except ValueError as exc:
471
+ raise WalletBackendError("wdk-evm-wallet returned an invalid x402 EVM signature.") from exc
472
+
443
473
  async def get_balance(self, address: str | None = None) -> dict[str, Any]:
444
474
  resolved_address = await self.get_address()
445
475
  if address is not None and address.strip() and address.strip() != resolved_address:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.17"
7
+ version = "0.1.19"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "python-dotenv>=1.0.0",
16
16
  "solana>=0.36.0",
17
17
  "solders>=0.27.0",
18
+ "x402[httpx,svm,evm]>=2.10.0",
18
19
  ]
19
20
 
20
21
  [project.optional-dependencies]
@@ -33,6 +33,7 @@ INCLUDED_TOP_LEVEL_DIRS = [
33
33
  EXCLUDED_EXACT_RELATIVE_PATHS = {
34
34
  ".openclaw/extensions-local",
35
35
  ".openclaw/openclaw.local.example.json",
36
+ ".openclaw/extensions/pay-bridge",
36
37
  }
37
38
  EXCLUDED_DIR_NAMES = {
38
39
  ".git",
@@ -201,6 +201,9 @@ def _ignore_runtime_entries(_directory: str, names: list[str]) -> set[str]:
201
201
  keep_dist = ".openclaw" in directory.parts and "extensions" in directory.parts
202
202
  ignored: set[str] = set()
203
203
  for name in names:
204
+ if name == "pay-bridge" and directory.parts[-2:] == (".openclaw", "extensions"):
205
+ ignored.add(name)
206
+ continue
204
207
  if name == "dist" and keep_dist:
205
208
  continue
206
209
  if name in EXCLUDED_RUNTIME_DIR_NAMES:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import os
8
- import shutil
8
+ import secrets
9
9
  import sys
10
10
  from datetime import datetime, timezone
11
11
  from pathlib import Path
@@ -49,13 +49,11 @@ OPTIONAL_TOOLS = [
49
49
  "flash_trade_close_position",
50
50
  ]
51
51
 
52
- PAY_BRIDGE_PLUGIN_ID = "pay-bridge"
53
- PAY_BRIDGE_TOOLS = [
54
- "pay_status",
55
- "pay_wallet_info",
56
- "pay_search_services",
57
- "pay_get_service_endpoints",
58
- "pay_api_request",
52
+ X402_TOOLS = [
53
+ "x402_search_services",
54
+ "x402_get_service_details",
55
+ "x402_preview_request",
56
+ "x402_pay_request",
59
57
  ]
60
58
 
61
59
 
@@ -97,13 +95,6 @@ def _default_extension_path() -> Path:
97
95
  return _repo_root() / ".openclaw" / "extensions" / "agent-wallet"
98
96
 
99
97
 
100
- def _default_pay_bridge_extension_path() -> Path:
101
- runtime_root = _trusted_runtime_root()
102
- if runtime_root is not None:
103
- return runtime_root / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
104
- return _repo_root() / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
105
-
106
-
107
98
  def _default_package_root() -> Path:
108
99
  runtime_root = _trusted_runtime_root()
109
100
  if runtime_root is not None:
@@ -126,14 +117,6 @@ def _default_python_bin() -> str:
126
117
  return sys.executable
127
118
 
128
119
 
129
- def _default_pay_binary() -> str:
130
- explicit = os.getenv("OPENCLAW_PAY_BINARY", "").strip()
131
- if explicit:
132
- return explicit
133
- resolved = shutil.which("pay")
134
- return resolved or "pay"
135
-
136
-
137
120
  def _default_user_id() -> str:
138
121
  return f"{os.getenv('USER', 'openclaw-user')}-local"
139
122
 
@@ -171,10 +154,8 @@ def build_parser() -> argparse.ArgumentParser:
171
154
  default=True,
172
155
  )
173
156
  parser.add_argument("--extension-path", default=str(_default_extension_path()))
174
- parser.add_argument("--pay-bridge-extension-path", default=str(_default_pay_bridge_extension_path()))
175
157
  parser.add_argument("--package-root", default=str(_default_package_root()))
176
158
  parser.add_argument("--python-bin", default=_default_python_bin())
177
- parser.add_argument("--pay-binary", default=_default_pay_binary())
178
159
  parser.add_argument("--write-master-key", action=argparse.BooleanOptionalAction, default=False)
179
160
  return parser
180
161
 
@@ -184,12 +165,15 @@ def _collect_sealed_secret_updates() -> dict[str, str]:
184
165
  master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
185
166
  approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
186
167
  private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
168
+ evm_wallet_password = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()
187
169
  if master_key:
188
170
  updates["master_key"] = master_key
189
171
  if approval_secret:
190
172
  updates["approval_secret"] = approval_secret
191
173
  if private_key:
192
174
  updates["private_key"] = private_key
175
+ if evm_wallet_password:
176
+ updates["wdk_evm_wallet_password"] = evm_wallet_password
193
177
  return updates
194
178
 
195
179
 
@@ -198,10 +182,12 @@ def _maybe_install_sealed_keys() -> str | None:
198
182
  if not boot_key:
199
183
  return None
200
184
  updates = _collect_sealed_secret_updates()
201
- if not updates:
202
- return None
203
185
  sealed_path = resolve_sealed_keys_path()
204
186
  existing = unseal_keys(boot_key) if sealed_path.exists() else {}
187
+ if "wdk_evm_wallet_password" not in existing and "wdk_evm_wallet_password" not in updates:
188
+ updates["wdk_evm_wallet_password"] = secrets.token_urlsafe(24)
189
+ if not updates:
190
+ return None
205
191
  return str(seal_keys(boot_key, {**existing, **updates}))
206
192
 
207
193
 
@@ -248,17 +234,14 @@ def main() -> None:
248
234
  allow = plugins.setdefault("allow", [])
249
235
  if args.plugin_id not in allow:
250
236
  allow.append(args.plugin_id)
251
- if PAY_BRIDGE_PLUGIN_ID not in allow:
252
- allow.append(PAY_BRIDGE_PLUGIN_ID)
237
+ allow[:] = [item for item in allow if item != "pay-bridge"]
253
238
 
254
239
  load = plugins.setdefault("load", {})
255
240
  paths = load.setdefault("paths", [])
256
241
  extension_path_text = str(Path(args.extension_path).expanduser().resolve())
257
242
  if extension_path_text not in paths:
258
243
  paths.append(extension_path_text)
259
- pay_bridge_extension_path_text = str(Path(args.pay_bridge_extension_path).expanduser().resolve())
260
- if pay_bridge_extension_path_text not in paths:
261
- paths.append(pay_bridge_extension_path_text)
244
+ paths[:] = [item for item in paths if "extensions/pay-bridge" not in str(item)]
262
245
 
263
246
  entries = plugins.setdefault("entries", {})
264
247
  effective_network = _normalize_network(args.backend, args.network)
@@ -311,25 +294,19 @@ def main() -> None:
311
294
  "enabled": True,
312
295
  "config": plugin_config,
313
296
  }
314
- existing_pay_entry = entries.get(PAY_BRIDGE_PLUGIN_ID) if isinstance(entries.get(PAY_BRIDGE_PLUGIN_ID), dict) else {}
315
- existing_pay_config = (
316
- dict(existing_pay_entry.get("config"))
317
- if isinstance(existing_pay_entry.get("config"), dict)
318
- else {}
319
- )
320
- pay_bridge_config = {
321
- **existing_pay_config,
322
- "payBinary": args.pay_binary.strip() or _default_pay_binary(),
323
- "requireHttps": bool(existing_pay_config.get("requireHttps", True)),
324
- }
325
- entries[PAY_BRIDGE_PLUGIN_ID] = {
326
- "enabled": True,
327
- "config": pay_bridge_config,
328
- }
297
+ entries.pop("pay-bridge", None)
329
298
 
330
299
  tools = data.setdefault("tools", {})
331
300
  also_allow = tools.setdefault("alsoAllow", [])
332
- for tool_name in OPTIONAL_TOOLS + PAY_BRIDGE_TOOLS:
301
+ removed_pay_tools = {
302
+ "pay_status",
303
+ "pay_wallet_info",
304
+ "pay_search_services",
305
+ "pay_get_service_endpoints",
306
+ "pay_api_request",
307
+ }
308
+ also_allow[:] = [tool_name for tool_name in also_allow if tool_name not in removed_pay_tools]
309
+ for tool_name in OPTIONAL_TOOLS + X402_TOOLS:
333
310
  if tool_name not in also_allow:
334
311
  also_allow.append(tool_name)
335
312
 
@@ -345,7 +322,6 @@ def main() -> None:
345
322
  "config_path": str(config_path),
346
323
  "backup_path": str(backup_path),
347
324
  "extension_path": extension_path_text,
348
- "pay_bridge_extension_path": pay_bridge_extension_path_text,
349
325
  "python_bin": args.python_bin,
350
326
  "package_root": plugin_config["packageRoot"],
351
327
  "plugin_id": args.plugin_id,
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import os
8
+ import secrets as py_secrets
8
9
  import sys
9
10
  from pathlib import Path
10
11
 
@@ -49,12 +50,15 @@ def _collect_secret_updates() -> dict[str, str]:
49
50
  master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
50
51
  approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
51
52
  private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
53
+ evm_wallet_password = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()
52
54
  if master_key:
53
55
  updates["master_key"] = master_key
54
56
  if approval_secret:
55
57
  updates["approval_secret"] = approval_secret
56
58
  if private_key:
57
59
  updates["private_key"] = private_key
60
+ if evm_wallet_password:
61
+ updates["wdk_evm_wallet_password"] = evm_wallet_password
58
62
  return updates
59
63
 
60
64
 
@@ -80,6 +84,10 @@ def main() -> None:
80
84
  sealed_path = resolve_sealed_keys_path()
81
85
  existing = unseal_keys(boot_key) if sealed_path.exists() and not args.replace else {}
82
86
  secrets = {**existing, **updates}
87
+ generated_keys: list[str] = []
88
+ if "wdk_evm_wallet_password" not in secrets:
89
+ secrets["wdk_evm_wallet_password"] = py_secrets.token_urlsafe(24)
90
+ generated_keys.append("wdk_evm_wallet_password")
83
91
  if not secrets:
84
92
  raise SystemExit(
85
93
  "No secrets provided. Set AGENT_WALLET_MASTER_KEY, AGENT_WALLET_APPROVAL_SECRET, "
@@ -93,7 +101,7 @@ def main() -> None:
93
101
  "ok": True,
94
102
  "path": str(path),
95
103
  "stored_keys": sorted(secrets.keys()),
96
- "updated_keys": sorted(updates.keys()),
104
+ "updated_keys": sorted(set(updates.keys()) | set(generated_keys)),
97
105
  "replaced": bool(args.replace),
98
106
  },
99
107
  indent=2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,7 +40,6 @@
40
40
  "agent-wallet/pyproject.toml",
41
41
  ".openclaw/AGENTS.md",
42
42
  ".openclaw/extensions/agent-wallet/",
43
- ".openclaw/extensions/pay-bridge/",
44
43
  "hermes/plugins/agent_wallet/",
45
44
  "wdk-btc-wallet/src/",
46
45
  "wdk-btc-wallet/bootstrap.sh",
@@ -554,6 +554,12 @@ async function handleRequest(request, response) {
554
554
  return sendJson(response, 200, { ok: true, data });
555
555
  }
556
556
 
557
+ if (method === "POST" && url.pathname === "/v1/evm/x402/exact/sign") {
558
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
559
+ const data = await service.signX402ExactTypedData(body);
560
+ return sendJson(response, 200, { ok: true, data });
561
+ }
562
+
557
563
  return notFound(response);
558
564
  } catch (error) {
559
565
  const shaped = toErrorResponse(error, new URL(request.url || "/", "http://localhost").pathname, 400);
@@ -149,6 +149,14 @@ function assertPositiveBigIntString(value, fieldName) {
149
149
  return parsed;
150
150
  }
151
151
 
152
+ function assertNonNegativeBigIntString(value, fieldName) {
153
+ const normalized = String(value ?? "").trim();
154
+ if (!/^[0-9]+$/.test(normalized)) {
155
+ throw new Error(`${fieldName} must be a non-negative base-10 integer string.`);
156
+ }
157
+ return normalized;
158
+ }
159
+
152
160
  function normalizeAddress(value, fieldName) {
153
161
  const address = assertNonEmptyString(value, fieldName);
154
162
  if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
@@ -305,6 +313,71 @@ function mergeBridgeLists(...values) {
305
313
  return items.length > 0 ? items.join(",") : null;
306
314
  }
307
315
 
316
+ function assertPlainObject(value, fieldName) {
317
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
318
+ throw new Error(`${fieldName} must be an object.`);
319
+ }
320
+ return value;
321
+ }
322
+
323
+ function normalizeX402ExactTypedData({ domain, types, primaryType, message }, runtimeConfig) {
324
+ const normalizedPrimaryType = assertNonEmptyString(primaryType, "primaryType");
325
+ if (normalizedPrimaryType !== "TransferWithAuthorization") {
326
+ throw new Error("primaryType must be TransferWithAuthorization for x402 exact EVM payments.");
327
+ }
328
+
329
+ const domainObject = assertPlainObject(domain, "domain");
330
+ const domainChainId = assertNonNegativeInteger(domainObject.chainId, "domain.chainId");
331
+ if (domainChainId !== runtimeConfig.chainId) {
332
+ throw new Error("domain.chainId must match the active network chain id.");
333
+ }
334
+ const normalizedDomain = {
335
+ name: assertNonEmptyString(domainObject.name, "domain.name"),
336
+ version: assertNonEmptyString(domainObject.version, "domain.version"),
337
+ chainId: domainChainId,
338
+ verifyingContract: normalizeAddress(domainObject.verifyingContract, "domain.verifyingContract"),
339
+ };
340
+
341
+ const typesObject = assertPlainObject(types, "types");
342
+ const primaryFields = typesObject[normalizedPrimaryType];
343
+ if (!Array.isArray(primaryFields) || primaryFields.length === 0) {
344
+ throw new Error(`types.${normalizedPrimaryType} must be a non-empty array.`);
345
+ }
346
+ const normalizedTypes = {};
347
+ for (const [typeName, fields] of Object.entries(typesObject)) {
348
+ if (!Array.isArray(fields) || fields.length === 0) {
349
+ throw new Error(`types.${typeName} must be a non-empty array.`);
350
+ }
351
+ normalizedTypes[typeName] = fields.map((field, index) => {
352
+ const normalizedField = assertPlainObject(field, `types.${typeName}[${index}]`);
353
+ return {
354
+ name: assertNonEmptyString(normalizedField.name, `types.${typeName}[${index}].name`),
355
+ type: assertNonEmptyString(normalizedField.type, `types.${typeName}[${index}].type`),
356
+ };
357
+ });
358
+ }
359
+
360
+ const messageObject = assertPlainObject(message, "message");
361
+ const normalizedMessage = {
362
+ from: normalizeAddress(messageObject.from, "message.from"),
363
+ to: normalizeAddress(messageObject.to, "message.to"),
364
+ value: assertPositiveBigIntString(messageObject.value, "message.value").toString(),
365
+ validAfter: assertNonNegativeBigIntString(messageObject.validAfter, "message.validAfter"),
366
+ validBefore: assertPositiveBigIntString(messageObject.validBefore, "message.validBefore").toString(),
367
+ nonce: assertNonEmptyString(messageObject.nonce, "message.nonce"),
368
+ };
369
+ if (!/^0x[a-fA-F0-9]{64}$/.test(normalizedMessage.nonce)) {
370
+ throw new Error("message.nonce must be a 32-byte hex string.");
371
+ }
372
+
373
+ return {
374
+ domain: normalizedDomain,
375
+ types: normalizedTypes,
376
+ primaryType: normalizedPrimaryType,
377
+ message: normalizedMessage,
378
+ };
379
+ }
380
+
308
381
  function buildSwapRequest({ tokenIn, tokenOut, tokenInAmount }) {
309
382
  const swapRequest = {
310
383
  tokenIn: normalizeAddress(tokenIn, "tokenIn"),
@@ -2335,6 +2408,41 @@ export class WdkEvmWalletService {
2335
2408
  });
2336
2409
  }
2337
2410
 
2411
+ async signX402ExactTypedData({
2412
+ seedPhrase,
2413
+ accountIndex = 0,
2414
+ network,
2415
+ domain,
2416
+ types,
2417
+ primaryType,
2418
+ message,
2419
+ }) {
2420
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2421
+ const typedData = normalizeX402ExactTypedData(
2422
+ { domain, types, primaryType, message },
2423
+ runtimeConfig
2424
+ );
2425
+ const signerAddress = normalizeAddress(await account.getAddress(), "accountAddress");
2426
+ if (typedData.message.from.toLowerCase() !== signerAddress.toLowerCase()) {
2427
+ throw new Error("message.from must match the active wallet account address.");
2428
+ }
2429
+ const signature = await account.signTypedData({
2430
+ domain: typedData.domain,
2431
+ types: typedData.types,
2432
+ message: typedData.message,
2433
+ });
2434
+ return {
2435
+ network: runtimeConfig.network,
2436
+ chainId: runtimeConfig.chainId,
2437
+ accountIndex,
2438
+ address: signerAddress,
2439
+ primaryType: typedData.primaryType,
2440
+ signature,
2441
+ source: "wdk-wallet-evm",
2442
+ };
2443
+ });
2444
+ }
2445
+
2338
2446
  #resolveRuntimeConfig(networkOverride) {
2339
2447
  const network = assertValidNetwork(networkOverride) || this.config.network;
2340
2448
  const profile = this.config.networkProfiles?.[network];
@@ -1,38 +0,0 @@
1
- # pay-bridge
2
-
3
- Thin OpenClaw bridge to the locally installed `pay` CLI.
4
-
5
- External install path:
6
-
7
- ```bash
8
- openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
9
- ```
10
-
11
- This plugin is intentionally separate from `agent-wallet`:
12
-
13
- - `agent-wallet` remains the execution wallet stack for Solana/EVM/BTC
14
- - `pay-bridge` only discovers and calls paid APIs through `pay`
15
- - the `pay` wallet stays separate from the AgentLayer wallet runtime
16
-
17
- ## Exposed tools
18
-
19
- - `pay_status`
20
- - `pay_wallet_info`
21
- - `pay_search_services`
22
- - `pay_get_service_endpoints`
23
- - `pay_api_request`
24
-
25
- ## Intended workflow
26
-
27
- 1. `pay_status`
28
- 2. `pay_search_services`
29
- 3. `pay_get_service_endpoints`
30
- 4. `pay_api_request`
31
-
32
- `pay_api_request` is deliberately narrow:
33
-
34
- - it requires a `service_fqn`, `resource`, and `url`
35
- - it validates the URL against `pay skills endpoints`
36
- - it requires `purpose` and `user_confirmed=true`
37
-
38
- This keeps the bridge thin and prevents it from becoming a generic arbitrary paid-curl launcher.