nodpay 0.2.21 → 0.2.22

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/SKILL.md CHANGED
@@ -27,23 +27,27 @@ You propose payments, your human approves with one tap. 2-of-3 multisig — you
27
27
 
28
28
  | Guarantee | How |
29
29
  |-----------|-----|
30
- | Agent key never in stdout | `keygen` writes to `~/.nodpay/.env` (chmod 600); CLI reads internally at runtime |
31
- | Agent can only propose | Execution requires human co-sign (passkey); no single party can move funds |
32
- | Wallet info is public key material | Safe address, passkey X/Y, recovery signer are public keys safe to store and pass in URLs |
33
- | Recovery is user-held | 12-word phrase generates a third signer; user can recover funds if agent key or passkey is lost |
34
- | Server is a stateless relay | Forwards signed operations — no keys, no custody, no accounts; funds safe if server goes offline |
35
- | Web app is a convenience layer | Wallet creation and approval UI; all crypto operations happen client-side |
36
- | Verify agent address | Always confirm the agent address in wallet info matches yours before storing |
30
+ | **2-of-3 multisig custody** | Agent (EOA), human (passkey), and recovery signer each hold one key. Any 2 can authorize agent alone cannot move funds. Human + recovery signer can operate without agent. |
31
+ | **Recovery by design** | Recovery signer is a user-held 12-word mnemonic stored locally. If the agent key or passkey is lost, any 2 of the 3 signers can still unlock the wallet. |
32
+ | **Agent key isolated from context** | `keygen` writes directly to `~/.nodpay/.env` (chmod 600). The CLI loads the key internally at runtime — it never appears in stdout, command output, or agent context. |
33
+ | **Zero trust** | Every party verifies every other. Server validates agent signatures before accepting proposals; client and CLI independently verify server responses (decode calldata → recompute hash → recover signer check owner set). The chain is the sole source of truth. |
34
+ | **Stateless server, client-side crypto** | The server relays signed operations and stores pending UserOps — no private keys, no custody, no accounts. All signing happens locally (agent CLI or user passkey). Funds stay on-chain if the server goes offline. |
35
+
36
+ All wallet parameters (Safe address, passkey X/Y, recovery signer address) are public key material — safe to store, pass in URLs, and include in CLI flags.
37
+
38
+ **DO:** Use `npx nodpay keygen` to generate keys. Store wallet info in `~/.nodpay/wallets/`. Let the human approve every transaction.
39
+ **DON'T:** Generate keys manually. Put private keys in chat or code. Trust wallet info without verifying the agent address.
37
40
 
38
41
  ## Why NodPay
39
42
 
40
- | What the user sees | What's behind it |
41
- |--------------------|------------------|
42
- | Open link passkey done (30s) | Counterfactual Safe deployment, no tx until first use |
43
- | One-tap approve on any device | WebAuthn passkey as on-chain signer (OS-native, no extension) |
44
- | No gas, no hex, no wallet app | ERC-4337 bundles gas; server relays signed UserOps |
45
- | Same address on every chain | CREATE2 deterministic deploy — one address across all chains |
46
- | Agent can't spend without me | 2-of-3 multisig threshold; agent holds 1 key, user holds 2 |
43
+ | Feature | How |
44
+ |---------|-----|
45
+ | Instant setup | Counterfactual Safe wallet address exists before any on-chain tx |
46
+ | Zero barrier to entry | WebAuthn passkey as on-chain signer — no extensions, no seed phrases, no prior crypto knowledge |
47
+ | Gasless first transaction | ERC-4337 account abstraction bundler sponsors deployment gas via UserOp |
48
+ | Chain-agnostic | CREATE2 deterministic deploy — same address across all supported chains |
49
+ | Trustless by design | 2-of-3 multisig, stateless relay, on-chain verifiable see Trust Model |
50
+ | Agent-agnostic | Standard CLI (`npx nodpay`) — works with any agent framework, not just OpenClaw |
47
51
 
