warpmetal 0.6.0 → 0.7.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/README.md CHANGED
@@ -32,7 +32,7 @@ published wallet CLI with the exact version WarpMetal reports:
32
32
  npm install --global warpmetal
33
33
  warpmetal --help
34
34
 
35
- npm install --global @x402api/agent-wallet-cli@0.2.1
35
+ npm install --global @x402api/agent-wallet-cli@0.2.2
36
36
  x402api help --json
37
37
  ```
38
38
 
@@ -85,10 +85,13 @@ After preparing an order, request its live payment challenge:
85
85
  warpmetal checkout challenge --task <taskId> --json
86
86
  ```
87
87
 
88
- On HTTP 402 the CLI returns exact `paymentTerms`, the pinned
88
+ On HTTP 402 the CLI returns exact `paymentTerms`, the opaque `challengeHandle`
89
+ that WarpMetal uses for merchant-side reconciliation, the pinned
89
90
  `@x402api/agent-wallet-cli` package contract, and argv arrays under
90
91
  `paymentWorkflow`. It also writes an owner-only request envelope that contains
91
- the exact checkout URL and body but no WarpMetal credential. The published
92
+ the exact checkout URL and body but no WarpMetal credential or challenge
93
+ handle. The handle is not a buyer payment identifier and is never a wallet
94
+ signing input. The published
92
95
  launch wallet accepts sponsored Base USDC and sponsored Solana USDC/USDT only;
93
96
  the returned terms identify compatible alternatives and confirm that the buyer
94
97
  does not need ETH or SOL. Payment authority depends on execution context:
@@ -141,11 +144,13 @@ after authorization.
141
144
 
142
145
  If funding is short in an interactive conversation, tell the human the exact
143
146
  top-up in normal and atomic units, the network, stablecoin and contract/mint,
144
- and the payer wallet's public receiving address. The human sends the token to
145
- that wallet address, never to the token contract/mint or WarpMetal's payment
146
- recipient, and never sends ETH or SOL for a sponsored payment. In an unattended
147
- run, use a preconfigured refill or escalation mechanism or stop with
148
- `funding_required`.
147
+ and the payer wallet's public receiving address. The returned
148
+ `paymentWorkflow.fundingWorkflow` provides safe address and balance argv plus a
149
+ presentation contract: render that public address as both a QR code and
150
+ copyable text. The human sends the token to that wallet address, never to the
151
+ token contract/mint or WarpMetal's payment recipient, and never sends ETH or
152
+ SOL for a sponsored payment. In an unattended run, use a preconfigured refill
153
+ or escalation mechanism or stop with `funding_required`.
149
154
 
150
155
  ## Autonomous renewal and refill
151
156
 
@@ -168,7 +173,11 @@ warpmetal renewal configure \
168
173
  ```
169
174
 
170
175
  The recipient must follow the one-time verification link before lifecycle or
171
- refill mail is sent. A recurring unattended agent can then run:
176
+ refill mail is sent. If no verified notification email exists, `renewal
177
+ configure` returns `email_required` before changing policy. Supply `--email`,
178
+ or deliberately continue with `--without-email-notifications`; the latter does
179
+ not enable signed refill-email workflows. A recurring unattended agent can then
180
+ run:
172
181
 
173
182
  ```sh
174
183
  warpmetal renewal due --all --json
@@ -176,14 +185,16 @@ warpmetal renewal run --all-due --json
176
185
  ```
177
186
 
178
187
  Inside policy, the CLI returns the exact Agent Wallet authorization and submit
179
- argv. If balance is insufficient, run the returned `refillWorkflow.argv` with
180
- its `X402API_NOTIFICATION_URL` environment value. `x402api wallet
188
+ argv. If balance is insufficient, `refillWorkflow` is returned only when the
189
+ server has an active verified notification subscription. Run its argv with the
190
+ returned `X402API_NOTIFICATION_URL` environment value. `x402api wallet
181
191
  notify-refill` signs an opaque subscription reference and wallet-produced
182
192
  balance fields; it cannot choose an email address. WarpMetal verifies the
183
193
  wallet signature and current on-chain balance before emailing the verified
184
- human the network, stablecoin, public wallet address, and required minimum
185
- top-up. The human may transfer more than that minimum; the renewal policy—not
186
- the refill target—remains the spending authority.
194
+ human the network, stablecoin, public wallet address, required minimum top-up,
195
+ and a locally generated QR encoding only that wallet address. The address is
196
+ also repeated as copyable text. The human may transfer more than that minimum;
197
+ the renewal policy—not the refill target—remains the spending authority.
187
198
 
