@nuanu-ai/agentify-contracts 0.3.2 → 0.5.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
@@ -23,7 +23,7 @@ The contract is versioned, and the version is how an SDK and a gateway agree
23
23
  that they read the same vocabulary; a worker whose version the gateway does not
24
24
  share stops at startup rather than half understanding a document. What the
25
25
  merchant-facing side of all this looks like in use is at
26
- <https://app.agentify.ad/docs/>.
26
+ <https://agentify.ad/docs/>.
27
27
 
28
28
  ## License
29
29
 
package/dist/api.d.ts CHANGED
@@ -225,9 +225,11 @@ export declare const ReceiptListSchema: z.ZodObject<{
225
225
  /**
226
226
  * The question a merchant puts in the query string when listing orders.
227
227
  *
228
- * `open=true` narrows the list to the orders that are still owed something —
229
- * which includes the two that stay open after the purchase itself is over, an
230
- * order owing a refund and one delivered but never paid for. Leaving it out
228
+ * `open=true` narrows the list to the orders a restarted worker should walk:
229
+ * ones the merchant was handed and has not finished, and the two that stay
230
+ * open after the purchase itself is over, an order owing a refund and one
231
+ * delivered but never paid for. An order that was priced and never paid, and
232
+ * never reached the handler, is not in that narrowing. Leaving the field out
231
233
  * asks for everything.
232
234
  *
233
235
  * The value is text and not a boolean, because that is what a query string
@@ -455,9 +457,9 @@ export declare const PurchaseRequestSchema: z.ZodObject<{
455
457
  /**
456
458
  * What became of a purchase, told to the agent that made it.
457
459
  *
458
- * Four fields, and the shape of the list is the decision rather than its
459
- * length. This document is what the buyer is owed — where their order stands,
460
- * what it cost them, and the goods once those exist — and it is deliberately
460
+ * The shape of the list is the decision rather than its length. This document
461
+ * is what the buyer is owed — where their order stands, what it cost them, the
462
+ * goods once those exist, and where to ask again — and it is deliberately
461
463
  * smaller than the merchant's own view of the same order. It carries no
462
464
  * merchant, no merchant's key for the product, none of the parameters the
463
465
  * buyer sent and nothing about any other order, because whoever holds an
@@ -490,6 +492,7 @@ export declare const PurchaseRequestSchema: z.ZodObject<{
490
492
  */
491
493
  export declare const AgentOrderStatusSchema: z.ZodObject<{
492
494
  order_id: z.ZodString;
495
+ status_url: z.ZodURL;
493
496
  status: z.ZodEnum<{
494
497
  delivered: "delivered";
495
498
  in_progress: "in_progress";
@@ -780,7 +783,7 @@ export type ErrorEnvelope = z.infer<typeof ErrorEnvelopeSchema>;
780
783
  * has to act on it belongs to the route that sends it and to the sentence the
781
784
  * refusal carries.
782
785
  */
783
- export declare const ERROR_CODES: readonly ["body_too_large", "body_undecodable", "call_refused", "charset_unsupported", "encoding_unsupported", "gateway_failed", "key_made_for_a_cabinet", "key_opened_this_call", "malformed_body", "malformed_query", "merchant_departed", "no_such_item", "no_such_key", "no_such_order", "no_such_route", "not_a_cabinet_key", "not_authorised", "not_invited", "not_selling", "not_this_purchase", "order_closed_before_it_was_priced", "order_not_priced_yet", "params_do_not_fit", "payment_already_spent", "payment_not_taken", "payment_not_verified"];
786
+ export declare const ERROR_CODES: readonly ["body_too_large", "body_undecodable", "call_refused", "charset_unsupported", "encoding_unsupported", "gateway_failed", "key_made_for_a_cabinet", "key_opened_this_call", "malformed_body", "malformed_query", "merchant_departed", "no_such_item", "no_such_key", "no_such_order", "no_such_route", "not_a_cabinet_key", "not_authorised", "not_invited", "not_selling", "not_this_purchase", "order_closed_before_it_was_priced", "order_not_priced_yet", "params_do_not_fit", "payment_already_spent", "payment_not_taken", "payment_not_verified", "wallet_change_not_announced", "wallet_change_nobody_to_tell", "wallet_change_raced", "wallet_change_unconfirmed"];
784
787
  /** One of the codes this gateway is known to refuse a call with. */
785
788
  export type ErrorCode = (typeof ERROR_CODES)[number];
786
789
  /**
@@ -1277,6 +1280,10 @@ export declare const API_ROUTES: Readonly<{
1277
1280
  response: {
1278
1281
  document: z.ZodObject<{
1279
1282
  payout_wallet: z.ZodNullable<z.ZodString>;
1283
+ pending: z.ZodNullable<z.ZodObject<{
1284
+ payout_wallet: z.ZodString;
1285
+ takes_effect_at: z.ZodISODateTime;
1286
+ }, z.core.$strict>>;
1280
1287
  }, z.core.$strict>;
1281
1288
  };
1282
1289
  };
@@ -1291,6 +1298,10 @@ export declare const API_ROUTES: Readonly<{
1291
1298
  response: {
1292
1299
  document: z.ZodObject<{
1293
1300
  payout_wallet: z.ZodNullable<z.ZodString>;
1301
+ pending: z.ZodNullable<z.ZodObject<{
1302
+ payout_wallet: z.ZodString;
1303
+ takes_effect_at: z.ZodISODateTime;
1304
+ }, z.core.$strict>>;
1294
1305
  }, z.core.$strict>;
1295
1306
  };
1296
1307
  };
@@ -1826,6 +1837,7 @@ export declare const API_ROUTES: Readonly<{
1826
1837
  response: {
1827
1838
  document: z.ZodObject<{
1828
1839
  order_id: z.ZodString;
1840
+ status_url: z.ZodURL;
1829
1841
  status: z.ZodEnum<{
1830
1842
  delivered: "delivered";
1831
1843
  in_progress: "in_progress";
@@ -1861,6 +1873,7 @@ export declare const API_ROUTES: Readonly<{
1861
1873
  response: {
1862
1874
  document: z.ZodObject<{
1863
1875
  order_id: z.ZodString;
1876
+ status_url: z.ZodURL;
1864
1877
  status: z.ZodEnum<{
1865
1878
  delivered: "delivered";
1866
1879
  in_progress: "in_progress";
package/dist/api.js CHANGED
@@ -105,7 +105,7 @@ export const MerchantCardListSchema = z
105
105
  cards: z.array(MerchantCardSchema),
106
106
  })
107
107
  .meta({
108
- description: "A merchant's own catalog: every card they have published, and whether they are taking new orders at all. The merchant's own selling word is here as well as on each card because the two are different facts, and neither can be worked out from the other. A card reads open only where all of it holds at once: the merchant is selling, the card is not paused in its own right, the merchant has a name for it to be sold under, and — on a deployment that settles on a real chain — a wallet for the money to be paid into. So a card can read paused while this field reads open, and this field does not say which of the reasons it was. What does say is the refusal at the publish and the merchant's own settings, each of which names the piece that is missing. This document does not say whether it is the whole catalog: paging is not designed, and when it is, this object grows the field that answers it.",
108
+ description: "A merchant's own catalog: every card they have published, and whether they are taking new orders at all. The merchant's own selling word is here as well as on each card because the two are different facts, and neither can be worked out from the other. A card reads open only where all of it holds at once: the merchant is selling, the card is not paused in its own right, the merchant has a name for it to be sold under, on a deployment that settles on a real chain a wallet for the money to be paid into, and on the live deployment the operator's approval. So a card can read paused while this field reads open, and this field does not say which of the reasons it was. What does say is the refusal at the publish, whose findings about the merchant carry the codes no_seller_name, no_payout_wallet and no_operator_approval, and the merchant's own settings. This document does not say whether it is the whole catalog: paging is not designed, and when it is, this object grows the field that answers it.",
109
109
  });
110
110
  /**
111
111
  * Every receipt this merchant has.
@@ -129,9 +129,11 @@ export const ReceiptListSchema = z
129
129
  /**
130
130
  * The question a merchant puts in the query string when listing orders.
131
131
  *
132
- * `open=true` narrows the list to the orders that are still owed something —
133
- * which includes the two that stay open after the purchase itself is over, an
134
- * order owing a refund and one delivered but never paid for. Leaving it out
132
+ * `open=true` narrows the list to the orders a restarted worker should walk:
133
+ * ones the merchant was handed and has not finished, and the two that stay
134
+ * open after the purchase itself is over, an order owing a refund and one
135
+ * delivered but never paid for. An order that was priced and never paid, and
136
+ * never reached the handler, is not in that narrowing. Leaving the field out
135
137
  * asks for everything.
136
138
  *
137
139
  * The value is text and not a boolean, because that is what a query string
@@ -146,7 +148,7 @@ export const OrderListQuerySchema = z
146
148
  open: z.enum(["true", "false"]).optional(),
147
149
  })
148
150
  .meta({
149
- description: 'Which orders to list. Written as text because a query string carries text. "true" narrows the list to the orders that are still owed something, which includes the two that stay open after the purchase itself is over: an order owing a refund, and one delivered but never paid for. Leaving the field out asks for everything.',
151
+ description: 'Which orders to list. Written as text because a query string carries text. "true" narrows the list to the orders a restarted worker should walk: ones the merchant was handed and has not finished, an order owing a refund, and one delivered but never paid for. An order that was priced and never paid, and never reached the handler, is not in that narrowing. Leaving the field out asks for everything.',
150
152
  });
151
153
  /**
152
154
  * What a worker asks of one poll.
@@ -309,9 +311,9 @@ export const PurchaseRequestSchema = z
309
311
  /**
310
312
  * What became of a purchase, told to the agent that made it.
311
313
  *
312
- * Four fields, and the shape of the list is the decision rather than its
313
- * length. This document is what the buyer is owed — where their order stands,
314
- * what it cost them, and the goods once those exist — and it is deliberately
314
+ * The shape of the list is the decision rather than its length. This document
315
+ * is what the buyer is owed — where their order stands, what it cost them, the
316
+ * goods once those exist, and where to ask again — and it is deliberately
315
317
  * smaller than the merchant's own view of the same order. It carries no
316
318
  * merchant, no merchant's key for the product, none of the parameters the
317
319
  * buyer sent and nothing about any other order, because whoever holds an
@@ -345,6 +347,38 @@ export const PurchaseRequestSchema = z
345
347
  export const AgentOrderStatusSchema = z
346
348
  .strictObject({
347
349
  order_id: IdentifierSchema,
350
+ /**
351
+ * Where this order is read again: the whole address of its status route,
352
+ * scheme and host included.
353
+ *
354
+ * It exists for the agent that bought goods that come later. That agent
355
+ * holds this document and nothing else, it has already paid, and the
356
+ * goods are collected at this address. The path is not something it could
357
+ * work out from what it holds — the purchase was made at an address that
358
+ * names the product rather than the order — and a guess that misses is
359
+ * answered `no_such_route`, which says nothing about the order at all.
360
+ *
361
+ * Absolute, so that it is called as it stands: a bare path would have to
362
+ * be joined onto a host the agent was never told was the right one. It is
363
+ * built from the address the gateway is configured to answer as and never
364
+ * from the request that asked, because behind the proxy that ends an
365
+ * agent's TLS connection the request says plain http and names whatever
366
+ * host the proxy or the caller wrote. Plain http is allowed by the pattern
367
+ * all the same, because a gateway on a developer's own machine answers on
368
+ * it; a deployment's address is https because its configuration is.
369
+ *
370
+ * The scheme is a pattern rather than zod's own protocol option for the
371
+ * reason the price hook's is (`PriceCheckSchema` in `card.ts`): a pattern
372
+ * is what survives into the JSON Schema export, so a client generated from
373
+ * that document refuses a bare path as well. It is case-sensitive because
374
+ * a flag on a pattern does not survive that export, and a schema that read
375
+ * the scheme without regard to case would accept what the exported
376
+ * document refuses. Nothing is lost by it: the gateway's configuration
377
+ * already requires its public address in lower case.
378
+ */
379
+ status_url: z
380
+ .url()
381
+ .regex(/^https?:\/\//, "an order's status address is a whole http or https address, not a path"),
348
382
  status: OrderStatusSchema,
349
383
  /**
350
384
  * The price this order was priced at, or null where nobody ever named one
@@ -412,7 +446,7 @@ export const AgentOrderStatusSchema = z
412
446
  refusal: RefusalSchema.optional(),
413
447
  })
414
448
  .meta({
415
- description: 'What became of one purchase, in the words an agent and a merchant both read: where the order stands, what it was priced at, the goods once they are the buyer\'s, and why the merchant would not sell where that is what ended it. It is smaller than the merchant\'s own view of the same order on purpose — no merchant, no merchant\'s own key for the product, none of the purchase parameters and nothing about any other order. The price is what the buyer was asked for and not proof that anything was charged: an order that was priced and then ended without a sale still carries it, and the status is what says which happened. A null price means nobody ever named one for this order, and a null delivery means there are no goods here to hand over; both fields are always present, because an absent field is a silence a reader cannot tell from an oversight. "refusal" is the exception and is present only where a merchant refused: their own short code to branch on and their own sentence to show, carried across unchanged. The code is an open set — "out_of_stock", "invalid_params" and "cannot_fulfill" are read the same way by everybody, and a merchant whose reason fits none of them sends their own word, so an unfamiliar code has to fall through to the sentence rather than break a reader. An absent "refusal" means there is no refusal to quote and never that one was dropped, which leaves two endings still coarse: "rejected" also covers a product that was gone and a payment that failed its check, and neither of those was worded by anybody — the first because a price answer of "not available" carries no words, the second because it is refused at the door in an error envelope instead. A null delivery is likewise not a promise that no goods were ever made: a purchase whose charge failed or went unanswered can leave goods the buyer has not paid for, and this document withholds them rather than describing them. Every answer says whether the money behind the purchase was real: a gateway settling against nothing produces every other field here exactly as a real charge would, so a reader taking this for proof of a payment has to read that word first.',
449
+ description: 'What became of one purchase, in the words an agent and a merchant both read: where the order stands, what it was priced at, the goods once they are the buyer\'s, and why the merchant would not sell where that is what ended it. "status_url" is where this document is read again — the whole address of the order\'s status route, and the place an agent that bought goods that come later collects them. It is absolute and called as it stands, and it is the address the gateway is configured to answer as, never one taken from the request that asked. It is smaller than the merchant\'s own view of the same order on purpose — no merchant, no merchant\'s own key for the product, none of the purchase parameters and nothing about any other order. The price is what the buyer was asked for and not proof that anything was charged: an order that was priced and then ended without a sale still carries it, and the status is what says which happened. A null price means nobody ever named one for this order, and a null delivery means there are no goods here to hand over; both fields are always present, because an absent field is a silence a reader cannot tell from an oversight. "refusal" is the exception and is present only where a merchant refused: their own short code to branch on and their own sentence to show, carried across unchanged. The code is an open set — "out_of_stock", "invalid_params" and "cannot_fulfill" are read the same way by everybody, and a merchant whose reason fits none of them sends their own word, so an unfamiliar code has to fall through to the sentence rather than break a reader. An absent "refusal" means there is no refusal to quote and never that one was dropped, which leaves two endings still coarse: "rejected" also covers a product that was gone and a payment that failed its check, and neither of those was worded by anybody — the first because a price answer of "not available" carries no words, the second because it is refused at the door in an error envelope instead. A null delivery is likewise not a promise that no goods were ever made: a purchase whose charge failed or went unanswered can leave goods the buyer has not paid for, and this document withholds them rather than describing them. Every answer says whether the money behind the purchase was real: a gateway settling against nothing produces every other field here exactly as a real charge would, so a reader taking this for proof of a payment has to read that word first.',
416
450
  });
417
451
  /**
418
452
  * The catalog as an agent reads it.
@@ -646,6 +680,10 @@ export const ERROR_CODES = Object.freeze([
646
680
  "payment_already_spent",
647
681
  "payment_not_taken",
648
682
  "payment_not_verified",
683
+ "wallet_change_not_announced",
684
+ "wallet_change_nobody_to_tell",
685
+ "wallet_change_raced",
686
+ "wallet_change_unconfirmed",
649
687
  ]);
650
688
  /**
651
689
  * Every call of the surface, under the name it is known by in both programs.
@@ -680,7 +718,7 @@ export const API_ROUTES = Object.freeze({
680
718
  method: "POST",
681
719
  path: "/v0/catalog/publish",
682
720
  auth: "merchant_key",
683
- description: "Publishes one product, or says what is wrong with the card. Republishing under the same merchant_item_id is how a card is changed rather than how a second one appears. The catalog identifier comes back beside ok, and from then on it is what an agent, a purchase and a receipt all use. A card that is not published comes back as ok:false under the code card_rejected, never retryable, with every finding named in the error's problems. Two of the findings this can come back with are not about the card at all. A merchant who has not set the name their products are sold under gets a finding whose code is no_seller_name, and the way past it is POST /v0/seller-name: a card published without one reaches a buyer's agent inside a payment request that names no seller. And on a deployment that settles on a real chain, a merchant who has set no wallet gets one whose code is no_payout_wallet, with POST /v0/payout-wallet as the way past it: the money from that card's sales is paid to the merchant's own address directly, and without one there is nowhere for it to go. Both come back in the same problems list as whatever is wrong with the card, so one answer carries everything standing between this card and the catalog.",
721
+ description: "Publishes one product, or says what is wrong with the card. Republishing under the same merchant_item_id is how a card is changed rather than how a second one appears. The catalog identifier comes back beside ok, and from then on it is what an agent, a purchase and a receipt all use. A card that is not published comes back as ok:false under the code card_rejected, never retryable, with every finding named in the error's problems. Three findings this can come back with are not about the card at all. A merchant who has not set the name their products are sold under gets no_seller_name, and the way past it is POST /v0/seller-name: a card published without one reaches a buyer's agent inside a payment request that names no seller. On a deployment that settles on a real chain, a merchant who has set no wallet gets no_payout_wallet, and the way past it is the Settings screen of the merchant's cabinet, where a person signed in sets one: the money from that card's sales is paid to the merchant's own address directly, and without one there is nowhere for it to go. On the live deployment, a merchant the operator has not admitted once gets no_operator_approval. That decision has no merchant API: test publication remains available, while live publication remains closed until the operator grants it. Every applicable finding comes back in the same problems list as whatever is wrong with the card, so one answer carries everything standing between this card and the catalog.",
684
722
  request: CardSchema,
685
723
  response: { document: PublishResultSchema },
686
724
  },
@@ -723,7 +761,7 @@ export const API_ROUTES = Object.freeze({
723
761
  method: "POST",
724
762
  path: "/v0/merchants",
725
763
  auth: "none",
726
- description: "Makes a merchant and the key whoever registered them will call as them with — both of those or neither. It takes no key because nobody registering has one yet; what stands in the door instead is an invitation code out of the gateway's own configuration, and a gateway with no code configured refuses every registration in the same words a wrong code gets, so this call is not a way of finding out whether registration is open. The key comes back once and is readable nowhere afterwards, and it is a key made for a cabinet: it is in no list of the merchant's keys and is not disabled through them, so no row for it comes back here — only the key itself. What the new merchant does not have is a name: they are listed under nothing until somebody sets one at POST /v0/seller-name, and until then publishing a card is refused, so a cabinet that registers a person and takes them straight to a publish screen has built a dead end. Nothing about an account, an address or a password reaches this call either: those belong to whatever signs a person in, on the other side of it. Unlike every other call on this surface that writes something, a repeat of this one is not safe and there is nothing here that could make it so: two calls make two merchants, and the caller holds a key to only the second. A caller whose connection drops before the answer arrives cannot find out from here whether the first landed — it has no key with which to ask — and the merchant it may have made cannot be swept away afterwards, because a merchant is what every card, order and receipt is owned by.",
764
+ description: "Makes a merchant and the key whoever registered them will call as them with — both of those or neither. This call is not on the public origin: only the cabinet makes it, from inside the stack, when a signed-in person presses its one control, and from outside the path answers as one the site does not have. It takes no key because nobody registering has one yet; what stands in the door instead is an invitation code out of the gateway's own configuration, and a gateway with no code configured refuses every registration in the same words a wrong code gets, so this call is not a way of finding out whether registration is open. The key comes back once and is readable nowhere afterwards, and it is a key made for a cabinet: it is in no list of the merchant's keys and is not disabled through them, so no row for it comes back here — only the key itself. What the new merchant does not have is a name: they are listed under nothing until somebody sets one at POST /v0/seller-name, and until then publishing a card is refused, so a cabinet that registers a person and takes them straight to a publish screen has built a dead end. Nothing about an account, an address or a password reaches this call either: those belong to whatever signs a person in, on the other side of it. Unlike every other call on this surface that writes something, a repeat of this one is not safe and there is nothing here that could make it so: two calls make two merchants, and the caller holds a key to only the second. A caller whose connection drops before the answer arrives cannot find out from here whether the first landed — it has no key with which to ask — and the merchant it may have made cannot be swept away afterwards, because a merchant is what every card, order and receipt is owned by.",
727
765
  request: RegistrationRequestSchema,
728
766
  response: { document: RegisteredMerchantSchema },
729
767
  },
@@ -746,14 +784,14 @@ export const API_ROUTES = Object.freeze({
746
784
  method: "GET",
747
785
  path: "/v0/payout-wallet",
748
786
  auth: "merchant_key",
749
- description: "The address the sales of the merchant this call's own key belongs to are paid into. Payments here are not held by anybody on the way: a buyer's agent pays this address directly and no balance of the merchant's is ever held on our side, which is why the address has to be theirs and why this call exists. Null is the ordinary answer for a merchant who has set none, and it is an answer rather than a refusal — a merchant with no wallet exists and has a settings screen to draw. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent to set it, so a screen showing it shows what the merchant copied out of their wallet character for character.",
787
+ description: "The address the sales of the merchant this call's own key belongs to are paid into now, and any change of it that is waiting. Payments here are not held by anybody on the way: a buyer's agent pays this address directly and no balance of the merchant's is ever held on our side, which is why the address has to be theirs and why this call exists. Any key of the merchant's reads it, the keys made for their own code included; only the merchant's cabinet sets it. Null is the ordinary answer for a merchant who has set none, and it is an answer rather than a refusal — a merchant with no wallet exists and has a settings screen to draw. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent to set it, so a screen showing it shows what the merchant copied out of their wallet character for character. On the live deployment a replacement for an address already set takes effect forty-eight hours after it was announced to the merchant, and until then this answers with the address still paid and names the waiting one, with the moment it takes effect, under pending. Null there means nothing is waiting, which is always the answer on the test channel and in a sandbox.",
750
788
  response: { document: PayoutWalletSchema },
751
789
  },
752
790
  set_payout_wallet: {
753
791
  method: "POST",
754
792
  path: "/v0/payout-wallet",
755
793
  auth: "merchant_key",
756
- description: "Sets the address the sales of the merchant this call's own key belongs to are paid into. The answer is the address as it now stands, read back from what was written rather than echoed, in the mixed-case spelling a wallet shows. Setting the same address twice changes nothing and answers the same way, so a retry after a dropped connection is safe. An address whose capital letters do not agree with the rest of it is refused and nothing is written, because those capitals are a checksum and letters that disagree mean a character is wrong — and an address that is wrong is another perfectly good address belonging to somebody else. What this call will not do is take an address away: null is refused, because without an address there is nowhere to send the money and every published card would quietly come off sale — ending the selling under the name of editing a setting; somebody reaching for that wants either a different address, which is this same call, or an end to selling, which is the pause. On a deployment that settles on a real chain, a merchant with no wallet set here cannot publish a card, and the refusal at the publish says so.",
794
+ description: "Sets the address the sales of the merchant this call's own key belongs to are paid into. Only the merchant's cabinet makes this call, with the key made for it, from inside the stack, when a person sets the wallet on its Settings screen: keys operate the shop, and where the shop's money goes is set through the cabinet. The public origin does not route this call at all, so from outside it is a path the site does not have; at the gateway a key made for the merchant's own code is refused under not_a_cabinet_key, for the first address as for any other, and nothing is written or announced. The first address a merchant sets applies at once; on the live deployment every cabinet account of the merchant is then told of it, and a message that cannot be sent refuses nothing. On the live deployment a different address after that does not: before anything is written, every account that names the merchant is sent a message saying what changes, when, and that it was asked for in the cabinet, and the change takes effect forty-eight hours after those messages were handed to the mail provider. Until then every payment request names the address that applies now, and the answer carries the waiting one under pending with the moment it takes effect. Asking again for the address already waiting changes nothing, sends no second message and restarts no clock, and answers with the same pending change, so a retry after a dropped connection is safe; a different address replaces the waiting one and starts the forty-eight hours again; asking for the address that applies now cancels the waiting change. On the test channel and in a sandbox every change applies at once and no message is sent. The answer is the wallet as it now stands, read back from what was written rather than echoed, in the mixed-case spelling a wallet shows. Setting the address that already applies, with nothing waiting, changes nothing and answers the same way. An address whose capital letters do not agree with the rest of it is refused and nothing is written, because those capitals are a checksum and letters that disagree mean a character is wrong — and an address that is wrong is another perfectly good address belonging to somebody else. What this call will not do is take an address away: null is refused, because without an address there is nowhere to send the money and every published card would quietly come off sale — ending the selling under the name of editing a setting; somebody reaching for that wants either a different address, which is this same call, or an end to selling, which is the pause. On a deployment that settles on a real chain, a merchant with no wallet set here cannot publish a card, and the refusal at the publish says so.",
757
795
  request: PayoutWalletRequestSchema,
758
796
  response: { document: PayoutWalletSchema },
759
797
  },
@@ -783,14 +821,14 @@ export const API_ROUTES = Object.freeze({
783
821
  method: "POST",
784
822
  path: "/v0/keys/cabinet",
785
823
  auth: "merchant_key",
786
- description: "Makes a key for a cabinet to call as this merchant with, and hands it back once. It is a key of a different kind from the ones at /v0/keys: the merchant did not ask for it, never sees it, and it appears in no list of theirs — so nothing comes back but the key itself. What this is for is a cabinet that replaces its own credential every time somebody signs in, which is what keeps a copy of a cabinet's database from being a set of working keys for long. The call is refused to a key made for the merchant's own code, under not_a_cabinet_key: these two calls are the cabinet's own, and a merchant asking for one would be asking for a credential to a cabinet they are not standing in.",
824
+ description: "Makes a key for a cabinet to call as this merchant with, and hands it back once. This call and the forgetting beside it are not on the public origin: only the cabinet makes them, from inside the stack, and from outside the path answers as one the site does not have. It is a key of a different kind from the ones at /v0/keys: the merchant did not ask for it, never sees it, and it appears in no list of theirs — so nothing comes back but the key itself. What this is for is a cabinet that replaces its own credential every time somebody signs in, which is what keeps a copy of a cabinet's database from being a set of working keys for long. The call is refused to a key made for the merchant's own code, under not_a_cabinet_key: these two calls are the cabinet's own, and a merchant asking for one would be asking for a credential to a cabinet they are not standing in.",
787
825
  response: { document: CabinetKeySchema },
788
826
  },
789
827
  forget_cabinet_key: {
790
828
  method: "DELETE",
791
829
  path: "/v0/keys/cabinet",
792
830
  auth: "merchant_key",
793
- description: "Removes the key this call was made with, and no other. It is removed rather than revoked: a merchant never issued one, never sees one and would never read a revoked one back, so a row kept for the history would be history for nobody. There are no parameters, and that is not a convenience — a key belonging to anybody else, this merchant included, is unreachable here by construction, because the only way to name a key is to be holding it. What a caller does with this is put a key of its own beyond use once it has stopped signing in with it: ask for a fresh key, write it down where the account is kept, then forget the one that was there. A caller that forgets first can be left naming a key that no longer exists, which is the one order that locks somebody out. Made a second time with the same key it is refused as a key that does not exist, which is safe and is the confirmation the first one landed: either way that key is gone and nothing else has moved. It is refused to a key made for the merchant's own code, under not_a_cabinet_key: those are revoked from the list they appear on, at an instant their owner can read back, and removing the row outright would take that history away.",
831
+ description: "Removes the key this call was made with, and no other. Like the call that makes such a key, it is not on the public origin: only the cabinet makes it, from inside the stack. It is removed rather than revoked: a merchant never issued one, never sees one and would never read a revoked one back, so a row kept for the history would be history for nobody. There are no parameters, and that is not a convenience — a key belonging to anybody else, this merchant included, is unreachable here by construction, because the only way to name a key is to be holding it. What a caller does with this is put a key of its own beyond use once it has stopped signing in with it: ask for a fresh key, write it down where the account is kept, then forget the one that was there. A caller that forgets first can be left naming a key that no longer exists, which is the one order that locks somebody out. Made a second time with the same key it is refused as a key that does not exist, which is safe and is the confirmation the first one landed: either way that key is gone and nothing else has moved. It is refused to a key made for the merchant's own code, under not_a_cabinet_key: those are revoked from the list they appear on, at an instant their owner can read back, and removing the row outright would take that history away.",
794
832
  response: { document: ForgottenCabinetKeySchema },
795
833
  },
796
834
  get_order: {
@@ -804,7 +842,7 @@ export const API_ROUTES = Object.freeze({
804
842
  method: "GET",
805
843
  path: "/v0/orders",
806
844
  auth: "merchant_key",
807
- description: "Orders and the states they are in. With open=true, only the ones still owed something — which includes the two that stay open after the purchase itself is over, an order owing a refund and one delivered but never paid for. One kind of order is not in this list at all, with or without the flag: one that closed before anybody named a price for it, because the product was gone or a price question went unanswered. Every row here is written in a document that carries a sale price and those orders have none, so they are readable one at a time by their identifier, where the refusal says what became of them. A merchant reconciling against this list is reconciling against the orders that were priced.",
845
+ description: "Orders and the states they are in. With open=true, only the ones a restarted worker should walk — ones the merchant was handed and has not finished, which includes the two that stay open after the purchase itself is over, an order owing a refund and one delivered but never paid for. An order that was priced and never paid, and never reached the handler, is not in that narrowing; it is readable by its identifier, where its status is the buyer's word for a purchase that has not finished. One kind of order is not in this list at all, with or without the flag: one that closed before anybody named a price for it, because the product was gone or a price question went unanswered. Every row here is written in a document that carries a sale price and those orders have none, so they are readable one at a time by their identifier, where the refusal says what became of them. A merchant reconciling against this list is reconciling against the orders that were priced.",
808
846
  query: OrderListQuerySchema,
809
847
  response: { document: OrderListSchema },
810
848
  },
@@ -874,7 +912,7 @@ export const API_ROUTES = Object.freeze({
874
912
  method: "POST",
875
913
  path: "/x402/:item_id/purchase",
876
914
  auth: "none",
877
- description: "Buying one product. The payment is what stands in for authorisation, so there is no key on this call. It begins with the payment exchange of the x402 protocol: a call with no payment on it is answered with a challenge, which travels in a header and carries no document. What a paid purchase is answered with is the state of the order it made — the same document the status route answers with, whatever the card's mode and whether the purchase ended in the goods or in something else, so an agent that bought and an agent that came back later read one shape. No receipt of ours is in it: a receipt is the merchant's record of the sale and is read behind the merchant's key. What an agent is told about its own money is the price and the word for whether that money was real, and, where the payment executed as the last step of this exchange, the settlement the payment layer signs into a header on this answer — a card whose money moves as the order is opened has already spent that step, so no settlement comes back here and the two fields are the whole of it. The address answers on GET as well as on POST, because that is how the validators and crawlers that list a paid resource ask for it. An unpaid call that carries no document — a GET, or a POST with nothing or with an empty document — is answered with the challenge and never a purchase, and a GET is never read for payment; the challenge describes the purchase, which is a POST with a JSON body. An unpaid POST that carries that document is answered with the price of the very order it opened, and a document of some other shape is refused with the fields that are wrong. A product that is not on sale answers neither method with a challenge: it is refused, so that a catalog built from these challenges never carries a product nobody can buy.",
915
+ description: "Buying one product. The payment is what stands in for authorisation, so there is no key on this call. It begins with the payment exchange of the x402 protocol: a call with no payment on it is answered with a challenge, which travels in a header and carries no document. What a paid purchase is answered with is the state of the order it made — the same document the status route answers with, whatever the card's mode and whether the purchase ended in the goods or in something else, so an agent that bought and an agent that came back later read one shape. That document names, in status_url, the whole address where the order is read again, which is where goods that come later are collected. No receipt of ours is in it: a receipt is the merchant's record of the sale and is read behind the merchant's key. What an agent is told about its own money is the price and the word for whether that money was real, and, where the payment executed as the last step of this exchange, the settlement the payment layer signs into a header on this answer — a card whose money moves as the order is opened has already spent that step, so no settlement comes back here and the two fields are the whole of it. The address answers on GET as well as on POST, because that is how the validators and crawlers that list a paid resource ask for it. An unpaid call that carries no document — a GET, or a POST with nothing or with an empty document — is answered with the challenge and never a purchase, and a GET is never read for payment; the challenge describes the purchase, which is a POST with a JSON body. An unpaid POST that carries that document is answered with the price of the very order it opened, and a document of some other shape is refused with the fields that are wrong. A product that is not on sale answers neither method with a challenge: it is refused, so that a catalog built from these challenges never carries a product nobody can buy.",
878
916
  request: PurchaseRequestSchema,
879
917
  also_answers_on: ["GET"],
880
918
  response: { document: AgentOrderStatusSchema },
@@ -883,7 +921,7 @@ export const API_ROUTES = Object.freeze({
883
921
  method: "GET",
884
922
  path: "/x402/orders/:order_id/status",
885
923
  auth: "order_id",
886
- description: "What became of a purchase, for the agent that made it: where the order stands, what it sold for, and the goods once they are the buyer's. It is the route an agent that bought a product whose goods come later collects them on, and without it half a catalogue takes money and hands back an order nobody can act on. Knowing the order's identifier is the proof (ADR-0011), so this call takes no key: an agent has no account and no registration, and the identifier is handed to exactly one party. No answer of ours hands the agent this address either — it is built from the order's identifier and the address the agent already bought at, which is why it is written the way a stranger would write it and carries no version to guess at. Two things follow for whoever mounts it. Which door a call is behind is read off auth and never off the address, whatever the prefixes happen to agree on today. And an identifier that names no order must be answered exactly as any other unknown one is, or the refusal becomes a way of counting the orders behind it.",
924
+ description: "What became of a purchase, for the agent that made it: where the order stands, what it sold for, and the goods once they are the buyer's. It is the route an agent that bought a product whose goods come later collects them on, and without it half a catalogue takes money and hands back an order nobody can act on. Every answer that carries the order document names this address in status_url, whole and ready to call, the purchase's own answer included. An order whose goods come later is owed them by the delivery deadline its card carried when the order was opened, counted from its payment: fulfill_deadline_seconds where the card names one, and a day where it names none. An order still without goods at that deadline becomes refund_due, and a paid order the merchant refuses or leaves behind becomes it sooner: the merchant owes the buyer the goods or the money back. That is not an ending, and it carries no further deadline. Goods the merchant delivers after it still appear here and settle the debt, and the order becomes delivered. Nor does refund_due say whether the money has come back: the merchant returns it from their own wallet, the gateway has no way yet to record that they did, and until it has one no order moves on to refunded — a buyer already paid back still reads refund_due here. So an agent holding refund_due can still collect goods here, nothing here tells it when to stop asking, and its own wallet is where a refund shows. Knowing the order's identifier is the proof (ADR-0011), so this call takes no key: an agent has no account and no registration, and the identifier is handed to exactly one party. Two things follow for whoever mounts it. Which door a call is behind is read off auth and never off the address, whatever the prefixes happen to agree on today. And an identifier that names no order must be answered exactly as any other unknown one is, or the refusal becomes a way of counting the orders behind it.",
887
925
  response: { document: AgentOrderStatusSchema },
888
926
  },
889
927
  });
package/dist/index.d.ts CHANGED
@@ -33,8 +33,8 @@ export type { EvmAddress } from "./evm-address.js";
33
33
  export { checksummedAddressOf, EvmAddressSchema } from "./evm-address.js";
34
34
  export type { Acceptance, Delivery, HandlerAnswer, Refusal, RefusalCode } from "./handler.js";
35
35
  export { AcceptanceSchema, DeliverySchema, HandlerAnswerSchema, RECOMMENDED_REFUSAL_CODES, RefusalCodeSchema, RefusalSchema, } from "./handler.js";
36
- export type { CabinetKey, DisabledKey, ForgottenCabinetKey, IssuedKey, IssueKeyRequest, MerchantKey, MerchantKeyList, PayoutWallet, PayoutWalletRequest, RegisteredMerchant, RegistrationRequest, SellerName, SellerNameRequest, } from "./merchant.js";
37
- export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
36
+ export type { CabinetKey, DisabledKey, ForgottenCabinetKey, IssuedKey, IssueKeyRequest, MerchantKey, MerchantKeyList, PayoutWallet, PayoutWalletRequest, PendingPayoutWallet, RegisteredMerchant, RegistrationRequest, SellerName, SellerNameRequest, } from "./merchant.js";
37
+ export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, PendingPayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
38
38
  export type { Order } from "./order.js";
39
39
  export { OrderSchema } from "./order.js";
40
40
  export type { OrderStatus } from "./order-status.js";
@@ -47,8 +47,8 @@ export type { QuotePurpose, QuoteRequest, QuoteResponse } from "./quote.js";
47
47
  export { QuotePurposeSchema, QuoteRequestSchema, QuoteResponseSchema } from "./quote.js";
48
48
  export type { Receipt, ReceiptOutcome } from "./receipt.js";
49
49
  export { ReceiptOutcomeSchema, ReceiptSchema } from "./receipt.js";
50
- export type { CallError, OrderCallResult, Problem, PublishResult } from "./results.js";
51
- export { CARD_REJECTED, CallErrorSchema, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
50
+ export type { CallError, MerchantFinding, OrderCallResult, Problem, PublishResult, } from "./results.js";
51
+ export { CARD_REJECTED, CallErrorSchema, MERCHANT_FINDINGS, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
52
52
  export type { SellingState } from "./selling.js";
53
53
  export { SELLING_STATES, SellingStateSchema } from "./selling.js";
54
54
  /**
@@ -76,6 +76,7 @@ export declare const schemas: Readonly<{
76
76
  }, z.core.$strict>;
77
77
  agent_order_status: z.ZodObject<{
78
78
  order_id: z.ZodString;
79
+ status_url: z.ZodURL;
79
80
  status: z.ZodEnum<{
80
81
  delivered: "delivered";
81
82
  in_progress: "in_progress";
@@ -615,10 +616,18 @@ export declare const schemas: Readonly<{
615
616
  }>;
616
617
  payout_wallet: z.ZodObject<{
617
618
  payout_wallet: z.ZodNullable<z.ZodString>;
619
+ pending: z.ZodNullable<z.ZodObject<{
620
+ payout_wallet: z.ZodString;
621
+ takes_effect_at: z.ZodISODateTime;
622
+ }, z.core.$strict>>;
618
623
  }, z.core.$strict>;
619
624
  payout_wallet_request: z.ZodObject<{
620
625
  payout_wallet: z.ZodPipe<z.ZodString, z.ZodString>;
621
626
  }, z.core.$strict>;
627
+ pending_payout_wallet: z.ZodObject<{
628
+ payout_wallet: z.ZodString;
629
+ takes_effect_at: z.ZodISODateTime;
630
+ }, z.core.$strict>;
622
631
  price_check: z.ZodUnion<readonly [z.ZodLiteral<"handler">, z.ZodObject<{
623
632
  url: z.ZodURL;
624
633
  }, z.core.$strict>]>;
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { WorkerEnvelopeSchema } from "./envelope.js";
27
27
  import { OrderEventSchema, RefundDueReasonSchema } from "./events.js";
28
28
  import { EvmAddressSchema } from "./evm-address.js";
29
29
  import { AcceptanceSchema, DeliverySchema, HandlerAnswerSchema, RefusalCodeSchema, RefusalSchema, } from "./handler.js";
30
- import { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
30
+ import { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, PendingPayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
31
31
  import { OrderSchema } from "./order.js";
32
32
  import { OrderStatusSchema } from "./order-status.js";
33
33
  import { FieldSpecSchema, ParamNameSchema, ParamSpecSchema, ParamTypeSchema, } from "./param-spec.js";
@@ -42,14 +42,14 @@ export { WORKER_ENVELOPE_KINDS, WORKER_ENVELOPE_PAYLOADS, WorkerEnvelopeSchema,
42
42
  export { ORDER_EVENT_TYPES, OrderEventSchema, RefundDueReasonSchema } from "./events.js";
43
43
  export { checksummedAddressOf, EvmAddressSchema } from "./evm-address.js";
44
44
  export { AcceptanceSchema, DeliverySchema, HandlerAnswerSchema, RECOMMENDED_REFUSAL_CODES, RefusalCodeSchema, RefusalSchema, } from "./handler.js";
45
- export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
45
+ export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, PendingPayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
46
46
  export { OrderSchema } from "./order.js";
47
47
  export { ORDER_STATUSES, OrderStatusSchema } from "./order-status.js";
48
48
  export { FieldSpecSchema, ParamNameSchema, ParamSpecSchema, ParamTypeSchema, PROTOTYPE_KEY_IS_DROPPED, paramSpecToValidator, } from "./param-spec.js";
49
49
  export { AmountSchema, CurrencyCodeSchema, IdentifierSchema, MoneySchema, SalePriceSchema, TimestampSchema, } from "./primitives.js";
50
50
  export { QuotePurposeSchema, QuoteRequestSchema, QuoteResponseSchema } from "./quote.js";
51
51
  export { ReceiptOutcomeSchema, ReceiptSchema } from "./receipt.js";
52
- export { CARD_REJECTED, CallErrorSchema, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
52
+ export { CARD_REJECTED, CallErrorSchema, MERCHANT_FINDINGS, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
53
53
  export { SELLING_STATES, SellingStateSchema } from "./selling.js";
54
54
  /**
55
55
  * The version of the public contract. It grows when the meaning of the fields
@@ -109,6 +109,7 @@ export const schemas = Object.freeze({
109
109
  param_type: ParamTypeSchema,
110
110
  payout_wallet: PayoutWalletSchema,
111
111
  payout_wallet_request: PayoutWalletRequestSchema,
112
+ pending_payout_wallet: PendingPayoutWalletSchema,
112
113
  price_check: PriceCheckSchema,
113
114
  problem: ProblemSchema,
114
115
  public_card: PublicCardSchema,
@@ -177,6 +177,25 @@ export declare const SellerNameSchema: z.ZodObject<{
177
177
  export declare const SellerNameRequestSchema: z.ZodObject<{
178
178
  seller_name: z.ZodPipe<z.ZodString, z.ZodString>;
179
179
  }, z.core.$strict>;
180
+ /**
181
+ * A change of the wallet that has been asked for, announced, and has not taken
182
+ * effect yet.
183
+ *
184
+ * It exists because a replacement does not apply at once where the money is
185
+ * real (ADR-0019). The address a merchant is paid at is the one setting whose
186
+ * change redirects money, and any key of theirs reaches it — the cabinet's, or
187
+ * one sitting in their own server's environment — so on the live deployment a
188
+ * replacement is told to every account of the merchant first and takes effect
189
+ * forty-eight hours after that. What this document says is the two facts a
190
+ * merchant needs in that window: what replaces the address, and from when.
191
+ *
192
+ * Both are required. An address with no moment says nothing about when the
193
+ * money moves, and a moment with no address says it moves without saying where.
194
+ */
195
+ export declare const PendingPayoutWalletSchema: z.ZodObject<{
196
+ payout_wallet: z.ZodString;
197
+ takes_effect_at: z.ZodISODateTime;
198
+ }, z.core.$strict>;
180
199
  /**
181
200
  * The wallet a merchant's sales are paid into, as the merchant reads it back.
182
201
  *
@@ -200,9 +219,27 @@ export declare const SellerNameRequestSchema: z.ZodObject<{
200
219
  * address in lower case they cannot tell it from a different address without
201
220
  * comparing character by character. On the one field money is sent to, that
202
221
  * glance is the whole of the checking anybody does.
222
+ *
223
+ * `pending` is the change waiting beside it, and it is always present for the
224
+ * same reason `payout_wallet` is: null says nothing is waiting, and an absent
225
+ * field would be a silence. It is the field that keeps a caller from taking its
226
+ * own write for a failure — a merchant who asked for a new address on the live
227
+ * deployment reads the old one back, correctly, for forty-eight hours, and
228
+ * without this they would ask again, or conclude the change was lost.
229
+ *
230
+ * It is carried without moving `CONTRACT_VERSION`, which is the one known
231
+ * exception to the rule that a new required field moves it (ADR-0006 §2): no
232
+ * worker of the SDK reads this route, so the version would stop every
233
+ * installed worker for a field none of them sees. What that costs is that a
234
+ * merchant's own code holding this schema from an older release of this
235
+ * package refuses the answer until the package is upgraded.
203
236
  */
204
237
  export declare const PayoutWalletSchema: z.ZodObject<{
205
238
  payout_wallet: z.ZodNullable<z.ZodString>;
239
+ pending: z.ZodNullable<z.ZodObject<{
240
+ payout_wallet: z.ZodString;
241
+ takes_effect_at: z.ZodISODateTime;
242
+ }, z.core.$strict>>;
206
243
  }, z.core.$strict>;
207
244
  /**
208
245
  * What a merchant sends to change where their sales are paid.
@@ -270,6 +307,7 @@ export type SellerName = z.infer<typeof SellerNameSchema>;
270
307
  export type SellerNameRequest = z.infer<typeof SellerNameRequestSchema>;
271
308
  export type PayoutWallet = z.infer<typeof PayoutWalletSchema>;
272
309
  export type PayoutWalletRequest = z.infer<typeof PayoutWalletRequestSchema>;
310
+ export type PendingPayoutWallet = z.infer<typeof PendingPayoutWalletSchema>;
273
311
  export type MerchantKey = z.infer<typeof MerchantKeySchema>;
274
312
  export type MerchantKeyList = z.infer<typeof MerchantKeyListSchema>;
275
313
  export type IssueKeyRequest = z.infer<typeof IssueKeyRequestSchema>;
package/dist/merchant.js CHANGED
@@ -51,6 +51,33 @@ import { IdentifierSchema, TimestampSchema } from "./primitives.js";
51
51
  const KeyLabelSchema = z
52
52
  .string()
53
53
  .regex(/^\S(?:[\s\S]*\S)?$/u, "a label must not be empty or padded with spaces");
54
+ /**
55
+ * The longest label a key is issued with.
56
+ *
57
+ * A label used to have no bound, on the argument that no channel outside us
58
+ * carries it. One does now: on the live deployment the message that tells a
59
+ * merchant of a new key, or of a wallet change made with one, names the key
60
+ * by its label (ADR-0019), and that message is Agentify's words in somebody's
61
+ * inbox. A hundred characters is a line on a list and in a message, and
62
+ * longer than any name a person gives a worker.
63
+ */
64
+ const LONGEST_KEY_LABEL = 100;
65
+ /**
66
+ * A label as a new key is issued with it: the rule above, on one line, and no
67
+ * longer than {@link LONGEST_KEY_LABEL}.
68
+ *
69
+ * Only the request carries the bound. The keys a merchant already holds are
70
+ * read back under whatever they were named, including a key issued at the
71
+ * server's terminal, because refusing the list over one old row would hide
72
+ * every key on it; the message that names such a key cuts it to one line of
73
+ * the same length itself.
74
+ */
75
+ const IssuedKeyLabelSchema = KeyLabelSchema.max(LONGEST_KEY_LABEL, `a label is at most ${LONGEST_KEY_LABEL} characters, the one line a key is known by`).regex(
76
+ // Written as code-point ranges rather than a Unicode property, because this
77
+ // pattern is published in the JSON Schema a validator in another language
78
+ // reads, and not every one of those knows the property escapes.
79
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what it refuses
80
+ /^[^\u0000-\u001F\u007F-\u009F\u2028\u2029]*$/u, "a label is one line: it cannot carry a line break, a tab or another control character");
54
81
  /**
55
82
  * The key itself, in the only form its owner will ever see it.
56
83
  *
@@ -164,10 +191,10 @@ export const MerchantKeyListSchema = z
164
191
  /** What a merchant sends to have a key made. */
165
192
  export const IssueKeyRequestSchema = z
166
193
  .strictObject({
167
- label: KeyLabelSchema,
194
+ label: IssuedKeyLabelSchema,
168
195
  })
169
196
  .meta({
170
- description: "What a merchant asks for when they want another key: the name they will know it by, and nothing else. There is nowhere here to put a secret, because a key is generated rather than chosen — one somebody picks is one somebody reuses somewhere else.",
197
+ description: "What a merchant asks for when they want another key: the name they will know it by, and nothing else. The name is one line of at most 100 characters, not empty and not padded with spaces; on the live deployment it is also how the message telling the merchant of the new key names it. There is nowhere here to put a secret, because a key is generated rather than chosen — one somebody picks is one somebody reuses somewhere else.",
171
198
  });
172
199
  /**
173
200
  * A key that has just been made: the row, and the secret, once.
@@ -306,6 +333,31 @@ export const SellerNameRequestSchema = z
306
333
  .meta({
307
334
  description: "What a merchant sends to change the name their products are sold under. The same rule as the answer — at most 32 characters of printable ASCII, the catalog's rule rather than ours — and one difference: null is refused. A merchant goes from no name to a name and from one name to another, never back to none, because a payment request names the seller and there would be nobody to name: every card they have published would come off sale, which is an end to their selling arriving under the name of editing a setting. Somebody reaching for null wants one of two other things: a different name, which is this call with a different value, or an end to selling, which is the pause.",
308
335
  });
336
+ /**
337
+ * A change of the wallet that has been asked for, announced, and has not taken
338
+ * effect yet.
339
+ *
340
+ * It exists because a replacement does not apply at once where the money is
341
+ * real (ADR-0019). The address a merchant is paid at is the one setting whose
342
+ * change redirects money, and any key of theirs reaches it — the cabinet's, or
343
+ * one sitting in their own server's environment — so on the live deployment a
344
+ * replacement is told to every account of the merchant first and takes effect
345
+ * forty-eight hours after that. What this document says is the two facts a
346
+ * merchant needs in that window: what replaces the address, and from when.
347
+ *
348
+ * Both are required. An address with no moment says nothing about when the
349
+ * money moves, and a moment with no address says it moves without saying where.
350
+ */
351
+ export const PendingPayoutWalletSchema = z
352
+ .strictObject({
353
+ /** The address the merchant will be paid at once the wait is over. */
354
+ payout_wallet: EvmAddressSchema,
355
+ /** The moment it replaces the address paid now. */
356
+ takes_effect_at: TimestampSchema,
357
+ })
358
+ .meta({
359
+ description: "A replacement wallet that has been asked for and announced and has not taken effect yet. payout_wallet is the address sales will be paid into from takes_effect_at on, in the mixed-case spelling a wallet shows; until that moment every payment request still names the address paid now. The change takes effect then only if it is still the one waiting: asking for the address paid now cancels it, and asking for a different address replaces it and starts the wait again.",
360
+ });
309
361
  /**
310
362
  * The wallet a merchant's sales are paid into, as the merchant reads it back.
311
363
  *
@@ -329,14 +381,30 @@ export const SellerNameRequestSchema = z
329
381
  * address in lower case they cannot tell it from a different address without
330
382
  * comparing character by character. On the one field money is sent to, that
331
383
  * glance is the whole of the checking anybody does.
384
+ *
385
+ * `pending` is the change waiting beside it, and it is always present for the
386
+ * same reason `payout_wallet` is: null says nothing is waiting, and an absent
387
+ * field would be a silence. It is the field that keeps a caller from taking its
388
+ * own write for a failure — a merchant who asked for a new address on the live
389
+ * deployment reads the old one back, correctly, for forty-eight hours, and
390
+ * without this they would ask again, or conclude the change was lost.
391
+ *
392
+ * It is carried without moving `CONTRACT_VERSION`, which is the one known
393
+ * exception to the rule that a new required field moves it (ADR-0006 §2): no
394
+ * worker of the SDK reads this route, so the version would stop every
395
+ * installed worker for a field none of them sees. What that costs is that a
396
+ * merchant's own code holding this schema from an older release of this
397
+ * package refuses the answer until the package is upgraded.
332
398
  */
333
399
  export const PayoutWalletSchema = z
334
400
  .strictObject({
335
- /** Where this merchant's sales are paid, or nothing at all. */
401
+ /** Where this merchant's sales are paid now, or nothing at all. */
336
402
  payout_wallet: EvmAddressSchema.nullable(),
403
+ /** A replacement that has been announced and is waiting, or nothing. */
404
+ pending: PendingPayoutWalletSchema.nullable(),
337
405
  })
338
406
  .meta({
339
- description: "The address a merchant's sales are paid into. Payments are not held by anybody on the way: a buyer's agent pays this address directly, and it is the payTo of every payment request made for this merchant's products. Null means nobody has set one, which is where every merchant starts; the field is always present rather than left out, because an absent field is indistinguishable from a client that dropped it. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent — so what a merchant reads back on a screen is character for character what they copied out of their wallet. On a deployment that settles on a real chain a merchant with no wallet here cannot publish a card, because the money from that card's sales would have nowhere to go.",
407
+ description: "The address a merchant's sales are paid into, and any change of it that is waiting. Payments are not held by anybody on the way: a buyer's agent pays payout_wallet directly, and it is the payTo of every payment request made for this merchant's products now. Null means nobody has set one, which is where every merchant starts; the field is always present rather than left out, because an absent field is indistinguishable from a client that dropped it. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent — so what a merchant reads back on a screen is character for character what they copied out of their wallet. On a deployment that settles on a real chain a merchant with no wallet here cannot publish a card, because the money from that card's sales would have nowhere to go. pending is a replacement that has been asked for and has not taken effect: on the live deployment a merchant who already has a wallet and asks for a different one is told of it by message, and the new address takes effect forty-eight hours later, so until takes_effect_at this answer names the address still paid and the waiting one beside it. A caller reading its old address back beside a pending change has not failed to write; the change is waiting. Null means nothing is waiting, which is every answer on the test channel and in a sandbox, where a change applies at once.",
340
408
  });
341
409
  /**
342
410
  * What a merchant sends to change where their sales are paid.
@@ -68,8 +68,10 @@ export const ORDER_STATUSES = Object.freeze([
68
68
  */
69
69
  "rejected",
70
70
  /**
71
- * Closed, and nobody can say whether the buyer was charged: the payment
72
- * network was asked and never answered.
71
+ * Nobody can say whether the buyer was charged: the payment network was
72
+ * asked and never answered. The order may still be open — the goods may
73
+ * already be made — and a later read can still move. Closed is not what
74
+ * this word says.
73
75
  *
74
76
  * Deliberately not `rejected`, and this is the fifth gate in one value. An
75
77
  * agent told its purchase did not happen goes and buys the same thing
@@ -88,7 +90,18 @@ export const ORDER_STATUSES = Object.freeze([
88
90
  "expired",
89
91
  /** The merchant left, and orders still open closed with them. */
90
92
  "cancelled",
91
- /** The money moved and the delivery did not happen. */
93
+ /**
94
+ * The money moved and the goods have not come: the merchant owes the buyer
95
+ * the goods or the money back. Not an ending, and it carries no deadline of
96
+ * its own. Goods the merchant delivers after it still reach the buyer, settle
97
+ * the debt and make the order `delivered`.
98
+ *
99
+ * What it cannot say is whether the money has already gone back. The
100
+ * merchant pays a refund from their own wallet and the gateway has no way yet
101
+ * to record that they did, so until it has one nothing moves an order on to
102
+ * `refunded`, and a buyer the merchant has already paid back still reads
103
+ * this word.
104
+ */
92
105
  "refund_due",
93
106
  /** That debt has since been paid back to the buyer. */
94
107
  "refunded",
package/dist/results.d.ts CHANGED
@@ -194,6 +194,30 @@ export declare const CallErrorSchema: z.ZodObject<{
194
194
  * writes the branch that reads it.
195
195
  */
196
196
  export declare const CARD_REJECTED = "card_rejected";
197
+ /**
198
+ * The findings of a refused publish that are about the merchant rather than
199
+ * the card.
200
+ *
201
+ * Each arrives in the refusal's `problems` with an empty path, beside whatever
202
+ * is wrong with the card, and none of them is cleared by editing the card: the
203
+ * merchant has no name set for buyers to read, no wallet set for their sales to
204
+ * be paid into, or no approval from the operator for the live catalog. The
205
+ * name is set with a call of the merchant's own (`POST /v0/seller-name`) or in
206
+ * the cabinet; the wallet in the cabinet's Settings alone, since no key of the
207
+ * merchant's code may say where the money goes; and the approval is the
208
+ * operator's decision, with no call. The name is asked for everywhere, the
209
+ * wallet wherever a payment settles and the approval on the live deployment
210
+ * alone, as the publish route's description says; a program needs no copy of
211
+ * that rule, because it learns what is missing where it is refused, at the
212
+ * publish.
213
+ */
214
+ export declare const MERCHANT_FINDINGS: Readonly<{
215
+ readonly NO_SELLER_NAME: "no_seller_name";
216
+ readonly NO_PAYOUT_WALLET: "no_payout_wallet";
217
+ readonly NO_OPERATOR_APPROVAL: "no_operator_approval";
218
+ }>;
219
+ /** One of the findings about the merchant, as its code travels on the wire. */
220
+ export type MerchantFinding = (typeof MERCHANT_FINDINGS)[keyof typeof MERCHANT_FINDINGS];
197
221
  /**
198
222
  * The answer to publishing a card: the catalog id, or what stands in its way.
199
223
  *
package/dist/results.js CHANGED
@@ -161,18 +161,27 @@ export const ProblemSchema = z
161
161
  * value, and it covers two kinds of finding that a merchant tells apart by
162
162
  * the words rather than by the shape: one about the whole of what was
163
163
  * sent, and one about the merchant sending it — their having set no name
164
- * for buyers to read, or no wallet for their sales to be paid into.
164
+ * for buyers to read, no wallet for their sales to be paid into, or no
165
+ * operator approval for the live catalogue.
165
166
  * Leaving the field out entirely would make an empty path
166
167
  * indistinguishable from a path nobody filled in.
167
168
  */
168
169
  path: z.array(z.string()),
169
- /** What kind of finding it is, for the code that reads it. */
170
- code: z.string().regex(/\S/, "a finding carries a code"),
170
+ /**
171
+ * What kind of finding it is, for the code that reads it.
172
+ *
173
+ * Open, because a finding about a field carries whatever the check that
174
+ * found it calls it. The three about the merchant are promised, and they
175
+ * are described for the export for the reason the error's codes are.
176
+ */
177
+ code: z.string().regex(/\S/, "a finding carries a code").meta({
178
+ description: 'What kind of finding it is, for the program that reads it. The set is open: a finding about a field of what was sent carries the name the check gave it. Three are promised, always with an empty path, and each says the merchant rather than the card is missing something, so no edit to the card clears it: "no_seller_name" (no name set for buyers to read), "no_payout_wallet" (no wallet set for the sales to be paid into, asked for wherever a payment settles) and "no_operator_approval" (the operator has not admitted this merchant to the live catalog, which only the operator can change).',
179
+ }),
171
180
  /** The same finding in words, for the person who has to fix the card. */
172
181
  message: z.string().regex(/\S/, "a finding carries an explanation a person can read"),
173
182
  })
174
183
  .meta({
175
- description: 'One thing wrong with what was sent: where it is, a code for the program that reads it, and the same finding in words for the person who has to fix it. The path names the field, innermost last — ["params", "email", "type"] — and an empty path is a statement rather than a missing value: the finding is about the whole of what was sent, or about the sender — a merchant with no name set for buyers, or no wallet set for their sales to be paid into, is refused with an empty path too — and not about any one field, which is also what a document that could not be read at all produces. The field is always present for that reason, because an absent path and an empty one would be indistinguishable. Findings never travel alone: they arrive as the problems list inside the error of a call that refused what it was handed — a card that was not published, a delivery that is not what its card declares, a body or a set of purchase parameters that did not fit.',
184
+ description: 'One thing wrong with what was sent: where it is, a code for the program that reads it, and the same finding in words for the person who has to fix it. The path names the field, innermost last — ["params", "email", "type"] — and an empty path is a statement rather than a missing value: the finding is about the whole of what was sent, or about the sender — a merchant with no name set for buyers, no wallet set for their sales to be paid into, or no operator approval for the live catalogue is refused with an empty path too — and not about any one field, which is also what a document that could not be read at all produces. The field is always present for that reason, because an absent path and an empty one would be indistinguishable. Findings never travel alone: they arrive as the problems list inside the error of a call that refused what it was handed — a card that was not published, a delivery that is not what its card declares, a body or a set of purchase parameters that did not fit.',
176
185
  });
177
186
  /**
178
187
  * Why a call did not go through: a code to branch on, a sentence to print, and
@@ -202,7 +211,7 @@ export const CallErrorSchema = z.strictObject({
202
211
  code: z.string().regex(/\S/, "an error carries a code").meta({
203
212
  // Same reason as the refusal code: the dictionary travels with the field
204
213
  // or it does not reach the reader the export exists for.
205
- description: 'Why the call did not go through. The set is open, and five are promised to mean one thing each — but not on the same calls, and which call a code can arrive on is part of what is promised about it. Publishing a card is refused with one word and no other: "card_rejected" (the card was not published, and every finding standing between it and the catalog is named in the error\'s problems — the fields at fault, and the merchant\'s own missing name or payout wallet where those are what is missing; it is never retryable, because the same card gets the same answer and what changes the outcome is fixing what the problems name). The calls that close an order — delivering, refusing, taking one on — are refused with the other four, and never with the first: "refund_already_settled" (the debt was paid back, so there is nothing left to deliver against). "order_already_closed" (the order reached an ending that no call reopens). "not_applicable_in_mode" (the call does not exist for this card\'s mode — refusing separately does not, in the synchronous one, where the handler\'s own answer is the refusal). "delivery_does_not_match_card" (the goods are not the ones the card for this order declares it delivers — nothing was written down, the problems name the fields that did not fit, and the message says whether the order still stands or has already ended). The last of those is retryable in a different sense from a lost connection: the call arrived and was understood, so sending the same goods again gives the same refusal, and what clears it is delivering what the card declares. It is not retryable at all where the order has already ended, because there is nothing left to deliver against.',
214
+ description: 'Why the call did not go through. The set is open, and five are promised to mean one thing each — but not on the same calls, and which call a code can arrive on is part of what is promised about it. Publishing a card is refused with one word and no other: "card_rejected" (the card was not published, and every finding standing between it and the catalog is named in the error\'s problems — the fields at fault, and the merchant\'s own missing name, payout wallet or live operator approval where those are what is missing; it is never retryable, because the same card gets the same answer and what changes the outcome is fixing what the problems name). The calls that close an order — delivering, refusing, taking one on — are refused with the other four, and never with the first: "refund_already_settled" (the debt was paid back, so there is nothing left to deliver against). "order_already_closed" (the order reached an ending that no call reopens). "not_applicable_in_mode" (the call does not exist for this card\'s mode — refusing separately does not, in the synchronous one, where the handler\'s own answer is the refusal). "delivery_does_not_match_card" (the goods are not the ones the card for this order declares it delivers — nothing was written down, the problems name the fields that did not fit, and the message says whether the order still stands or has already ended). The last of those is retryable in a different sense from a lost connection: the call arrived and was understood, so sending the same goods again gives the same refusal, and what clears it is delivering what the card declares. It is not retryable at all where the order has already ended, because there is nothing left to deliver against.',
206
215
  }),
207
216
  message: z.string().regex(/\S/, "an error carries an explanation a person can read"),
208
217
  retryable: z.boolean(),
@@ -218,7 +227,7 @@ export const CallErrorSchema = z.strictObject({
218
227
  * is the field the whole shape is named after.
219
228
  */
220
229
  problems: z.array(ProblemSchema).min(1).optional().meta({
221
- description: 'What was wrong with what was sent, one finding at a time: where it is, a code for the program that reads it, and the same finding in words. Present where the call is refusing what it was handed — a card that was not published, a delivery that is not what its card declares — and absent where the refusal is about a state of the world instead: a refund already settled is about the order, not about a field of the request. Never empty where it is present, and a refused publish always carries it. This field is the complete account of what stands in the way, and it is the one to read the findings from. The error\'s "message" is a single line written to be read in a log and does not carry the list: it says how many findings there are, quotes one or a few of them, and marks the place where a long one was cut — so a reader can always tell a short refusal from a shortened account of a long one.',
230
+ description: "What was wrong with what was sent, one finding at a time: where it is, a code for the program that reads it, and the same finding in words. Present where the call is refusing what it was handed — a card that was not published, a delivery that is not what its card declares — and absent where the refusal is about a state of the world instead: a refund already settled is about the order, not about a field of the request. Never empty where it is present, and a refused publish always carries it. This field is the complete account of what stands in the way, and it is the one to read the findings from. The error's \"message\" is a single line written to be read in a log and does not carry the list. Where what stands in the way is the merchant's own — no seller name, no payout wallet, no operator approval — the message names each of those plainly; for the rest it says how many findings there are, quotes one of them, and marks the place where a long one was cut — so a reader can always tell a short refusal from a shortened account of a long one.",
222
231
  }),
223
232
  });
224
233
  /**
@@ -229,6 +238,28 @@ export const CallErrorSchema = z.strictObject({
229
238
  * writes the branch that reads it.
230
239
  */
231
240
  export const CARD_REJECTED = "card_rejected";
241
+ /**
242
+ * The findings of a refused publish that are about the merchant rather than
243
+ * the card.
244
+ *
245
+ * Each arrives in the refusal's `problems` with an empty path, beside whatever
246
+ * is wrong with the card, and none of them is cleared by editing the card: the
247
+ * merchant has no name set for buyers to read, no wallet set for their sales to
248
+ * be paid into, or no approval from the operator for the live catalog. The
249
+ * name is set with a call of the merchant's own (`POST /v0/seller-name`) or in
250
+ * the cabinet; the wallet in the cabinet's Settings alone, since no key of the
251
+ * merchant's code may say where the money goes; and the approval is the
252
+ * operator's decision, with no call. The name is asked for everywhere, the
253
+ * wallet wherever a payment settles and the approval on the live deployment
254
+ * alone, as the publish route's description says; a program needs no copy of
255
+ * that rule, because it learns what is missing where it is refused, at the
256
+ * publish.
257
+ */
258
+ export const MERCHANT_FINDINGS = Object.freeze({
259
+ NO_SELLER_NAME: "no_seller_name",
260
+ NO_PAYOUT_WALLET: "no_payout_wallet",
261
+ NO_OPERATOR_APPROVAL: "no_operator_approval",
262
+ });
232
263
  /**
233
264
  * The error a refused publish carries: the shared shape, with the findings made
234
265
  * required.
@@ -236,11 +267,10 @@ export const CARD_REJECTED = "card_rejected";
236
267
  * "Refused, and here is nothing" is the one answer a merchant cannot act on,
237
268
  * and publishing is the call where that would be easiest to send — a card is
238
269
  * refused precisely because something about it is wrong, so there is always
239
- * something to name. Not every finding is about the card. A merchant who has
240
- * set no name for buyers to read is refused here too, and so is one who has set
241
- * no wallet for their sales to be paid into; both ride in the same list, so one
242
- * answer carries everything standing between this card and the catalog rather
243
- * than handing it over one round trip at a time.
270
+ * something to name. Not every finding is about the card: the merchant's own
271
+ * missing settings (`MERCHANT_FINDINGS`) ride in the same list, so one answer
272
+ * carries everything standing between this card and the catalog rather than
273
+ * handing it over one round trip at a time.
244
274
  */
245
275
  const PublishRefusalSchema = CallErrorSchema.extend({
246
276
  problems: z.array(ProblemSchema).min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuanu-ai/agentify-contracts",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "The Agentify wire contract as code: the schemas for cards, orders, price checks and receipts that the gateway and the merchant SDK both read.",
6
6
  "keywords": [
@@ -9,7 +9,7 @@
9
9
  "zod-schemas",
10
10
  "json-schema"
11
11
  ],
12
- "homepage": "https://app.agentify.ad/docs/",
12
+ "homepage": "https://agentify.ad/docs/",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {
15
15
  "type": "git",