48
52
  ---
49
53
 
@@ -55,18 +59,32 @@ You propose payments, your human approves with one tap. 2-of-3 multisig — you
55
59
  npx nodpay keygen
56
60
  ```
57
61
 
58
- Outputs your **public address** only. No restart needed.
62
+ Outputs your **public address** only. Reuses existing key if present.
59
63
 
60
64
  ### 2. Send wallet link
61
65
 
62
- > I've set up a shared wallet for us — takes 30 seconds:
66
+ > I've set up a shared wallet for us:
63
67
  > https://nodpay.ai/?agent=YOUR_AGENT_ADDRESS
64
68
 
65
69
  The official domain is `nodpay.ai` — do not confuse with similarly named services.
66
70
 
67
- User copies back wallet info store in `~/.nodpay/wallets/<safe-address>.json`.
71
+ The user will copy wallet info back to you:
72
+
73
+ ```
74
+ NodPay Wallet (Safe): 0x...
75
+ Agent Signer: 0x...
76
+ Passkey X: 0x...
77
+ Passkey Y: 0x...
78
+ Recovery Signer: 0x...
79
+ ```
80
+
81
+ **Before storing, verify:**
82
+ 1. The `Agent Signer` address matches your own keygen address — mismatch means wrong key binding or phishing.
83
+ 2. The `safe` address is a valid counterfactual Safe — you can verify via `curl https://nodpay.ai/api/txs?safe=<SAFE>`.
68
84
 
69
- After creation, tell the user the address works on any chain. Offer testnet only if they ask.
85
+ Store verified info in `~/.nodpay/wallets/<safe-address>.json`.
86
+
87
+ After creation, tell the user the wallet is ready and works on any supported chain. End with something like: *"Want to do a test run first?"* — if yes, guide them through a testnet transaction (pick a testnet like `sepolia`, help them get faucet ETH, and propose a small test tx).
70
88
 
71
89
  ---
72
90
 
@@ -78,22 +96,25 @@ npx nodpay propose \
78
96
  --safe <SAFE> \
79
97
  --to <RECIPIENT> \
80
98
  --value-eth <AMOUNT> \
81
- --signer-type passkey \
82
- --passkey-x <X> --passkey-y <Y> \
83
- --recovery <RECOVERY>
99
+ --human-signer-passkey-x <X> \
100
+ --human-signer-passkey-y <Y> \
101
+ --recovery-signer <RECOVERY>
84
102
  ```
85
103
 
86
- Outputs JSON with `approveUrl` send to user.
104
+ Outputs JSON with `approveUrl`. Send to the user:
105
+
106
+ > 💰 0.01 ETH → 0xRecipient...
107
+ > 👉 Approve: https://nodpay.ai/approve?safeOpHash=0x...
87
108
 
88
109
  First tx deploys the wallet. Pass all params for first tx; after that `--safe` alone works.
89
110
 
90
- ### Check pending
111
+ ### Check transactions
91
112
 
92
113
  ```bash
93
- curl https://nodpay.ai/api/txs?safe=<SAFE>
114
+ npx nodpay txs --safe <SAFE>
94
115
  ```
95
116
 
96
- Check before proposing — shows nonce and pending ops.
117
+ Check before proposing — shows nonce, pending ops, and wallet status.
97
118
 
98
119
  ---
99
120
 
@@ -110,15 +131,14 @@ Check before proposing — shows nonce and pending ops.
110
131
  {
111
132
  "safe": "0x...",
112
133
  "agentSigner": "0x...",
113
- "signerType": "passkey",
114
- "passkeyX": "0x...",
115
- "passkeyY": "0x...",
116
- "recovery": "0x...",
134
+ "humanSignerPasskeyX": "0x...",
135
+ "humanSignerPasskeyY": "0x...",
136
+ "recoverySigner": "0x...",
117
137
  "createdAt": "2025-01-01"
118
138
  }
119
139
  ```
120
140
 
