blue-js-sdk 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  Every fix made during SDK creation, why it matters, and what happens if you use upstream Sentinel code directly without these fixes.
4
4
 
5
+ ## v2.3.0 — RPC-First Migration (2026-04-14)
6
+
7
+ **100% of chain queries now use RPC-first with LCD fallback.** Protobuf/ABCI queries via Tendermint37Client are ~912x faster than LCD REST. If RPC fails, every query automatically falls back to LCD.
8
+
9
+ ### JS SDK Changes
10
+ - **chain/rpc.js**: Added 4 new RPC functions — `rpcQueryFeeGrants`, `rpcQueryFeeGrantsIssued`, `rpcQueryAuthzGrants`, `rpcQueryProvider`
11
+ - **chain/queries.js**: All 22 query functions are RPC-first with LCD fallback
12
+ - **chain/fee-grants.js**: All 7 functions are RPC-first with LCD fallback
13
+ - **cosmjs-setup.js**: All 28 query bodies replaced with thin wrappers delegating to RPC-first modules
14
+ - **session-manager.js**: `buildSessionMap()` now uses RPC-first `querySessions()`
15
+ - **batch.js**: `waitForBatchSessions()` now uses RPC-first `querySessions()`
16
+ - **defaults.js**: Added runtime endpoint management — `addRpcEndpoint`, `removeRpcEndpoint`, `setEndpoints`, `getEndpoints`, `checkRpcEndpointHealth`, `optimizeEndpoints`
17
+ - **index.js**: 16 RPC query exports + 8 endpoint management exports
18
+ - **SDK_VERSION**: Bumped to 2.3.0
19
+
20
+ ### C# SDK Changes
21
+ - **RpcClient.cs**: Wired into ChainClient. 17 typed query methods (sessions, subscriptions, nodes, balance, provider, fee grants, authz, allocations)
22
+ - **ProtobufReader.cs**: Added `DecodeSession`, `DecodeSubscription`, `DecodeProvider` decoders
23
+ - **ChainClient.Queries.cs**: 13 methods upgraded to RPC-first with LCD fallback
24
+ - **ChainClient.FeeGrants.cs**: 2 methods upgraded to RPC-first with LCD fallback
25
+ - **Total**: 15 direct + 9 transitive = 24 query methods are RPC-first
26
+
27
+ ### Coverage
28
+ | Module | RPC-First | Total |
29
+ |--------|-----------|-------|
30
+ | JS chain/queries.js | 22/22 | 100% |
31
+ | JS chain/fee-grants.js | 7/7 | 100% |
32
+ | JS session-manager.js | 1/1 | 100% |
33
+ | JS batch.js | 1/1 | 100% |
34
+ | C# ChainClient.Queries | 13/13 | 100% |
35
+ | C# ChainClient.FeeGrants | 2/2 | 100% |
36
+
37
+ ---
38
+
5
39
  ## Documentation Versions
6
40
 
7
41
  | Version | Date | Changes |
@@ -56,38 +56,59 @@ Agent needs wallet
56
56
 
57
57
  └── Is wallet funded?
58
58
  ├── getBalance(mnemonic) → { funded: true } → continue
