warpmetal 0.8.0 → 0.8.1

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
@@ -34,7 +34,7 @@ published wallet CLI with the exact version WarpMetal reports:
34
34
  npm install --global warpmetal
35
35
  warpmetal --help
36
36
 
37
- npm install --global @x402api/agent-wallet-cli@0.2.8
37
+ npm install --global @x402api/agent-wallet-cli@0.2.9
38
38
  x402api help --json
39
39
  ```
40
40
 
@@ -58,7 +58,7 @@ integration.
58
58
 
59
59
  The repository also contains a skills-only WarpMetal plugin for the public
60
60
  Plugins Directory shared by Codex and ChatGPT. The plugin remains a separate
61
- artifact from the npm CLI and requires `warpmetal` CLI version 0.7.8 or newer.
61
+ artifact from the npm CLI and requires `warpmetal` CLI version 0.8.1 or newer.
62
62
 
63
63
  To test the repository marketplace after the plugin lands on `main`:
64
64
 
@@ -130,6 +130,12 @@ external-recipient extension bindings to match the advertised alternatives
130
130
  exactly, then preserves the chosen authorization artifact byte-for-byte for
131
131
  every pending or ambiguous retry.
132
132
 
133
+ Submission output includes the server-validated `paymentId`, `confirmed`, and
134
+ `finalized` fields when WarpMetal supplies lifecycle evidence. Retry only while
135
+ confirmation is pending. As soon as `confirmed` is true, stop all payment
136
+ submission—even when `finalized` is false—and let WarpMetal continue signed-
137
+ receipt finality while provisioning or renewal proceeds.
138
+
133
139
  - When a human is actively chatting with the agent, show the exact live terms
134
140
  and ask for confirmation immediately before authorizing and submitting.
135
141
  - In an unattended run, a pre-funded dedicated wallet is standing spend
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warpmetal",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Agent-safe CLI and skill for purchasing, renewing, and managing WarpMetal VPS servers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@ with ad hoc HTTP commands.
11
11
 
12
12
  ## Start safely
13
13
 
14
- 1. Run `warpmetal --version` and require version `0.7.8` or newer for this
14
+ 1. Run `warpmetal --version` and require version `0.8.1` or newer for this
15
15
  plugin. If it is missing or older, explain the compatibility requirement,
16
16
  ask before installing or upgrading software, and use only the official npm
17
17
  package from `https://www.npmjs.com/package/warpmetal`.
@@ -178,7 +178,8 @@ SSH-authorized human recipients;
178
178
  never add an email address to it. Never make a partial payment.
179
179
 
180
180
  After funding, prepare again, authorize exactly once, submit with WarpMetal,
181
- and confirm the returned `termEndsAt`. On `reconcile_pending` or
181
+ and require `confirmed: true` before accepting the returned `termEndsAt`.
182
+ Stop payment submission at confirmation even when `finalized` is false. On `reconcile_pending` or
182
183
  `manual_review`, do not create another authorization.
183
184
 
184
185
  ## Provision and manage
@@ -72,7 +72,7 @@ retire that attempt and return a new `paymentAttemptId`. The CLI replaces the
72
72
  saved challenge and stale wallet-attempt metadata; use only the newly returned
73
73
  workflow.
74
74
 
75
- The current integration targets `@x402api/agent-wallet-cli@0.2.8`. A compatible
75
+ The current integration targets `@x402api/agent-wallet-cli@0.2.9`. A compatible
76
76
  live term is marked `agentWalletSupported: true` and must use the sponsored
77
77
  Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
78
78
  For the current declaration, x402api pays actual gas from its platform treasury
@@ -83,6 +83,12 @@ term. Because the checkout is authenticated, do not substitute `x402api pay`,
83
83
  `payment submit`, or `payment reconcile` for the returned WarpMetal submission
84
84
  command.
85
85
 
86
+ `warpmetal checkout submit` and `warpmetal renewal submit` surface
87
+ `paymentId`, `confirmed`, and `finalized` from WarpMetal and persist those safe
88
+ lifecycle fields in private CLI state. `confirmed: true` is terminal for
89
+ payment submission and starts the business operation; `finalized: false` only
90
+ means signed-receipt reconciliation continues asynchronously.
91
+
86
92
  `--payment-artifact` accepts the owner-only JSON artifact produced by a