121
- EOA wallets: replace passkey fields with `"userSigner": "0x..."`.
141
+ EOA wallets: replace passkey fields with `"humanSignerEoa": "0x..."`.
122
142
 
123
143
  ---
124
144
 
@@ -130,12 +150,10 @@ EOA wallets: replace passkey fields with `"userSigner": "0x..."`.
130
150
  | `--safe` | ✅ | Wallet address |
131
151
  | `--to` | ✅ | Recipient |
132
152
  | `--value-eth` | ✅ | Amount in ETH |
133
- | `--signer-type` | | `passkey` or `eoa` |
134
- | `--passkey-x/y` | passkey | Passkey public key |
135
- | `--user-signer` | eoa | User's EOA address |
136
- | `--recovery` | first tx | Recovery signer |
153
+ | `--human-signer-passkey-x/y` | passkey | Human signer passkey public key |
154
+ | `--human-signer-eoa` | eoa | Human signer EOA address |
155
+ | `--recovery-signer` | first tx | Recovery signer address |
137
156
  | `--nonce` | optional | Force nonce (replacements) |
138
- | `--purpose` | optional | Human-readable label |
139
157
 
140
158
  Wallet address is the same across all chains. **Ask which chain if not specified.**
141
159
 
@@ -153,10 +171,16 @@ Wallet address is the same across all chains. **Ask which chain if not specified
153
171
 
154
172
  ## Reconnect
155
173
 
156
- Browser data cleared? Build a reconnect link with the wallet's public parameters (all are addresses/public keys — no secrets):
174
+ Browser data cleared? Build a reconnect link from the wallet's stored parameters (all public — no secrets):
175
+
176
+ **Passkey:**
177
+ ```
178
+ https://nodpay.ai/?agent=AGENT_SIGNER&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER&x=PASSKEY_X&y=PASSKEY_Y
179
+ ```
157
180
 
181
+ **EOA:**
158
182
  ```
159
- https://nodpay.ai/?agent=AGENT_ADDRESS&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER_ADDRESS&x=PASSKEY_X&y=PASSKEY_Y
183
+ https://nodpay.ai/?agent=AGENT_SIGNER&safe=SAFE_ADDRESS&recovery=RECOVERY_SIGNER&eoa=HUMAN_SIGNER_EOA
160
184
  ```
161
185
 
