@nuanu-ai/agentify 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # `@nuanu-ai/agentify`
2
2
 
3
- Agentify puts a merchant's existing products where AI agents buy. Each product
4
- gets a card in an agent-facing storefront and a paid address of its own,
5
- `/x402/<item>/purchase`, where an agent pays for it with the x402 payment
6
- protocol. The buyer pays into the merchant's own payout contract, which nobody
3
+ Agentify publishes a merchant's existing products in its own catalogue, which
4
+ AI agents read and buy from. Each product gets a card there and a paid address
5
+ of its own, `/x402/<item>/purchase`, where an agent pays for it with the x402
6
+ payment protocol. The buyer pays into the merchant's own payout contract, which nobody
7
7
  owns and nobody can redirect. It keeps the money until it is paid out, which
8
8
  the merchant can do at any time, and then sends 99.9% to the merchant's wallet
9
9
  and 0.1% to Agentify ([Money](https://agentify.ad/docs/money)).
@@ -14,6 +14,15 @@ each order with the goods or with a refusal. Nothing of the merchant's has to
14
14
  be reachable from outside — the process opens that connection itself, so no
15
15
  public address and no open port are needed.
16
16
 
17
+ ## Before the first call
18
+
19
+ The key comes from the test channel's dashboard at
20
+ [test.agentify.ad/dashboard](https://test.agentify.ad/dashboard): sign in with
21
+ your email, and on API Keys issue a key, which is shown once. Before your first
22
+ card can be published, the dashboard also needs the seller name buyers will
23
+ see and, in Settings, the wallet your share of each sale is paid out to; a
24
+ publish without them is refused with findings that name what is missing.
25
+
17
26
  ## Install
18
27
 
19
28
  ```sh
@@ -23,7 +32,7 @@ npm install @nuanu-ai/agentify
23
32
  Node.js 24 or newer is required. What arrives with it is our own contracts
24
33
  package and zod underneath that, and nothing else.
25
34
 
26
- ## The address you give it is the world you are in
35
+ ## Test or live: the address decides
27
36
 
28
37
  ```ts
29
38
  import { createClient } from '@nuanu-ai/agentify'
@@ -34,12 +43,12 @@ const agentify = createClient({
34
43
  })
35
44
  ```
36
45
 
37
- There are two addresses, one per world. At `https://test.agentify.ad` you
46
+ There are two channels, one address each. At `https://test.agentify.ad` you
38
47
  build: payments settle on a test chain and the money there is not real. At
39
- `https://agentify.ad` you sell, and it is. A key issued in one does not
40
- open the other, and the key says which one you are holding: `csk_test_` for the
48
+ `https://agentify.ad` you sell, and it is. A key issued on one does not open
49
+ the other, and the key says which one you are holding: `csk_test_` for the
41
50
  first, `csk_live_` for the second. This package picks neither address for you,
42
- and the worker's first log line names the one it started against and whether
51
+ and your process's first log line names the one it started against and whether
43
52
  the money there is real.
44
53
 
45
54
  ## A card, and the handler that fills the orders
@@ -88,12 +97,39 @@ yours, so its `id` is also its idempotency key: pass it into your own delivery
88
97
  system and the second arrival hands back the first result instead of delivering
89
98
  again.
90
99
 
91
- A call that can fail hands the failure back rather than throwing it, in one
92
- envelope: `ok` says which it was, and a failure carries a code, a sentence a
93
- person can read, and whether repeating could change the outcome. A card we
94
- would not publish carries its findings under `problems`, and what no envelope
95
- can carry — a call that reached nothing at all — is thrown as an `AgentifyError`
96
- under those same codes.
100
+ A card we would not publish comes back as `ok: false`, with a code, a sentence a
101
+ person can read, whether repeating could change the outcome, and its findings
102
+ under `problems`. `deliver`, `refuse`, `accept` and `close` hand every failure back in
103
+ that same envelope, and throw only on a client built with no address. `catalog.publish`, `orders.get` and
104
+ `orders.list` throw an `AgentifyError` whenever no publish result or order came
105
+ back — a refused key included — with the same codes, the flag and the call it
106
+ happened on.
107
+
108
+ To stop Agentify accepting deliveries for an order, call its `close` method.
109
+ The optional message is your statement to the buyer, which their agent reads
110
+ as your words: plain text of at most 2000 characters, with no markup and no
111
+ control characters. A message that breaks that rule is refused before anything
112
+ is recorded.
113
+
114
+ ```ts
115
+ const order = await agentify.orders.get(orderId)
116
+ const closed = await order.close({ message: 'Please contact our shop to continue.' })
117
+ ```
118
+
119
+ Closing cannot be undone, moves no money and confirms no refund; you and the
120
+ buyer handle any refund or other agreement outside Agentify. A repeat of the
121
+ call keeps the first closure time and message. Two refusals leave the order as
122
+ it was: `fulfillment_complete` where the goods were delivered or the parcel's
123
+ shipment is recorded, and `nothing_to_close` where the order ended with
124
+ nothing charged.
125
+
126
+ An order read back carries `payment_status` for its original payment and
127
+ `merchant_closure` for your closure. Closing does not cancel a charge Agentify
128
+ had already sent to the facilitator. If that charge completes after you
129
+ closed, you receive `order.unfulfilled` with the reason `paid_after_closure`,
130
+ and the order stays on `orders.list({ open: true })` until you close it again;
131
+ that second closure records its own time and message. The whole rule is on
132
+ [Orders](https://agentify.ad/docs/orders#closing-fulfillment).
97
133
 
98
134
  ## The check that needs no key
99
135
 
@@ -107,15 +143,16 @@ check is exported as `checkCard`, for cards assembled in code rather than kept
107
143
  in files.
108
144
 
109
145
  That is one half of checking yourself. The other half — whether your handler
110
- holds against a real order, and against that same order arriving twice — is a
111
- test purchase on the test site rather than anything this command can do; the
112
- [quickstart](https://agentify.ad/docs/quickstart) walks you through one.
146
+ holds against a real order — is a test purchase you make yourself on the test
147
+ channel, buying your own card with an x402 client and test USDC; the
148
+ [quickstart](https://agentify.ad/docs/quickstart#_6-make-your-first-test-purchase)
149
+ walks you through one.
113
150
 
114
151
  ## Where the rest of it is written
115
152
 
116
153
  The merchant documentation is public. [The first test
117
154
  sale](https://agentify.ad/docs/quickstart) walks from an empty project to
118
- a test purchase your side runs end to end. Beside it are [the product
155
+ a test purchase you make yourself. Beside it are [the product
119
156
  card](https://agentify.ad/docs/cards) field by field, [orders and fulfillment
120
157
  modes](https://agentify.ad/docs/orders) for delivering later rather than in
121
158
  the answer to the purchase, and [what can go
@@ -128,11 +165,12 @@ than the engineer.
128
165
  ## Versions
129
166
 
130
167
  This is an early `0.x` pilot surface and the names in it can still change. The
131
- SDK and the gateway agree on a contract version, and a worker whose version the
132
- gateway does not share stops before it takes an order. During the pilot that
133
- version does not move with every change, so a release older than the gateway
134
- can read a word it does not know as a failure rather than stop: keep the
135
- package current. Register `agentify.on('problem', …)`, which is where you
168
+ SDK and Agentify agree on a contract version, and a process whose version
169
+ Agentify does not share stops before it takes an order. During the pilot that
170
+ version does not move with every change, so a release older than Agentify can
171
+ read a word it does not know as a failure rather than stop: keep the package
172
+ current. What changed in each release is in the
173
+ [changelog](https://github.com/nuanu-ai/agentify/blob/main/packages/sdk/CHANGELOG.md). Register `agentify.on('problem', …)`, which is where you
136
174
  hear about that and about everything else that did not get through.
137
175
 
138
176
  ## License
package/dist/client.d.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * that went out and produced no answer its route can carry is an
23
23
  * `AgentifyError` under the same codes.
24
24
  */
25
- import type { CardInput, Delivery, HandlerAnswer, Money, Order, OrderAcceptResponse, OrderCallResponse, OrderWithStatus, PublishResult, QuoteRequest, QuoteResponse, Refusal } from "@nuanu-ai/agentify-contracts";
25
+ import type { CardInput, Delivery, HandlerAnswer, Money, Order, OrderAcceptResponse, OrderCallResponse, OrderCloseRequest, OrderCloseResponse, OrderWithStatus, PublishResult, QuoteRequest, QuoteResponse, Refusal } from "@nuanu-ai/agentify-contracts";
26
26
  import { type EventHandler, type ProblemReporter, type StreamHandlerKind } from "./worker.js";
27
27
  export interface ClientOptions {
28
28
  /**
@@ -78,6 +78,26 @@ export interface OrderCalls {
78
78
  refuse(refusal: Refusal): Promise<OrderCallResponse>;
79
79
  /** Takes the order on from outside a handler. Repeating it is harmless. */
80
80
  accept(): Promise<OrderAcceptResponse>;
81
+ /**
82
+ * Stops future fulfillment through Agentify for this order. The message is
83
+ * your statement to the buyer, plain text of at most 2000 characters, which
84
+ * their agent reads as your words; Agentify confirms no refund or agreement.
85
+ * Closing cannot be undone, and it causes no new charge.
86
+ *
87
+ * Calling it again succeeds and keeps the first closure's time and message,
88
+ * with one exception. Closing cancels no payment: a payment Agentify had
89
+ * already asked the facilitator to execute may still go through after you
90
+ * close. If it does, you receive `order.unfulfilled` with reason
91
+ * `paid_after_closure`, the order is on `orders.list({ open: true })`, and
92
+ * closing it again then records that closure's own time and message and
93
+ * takes it off the list.
94
+ *
95
+ * Resolves to `{ ok: true }`, or to `ok: false` with an `error`, leaving the
96
+ * order unchanged: `fulfillment_complete` where its goods were delivered or
97
+ * its parcel's shipment is recorded, and `nothing_to_close` where it ended
98
+ * without a charge and holds nothing for its buyer. Neither is retryable.
99
+ */
100
+ close(options?: OrderCloseRequest): Promise<OrderCloseResponse>;
81
101
  }
82
102
  /**
83
103
  * The least an order can be and still be answered: its identifier and the
@@ -162,7 +182,8 @@ export interface OrdersNamespace {
162
182
  /**
163
183
  * Orders and the states they are in. With `open: true`, only the ones a
164
184
  * restarted worker should walk: orders it was handed and has not finished,
165
- * an order owing a refund, and one delivered but never paid for. An order
185
+ * an unfulfilled paid order, one delivered but never paid for, and a closed
186
+ * one whose payment completed after you closed it. An order
166
187
  * that was priced and never paid never reached the handler, so it is not on
167
188
  * that list; `get` still reads it. Throws `AgentifyError` when the call
168
189
  * fails.
@@ -197,7 +218,6 @@ export interface AgentifyClient {
197
218
  * answer.
198
219
  */
199
220
  start(): Promise<void>;
200
- /**
201
221
  /**
202
222
  * Stops the subscription and waits for it to finish. Safe to call twice,
203
223
  * and on a client that was never started. Handlers stay registered.
@@ -253,5 +273,21 @@ export declare class AgentifyError extends Error {
253
273
  readonly retryable: boolean;
254
274
  constructor(code: string, route: string, message: string, retryable: boolean);
255
275
  }
276
+ /**
277
+ * Builds the client a merchant's code publishes cards and answers orders with,
278
+ * against one gateway: `https://test.agentify.ad` while building,
279
+ * `https://agentify.ad` once live, named in `baseUrl`.
280
+ *
281
+ * Building it calls nothing. It throws a `TypeError` at once when `apiKey` is
282
+ * missing or blank and when `baseUrl` is given but is not an http or https
283
+ * address. Left without `baseUrl`, it is still built, and every call that has to
284
+ * reach the gateway throws a `TypeError` naming both addresses, since there is
285
+ * no default to fall back on. Whether the key opens that gateway is learned at
286
+ * the first call.
287
+ *
288
+ * Returns the client: `catalog.publish` for cards, `orders` to read and close
289
+ * orders, `on` to register handlers, and `start` and `stop` for the
290
+ * subscription that brings orders, price questions and order events in.
291
+ */
256
292
  export declare const createClient: (options: ClientOptions) => AgentifyClient;
257
293
  export {};
package/dist/client.js CHANGED
@@ -101,7 +101,7 @@ export class AgentifyError extends Error {
101
101
  * statement about what this package knows, not about the order.
102
102
  *
103
103
  * The sentence saying the call may be repeated safely is added only where the
104
- * call is idempotent (delivering, accepting; not refusing) and repeating it
104
+ * call is idempotent (delivering, accepting or closing; not refusing) and repeating it
105
105
  * could get anywhere.
106
106
  */
107
107
  const failedCall = (repeatIsSafe, failure) => {
@@ -175,6 +175,22 @@ const addressOf = (baseUrl) => {
175
175
  const assertEveryKindIsRegistered = (kind) => {
176
176
  throw new TypeError(`on() has no place to put a handler for ${JSON.stringify(kind)}`);
177
177
  };
178
+ /**
179
+ * Builds the client a merchant's code publishes cards and answers orders with,
180
+ * against one gateway: `https://test.agentify.ad` while building,
181
+ * `https://agentify.ad` once live, named in `baseUrl`.
182
+ *
183
+ * Building it calls nothing. It throws a `TypeError` at once when `apiKey` is
184
+ * missing or blank and when `baseUrl` is given but is not an http or https
185
+ * address. Left without `baseUrl`, it is still built, and every call that has to
186
+ * reach the gateway throws a `TypeError` naming both addresses, since there is
187
+ * no default to fall back on. Whether the key opens that gateway is learned at
188
+ * the first call.
189
+ *
190
+ * Returns the client: `catalog.publish` for cards, `orders` to read and close
191
+ * orders, `on` to register handlers, and `start` and `stop` for the
192
+ * subscription that brings orders, price questions and order events in.
193
+ */
178
194
  export const createClient = (options) => {
179
195
  const gateway = {
180
196
  apiKey: keyOf(options.apiKey),
@@ -256,6 +272,14 @@ export const createClient = (options) => {
256
272
  },
257
273
  deliver: (delivery) => orderCall("deliver_order", orderId, delivery),
258
274
  refuse: (refusal) => orderCall("refuse_order", orderId, refusal),
275
+ close: async (options = {}) => {
276
+ reachable();
277
+ const answer = await callRoute(gateway, "close_order", {
278
+ path: { order_id: orderId },
279
+ body: options,
280
+ });
281
+ return answer.ok ? answer.document : { ok: false, error: failedCall(true, answer.failure) };
282
+ },
259
283
  accept: (...given) => {
260
284
  if (given.length > 0)
261
285
  reportSafely(registry.problem, acceptanceArgumentWarning(orderId));
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * an order, a handler's answer — are re-exported here, so integration code
21
21
  * needs one import.
22
22
  */
23
- export type { CallError, Card, CardInput, Delivery, Fulfillment, HandlerAnswer, MerchantFinding, Money, Order, OrderCallResponse, OrderCallResult, OrderEvent, OrderList, OrderStatus, OrderWithStatus, Problem, PublishResult, QuotePurpose, QuoteRequest, QuoteResponse, Refusal, RefusalCode, SalePrice, Shipment, ShipTo, } from "@nuanu-ai/agentify-contracts";
23
+ export type { CallError, Card, CardInput, Delivery, Fulfillment, HandlerAnswer, MerchantClosure, MerchantFinding, Money, Order, OrderCallResponse, OrderCallResult, OrderCloseRequest, OrderCloseResponse, OrderEvent, OrderList, OrderStatus, OrderWithStatus, PaymentStatus, Problem, PublishResult, QuotePurpose, QuoteRequest, QuoteResponse, Refusal, RefusalCode, SalePrice, Shipment, ShipTo, } from "@nuanu-ai/agentify-contracts";
24
24
  export { CARD_REJECTED, MERCHANT_FINDINGS, ORDER_EVENT_TYPES, RECOMMENDED_REFUSAL_CODES, } from "@nuanu-ai/agentify-contracts";
25
25
  export type { CardCheck } from "./check-card.js";
26
26
  export { checkCard } from "./check-card.js";
package/dist/worker.d.ts CHANGED
@@ -44,7 +44,8 @@ export interface Delivered {
44
44
  * question to Agentify afterwards, since an event is answered against nothing.
45
45
  * An event is sent at most once, so the risk to write against is an event that
46
46
  * never arrives, not one that arrives twice; reading open orders with
47
- * `orders.list({ open: true })` finds a refund owed whose event went missing.
47
+ * `orders.list({ open: true })` finds a paid order whose `order.unfulfilled`
48
+ * went missing, including one paid after you closed it.
48
49
  */
49
50
  export type EventHandler = (event: OrderEvent, delivered: Delivered) => void | Promise<void>;
50
51
  /**
package/dist/worker.js CHANGED
@@ -369,7 +369,7 @@ deadlineMs = POLL_DEADLINE_MS) => {
369
369
  subject: event.order_id,
370
370
  // An event is sent at most once and carries no acknowledgement, so
371
371
  // nothing here can ask for it again: what the handler failed on is
372
- // lost, and the open-orders list is where a refund owed shows up.
372
+ // lost, and the open-orders list is where an unfulfilled order shows up.
373
373
  message: `the event handler threw on ${event.type} for order ${event.order_id}; nothing here can ask for that event again, and whether it is sent again is not something this SDK knows: ${String(cause)}`,
374
374
  });
375
375
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuanu-ai/agentify",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Sell your existing products to AI agents: publish product cards to an x402 storefront and fill the paid orders from your own service.",
6
6
  "keywords": [
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@nuanu-ai/agentify-contracts": "0.11.0"
39
+ "@nuanu-ai/agentify-contracts": "0.12.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "vitest": "^4.1.11",