59
- └── getBalance(mnemonic) → { funded: false }
60
-
61
- ├── Agent has crypto on other chains?
62
- │ ├── Has ATOM swap on Osmosis DEX (ATOM → P2P)
63
- ├── Has USDT/USDC swap on Osmosis DEX (stable → ATOM → P2P)
64
- │ ├── Has ETHbridge to Cosmos via Gravity Bridge or Axelar, then swap
65
- └── Has SOL/other no direct bridge yet
66
- └── OUTPUT: { action: 'funding-request', address: 'sent1...', amount: '100000 udvpn', reason: 'minimum for 1 VPN session', acceptedTokens: ['udvpn', 'ATOM via Osmosis'] }
67
-
68
- ├── Agent has fiat payment method?
69
- └── OUTPUT: { action: 'fiat-not-supported-yet', workaround: 'buy ATOM on exchange send to Cosmos wallet → swap to P2P on Osmosis', futureSupport: 'planned' }
70
-
71
- ├── Agent is operated by a company/human?
72
- │ └── OUTPUT: { action: 'request-funding', to: 'operator', address: 'sent1...', minAmount: '1000000 udvpn (1 P2P)', estimatedUsage: '~10 GB or ~10 sessions' }
73
-
74
- └── Agent has no payment capability?
75
- └── OUTPUT: {
76
- action: 'funding-needed',
77
- address: 'sent1...',
78
- minAmount: { udvpn: 1000000, p2p: '1.0 P2P', note: 'Covers gas + 1 GB on cheapest node' },
79
- methods: [
80
- { method: 'direct-transfer', description: 'Send P2P tokens to this address' },
81
- { method: 'osmosis-swap', description: 'Swap ATOM/USDT → P2P on app.osmosis.zone' },
82
- { method: 'operator-grant', description: 'Fee grant from plan operator covers gas' },
83
- ],
84
- request: {
85
- type: 'funding-request',
86
- reason: 'Autonomous agent needs VPN access for [task description]',
87
- willingness: 'Will pay up to X per GB if funded',
88
- contact: null,
89
- }
90
- }
59
+ ├── getBalance(mnemonic) → { funded: false }
60
+
61
+ ├── Agent has operator-provisioned access (x402 / fee grant)?
62
+ ├── Has subscriptionId + feeGranter OPERATOR-PROVISIONED MODE
63
+ │ │ └── connect({ mnemonic, subscriptionId, feeGranter, nodeAddress })
64
+ │ │ ├── Fee grant valid connect with 0 P2P (operator pays gas)
65
+ │ │ ├── FEE_GRANT_NOT_FOUNDrequest provisioning from operator
66
+ │ │ ├── FEE_GRANT_EXPIRED request grant renewal from operator
67
+ │ │ ├── FEE_GRANT_EXHAUSTED → request operator to top up grant
68
+ │ │ │ └── Disconnect also uses fee grant (MsgCancelSessionRequest)
69
+ │ ├── Has planId + feeGranterPLAN MODE with fee grant
70
+ │ │ └── connect({ mnemonic, planId, feeGranter })
71
+ │ │ │ └── Subscribes to plan + starts session, operator pays gas
72
+ └── Has USDC on Base/Solana x402 PAYMENT MODE
73
+ │ └── 1. Register sent1... address with x402 server
74
+ │ │ 2. Pay USDC via EIP-3009 (Base) or SPL transfer (Solana)
75
+ │ │ 3. Server provisions: subscription share + fee grant
76
+ │ │ 4. connect({ mnemonic, subscriptionId, feeGranter, nodeAddress })
77
+ │ │ 5. Agent connects with 0 P2P
78
+ │ │
79
+ │ ├── Agent has crypto on other chains?
80
+ │ │ ├── Has ATOM swap on Osmosis DEX (ATOM P2P)
81
+ │ │ ├── Has USDT/USDC → swap on Osmosis DEX (stable → ATOM → P2P)
82
+ │ │ ├── Has ETH bridge to Cosmos via Gravity Bridge or Axelar, then swap
83
+ │ │ └── Has SOL/other → no direct bridge yet
84
+ │ │ └── OUTPUT: { action: 'funding-request', address: 'sent1...', amount: '100000 udvpn', reason: 'minimum for 1 VPN session', acceptedTokens: ['udvpn', 'ATOM via Osmosis'] }
85
+ │ │
86
+ │ ├── Agent has fiat payment method?
87
+ │ │ └── OUTPUT: { action: 'fiat-not-supported-yet', workaround: 'buy ATOM on exchange → send to Cosmos wallet swap to P2P on Osmosis', futureSupport: 'planned' }
88
+ │ │
89
+ │ ├── Agent is operated by a company/human?
90
+ │ │ └── OUTPUT: { action: 'request-funding', to: 'operator', address: 'sent1...', minAmount: '1000000 udvpn (1 P2P)', estimatedUsage: '~10 GB or ~10 sessions' }
91
+ │ │
92
+ │ └── Agent has no payment capability?
93
+ │ └── OUTPUT: {
94
+ │ action: 'funding-needed',
95
+ │ address: 'sent1...',
96
+ │ minAmount: { udvpn: 1000000, p2p: '1.0 P2P', note: 'Covers gas + 1 GB on cheapest node' },
97
+ │ methods: [
98
+ │ { method: 'direct-transfer', description: 'Send P2P tokens to this address' },
99
+ │ { method: 'osmosis-swap', description: 'Swap ATOM/USDT → P2P on app.osmosis.zone' },
100
+ │ { method: 'operator-grant', description: 'Fee grant from plan operator covers gas' },
101
+ │ { method: 'x402-payment', description: 'Pay USDC on Base/Solana via x402 protocol — operator provisions access' },
102
+ │ ],
103
+ │ request: {
104
+ │ type: 'funding-request',
105
+ │ reason: 'Autonomous agent needs VPN access for [task description]',
106
+ │ willingness: 'Will pay up to X per GB if funded',
107
+ │ contact: null,
108
+ │ }
109
+ │ }
110
+
111
+ └── getBalance(mnemonic) → { funded: true } → continue
91
112
  ```
92
113
 
93
114
  ## Decision Node 4: WHICH NODE DO I CONNECT TO?
@@ -315,28 +336,47 @@ Agent can't solve problem alone
315
336
 
316
337
  ```
317
338
  Agent has funds but wrong type
339
+
340
+ ├── Has USDC on Base (preferred — cheapest, x402 native)
341
+ │ ├── x402 protocol: pay USDC on Base → get VPN access instantly
342
+ │ ├── Price: $0.033/day, $0.233/week, $1.00/month
343
+ │ ├── Flow: register sent1... → pay via EIP-3009 → server provisions sub + fee grant
344
+ │ └── OUTPUT: {
345
+ │ action: 'x402-payment',
346
+ │ chain: 'base',
347
+ │ contract: '0x923F41eb162d4F7D274CE8cE8b8178883E7E2196',
348
+ │ steps: ['register sent1... with x402 server', 'approve USDC', 'pay via contract', 'connect with subscriptionId + feeGranter'],
349
+ │ estimatedTime: '<30s',
350
+ │ estimatedFees: '<$0.001 (Base L2 gas)',
351
+ │ pricing: { '1day': '$0.033', '7days': '$0.233', '30days': '$1.00' },
352
+ │ }
353
+
354
+ ├── Has USDC on Solana
355
+ │ ├── x402 protocol: SPL transfer + memo to operator
356
+ │ └── OUTPUT: { action: 'x402-payment', chain: 'solana', steps: ['SPL transfer to operator ATA', 'memo: x402:sent1...:hours:N'], estimatedTime: '<5s' }
318
357
 