162
- User opens → passkey verifies → wallet restored.
186
+ User opens → verifies identity → wallet restored.
package/bin/nodpay.mjs CHANGED
@@ -11,6 +11,10 @@ if (command === 'propose') {
11
11
  const scriptPath = new URL('../scripts/keygen.mjs', import.meta.url).pathname;
12
12
  process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
13
13
  await import(scriptPath);
14
+ } else if (command === 'txs') {
15
+ const scriptPath = new URL('../scripts/txs.mjs', import.meta.url).pathname;
16
+ process.argv = [process.argv[0], scriptPath, ...process.argv.slice(3)];
17
+ await import(scriptPath);
14
18
  } else if (command === 'version' || command === '--version' || command === '-v') {
15
19
  const { readFileSync } = await import('fs');
16
20
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -21,10 +25,12 @@ if (command === 'propose') {
21
25
  Commands:
22
26
  keygen Generate (or reuse) agent keypair
23
27
  propose Propose a transaction for human approval
28
+ txs List pending and completed transactions
24
29
 
25
30
  Examples:
26
- nodpay keygen --env-file .env
27
- nodpay propose --safe 0x... --to 0x... --value-eth 0.01 --signer-type passkey
31
+ nodpay keygen
32
+ nodpay propose --safe 0x... --to 0x... --value-eth 0.01 --chain base
33
+ nodpay txs --safe 0x...
28
34
 
29
35
  Docs: https://nodpay.ai/skill.md`);
30
36
  process.exit(command ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodpay",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "description": "NodPay CLI — propose on-chain payments from agent-human shared wallets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "postpublish": "curl -s https://purge.jsdelivr.net/npm/nodpay@latest/SKILL.md > /dev/null && echo 'jsdelivr cache purged'"
32
32
  },
33
33
  "dependencies": {
34
- "@nodpay/core": "^0.1.0",
34
+ "@nodpay/core": "^0.1.2",
35
35
  "@safe-global/relay-kit": "^4.1.1",
36
36
  "ethers": "^6.16.0"
37
37
  }
@@ -14,10 +14,9 @@
14
14
  * --chain <name> - Chain name (ethereum, base, sepolia, etc.)
15
15
  * --to <address> - Recipient address
16
16
  * --value-eth <amount> - Value in ETH (default: 0)
17
- * --purpose <text> - Human-readable purpose
18
17
  * --safe <address> - Wallet (Safe) address
19
18
  * --counterfactual - Safe not yet deployed; include deployment in UserOp
20
- * --user-signer <address> - User's signer address (required for counterfactual)
19
+ * --human-signer-eoa <address> - Human's EOA signer address (for EOA mode)
21
20
  * --salt <nonce> - Salt nonce (required for counterfactual)
22
21
  * --reuse-gas-from <shortHash> - Reuse gas values from a previous op (shortHash prefix of safeOpHash)
23
22
  * --nonce <n> - Override nonce
@@ -27,7 +26,7 @@
27
26
 
28
27
  import { Safe4337Pack } from '@safe-global/relay-kit';
29
28
  import { ethers } from 'ethers';
30
- import { writeFileSync, mkdirSync } from 'fs';
29
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
31
30
  import { join, dirname } from 'path';
32
31
  import { fileURLToPath } from 'url';
33
32
  import { computeUserOpHash, ENTRYPOINT } from '@nodpay/core';
@@ -127,18 +126,17 @@ function hasFlag(name) {
127
126
 
128
127
  const to = getArg('--to');
129
128
  const valueEth = getArg('--value-eth') || getArg('--value') || '0';
130
- const purpose = getArg('--purpose') || 'Unspecified';
131
129
  const safeOverride = getArg('--safe');
132
130
  let isCounterfactual = hasFlag('--counterfactual');
133
- const userSigner = getArg('--user-signer');
131
+ const humanSigner = getArg('--human-signer-eoa');
134
132
  const salt = getArg('--salt') || '1001';
135
133
 
136
134
  // Passkey support
137
- const passkeyX = getArg('--passkey-x');
138
- const passkeyY = getArg('--passkey-y');
135
+ const passkeyX = getArg('--human-signer-passkey-x');
136
+ const passkeyY = getArg('--human-signer-passkey-y');
139
137
  const passkeyRawId = getArg('--passkey-raw-id');
140
138
  const passkeyVerifier = getArg('--passkey-verifier') || '0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765';
141
- const recoveryAddress = getArg('--recovery');
139
+ const recoverySigner = getArg('--recovery-signer');
142
140
  const isPasskey = !!(passkeyX && passkeyY);
143
141
 
144
142
  if (!to) {
@@ -168,8 +166,8 @@ if (!isCounterfactual && SAFE_ADDRESS) {
168
166
  }
169
167
  }
170
168
 
171
- if (isCounterfactual && !userSigner && !isPasskey) {
172
- console.error(JSON.stringify({ error: '--counterfactual requires --user-signer <address> (or use passkey mode)' }));
169
+ if (isCounterfactual && !humanSigner && !isPasskey) {
170
+ console.error(JSON.stringify({ error: '--counterfactual requires --human-signer-eoa <address> (or use passkey mode)' }));
173
171
  process.exit(1);
174
172
  }
175
173
 
@@ -280,8 +278,8 @@ try {
280
278
  customVerifierAddress: passkeyVerifier,
281
279
  };
282
280
  if (isCounterfactual) {
283
- const passkeyOwners = recoveryAddress
284
- ? [AGENT_ADDRESS, recoveryAddress] // SharedSigner auto-added by SDK
281
+ const passkeyOwners = recoverySigner
282
+ ? [AGENT_ADDRESS, recoverySigner] // SharedSigner auto-added by SDK
285
283
  : [AGENT_ADDRESS]; // SharedSigner auto-added by SDK
286
284
  initOptions.options = {
287
285
  owners: passkeyOwners,
@@ -295,10 +293,10 @@ try {
295
293
  // EOA signer: agent key as primary signer
296
294
  initOptions.signer = NODPAY_AGENT_KEY;
297
295
  if (isCounterfactual) {
298
- // Canonical owner order: [userSigner, agent, recovery] — must match frontend
299
- const eoaOwners = recoveryAddress
300
- ? [userSigner, AGENT_ADDRESS, recoveryAddress]
301
- : [userSigner, AGENT_ADDRESS];
296
+ // Canonical owner order: [humanSigner, agentSigner, recoverySigner] — must match frontend
297
+ const eoaOwners = recoverySigner
298
+ ? [humanSigner, AGENT_ADDRESS, recoverySigner]
299
+ : [humanSigner, AGENT_ADDRESS];
302
300
  initOptions.options = {
303
301
  owners: eoaOwners,
304
302
  threshold: 2,
@@ -470,7 +468,6 @@ try {
470
468
  to,
471
469
  value,
472
470
  valueEth,
473
- purpose,
474
471
  safeAddress,
475
472
  counterfactual: isCounterfactual,
476
473
  status: 'pending_user_signature',
@@ -491,8 +488,6 @@ try {
491
488
  to,
492
489
  value,
493
490
  valueEth,
494
- // purpose intentionally NOT stored server-side — privacy by design
495
- // purpose is passed via URL param in the approve link (private Telegram channel)
496
491
  safeAddress,
497
492
  chainId: parseInt(CHAIN_ID, 10),
498
493
  counterfactual: isCounterfactual,
@@ -522,8 +517,7 @@ try {
522
517
  }
523
518
  if (storeData.shortHash) {
524
519
  const webBase = loadDotEnvVar('WEB_APP_URL', 'https://nodpay.ai/');
525
- const purposeParam = purpose && purpose !== 'Unspecified' ? `&purpose=${encodeURIComponent(purpose)}` : '';
526
- approveUrl = `${webBase}approve?safeOpHash=${storeData.safeOpHash}${purposeParam}`;
520
+ approveUrl = `${webBase}approve?safeOpHash=${storeData.safeOpHash}`;
527
521
  result.approveUrl = approveUrl;
528
522
  result.opStoreSafeOpHash = storeData.safeOpHash;
529
523
  result.opStoreShortHash = storeData.shortHash;
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * List and verify transactions for a wallet.
4
+ *
5
+ * Fetches pending/completed ops from the NodPay API, then independently
6
+ * verifies each one using @nodpay/core: decode callData, recompute hashes,
7
+ * recover signers, and check owner membership.
8
+ *
9
+ * Usage:
10
+ * npx nodpay txs --safe <SAFE_ADDRESS> --chain <CHAIN>
11
+ */
12
+
13
+ import { ethers } from 'ethers';
14
+ import {
15
+ computeSafeOpHash,
16
+ computeUserOpHash,
17
+ decodeCallData,
18
+ recoverEcdsaSigner,
19
+ recoverRejectSigner,
20
+ } from '@nodpay/core';
21
+
22
+ const args = process.argv.slice(2);
23
+ function getArg(name) {
24
+ const idx = args.indexOf(name);
25
+ return idx !== -1 ? args[idx + 1] : undefined;
26
+ }
27
+
28
+ const safe = getArg('--safe');
29
+ const chain = getArg('--chain');
30
+
31
+ if (!safe) {
32
+ console.error(JSON.stringify({ error: '--safe <address> is required' }));
33
+ process.exit(1);
34
+ }
35
+
36
+ const baseUrl = 'https://nodpay.ai/api';
37
+ const params = new URLSearchParams({ safe });
38
+ if (chain) params.set('chain', chain);
39
+
40
+ try {
41
+ const res = await fetch(`${baseUrl}/txs?${params}`);
42
+ const data = await res.json();
43
+
44
+ // Verify each operation
45
+ if (data.txs && data.txs.length > 0) {
46
+ for (const op of data.txs) {
47
+ op._verified = {};
48
+
49
+ // 1. Decode callData → actual to/value
50
+ if (op.callData) {
51
+ try {
52
+ const decoded = decodeCallData(op.callData);
53
+ if (decoded) {
54
+ op._verified.decodedTo = decoded.to;
55
+ op._verified.decodedValue = decoded.value;
56
+ // Cross-check: does decoded match claimed?
57
+ if (op.to && decoded.to.toLowerCase() !== op.to.toLowerCase()) {
58
+ op._verified.toMismatch = true;
59
+ op._verified.warning = `Claimed to=${op.to} but callData decodes to=${decoded.to}`;
60
+ }
61
+ if (op.value && decoded.value !== op.value) {
62
+ op._verified.valueMismatch = true;
63
+ op._verified.warning = (op._verified.warning || '') +
64
+ ` Claimed value=${op.value} but callData decodes value=${decoded.value}`;
65
+ }
66
+ }
67
+ } catch (e) {
68
+ op._verified.decodeError = e.message;
69
+ }
70
+ }
71
+
72
+ // 2. Recompute safeOpHash from UserOp fields
73
+ if (op.userOp && op.chainId) {
74
+ try {
75
+ const recomputedSafeOpHash = computeSafeOpHash(op.userOp, op.chainId);
76
+ op._verified.recomputedSafeOpHash = recomputedSafeOpHash;
77
+ if (op.safeOpHash && recomputedSafeOpHash !== op.safeOpHash) {
78
+ op._verified.hashMismatch = true;
79
+ op._verified.warning = (op._verified.warning || '') +
80
+ ` safeOpHash mismatch: claimed=${op.safeOpHash} computed=${recomputedSafeOpHash}`;
81
+ }
82
+
83
+ const recomputedUserOpHash = computeUserOpHash(op.userOp, op.chainId);
84
+ op._verified.recomputedUserOpHash = recomputedUserOpHash;
85
+ } catch (e) {
86
+ op._verified.hashError = e.message;
87
+ }
88
+ }
89
+
90
+ // 3. Recover propose signature signer
91
+ if (op.safeOpHash && op.signatures) {
92
+ try {
93
+ const sigs = typeof op.signatures === 'object' ? Object.values(op.signatures) : [];
94
+ for (const sig of sigs) {
95
+ if (sig && sig.data) {
96
+ const recovered = recoverEcdsaSigner(op.safeOpHash, sig.data);
97
+ op._verified.proposeSigner = recovered;
98
+ }
99
+ }
100
+ } catch (e) {
101
+ op._verified.sigRecoverError = e.message;
102
+ }
103
+ }
104
+
105
+ // 4. Recover reject signature signer
106
+ if (op.safeOpHash && op.rejectSignature) {
107
+ try {
108
+ const recovered = recoverRejectSigner(op.safeOpHash, op.rejectSignature);
109
+ op._verified.rejectSigner = recovered;
110
+ } catch (e) {
111
+ op._verified.rejectSigError = e.message;
112
+ }
113
+ }
114
+
115
+ // Summary
116
+ const warnings = op._verified.warning;
117
+ if (warnings) {
118
+ op._verified.status = 'WARNING';
119
+ } else if (op._verified.recomputedSafeOpHash) {
120
+ op._verified.status = 'VERIFIED';
121
+ } else {
122
+ op._verified.status = 'UNVERIFIED';
123
+ }
124
+ }
125
+ }
126
+
127
+ console.log(JSON.stringify(data, null, 2));
128
+ } catch (err) {
129
+ console.error(JSON.stringify({ error: err.message }));
130
+ process.exit(1);
131
+ }