87
93
  compatible pinned x402api Agent Wallet release. WarpMetal validates its request
88
94
  and payment-requirement digests, resource, extensions, buyer payment identifier,
@@ -25,7 +25,7 @@ contract.
25
25
  WarpMetal runs on Node.js 20 or 22. The x402api Agent Wallet currently requires
26
26
  Node.js 22. Use the exact published package reported by
27
27
  `paymentWorkflow.signerPackage.spec`; the current contract is
28
- `@x402api/agent-wallet-cli@0.2.8`. Do not add it as a WarpMetal dependency,
28
+ `@x402api/agent-wallet-cli@0.2.9`. Do not add it as a WarpMetal dependency,
29
29
  install executable wallet code from an unpinned repository URL, or substitute
30
30
  a similarly named package.
31
31
 
@@ -39,7 +39,9 @@ skill automatically.
39
39
 
40
40
  1. Install the exact `paymentWorkflow.signerPackage.install.argv` under Node
41
41
  22. Run `command -v x402api` and the returned
42
- `paymentWorkflow.signerContract.probe.argv`. Require contract version 1.
42
+ `paymentWorkflow.signerContract.probe.argv`. Require contract version 1 and
43
+ every entry in `paymentWorkflow.signerContract.requiredCommands` to appear
44
+ in the probe's `commands` array.
43
45
  2. Run the exact `paymentWorkflow.walletWorkflow.setup.argv`. The idempotent
44
46
  `x402api wallet setup --json` command creates an owner-only managed unlock
45
47
  file inside the x402api home directory and never prints its generated
@@ -152,7 +154,7 @@ reservation or any buyer-funded, unsupported, or unbound alternative.
152
154
 
153
155
  x402api pays actual gas from its platform treasury. The merchant tenant's
154
156
  active sponsorship allowance controls admission, but actual gas is not a
155
- tenant debit. During the coordinated rollout, accept only the exact 0.2.8
157
+ tenant debit. During the coordinated rollout, accept only the exact 0.2.9
156
158
  platform-treasury declaration or the matched legacy tenant-credit declaration;
157
159
  never accept a mixed billing and final-charge policy.
158
160
 
@@ -193,9 +195,15 @@ current `termEndsAt` generation are different.
193
195
  wallet-attempt metadata when it saves the new challenge. Inspect the new
194
196
  terms and authorize only the new workflow; never reuse an expired envelope or
195
197
  artifact.
196
- - `payment_pending` or `payment_finalizing`: keep the same checkout bytes and
197
- artifact. Preserve the returned durable `paymentId`; `--wait` records it in
198
- private WarpMetal state and performs bounded retries with that exact authorization.
198
+ - `payment_pending`, or legacy `payment_finalizing`, without
199
+ `confirmed: true`: keep the same checkout bytes and artifact. Preserve the
200
+ returned durable `paymentId`; `--wait` records it in private WarpMetal state
201
+ and performs bounded retries with that exact authorization.
202
+ - `confirmed: true`: stop payment submission immediately, including when
203
+ `finalized` is false or a legacy pending-like status is present. WarpMetal
204
+ has admitted provisioning or renewal exactly once and will reconcile payment
205
+ detail plus the signed final receipt asynchronously. Never authorize a
206
+ replacement because receipt finality is pending.
199
207
  - Timeout or process restart after authorization: use the saved x402api attempt
200
208
  ID and artifact. Reconcile with WarpMetal and reuse its submit argv. Never
201
209
  authorize again merely because submission is unknown.
@@ -59,7 +59,8 @@ The CLI does not install a daemon. Treat actions as follows:
59
59
  - `refill_required`: follow the signed refill workflow and stop.
60
60
  - `reconcile_pending`: preserve the artifact and reconcile; never authorize again.
61
61
  - `manual_review`: stop all payment and mutation retries.
62
- - `renewed`: verify the new `termEndsAt` and stop successfully.
62
+ - `renewed` with `confirmed: true`: verify the new `termEndsAt` and stop
63
+ payment submission successfully, even when `finalized` is false.
63
64
 