188
199
  The agent never sends a partial x402 payment. If no verified refill path
189
200
  exists, it reports `funding_required`. If a previous payment is pending or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warpmetal",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Agent-safe CLI and skill for purchasing, renewing, and managing WarpMetal VPS servers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -111,7 +111,9 @@ task or operator limit.
111
111
  After interactive confirmation or autonomous policy validation, invoke the
112
112
  returned authorize argv once. The separate
113
113
  `x402api` executable owns the wallet, validates the envelope, and writes the
114
- payment artifact. Then invoke the returned submit argv, equivalent to:
114
+ payment artifact. The returned `challengeHandle` is opaque merchant
115
+ reconciliation metadata; never copy it into the request envelope or treat it as
116
+ the buyer payment identifier. Then invoke the returned submit argv, equivalent to:
115
117
 
116
118
  ```sh
117
119
  warpmetal checkout submit \
@@ -152,9 +154,12 @@ price, asset, network, count, horizon, or total-budget mismatch.
152
154
  Run `warpmetal renewal prepare --server <serverId> --json`, then invoke only
153
155
  the returned `paymentWorkflow.authorize.argv` and
154
156
  `paymentWorkflow.submit.argv`. If authorization reports insufficient balance,
155
- set the returned `refillWorkflow.environment`, invoke its exact argv once, and
156
- stop until funding arrives. The signed refill intent resolves to a verified
157
- human contact; never add an email address to it. Never make a partial payment.
157
+ use `paymentWorkflow.fundingWorkflow` to obtain the public address and balance,
158
+ then show the address as both a QR code and copyable text. Only when
159
+ `refillNotification.available` is true, set the returned
160
+ `refillWorkflow.environment`, invoke its exact argv once, and stop until
161
+ funding arrives. The signed refill intent resolves to a verified human contact;
162
+ never add an email address to it. Never make a partial payment.
158
163
 
159
164
  After funding, prepare again, authorize exactly once, submit with WarpMetal,
160
165
  and confirm the returned `termEndsAt`. On `reconcile_pending` or
@@ -53,14 +53,16 @@ warpmetal order status \
53
53
  ```
54
54
 
55
55
  On HTTP 402, `checkout challenge` validates and displays exact payment terms,
56
- writes a credential-free x402api V1 request envelope with owner-only
56
+ writes the opaque x402api `challengeHandle` to private WarpMetal state for
57
+ merchant reconciliation, and writes a credential-free x402api V1 request envelope with owner-only
57
58
  permissions, and returns the exact pinned wallet package, V1 contract probe,
58
59
  matching wallet-skill install, authorization, and WarpMetal submission argv
59
60
  arrays. The default envelope and suggested artifact paths live under the
60
61
  private WarpMetal state directory. An explicit output path must not already
61
- contain different content.
62
+ contain different content. `challengeHandle` is intentionally absent from the
63
+ wallet envelope: it is neither a buyer payment identifier nor signing input.
62
64
 
63
- The current integration targets `@x402api/agent-wallet-cli@0.2.1`. A compatible
65
+ The current integration targets `@x402api/agent-wallet-cli@0.2.2`. A compatible
64
66
  live term is marked `agentWalletSupported: true` and must use the sponsored
65
67
  Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
66
68
  WarpMetal rejects a challenge with no compatible sponsored term. Because the
@@ -101,7 +103,8 @@ warpmetal renewal configure \
101
103
  (--maximum-renewals <n> | --renew-through <UTC>) \
102
104
  [--maximum-total-spend-atomic <amount>] \
103
105
  --allowed-network <network> --allowed-asset <asset> \
104
- --wallet <name> [--refill-target-atomic <amount>] [--email <address>] --json
106
+ --wallet <name> [--refill-target-atomic <amount>] \
107
+ [--email <address> | --without-email-notifications] --json
105
108
  warpmetal renewal status --server <serverId> --json
106
109
  warpmetal renewal due (--server <serverId> | --all) --json
107
110
  warpmetal renewal prepare --server <serverId> --json
@@ -113,9 +116,12 @@ warpmetal notifications configure --server <serverId> --email <address> \
113
116
  warpmetal notifications status --server <serverId> --json