319
358
  ├── Has USDT (ERC-20 Ethereum)
320
359
  │ ├── Bridge: Gravity Bridge or Axelar to Cosmos
321
360
  │ ├── Then: Osmosis DEX swap USDT → P2P
322
361
  │ └── OUTPUT: { action: 'cross-chain-swap', steps: ['bridge to cosmos', 'swap on osmosis'], estimatedTime: '15-30min', estimatedFees: '~$2-5 bridge + swap' }
323
362
 
324
- ├── Has USDC (multiple chains)
363
+ ├── Has USDC on other chains (Ethereum, Cosmos/Noble)
325
364
  │ ├── If on Cosmos/Noble → direct swap on Osmosis
326
- │ ├── If on Ethereum → bridge first
327
- │ └── OUTPUT: { steps: [...], cheapestPath: 'Noble USDCOsmosis → P2P' }
365
+ │ ├── If on Ethereum → bridge first (or swap to Base USDC and use x402)
366
+ │ └── OUTPUT: { steps: [...], cheapestPath: 'Bridge to Base x402 payment' }
328
367
 
329
- ├── Has SOL
330
- │ ├── No direct bridge to Cosmos yet
331
- │ └── OUTPUT: { action: 'unsupported-currently', workaround: 'swap SOL→USDT on DEX, bridge to Cosmos, swap to P2P' }
368
+ ├── Has SOL (not USDC)
369
+ │ ├── Swap SOL USDC on Jupiter/Raydium, then use x402 Solana payment
370
+ │ └── OUTPUT: { action: 'swap-then-x402', steps: ['swap SOL→USDC on Jupiter', 'x402 payment via Solana'], estimatedTime: '<30s' }
332
371
 
333
372
  ├── Has BTC
334
373
  │ ├── Nomic bridge (BTC → nBTC on Cosmos) → Osmosis → P2P
335
374
  │ └── OUTPUT: { action: 'cross-chain-swap', via: 'Nomic Bridge', steps: [...] }
336
375
 
337
376
  ├── Has credit card / fiat only
338
- │ ├── Buy ATOM on Coinbase/Binance/Krakenwithdraw to Cosmos wallet → Osmosis P2P
339
- └── OUTPUT: { action: 'fiat-onramp', steps: ['buy ATOM on exchange', 'withdraw to sent1... address', 'swap on Osmosis'], estimatedTime: '1-24h depending on exchange' }
377
+ │ ├── Buy USDC on Coinbase → send to Base wallet → use x402 (fastest)
378
+ ├── Buy ATOM on exchange withdraw to Cosmos Osmosis P2P (slower)
379
+ │ └── OUTPUT: { action: 'fiat-onramp', steps: ['buy USDC on Coinbase/Binance', 'withdraw to Base wallet', 'x402 payment'], estimatedTime: '1-24h depending on exchange' }
340
380
 
341
381
  └── Has nothing — pure autonomous agent with no funds
342
382
  └── OUTPUT: {
@@ -349,6 +389,7 @@ Agent has funds but wrong type
349
389
  planOperatorBenefit: 'Agent drives usage and network growth',
350
390
  },