64
65
  ## Authorize and submit renewal
65
66
 
@@ -117,6 +117,9 @@ Confirm the plan, hostname, OS, and public key before preparing it.
117
117
  - For `payment_pending`, retry the exact checkout body and the exact same
118
118
  payment artifact. Preserve the returned `paymentId` as the durable x402api
119
119
  reconciliation key. Do not create a replacement payment.
120
+ - For `confirmed: true`, stop payment submission even when `finalized` is
121
+ false. Finality and reorg checks are read-only WarpMetal backend work; they
122
+ never authorize another payment.
120
123
  - A signed HTTP 402 rejects that signature. A `paymentId` can be absent;
121
124
  preserve the safe `errorCode`, `requestId`, and `replacementAllowed` result.
122
125
  Request a new live challenge and create the one permitted replacement only
package/src/cli.js CHANGED
@@ -215,6 +215,42 @@ function suggestedDelay(result, fallback = 2) {
215
215
  );
216
216
  }
217
217
 
218
+ function paymentLifecycle(result) {
219
+ const confirmed = result.data?.confirmed;
220
+ const finalized = result.data?.finalized;
221
+ if (confirmed === undefined && finalized === undefined) {
222
+ return { confirmed: undefined, finalized: undefined };
223
+ }
224
+ if (
225
+ typeof confirmed !== "boolean" ||
226
+ typeof finalized !== "boolean" ||
227
+ (finalized && !confirmed)
228
+ ) {
229
+ throw new CliError("WarpMetal returned contradictory payment lifecycle evidence.", {
230
+ exitCode: 3,
231
+ });
232
+ }
233
+ if (confirmed && !result.data?.paymentId) {
234
+ throw new CliError("WarpMetal confirmed payment without a payment ID.", {
235
+ exitCode: 3,
236
+ });
237
+ }
238
+ if (["provisioning", "renewed"].includes(result.data?.status) && !confirmed) {
239
+ throw new CliError("WarpMetal returned a terminal payment status without confirmation.", {
240
+ exitCode: 3,
241
+ });
242
+ }
243
+ return { confirmed, finalized };
244
+ }
245
+
246
+ function paymentSubmissionPending(result, lifecycle) {
247
+ return (
248
+ result.status === 202 &&
249
+ lifecycle.confirmed !== true &&
250
+ ["payment_pending", "payment_finalizing"].includes(result.data?.status)
251
+ );
252
+ }
253
+
218
254
  async function readHeaderValueFile(path, label) {
219
255
  const value = (await readFile(resolve(path), "utf8")).trim();
220
256
  if (!value || value.includes("\n") || value.includes("\r")) {
@@ -806,8 +842,12 @@ async function handleCheckoutSubmit(client, store, options, context) {
806
842
  token,
807
843
  paymentSignature,
808
844
  });
845
+ const lifecycle = paymentLifecycle(response);
809
846
  if (response.data?.paymentId) {
810
847
  await store.saveOrderPaymentId(taskId, response.data.paymentId);
848
+ if (lifecycle.confirmed !== undefined) {
849
+ await store.saveOrderPaymentOutcome(taskId, response.data.paymentId, lifecycle);
850
+ }
811
851
  }
812
852
  const safe = await attachPaymentWorkflow(
813
853
  client,
@@ -826,9 +866,7 @@ async function handleCheckoutSubmit(client, store, options, context) {
826
866
  replacementAllowed: safe.replacementAllowed,
827
867
  });
828
868
  }