114
117
  ```
115
118
 
116
- `renewal prepare` returns exact payment and refill argv arrays. `renewal run`
117
- is an agent-facing state machine, not a wallet-signing daemon. See
118
- [renewals.md](renewals.md).
119
+ Without a verified notification email, renewal configuration asks for an email
120
+ before mutating policy unless `--without-email-notifications` explicitly opts
121
+ out. `renewal prepare` always returns safe funding address/balance argv and
122
+ returns signed refill-email argv only for a verified active subscription.
123
+ `renewal run` is an agent-facing state machine, not a wallet-signing daemon.
124
+ See [renewals.md](renewals.md).
119
125
 
120
126
  ## Server management
121
127
 
@@ -19,7 +19,7 @@ files or pass a WarpMetal credential to `x402api`.
19
19
  WarpMetal runs on Node.js 20 or 22. The x402api Agent Wallet currently requires
20
20
  Node.js 22. Use the exact published package reported by
21
21
  `paymentWorkflow.signerPackage.spec`; the current contract is
22
- `@x402api/agent-wallet-cli@0.2.1`. Do not add it as a WarpMetal dependency,
22
+ `@x402api/agent-wallet-cli@0.2.2`. Do not add it as a WarpMetal dependency,
23
23
  install executable wallet code from an unpinned repository URL, or substitute
24
24
  a similarly named package.
25
25
 
@@ -66,10 +66,12 @@ skill automatically.
66
66
 
67
67
  6. If funding is short in an interactive conversation, show the exact deficit
68
68
  in normal token units and atomic units, the exact network, token symbol and
69
- contract/mint, and the payer wallet's public receiving address. Tell the
70
- human to transfer the token to the payer wallet address, never to the token
71
- contract/mint or `paymentTerms.recipient`. Sponsored launch payments never
72
- ask the buyer to fund ETH or SOL. Recheck the balance after the transfer.
69
+ contract/mint, and the payer wallet's public receiving address. Render only
70
+ that public address as a QR code and repeat the full address as copyable
71
+ text. Tell the human to transfer the token to the payer wallet address,
72
+ never to the token contract/mint or `paymentTerms.recipient`. Sponsored
73
+ launch payments never ask the buyer to fund ETH or SOL. Recheck the balance
74
+ after the transfer.
73
75
  7. If funding is short in an unattended run, use only a preconfigured refill
74
76
  mechanism returned by WarpMetal. For a configured renewal, set the returned
75
77
  `refillWorkflow.environment`, invoke `refillWorkflow.argv` once, and stop
@@ -114,7 +116,11 @@ paymentWorkflow.submit.argv
114
116
  Replace only `<wallet-name>` after selecting the wallet for the advertised
115
117
  network. Do not parse, rewrite, copy, or display the request envelope or payment
116
118
  artifact. The envelope excludes the WarpMetal owner token; the artifact holds
117
- the complete payment signature and remains owner-only.
119
+ the complete payment signature and remains owner-only. The safe challenge JSON
120
+ also contains `challengeHandle`, an opaque value retained by WarpMetal for
121
+ merchant reconciliation. It is deliberately excluded from the x402api V1
122
+ request envelope and must not be confused with the wallet-created buyer payment
123
+ identifier.
118
124
 
119
125
  Choose only a term marked `agentWalletSupported: true`,
120
126
  `sponsoredNetworkFee: true`, and `buyerNativeFeeRequired: false`. The supported
@@ -23,15 +23,18 @@ warpmetal renewal configure \
23
23
  --allowed-asset <exact-contract-or-mint> \
24
24
  --wallet <agent-wallet-name> \
25
25
  [--refill-target-atomic <target>] \
26
- [--email <human-address>] \
26
+ [--email <human-address> | --without-email-notifications] \
27
27
  --json
28
28
  ```
29
29
 
30
- `--email` queues verification; refill and expanded lifecycle notices remain
31
- disabled until the human opens the verification link. A refill target is a
32
- funding target, not spend permission, and cannot exceed the per-renewal cap.
33
- The human may transfer more than the displayed deficit. The agent may spend
34
- only the exact renewal amount allowed by policy.
30
+ Without an active verified notification email, configuration asks for one
31
+ before changing policy. `--email` queues verification; refill and expanded
32
+ lifecycle notices remain disabled until the human opens the verification link.
33
+ Use `--without-email-notifications` only as an explicit opt-out; no signed
34
+ refill-email workflow is returned in that state. A refill target is a funding
35
+ target, not spend permission, and cannot exceed the per-renewal cap. The human
36
+ may transfer more than the displayed deficit. The agent may spend only the
37
+ exact renewal amount allowed by policy.
35
38
 