351
391
  alternatives: [
392
+ 'x402 payment: pay USDC on Base/Solana (cheapest — $0.033/day)',
352
393
  'Apply for fee grant from plan operator',
353
394
  'Request funding from agent marketplace (future)',
354
395
  'Earn tokens by providing node quality reports (future)',
@@ -360,7 +401,10 @@ Agent has funds but wrong type
360
401
 
361
402
  | Error Code | Agent Action |
362
403
  |-----------|-------------|
363
- | `INSUFFICIENT_BALANCE` | Fund wallet → Decision Node 3 |
404
+ | `INSUFFICIENT_BALANCE` | Fund wallet → Decision Node 3 (or use x402/fee grant) |
405
+ | `FEE_GRANT_NOT_FOUND` | No fee grant from operator → request provisioning |
406
+ | `FEE_GRANT_EXPIRED` | Fee grant expired → request renewal from operator |
407
+ | `FEE_GRANT_EXHAUSTED` | Fee grant spend limit too low → request operator to top up |
364
408
  | `NODE_OFFLINE` | SDK auto-retries. If all fail → try different country |
365
409
  | `NODE_INACTIVE` | SDK retries after 15s. If fails → different node |
366
410
  | `V2RAY_NOT_FOUND` | Run `node setup.js` |
@@ -532,6 +532,158 @@ Event attributes may be base64-encoded (depends on CosmJS version). The SDK hand
532
532
 
533
533
  **Minimum 7 seconds between transactions.** Rapid TX submission causes sequence mismatch cascades. NEVER run parallel chain operations -- rate limits will kill your internet connectivity.
534
534
 
535
+ ### Fee-Granted Session Creation (Operator-Provisioned Mode)
536
+
537
+ When an operator has provisioned a subscription share and fee grant for the agent, the session creation flow changes:
538
+
539
+ #### Pre-Check: Fee Grant Validity
540
+
541
+ Before attempting the session TX, the SDK validates the fee grant on-chain using **RPC first** (protobuf, ~250ms), with **LCD fallback** (~880ms). This matches the SDK standard: all chain queries use RPC with LCD as a safety net.
542
+
543
+ **RPC path (primary):**
544
+
545
+ ```javascript
546
+ import { createRpcQueryClientWithFallback, rpcQueryFeeGrant } from 'sentinel-dvpn-sdk';
547
+
548
+ const rpcClient = await createRpcQueryClientWithFallback();
549
+ const grant = await rpcQueryFeeGrant(rpcClient, granter, grantee);
550
+ ```
551
+
552
+ `rpcQueryFeeGrant` performs a protobuf ABCI query to `/cosmos.feegrant.v1beta1.Query/Allowance`. It decodes the nested protobuf response: `Grant` → `Any` (allowance) → `AllowedMsgAllowance` → `BasicAllowance` → `spend_limit` + `expiration`.
553
+
554
+ **LCD path (fallback):**
555
+
556
+ ```
557
+ GET /cosmos/feegrant/v1beta1/allowance/{granter}/{grantee}
558
+ ```
559
+
560
+ Used only when RPC fails. Falls back across `LCD_ENDPOINTS` via `tryWithFallback()`.
561
+
562
+ **Response structure** (both RPC and LCD normalize to this):
563
+
564
+ ```json
565
+ {
566
+ "allowance": {
567
+ "@type": "/cosmos.feegrant.v1beta1.AllowedMsgAllowance",
568
+ "allowance": {
569
+ "@type": "/cosmos.feegrant.v1beta1.BasicAllowance",
570
+ "spend_limit": [{ "denom": "udvpn", "amount": "5000000" }],
571
+ "expiration": "2026-04-16T00:00:00Z"
572
+ },
573
+ "allowed_messages": [
574
+ "/sentinel.subscription.v3.MsgStartSessionRequest",
575
+ "/sentinel.session.v3.MsgCancelSessionRequest",
576
+ "/sentinel.session.v3.MsgUpdateSessionRequest",
577
+ "/sentinel.node.v3.MsgStartSessionRequest"
578
+ ]
579
+ }
580
+ }
581
+ ```
582
+
583
+ **The SDK performs five checks:**
584
+
585
+ 1. **Existence** — grant is non-null → `FEE_GRANT_NOT_FOUND` if missing
586
+ 2. **Structure detection** — robust `@type`-based detection of `AllowedMsgAllowance` vs bare `BasicAllowance` (handles both wrapped and unwrapped)
587
+ 3. **Expiration** — `FEE_GRANT_EXPIRED` if past; warns if < 1 hour remaining
588
+ 4. **Spend limit** — parses `spend_limit` array, finds `udvpn` coin. `FEE_GRANT_EXHAUSTED` if < 20,000 udvpn (minimum for one session TX)
589
+ 5. **Allowed messages** — warns (non-blocking) if `MsgStartSession` / `MsgStartSessionRequest` not in list
590
+
591
+ Errors: `FEE_GRANT_NOT_FOUND`, `FEE_GRANT_EXPIRED`, `FEE_GRANT_EXHAUSTED`
592
+
593
+ #### Session TX via Subscription
594
+
595
+ Message type: `/sentinel.subscription.v3.MsgStartSessionRequest`
596
+
597
+ | Field # | Name | Type | Description |
598
+ |---------|------|------|-------------|
599
+ | 1 | `from` | string | Agent's `sent1...` address |
600
+ | 2 | `id` | uint64 | Subscription ID (from operator provisioning) |
601
+ | 3 | `node_address` | string | Target `sentnode1...` address |
602
+
603
+ The TX is broadcast via `broadcastWithFeeGrant()` which sets the `granter` field in the TX fee:
604
+
605
+ ```javascript
606
+ const fee = {
607
+ amount: [{ denom: 'udvpn', amount: '20000' }],
608
+ gas: '200000',
609
+ granter: feeGranterAddress, // Chain deducts from granter's account
610
+ };
611
+ await client.signAndBroadcast(signerAddress, [msg], fee);
612
+ ```
613
+
614
+ **Fallback:** If fee grant broadcast fails (expired grant, spend limit exhausted), the SDK falls back to `broadcastWithInactiveRetry()` using the agent's own balance. If agent has 0 P2P, this also fails and the error is propagated.
615
+
616
+ #### Disconnect with Fee Grant
617
+
618
+ On disconnect, the SDK sends `MsgCancelSessionRequest` using the same fee grant:
619
+
620
+ ```javascript
621
+ // _endSessionOnChain stores feeGranter from connect-time
622
+ const msg = buildEndSessionMsg(agentAddress, sessionId);
623
+ if (feeGranter) {
624
+ result = await broadcastWithFeeGrant(client, agentAddress, [msg], feeGranter);
625
+ } else {
626
+ result = await client.signAndBroadcast(agentAddress, [msg], fee);
627
+ }
628
+ ```
629
+
630
+ The `feeGranter` is stored in `ConnectionState._feeGranter` during connect and cleared on disconnect. All 9 disconnect call sites (graceful, abort, error cleanup, crash handler, etc.) pass `state._feeGranter` to `_endSessionOnChain`.
631
+
632
+ #### Crash Persistence of feeGranter
633
+
634
+ The `feeGranter` is persisted to `credentials.enc.json` (AES-256-GCM encrypted, per-node credential store at `~/.sentinel-sdk/credentials.enc.json`). Both WireGuard and V2Ray `saveCredentials()` calls include the field:
635
+
636
+ ```javascript
637
+ saveCredentials(nodeAddress, String(sessionId), {
638
+ serviceType: 'wireguard', // or 'v2ray'
639
+ // ... protocol-specific fields ...
640
+ ...(state._feeGranter ? { feeGranter: state._feeGranter } : {}),
641
+ });
642
+ ```
643
+
644
+ On crash recovery, `tryFastReconnect()` restores it:
645
+
646
+ ```javascript
647
+ if (saved.feeGranter && state) {
648
+ state._feeGranter = saved.feeGranter;
649
+ }
650
+ ```
651
+
652
+ Without this, a crashed agent with 0 P2P would restore the tunnel but be unable to end the session on-chain (disconnect TX would fail with insufficient funds).
653
+
654
+ **Rule:** Every in-memory state that affects disconnect MUST be persisted to `credentials.enc.json`. See FAILURES.md W4.
655
+
656
+ #### autoReconnect Dispatch
657
+
658
+ `autoReconnect()` in `resilience.js` dispatches based on original connection mode:
659
+
660
+ ```javascript
661
+ if (opts.subscriptionId) {
662
+ result = await connectViaSubscription(opts); // fee grant preserved
663
+ } else if (opts.planId) {
664
+ result = await connectViaPlan(opts); // fee grant preserved
665
+ } else {
666
+ result = await connectAuto(opts); // self-pay mode
667
+ }
668
+ ```
669
+
670
+ Previously hardcoded to `connectAuto()`, which would fail with `INSUFFICIENT_BALANCE` for 0-P2P agents. See FAILURES.md W6.
671
+
672
+ **If disconnect TX fails** (fee grant expired, spend limit exhausted, agent has 0 P2P): the session is NOT ended on-chain. It expires naturally based on the subscription's allocation. This is acceptable behavior — the session simply times out.
673
+
674
+ #### Required Fee Grant Allowed Messages
675
+
676
+ The operator's fee grant must include these message types:
677
+
678
+ | Message Type | Purpose |
679
+ |-------------|---------|
680
+ | `/sentinel.subscription.v3.MsgStartSessionRequest` | Start session via subscription |
681
+ | `/sentinel.node.v3.MsgStartSessionRequest` | Start session via node (direct) |
682
+ | `/sentinel.session.v3.MsgCancelSessionRequest` | End/cancel session |
683
+ | `/sentinel.session.v3.MsgUpdateSessionRequest` | Update session allocation |
684
+
685
+ If `MsgCancelSessionRequest` is NOT in the allowed messages, the agent cannot end sessions cleanly. Sessions will expire naturally, but the agent cannot reclaim unused allocation.
686
+
535
687
  ---
536
688
 
537
689
  ## Phase 7: Index Wait
@@ -1086,7 +1238,7 @@ await conn.cleanup();
1086
1238
  4. **Clear system proxy:** Restore previous proxy settings (Windows registry / macOS / Linux)
1087
1239
  5. **Kill V2Ray:** `process.kill()` on the V2Ray child process
1088
1240
  6. **Uninstall WireGuard:** `wireguard.exe /uninstalltunnelservice wgsent0`
1089
- 7. **End session on chain:** `MsgCancelSessionRequest` (fire-and-forget, best-effort)
1241
+ 7. **End session on chain:** `MsgCancelSessionRequest` (fire-and-forget, best-effort). If `state._feeGranter` is set (operator-provisioned mode), uses `broadcastWithFeeGrant` so agent with 0 P2P can still end the session on-chain.
1090
1242
  8. **Zero mnemonic:** `state._mnemonic = null`
1091
1243
  9. **Clear connection state:** `state.connection = null`
1092
1244
  10. **Clear persisted state:** Remove crash recovery files
@@ -48,6 +48,9 @@
48
48
  | 37 | **V2Ray split tunnel IS the SOCKS5 proxy** -- V2Ray does not change system routing. Only traffic you explicitly send through `socks5://127.0.0.1:{port}` goes through the VPN. Everything else is direct. There is no `fullTunnel` for V2Ray — `systemProxy: true` sets Windows proxy but that's opt-in, not default. | protocol | Agent assumes all traffic is encrypted when only proxied traffic is |
49
49
  | 38 | **WireGuard split tunnel requires exact destination IPs** -- `splitIPs: ['example.com']` does NOT work. WireGuard routes by IP, not domain. CDN/anycast services (Cloudflare, Google) resolve to hundreds of IPs. Use V2Ray SOCKS5 for per-app split tunnel, use WireGuard splitIPs only for known static IPs. | tunnel | Agent sets splitIPs for a CDN domain, traffic goes direct because DNS resolved to a different IP |
50
50
  | 39 | **WireGuard disconnect MUST restore DNS to DHCP** -- WireGuard config sets system DNS (10.8.0.1 or custom). This persists in the OS adapter AFTER the WG interface is removed. Every disconnect path (normal, error, emergency) must call `disableDnsLeakPrevention()` or `netsh interface ipv4 set dnsservers Wi-Fi dhcp`. Discovered 2026-03-27: Cloudflare DNS persisted after split tunnel test, broke all V2Ray and node tester connections. | tunnel | System DNS silently changed, all subsequent networking affected |
51
+ | 40 | **Every in-memory state that affects disconnect MUST be persisted** -- `_feeGranter` was in-memory only; crash recovery restored the tunnel but disconnect failed (0 P2P, no fee grant). Persist to `credentials.enc.json`, restore in `tryFastReconnect()`. | wallet | Agent crash → tunnel restored but cannot end session on-chain |
52
+ | 41 | **Reconnect must use same connection mode as original connect** -- `autoReconnect()` was hardcoded to `connectAuto()`, ignoring `opts.subscriptionId`/`opts.planId`. Fee-granted agents reconnected with direct payment and failed with INSUFFICIENT_BALANCE. | wallet | Agent drops → reconnect fails → permanent disconnect |
53
+ | 42 | **Fee grant pre-check must validate spend limit, not just existence** -- grant can exist and not be expired but have < 20,000 udvpn remaining (insufficient for one TX). Check `spend_limit` array before connecting. | wallet | Agent passes pre-check but fails at broadcast time with opaque chain error |
51
54
 
52
55
  ---
53
56
 
@@ -124,6 +127,10 @@
124
127
  | W1 | Fee grant auto-detection silently applied | Direct-connect app uses random stranger's fee grant without consent | SDK auto-detects fee grants and picks `grants[0]` on every transaction | Made fee grant opt-in via explicit `FeeGranter` option | Never auto-apply fee grants; make it explicit opt-in |
125
128
  | W2 | Insufficient funds with no dry-run option | AI builds working code but first real run fails with "insufficient funds" | Blockchain requires funded wallet; AI cannot purchase tokens | Added dry-run mode that validates everything except payment | Provide `dryRun: true` option to validate without spending tokens |
126
129
  | W3 | Fast reconnect never sets `state._mnemonic` | Sessions never end on-chain after fast reconnect; session leak | `connectDirect()` called `tryFastReconnect()` which skips `connectInternal()` where `_mnemonic` was set | Set `state._mnemonic = opts.mnemonic` BEFORE calling `tryFastReconnect()` | Set authentication state before any early-return code path |
130
+ | W4 | Fee granter lost on crash recovery | Agent crashes mid-session, restarts, `tryFastReconnect()` restores tunnel but `_feeGranter` is null — disconnect fails (0 P2P, no fee grant) | `_feeGranter` was in-memory only, not persisted to `credentials.enc.json` | Persist `feeGranter` in `saveCredentials()`, restore in `tryFastReconnect()` | Every in-memory state that affects disconnect MUST be persisted |
131
+ | W5 | Fee grant exhausted mid-session | Agent connects fine but session TX or disconnect TX fails with "fee-grant not found or exhausted" | Operator set low `spend_limit` on fee grant; pre-check didn't verify remaining budget | Added `spend_limit` check in fee grant pre-check — warns if <20,000 udvpn remaining | Always validate spend budget before connecting, not just existence + expiration |
132
+ | W6 | autoReconnect ignores subscription/plan mode | Agent connected via `connectViaSubscription`, drops, `autoReconnect` calls `connectAuto()` which tries direct payment — fails with INSUFFICIENT_BALANCE (0 P2P) | `autoReconnect()` hardcoded to `connectAuto()`, ignoring `opts.subscriptionId`/`opts.planId` | Dispatch based on `opts.subscriptionId` → `connectViaSubscription`, `opts.planId` → `connectViaPlan` | Reconnect must use same connection mode as original connect |
133
+ | W7 | Fee grant pre-check used LCD instead of RPC | Fee grant validation took ~880ms via LCD REST. SDK standard is RPC (protobuf) first — balance check already used `rpcQueryBalance` but fee grant check was LCD-only | Missing `rpcQueryFeeGrant` function; no protobuf decoder for fee grant response | Built `rpcQueryFeeGrant()` in `chain/rpc.js` with full protobuf decoding (AllowedMsgAllowance → BasicAllowance → spend_limit + expiration). LCD fallback via `tryWithFallback` | All chain queries must use RPC first, LCD fallback — never LCD-only for critical path operations |
127
134
 
128
135
  ### TIMING
129
136
 
package/ai-path/GUIDE.md CHANGED
@@ -306,6 +306,8 @@ await disconnect();
306
306
  // On-chain session end is fire-and-forget -- it may take a few seconds.
307
307
  ```
308
308
 
309
+ **Fee-granted disconnect:** When the connection was established with `feeGranter`, the SDK remembers the granter address and uses it for the `MsgCancelSessionRequest` TX on disconnect. If the fee grant has expired or been exhausted by disconnect time, the session ends naturally when the subscription allocation expires. No tokens are lost.
310
+
309
311
  ### Session Recovery
310
312
 
311
313
  If a connection partially succeeds (payment TX broadcast but tunnel failed), the session exists on-chain and can be recovered without paying again. Use the SDK's `recoverSession`:
@@ -865,6 +867,102 @@ await disconnect();
865
867
 
866
868
  **Important:** Node.js native `fetch()` silently ignores SOCKS5 proxy configuration. Use `axios` with an explicit agent, not `fetch()`.
867
869
 
870
+ ### Pattern 6: Operator-Provisioned Mode (Zero P2P / Fee-Granted)
871
+
872
+ When an operator has provisioned access for the agent (e.g., via x402 payment protocol), the agent connects with **zero P2P tokens**. The operator's fee grant covers all gas costs.
873
+
874
+ This mode requires three things from the operator:
875
+ 1. **Subscription share** -- operator added agent's `sent1...` address to their plan subscription
876
+ 2. **Fee grant** -- operator created a fee allowance so agent pays 0 gas on Sentinel
877
+ 3. **Node address** -- a node linked to the operator's plan
878
+
879
+ ```js
880
+ import { connect, disconnect } from 'sentinel-ai-connect';
881
+
882
+ // Agent has 0 P2P — operator covers gas via fee grant
883
+ const vpn = await connect({
884
+ mnemonic: process.env.MNEMONIC,
885
+
886
+ // Operator-provisioned fields (from provisioning response)
887
+ subscriptionId: 12345, // Operator's subscription
888
+ feeGranter: 'sent1operatoraddress...', // Operator pays gas
889
+ nodeAddress: 'sentnode1abc...', // Plan node
890
+
891
+ onProgress: (step, detail) => console.log(`[${step}] ${detail}`),
892
+ });
893
+
894
+ console.log(`Connected: ${vpn.protocol} | IP: ${vpn.ip}`);
895
+ await disconnect();
896
+ ```
897
+
898
+ **How it works:**
899
+ - Balance check is skipped when `feeGranter` is set (agent may have 0 P2P)
900
+ - Fee grant is validated before connecting via RPC (protobuf, ~250ms) with LCD fallback
901
+ - Session TX is broadcast via `broadcastWithFeeGrant` — chain deducts gas from granter
902
+ - Disconnect also uses fee grant for the `MsgCancelSessionRequest` TX
903
+ - If fee grant fails at any step, SDK falls back to agent's own balance (if available)
904
+
905
+ #### Fee Grant Pre-Check (Step 3.5)
906
+
907
+ Before attempting the session TX, the SDK validates the fee grant on-chain. This runs between the balance check (Step 3) and node selection (Step 4):
908
+
909
+ 1. **Query grant** — RPC first (`rpcQueryFeeGrant`, protobuf ABCI query, ~250ms), LCD fallback (`queryFeeGrant` via `tryWithFallback`, ~880ms). Matches the SDK standard: all chain queries use RPC with LCD as a safety net.
910
+ 2. **Existence check** — `FEE_GRANT_NOT_FOUND` if no grant exists from granter to grantee.
911
+ 3. **Expiration check** — `FEE_GRANT_EXPIRED` if the grant's expiration is in the past. Warns if < 1 hour remaining.
912
+ 4. **Spend limit check** — `FEE_GRANT_EXHAUSTED` if `spend_limit` has < 20,000 udvpn remaining (minimum for one session TX).
913
+ 5. **Allowed messages check** — Warns (non-blocking) if `MsgStartSession` / `MsgStartSessionRequest` is not in the grant's `allowed_messages` list.
914
+
915
+ The grant structure on-chain is `AllowedMsgAllowance` wrapping a `BasicAllowance`. The SDK uses robust `@type`-based detection to handle both wrapped and unwrapped grant formats.
916
+
917
+ **Fee grant pre-check errors:**
918
+
919
+ | Error Code | Meaning | `nextAction` | Action |
920
+ |---|---|---|---|
921
+ | `FEE_GRANT_NOT_FOUND` | No grant from granter to grantee on-chain | `request_fee_grant` | Request provisioning from operator |
922
+ | `FEE_GRANT_EXPIRED` | Grant existed but expiration is past | `request_fee_grant_renewal` | Request grant renewal from operator |
923
+ | `FEE_GRANT_EXHAUSTED` | Grant spend limit < 20,000 udvpn | `request_fee_grant_renewal` | Request operator to top up grant |
924
+
925
+ All errors include `err.code`, `err.nextAction`, and `err.details` (with `granter`, `grantee`, and context-specific fields) for programmatic handling by agents.
926
+
927
+ #### Crash Recovery
928
+
929
+ The `feeGranter` is persisted to `credentials.enc.json` (AES-256-GCM encrypted) alongside session credentials. If the agent process crashes and restarts:
930
+
931
+ 1. `tryFastReconnect()` loads credentials from disk
932
+ 2. Restores `state._feeGranter` from `saved.feeGranter`
933
+ 3. Tunnel is restored (WireGuard adapter or V2Ray process)
934
+ 4. Disconnect correctly uses fee grant — no tokens needed
935
+
936
+ Without this persistence, a crashed agent with 0 P2P would be unable to end its session on-chain after recovery.
937
+
938
+ #### Auto-Reconnect
939
+
940
+ `autoReconnect()` dispatches based on the original connection mode:
941
+
942
+ - `opts.subscriptionId` → `connectViaSubscription(opts)` — fee grant preserved
943
+ - `opts.planId` → `connectViaPlan(opts)` — fee grant preserved
944
+ - Neither → `connectAuto(opts)` — self-pay mode
945
+
946
+ This ensures fee-granted agents don't fall back to direct payment (which would fail with 0 P2P).
947
+
948
+ #### Fee-Granted Disconnect
949
+
950
+ When connected with `feeGranter`, the SDK stores the granter address in `state._feeGranter`. On `disconnect()`, `_endSessionOnChain` uses `broadcastWithFeeGrant()` for the `MsgCancelSessionRequest` TX. All 9 disconnect call sites (graceful, abort, error cleanup, crash handler, etc.) pass through `state._feeGranter`.
951
+
952
+ If the fee grant has expired or been exhausted by disconnect time, the session ends naturally when the subscription allocation expires. No tokens are lost.
953
+
954
+ **Alternative: Plan mode** -- if the agent doesn't have a specific subscription ID but knows the plan:
955
+
956
+ ```js
957
+ const vpn = await connect({
958
+ mnemonic: process.env.MNEMONIC,
959
+ planId: 42, // Subscribe + connect
960
+ feeGranter: 'sent1operatoraddress...', // Operator pays gas
961
+ });
962
+ ```
963
+
964
+ Plan mode subscribes to the plan AND starts a session in one flow. Use `subscriptionId` when the operator already provisioned a subscription; use `planId` when the agent subscribes itself.
965
+
868
966
  ---
869
967
 
870
968
  ## 9. Autonomous Agent Pattern
package/ai-path/README.md CHANGED
@@ -191,6 +191,47 @@ const vpn = await connect({
191
191
  | `signal` | `AbortSignal` | `null` | AbortController signal for cancellation |
192
192
  | `v2rayExePath` | `string` | `auto` | Path to V2Ray binary. Auto-detected from `bin/` |
193
193
 
194
+ ### Operator-Provisioned Mode (Zero P2P / Fee-Granted)
195
+
196
+ When an operator (like x402) provisions VPN access for your agent, you don't need P2P tokens. The operator shares their subscription and grants a fee allowance — your agent pays zero gas. Pass `subscriptionId` and `feeGranter` to connect:
197
+
198
+ ```js
199
+ const vpn = await connect({
200
+ mnemonic: process.env.MNEMONIC,
201
+ subscriptionId: '12345', // Operator's subscription ID
202
+ feeGranter: 'sent1operatoraddress...', // Operator's address
203
+ nodeAddress: 'sentnode1abc...', // Plan node
204
+ fullTunnel: false, // Recommended for agents
205
+ });
206
+ ```
207
+
208
+ This mode:
209
+ - **Skips balance check** — agent wallet can have 0 P2P
210
+ - **Validates fee grant via RPC** — checks existence, expiration, spend limit, and allowed messages before connecting (~250ms)
211
+ - **Uses fee grant for gas** — operator pays transaction fees (connect AND disconnect)
212
+ - **Connects via existing subscription** — no on-chain subscription creation needed
213
+ - **Crash-safe** — `feeGranter` is persisted to encrypted credentials; crash recovery restores it so disconnect still works
214
+ - **Auto-reconnect aware** — reconnects using same connection mode (subscription/plan), preserving fee grant
215
+
216
+ | Option | Type | Description |
217
+ |---|---|---|
218
+ | `subscriptionId` | `string\|number` | Operator-provisioned subscription ID |
219
+ | `feeGranter` | `string` | Operator's `sent1...` address (pays gas) |
220
+ | `planId` | `string\|number` | Alternative: subscribe to a plan (creates new subscription) |
221
+
222
+ **Fee grant pre-check errors** (thrown before connection attempt):
223
+
224
+ | Error Code | Meaning |
225
+ |---|---|
226
+ | `FEE_GRANT_NOT_FOUND` | No grant from operator to agent on-chain |
227
+ | `FEE_GRANT_EXPIRED` | Grant exists but has expired |
228
+ | `FEE_GRANT_EXHAUSTED` | Grant spend limit too low (< 20,000 udvpn) |
229
+
230
+ **When to use which:**
231
+ - `subscriptionId` — operator already added you to their subscription (x402 flow)
232
+ - `planId` — operator's plan is open; you subscribe yourself (operator grants gas via feeGranter)
233
+ - Neither — direct pay-per-use with your own P2P tokens (default mode)
234
+
194
235
  ### WARNING: `fullTunnel` and AI Agents
195
236
 
196
237
  When `fullTunnel: true` (the default), **ALL traffic** routes through the VPN tunnel — including the SDK's own chain queries (LCD, RPC), balance checks, and reconnect logic. On nodes with median speeds (~3 Mbps), this makes chain operations significantly slower and can cause timeouts.