829
- const retryable =
830
- response.status === 202 &&
831
- ["payment_pending", "payment_finalizing"].includes(response.data?.status);
869
+ const retryable = paymentSubmissionPending(response, lifecycle);
832
870
  if (wait && retryable) {
833
871
  ensureBeforeDeadline(deadline, `Checkout ${taskId}`);
834
872
  await delay(suggestedDelay(response));
@@ -839,6 +877,8 @@ async function handleCheckoutSubmit(client, store, options, context) {
839
877
  ...safe,
840
878
  task: response.data?.task,
841
879
  paymentId: response.data?.paymentId,
880
+ confirmed: lifecycle.confirmed,
881
+ finalized: lifecycle.finalized,
842
882
  message: response.data?.message,
843
883
  ...(walletPayment
844
884
  ? {
@@ -1404,8 +1444,16 @@ async function handleRenewalSubmit(client, store, options, context) {
1404
1444
  paymentSignature: walletPayment.paymentSignature,
1405
1445
  },
1406
1446
  );
1447
+ const lifecycle = paymentLifecycle(response);
1407
1448
  if (response.data?.paymentId) {
1408
1449
  await store.saveRenewalPaymentId(serverId, response.data.paymentId);
1450
+ if (lifecycle.confirmed !== undefined) {
1451
+ await store.saveRenewalPaymentOutcome(
1452
+ serverId,
1453
+ response.data.paymentId,
1454
+ lifecycle,
1455
+ );
1456
+ }
1409
1457
  }
1410
1458
  if (response.status === 402 && response.data?.status === "payment_rejected") {
1411
1459
  await store.saveRenewalPaymentRejection(serverId, {
@@ -1417,9 +1465,7 @@ async function handleRenewalSubmit(client, store, options, context) {
1417
1465
  replacementAllowed: response.data?.replacementAllowed,
1418
1466
  });
1419
1467
  }
1420
- const retryable =
1421
- response.status === 202 &&
1422
- ["payment_pending", "payment_finalizing"].includes(response.data?.status);
1468
+ const retryable = paymentSubmissionPending(response, lifecycle);
1423
1469
  if (booleanOption(options, "wait") && retryable) {
1424
1470
  ensureBeforeDeadline(deadline, `Renewal ${serverId}`);
1425
1471
  await delay(suggestedDelay(response));
@@ -1430,6 +1476,8 @@ async function handleRenewalSubmit(client, store, options, context) {
1430
1476
  serverId,
1431
1477
  task: response.data?.task,
1432
1478
  paymentId: response.data?.paymentId,
1479
+ confirmed: lifecycle.confirmed,
1480
+ finalized: lifecycle.finalized,
1433
1481
  paymentAttemptId:
1434
1482
  response.data?.paymentAttemptId ||
1435
1483
  response.headers["x-warpmetal-payment-attempt"],
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.8";
18
+ export const AGENT_WALLET_VERSION = "0.2.9";
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";
@@ -842,6 +842,9 @@ export function paymentWorkflow({
842
842
  signerContract: {
843
843
  version: 1,
844
844
  probe: { argv: ["x402api", "help", "--json"] },
845
+ requiredCommands: [
846
+ "payment authorize --wallet <name> --request-envelope <file> --artifact-out <file>",
847
+ ],
845
848
  },
846
849
  signerReleaseStatus:
847
850
  "published launch payer; sponsored Base USDC and Solana USDC/USDT only",
package/src/state.js CHANGED
@@ -14,6 +14,8 @@ const WALLET_PAYMENT_STATE_FIELDS = [
14
14
  "paymentArtifactExpiresAt",
15
15
  "paymentArtifactSavedAt",
16
16
  "gatewayPaymentId",
17
+ "gatewayPaymentConfirmed",
18
+ "gatewayPaymentFinalized",
17
19
  "rejectedPaymentAttemptId",
18
20
  "paymentRejectionErrorCode",
19
21
  "paymentRejectionRequestId",
@@ -437,6 +439,43 @@ export class StateStore {
437
439
  });
438
440
  }
439
441
 
442
+ async saveOrderPaymentOutcome(taskId, paymentId, { confirmed, finalized }) {
443
+ if (
444
+ !X402API_PAYMENT_ID.test(paymentId || "") ||
445
+ typeof confirmed !== "boolean" ||
446
+ typeof finalized !== "boolean" ||
447
+ (finalized && !confirmed)
448
+ ) {
449
+ throw new CliError("WarpMetal returned invalid payment lifecycle evidence.", {
450
+ exitCode: 3,
451
+ });
452
+ }
453
+ await this.update((state) => {
454
+ const order = state.orders[taskId];
455
+ if (!order) {
456
+ throw new CliError(`No local order state exists for ${taskId}.`, {
457
+ exitCode: 2,
458
+ });
459
+ }
460
+ if (order.gatewayPaymentId && order.gatewayPaymentId !== paymentId) {
461
+ throw new CliError("WarpMetal changed the x402api payment ID for this attempt.", {
462
+ exitCode: 3,
463
+ });
464
+ }
465
+ if (
466
+ (order.gatewayPaymentConfirmed === true && !confirmed) ||
467
+ (order.gatewayPaymentFinalized === true && !finalized)
468
+ ) {
469
+ throw new CliError("WarpMetal payment lifecycle evidence moved backward.", {
470
+ exitCode: 3,
471
+ });
472
+ }
473
+ order.gatewayPaymentId = paymentId;
474
+ order.gatewayPaymentConfirmed = confirmed;
475
+ order.gatewayPaymentFinalized = finalized;
476
+ });
477
+ }
478
+
440
479
  async saveOrderPaymentRejection(taskId, rejection) {
441
480
  const safe = validatedPaymentRejection(rejection);
442
481
  await this.update((state) => {
@@ -481,6 +520,43 @@ export class StateStore {
481
520
  });
482
521
  }
483
522
 
523
+ async saveRenewalPaymentOutcome(serverId, paymentId, { confirmed, finalized }) {
524
+ if (
525
+ !X402API_PAYMENT_ID.test(paymentId || "") ||
526
+ typeof confirmed !== "boolean" ||
527
+ typeof finalized !== "boolean" ||
528
+ (finalized && !confirmed)
529
+ ) {
530
+ throw new CliError("WarpMetal returned invalid payment lifecycle evidence.", {
531
+ exitCode: 3,
532
+ });
533
+ }
534
+ await this.update((state) => {
535
+ const renewal = state.renewals[serverId];
536
+ if (!renewal) {
537
+ throw new CliError(`No local renewal state exists for ${serverId}.`, {
538
+ exitCode: 2,
539
+ });
540
+ }
541
+ if (renewal.gatewayPaymentId && renewal.gatewayPaymentId !== paymentId) {
542
+ throw new CliError("WarpMetal changed the x402api payment ID for this renewal.", {
543
+ exitCode: 3,
544
+ });
545
+ }
546
+ if (
547
+ (renewal.gatewayPaymentConfirmed === true && !confirmed) ||
548
+ (renewal.gatewayPaymentFinalized === true && !finalized)
549
+ ) {
550
+ throw new CliError("WarpMetal renewal payment lifecycle evidence moved backward.", {
551
+ exitCode: 3,
552
+ });
553
+ }
554
+ renewal.gatewayPaymentId = paymentId;
555
+ renewal.gatewayPaymentConfirmed = confirmed;
556
+ renewal.gatewayPaymentFinalized = finalized;
557
+ });
558
+ }
559
+
484
560
  async saveRenewalPaymentRejection(serverId, rejection) {
485
561
  const safe = validatedPaymentRejection(rejection);
486
562
  await this.update((state) => {
@@ -631,6 +707,8 @@ export class StateStore {
631
707
  planId: order.planId,
632
708
  paymentAttemptId: order.paymentAttemptId,
633
709
  paymentId: order.gatewayPaymentId,
710
+ paymentConfirmed: order.gatewayPaymentConfirmed,
711
+ paymentFinalized: order.gatewayPaymentFinalized,
634
712
  challengeHandle: order.challengeHandle,
635
713
  walletPaymentAttemptId: order.walletPaymentAttemptId,
636
714
  walletName: order.walletName,
@@ -671,6 +749,8 @@ export class StateStore {
671
749
  policy: renewal.policy,
672
750
  paymentAttemptId: renewal.paymentAttemptId,
673
751
  paymentId: renewal.gatewayPaymentId,
752
+ paymentConfirmed: renewal.gatewayPaymentConfirmed,
753
+ paymentFinalized: renewal.gatewayPaymentFinalized,
674
754
  challengeHandle: renewal.challengeHandle,
675
755
  paymentArtifactExpiresAt: renewal.paymentArtifactExpiresAt,
676
756
  rejectedPaymentAttemptId: renewal.rejectedPaymentAttemptId,