36
39
  The WarpMetal CLI stores only the local wallet name. The backend stores
37
40
  non-secret limits, counters, payment rail, time horizon, and an opaque
@@ -70,8 +73,10 @@ one policy-compatible rail, and returns:
70
73
  ```text
71
74
  paymentWorkflow.authorize.argv
72
75
  paymentWorkflow.submit.argv
73
- refillWorkflow.environment
74
- refillWorkflow.argv
76
+ paymentWorkflow.fundingWorkflow
77
+ refillNotification
78
+ refillWorkflow.environment (verified email only)
79
+ refillWorkflow.argv (verified email only)
75
80
  ```
76
81
 
77
82
  Invoke the authorize argv once. It writes an owner-only artifact. Then invoke
@@ -112,10 +117,12 @@ authoritative balance before queuing email. The intent cannot name a recipient
112
117
  email, tenant, or product.
113
118
 
114
119
  In a human conversation, also display the exact deficit, network, token symbol,
115
- contract or mint, and payer wallet address. Tell the human to fund the payer
120
+ contract or mint, and payer wallet address. Render only that public address as
121
+ a QR code and repeat it as copyable text. Tell the human to fund the payer
116
122
  wallet address—not the token contract/mint and not WarpMetal's merchant
117
123
  recipient. Do not ask for ETH or SOL because supported network fees are
118
- sponsored.
124
+ sponsored. A refill email repeats the address as text and includes a locally
125
+ generated QR encoding only that address.
119
126
 
120
127
  ## Stop and recovery states
121
128
 
package/src/api.js CHANGED
@@ -216,6 +216,14 @@ export class WarpMetalClient {
216
216
  );
217
217
  }
218
218
 
219
+ deleteNotifications(serverId, token) {
220
+ return this.request(
221
+ "DELETE",
222
+ `/servers/${encodeURIComponent(serverId)}/notifications`,
223
+ { token },
224
+ );
225
+ }
226
+
219
227
  renewalCheckout(planId, { bodyText, token, paymentSignature }) {
220
228
  return this.checkout(`/checkout/${encodeURIComponent(planId)}/renew`, {
221
229
  bodyText,
package/src/cli.js CHANGED
@@ -84,6 +84,7 @@ Usage:
84
84
  warpmetal renewal configure --server <serverId> --renew-before-days <n>
85
85
  --maximum-payment-atomic <amount> (--maximum-renewals <n> | --renew-through <UTC>)
86
86
  --allowed-network <CAIP-2> --allowed-asset <asset> --wallet <name>
87
+ [--email <address> | --without-email-notifications]
87
88
  warpmetal renewal status|prepare|submit|run ...
88
89
  warpmetal renewal due (--server <serverId> | --all)
89
90
  warpmetal notifications configure --server <serverId> --email <address>
@@ -364,9 +365,15 @@ async function pollGrant(
364
365
 
365
366
  function challengeResult(taskId, checkoutBody, response) {
366
367
  const paymentRequired = response.headers["payment-required"];
368
+ const challengeHandle = response.headers["x-x402api-challenge-handle"];
367
369
  if (response.status === 402 && !paymentRequired) {
368
370
  throw new CliError("WarpMetal returned HTTP 402 without PAYMENT-REQUIRED.");
369
371
  }
372
+ if (response.status === 402 && !challengeHandle) {
373
+ throw new CliError(
374
+ "WarpMetal returned HTTP 402 without X-X402API-Challenge-Handle.",
375
+ );
376
+ }
370
377
  return {
371
378
  status: response.data?.status,
372
379
  taskId,
@@ -374,6 +381,7 @@ function challengeResult(taskId, checkoutBody, response) {
374
381
  response.data?.paymentAttemptId ||
375
382
  response.headers["x-warpmetal-payment-attempt"],
376
383
  paymentRequired,
384
+ challengeHandle,
377
385
  checkoutBodySha256: createHash("sha256").update(checkoutBody).digest("hex"),
378
386
  };
379
387
  }
@@ -645,7 +653,7 @@ async function handleCheckoutChallenge(client, store, options, context) {
645
653
  stringOption(options, "request-envelope-out"),
646
654
  );
647
655
  const paymentInstructions = safe.paymentWorkflow
648
- ? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
656
+ ? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nIf funding is short, run ${shellCommand(safe.paymentWorkflow.fundingWorkflow.address.argv)} and ${shellCommand(safe.paymentWorkflow.fundingWorkflow.balance.argv)}, then show the payer address as both a QR code and copyable text.\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
649
657
  : "";
650
658
  emit(
651
659
  context.stdout,
@@ -775,6 +783,44 @@ export function refillRenewBy(termEndsAt, currentTime = Date.now()) {
775
783
  return new Date(Number.isFinite(parsed) && parsed > minimum ? parsed : minimum).toISOString();
776
784
  }
777
785
 
786
+ function activeNotificationSubscription(notifications) {
787
+ const subscription = notifications?.subscription;
788
+ return Boolean(
789
+ notifications?.configured &&
790
+ subscription?.verified &&
791
+ !subscription?.disabled,
792
+ );
793
+ }
794
+
795
+ function refillNotificationState(notifications, policy) {
796
+ const subscription = notifications?.subscription;
797
+ if (!notifications?.configured || !subscription || subscription.disabled) {
798
+ return {
799
+ available: false,
800
+ reason: "verified_email_required",
801
+ subscription: subscription || null,
802
+ };
803
+ }
804
+ if (!subscription.verified) {
805
+ return {
806
+ available: false,
807
+ reason: "email_verification_required",
808
+ subscription,
809
+ };
810
+ }
811
+ if (
812
+ policy?.notificationReference &&
813
+ subscription.reference !== policy.notificationReference
814
+ ) {
815
+ return {
816
+ available: false,
817
+ reason: "notification_reference_mismatch",
818
+ subscription,
819
+ };
820
+ }
821
+ return { available: true, reason: null, subscription };
822
+ }
823
+
778
824
  async function handleRenewalConfigure(client, store, options, context) {
779
825
  const serverId = stringOption(options, "server", { required: true });
780
826
  const wallet = stringOption(options, "wallet", { required: true });
@@ -818,7 +864,72 @@ async function handleRenewalConfigure(client, store, options, context) {
818
864
  for (const [key, value] of Object.entries(body)) {
819
865
  if (value === undefined) delete body[key];
820
866
  }
867
+ const email = stringOption(options, "email");
868
+ const withoutEmailNotifications = booleanOption(
869
+ options,
870
+ "without-email-notifications",
871
+ );
872
+ if (email && withoutEmailNotifications) {
873
+ throw new CliError(
874
+ "Use either --email or --without-email-notifications, not both.",
875
+ { exitCode: 2 },
876
+ );
877
+ }
821
878
  const token = await requireServerToken(store, serverId, options, context.env);
879
+ let notifications = (await client.getNotifications(serverId, token)).data;
880
+ if (
881
+ body.enabled &&
882
+ !email &&
883
+ !withoutEmailNotifications &&
884
+ !activeNotificationSubscription(notifications)
885
+ ) {
886
+ const verificationPending = Boolean(
887
+ notifications.configured &&
888
+ notifications.subscription &&
889
+ !notifications.subscription.disabled,
890
+ );
891
+ const action = verificationPending
892
+ ? "email_verification_required"
893
+ : "email_required";
894
+ const output = {
895
+ action,
896
+ serverId,
897
+ reason: verificationPending
898
+ ? "Verify the existing notification email or resend verification with --email."
899
+ : "A verified email enables renewal and wallet-refill notifications.",
900
+ notifications,
901
+ next: {
902
+ configureEmail: `warpmetal renewal configure ... --email <address>`,
903
+ continueWithoutEmail:
904
+ "warpmetal renewal configure ... --without-email-notifications",
905
+ },
906
+ };
907
+ emit(
908
+ context.stdout,
909
+ output,
910
+ context.json,
911
+ verificationPending
912
+ ? `Verify the notification email for ${serverId}, or rerun with --email to resend verification. To opt out explicitly, rerun with --without-email-notifications.`
913
+ : `An email is needed for renewal and wallet-refill notifications. Rerun with --email <address>, or opt out explicitly with --without-email-notifications.`,
914
+ );
915
+ return 6;
916
+ }
917
+ if (email) {
918
+ notifications = (
919
+ await client.putNotifications(serverId, { email }, token)
920
+ ).data;
921
+ } else if (
922
+ withoutEmailNotifications &&
923
+ notifications.configured &&
924
+ notifications.subscription &&
925
+ !notifications.subscription.disabled
926
+ ) {
927
+ await client.deleteNotifications(serverId, token);
928
+ notifications = {
929
+ ...notifications,
930
+ subscription: { ...notifications.subscription, disabled: true },
931
+ };
932
+ }
822
933
  const result = await client.putRenewalPolicy(serverId, body, token);
823
934
  await store.saveRenewalPolicy(
824
935
  serverId,
@@ -826,19 +937,27 @@ async function handleRenewalConfigure(client, store, options, context) {
826
937
  wallet,
827
938
  refillTargetAtomic,
828
939
  );
829
- const email = stringOption(options, "email");
830
- let notifications;
831
- if (email) {
832
- notifications = (
833
- await client.putNotifications(serverId, { email }, token)
834
- ).data;
835
- }
836
- const output = { ...result.data, wallet, refillTargetAtomic, notifications };
940
+ const notificationState = {
941
+ optedOut: withoutEmailNotifications,
942
+ refillAvailable: activeNotificationSubscription(notifications),
943
+ status: withoutEmailNotifications
944
+ ? "opted_out"
945
+ : activeNotificationSubscription(notifications)
946
+ ? "verified"
947
+ : "verification_required",
948
+ };
949
+ const output = {
950
+ ...result.data,
951
+ wallet,
952
+ refillTargetAtomic,
953
+ notifications,
954
+ notificationState,
955
+ };
837
956
  emit(
838
957
  context.stdout,
839
958
  output,
840
959
  context.json,
841
- `Configured bounded renewal for ${serverId} with wallet ${wallet}.${email ? " Check the email verification link." : ""}`,
960
+ `Configured bounded renewal for ${serverId} with wallet ${wallet}.${email && !notificationState.refillAvailable ? " Check the email verification link." : withoutEmailNotifications ? " Email and wallet-refill notifications were explicitly skipped." : ""}`,
842
961
  );
843
962
  return 0;
844
963
  }
@@ -866,6 +985,7 @@ async function attachRenewalPaymentWorkflow(
866
985
  serverId,
867
986
  server,
868
987
  renewal,
988
+ notifications,
869
989
  response,
870
990
  requestedEnvelopePath,
871
991
  ) {
@@ -924,6 +1044,7 @@ async function attachRenewalPaymentWorkflow(
924
1044
  taskId: server.id,
925
1045
  serverId,
926
1046
  kind: "renewal",
1047
+ wallet: renewal.wallet,
927
1048
  requestEnvelopePath,
928
1049
  paymentArtifactPath: defaults.paymentArtifactPath,
929
1050
  });
@@ -932,27 +1053,39 @@ async function attachRenewalPaymentWorkflow(
932
1053
  BigInt(renewal.refillTargetAtomic) >= BigInt(term.amountAtomic)
933
1054
  ? renewal.refillTargetAtomic
934
1055
  : term.amountAtomic;
935
- const refillWorkflow = {
936
- environment: {
937
- X402API_NOTIFICATION_URL: `${client.baseUrl}/notifications/x402api/refill`,
938
- },
939
- argv: [
940
- "x402api",
941
- "wallet",
942
- "notify-refill",
943
- "--wallet",
944
- renewal.wallet,
945
- "--subscription-reference",
946
- renewal.policy.notificationReference,
947
- "--renew-by",
948
- refillRenewBy(server.termEndsAt),
949
- "--target-balance-atomic",
950
- refillTarget,
951
- "--reason",
952
- "renewal",
953
- "--json",
954
- ],
955
- };
1056
+ Object.assign(workflow.fundingWorkflow, {
1057
+ network: term.network,
1058
+ asset: term.asset,
1059
+ requiredPaymentAtomic: term.amountAtomic,
1060
+ targetBalanceAtomic: refillTarget,
1061
+ });
1062
+ const refillNotification = refillNotificationState(
1063
+ notifications,
1064
+ renewal.policy,
1065
+ );
1066
+ const refillWorkflow = refillNotification.available
1067
+ ? {
1068
+ environment: {
1069
+ X402API_NOTIFICATION_URL: `${client.baseUrl}/notifications/x402api/refill`,
1070
+ },
1071
+ argv: [
1072
+ "x402api",
1073
+ "wallet",
1074
+ "notify-refill",
1075
+ "--wallet",
1076
+ renewal.wallet,
1077
+ "--subscription-reference",
1078
+ refillNotification.subscription.reference,
1079
+ "--renew-by",
1080
+ refillRenewBy(server.termEndsAt),
1081
+ "--target-balance-atomic",
1082
+ refillTarget,
1083
+ "--reason",
1084
+ "renewal",
1085
+ "--json",
1086
+ ],
1087
+ }
1088
+ : undefined;
956
1089
  Object.assign(safe, {
957
1090
  serverId,
958
1091
  paymentTerms: request.terms,
@@ -960,6 +1093,7 @@ async function attachRenewalPaymentWorkflow(
960
1093
  paymentRequestDigest: request.requestDigest,
961
1094
  paymentChallengeDigest: request.challengeDigest,
962
1095
  paymentWorkflow: workflow,
1096
+ refillNotification,
963
1097
  refillWorkflow,
964
1098
  });
965
1099
  await store.saveRenewalChallenge(serverId, {
@@ -968,6 +1102,7 @@ async function attachRenewalPaymentWorkflow(
968
1102
  termEndsAt: server.termEndsAt,
969
1103
  paymentRequired: safe.paymentRequired,
970
1104
  paymentAttemptId: safe.paymentAttemptId,
1105
+ challengeHandle: safe.challengeHandle,
971
1106
  paymentRequestDigest: safe.paymentRequestDigest,
972
1107
  paymentChallengeDigest: safe.paymentChallengeDigest,
973
1108
  paymentRequestEnvelopePath: requestEnvelopePath,
@@ -988,9 +1123,10 @@ function renewalActionExitCode(action) {
988
1123
 
989
1124
  async function prepareRenewal(client, store, serverId, options, context) {
990
1125
  const token = await requireServerToken(store, serverId, options, context.env);
991
- const [serverResult, policyResult] = await Promise.all([
1126
+ const [serverResult, policyResult, notificationsResult] = await Promise.all([
992
1127
  client.getServer(serverId, token),
993
1128
  client.getRenewalPolicy(serverId, token),
1129
+ client.getNotifications(serverId, token),
994
1130
  ]);
995
1131
  if (!policyResult.data.configured) {
996
1132
  return {
@@ -1032,6 +1168,7 @@ async function prepareRenewal(client, store, serverId, options, context) {
1032
1168
  serverId,
1033
1169
  serverResult.data.task,
1034
1170
  local,
1171
+ notificationsResult.data,
1035
1172
  response,
1036
1173
  stringOption(options, "request-envelope-out"),
1037
1174
  );
@@ -1053,8 +1190,16 @@ async function prepareRenewal(client, store, serverId, options, context) {
1053
1190
  async function handleRenewalPrepare(client, store, options, context) {
1054
1191
  const serverId = stringOption(options, "server", { required: true });
1055
1192
  const safe = await prepareRenewal(client, store, serverId, options, context);
1193
+ const fundingInstructions = safe.paymentWorkflow
1194
+ ? `\nIf funding is short, run ${shellCommand(safe.paymentWorkflow.fundingWorkflow.address.argv)} and ${shellCommand(safe.paymentWorkflow.fundingWorkflow.balance.argv)}. Show the returned payer address as both a QR code and copyable text, with the exact network, asset, and deficit.`
1195
+ : "";
1196
+ const refillInstructions = safe.refillWorkflow
1197
+ ? `\nFor verified email refill, set ${Object.entries(safe.refillWorkflow.environment).map(([key, value]) => `${key}=${value}`).join(" ")} and run: ${shellCommand(safe.refillWorkflow.argv)}`
1198
+ : safe.paymentWorkflow
1199
+ ? `\nVerified email refill is unavailable (${safe.refillNotification.reason}). Configure and verify notifications, or fund the displayed payer address directly.`
1200
+ : "";
1056
1201
  const instructions = safe.paymentWorkflow
1057
- ? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nIf the wallet is short of funds, set ${Object.entries(safe.refillWorkflow.environment).map(([key, value]) => `${key}=${value}`).join(" ")} and run: ${shellCommand(safe.refillWorkflow.argv)}\nSubmit: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
1202
+ ? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}${fundingInstructions}${refillInstructions}\nSubmit: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
1058
1203
  : "";
1059
1204
  emit(
1060
1205
  context.stdout,
@@ -2325,6 +2470,7 @@ async function dispatch(positionals, options, passthrough, context) {
2325
2470
  "allowed-asset",
2326
2471
  "refill-target-atomic",
2327
2472
  "email",
2473
+ "without-email-notifications",
2328
2474
  "disabled",
2329
2475
  ]);
2330
2476
  return handleRenewalConfigure(client, store, options, context);
package/src/payment.js CHANGED
@@ -15,7 +15,7 @@ const MAX_ARTIFACT_BYTES = 1024 * 1024;
15
15
  const MAX_SIGNATURE_BYTES = 512 * 1024;
16
16
 
17
17
  export const AGENT_WALLET_PACKAGE = "@x402api/agent-wallet-cli";
18
- export const AGENT_WALLET_VERSION = "0.2.1";
18
+ export const AGENT_WALLET_VERSION = "0.2.2";
19
19
  const AGENT_WALLET_SPEC = `${AGENT_WALLET_PACKAGE}@${AGENT_WALLET_VERSION}`;
20
20
  const GAS_SPONSORSHIP_EXTENSION = "com.x402api.gas-sponsorship";
21
21
  const BASE_NETWORK = "eip155:8453";
@@ -696,6 +696,7 @@ export function paymentWorkflow({
696
696
  taskId,
697
697
  serverId,
698
698
  kind = "checkout",
699
+ wallet = "<wallet-name>",
699
700
  requestEnvelopePath,
700
701
  paymentArtifactPath,
701
702
  }) {
@@ -739,7 +740,7 @@ export function paymentWorkflow({
739
740
  "payment",
740
741
  "authorize",
741
742
  "--wallet",
742
- "<wallet-name>",
743
+ wallet,
743
744
  "--request-envelope",
744
745
  requestEnvelopePath,
745
746
  "--artifact-out",
@@ -747,6 +748,27 @@ export function paymentWorkflow({
747
748
  "--json",
748
749
  ],
749
750
  },
751
+ fundingWorkflow: {
752
+ action: "fund_wallet",
753
+ trigger: "insufficient_asset_balance",
754
+ wallet,
755
+ address: {
756
+ argv: ["x402api", "wallet", "address", "--wallet", wallet, "--json"],
757
+ },
758
+ balance: {
759
+ argv: ["x402api", "wallet", "balance", "--wallet", wallet, "--json"],
760
+ },
761
+ presentation: {
762
+ qrPayloadField: "address",
763
+ textPayloadField: "address",
764
+ showQr: true,
765
+ showAddressString: true,
766
+ showExactDeficit: true,
767
+ showNetworkAndAsset: true,
768
+ },
769
+ safety:
770
+ "Encode and fund only the payer wallet public address. Never fund the token contract, mint, or WarpMetal merchant recipient; sponsored payments do not require ETH or SOL.",
771
+ },
750
772
  submit: {
751
773
  argv:
752
774
  kind === "renewal"
package/src/state.js CHANGED
@@ -280,6 +280,7 @@ export class StateStore {
280
280
  {
281
281
  paymentRequired,
282
282
  paymentAttemptId,
283
+ challengeHandle,
283
284
  paymentRequestDigest,
284
285
  paymentChallengeDigest,
285
286
  paymentWorkflow,
@@ -293,6 +294,7 @@ export class StateStore {
293
294
  });
294
295
  order.paymentRequired = paymentRequired;
295
296
  order.paymentAttemptId = paymentAttemptId;
297
+ order.challengeHandle = challengeHandle;
296
298
  order.paymentRequestDigest = paymentRequestDigest;
297
299
  order.paymentChallengeDigest = paymentChallengeDigest;
298
300
  order.paymentRequestEnvelopePath = paymentWorkflow?.requestEnvelopePath;
@@ -465,6 +467,7 @@ export class StateStore {
465
467
  serverId: order.serverId,
466
468
  planId: order.planId,
467
469
  paymentAttemptId: order.paymentAttemptId,
470
+ challengeHandle: order.challengeHandle,
468
471
  walletPaymentAttemptId: order.walletPaymentAttemptId,
469
472
  walletName: order.walletName,
470
473
  payerAddress: order.walletPayerAddress,
@@ -499,6 +502,7 @@ export class StateStore {
499
502
  refillTargetAtomic: renewal.refillTargetAtomic,
500
503
  policy: renewal.policy,
501
504
  paymentAttemptId: renewal.paymentAttemptId,
505
+ challengeHandle: renewal.challengeHandle,
502
506
  paymentArtifactExpiresAt: renewal.paymentArtifactExpiresAt,
503
507
  })),
504
508
  };