@rine-network/eve 0.1.0 → 0.2.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
@@ -103,11 +103,33 @@ undecryptable mail is dropped by default.
103
103
  | `rine_thread` | Decrypted both-sided transcript of a conversation |
104
104
  | `rine_discover` / `rine_inspect` | Search the directory / read a profile |
105
105
  | `rine_group_create` / `_invite` / `_remove` / `_inspect` | MLS-by-default groups |
106
+ | `rine_pay` | Pay a received `rine.v1.x402_payment_required` quote under the local spend policy |
107
+ | `rine_fulfill` | Payee side: verify + settle a received payment and reply with a receipt |
106
108
 
107
109
  Identity is resolved from the environment (`RINE_CONFIG_DIR`, `RINE_AGENT`,
108
110
  `RINE_API_URL`) — never from a tool's model-visible input. Tools return plain text
109
111
  and never surface ciphertext.
110
112
 
113
+ ## Payments (x402)
114
+
115
+ `rine_pay` and `rine_fulfill` carry [x402](https://docs.rine.network/concepts/x402-payments/)
116
+ stablecoin payments as signed messages in the same encrypted thread; both wrap
117
+ `client.payments`. An inbound x402 frame **wakes a payment-aware turn** — a
118
+ `payment_required` points the agent at `rine_pay`, a `payment` at `rine_fulfill`, a
119
+ `receipt` is informational — and the terminal assistant prose into a payment thread stays
120
+ suppressed. `init --tools payments` scaffolds both tools. The wallet key stays on the host
121
+ and is never returned to the model, and a deny-by-default spend policy bounds every
122
+ signature. `rine_pay` returns one of the shared payer statuses (`payment-submitted`,
123
+ `no-wallet`, `not-payment-required`, `policy-refused`, `above-auto-pay-threshold`,
124
+ `already-paid`, `wallet-busy`); `rine_fulfill` reports `settled` / `settlement-failed` /
125
+ `verification-failed` / `facilitator-error` / `no-facilitator`.
126
+
127
+ Auto-pay is **opt-in, off by default**: `rineChannel({ payments: { autoPay: true } })` or
128
+ `RINE_X402_AUTO_PAY=1` lets an inbound quote at/below the wallet policy's auto-pay threshold
129
+ be paid with no LLM turn; a quote above the threshold falls back to surfacing it to the
130
+ model. `rine_fulfill`'s facilitator comes from `RINE_FACILITATOR` (a preset — `cdp` /
131
+ `payai` / `x402-rs` — or an `https://` base URL) or the tool's `facilitator` option.
132
+
111
133
  **Human-in-the-loop (optional).** The mutating tools (`send`, `send_and_wait`,
112
134
  `reply`, `group_*`) accept an opt-in approval gate — `rineSendTool({ needsApproval:
113
135
  "once" })` (or `"always"`) — wired to Eve's approval flow. It's off by default, since
@@ -127,6 +149,8 @@ forever. Configure via `rineChannel({ ignoreTypes: [...] })`.
127
149
  | `RINE_WEBHOOK_SECRET` | HMAC secret the channel verifies inbound with |
128
150
  | `RINE_WEBHOOK_ID` | registered webhook id (for `webhook --delete`) |
129
151
  | `RINE_INBOUND_PATH` | channel route path (default `/rine/v1/inbound`) |
152
+ | `RINE_X402_AUTO_PAY` | set `1` to auto-pay quotes at/below the policy's auto-pay threshold (default off) |
153
+ | `RINE_FACILITATOR` | `rine_fulfill` facilitator: a preset (`cdp` / `payai` / `x402-rs`) or an `https://` base URL |
130
154
 
131
155
  ## License
132
156
 
@@ -2,7 +2,7 @@ import { _ as senderLabel, f as renderMessageBody, g as renderThreadLine, n as a
2
2
  import { n as verifyRineSignature } from "./hmac-CoaKHmf6.js";
3
3
  import { t as getRineClient } from "./client-X_-9CpQT.js";
4
4
  import { POST, defineChannel } from "eve/channels";
5
- import { asMessageUuid } from "@rine-network/sdk";
5
+ import { X402Error, X402_MESSAGE_TYPE, asMessageUuid } from "@rine-network/sdk";
6
6
  //#region src/transcript.ts
7
7
  /** char ≈ 4 × tokens — the coarse proxy the spec specifies (REQ-CTX-01, OQ1). */
8
8
  const CHARS_PER_TOKEN = 4;
@@ -54,6 +54,17 @@ function renderTranscriptContext(entries, tokenBudget) {
54
54
  */
55
55
  /** Sentinel marking our token payload, robust to Eve's `<channel>:` namespacing. */
56
56
  const TOKEN_MARKER = "r1.";
57
+ /**
58
+ * Record a handled message id in the bounded per-runtime dedupe set. The set is
59
+ * cleared wholesale when it reaches {@link SEEN_CAP} (a coarse but allocation-free
60
+ * eviction — the server-side `delivered_at` ack is the durable dedupe). A no-op
61
+ * when no set is provided.
62
+ */
63
+ function rememberSeen(seen, id) {
64
+ if (!seen) return;
65
+ if (seen.size >= 5e3) seen.clear();
66
+ seen.add(id);
67
+ }
57
68
  /** base64url (no padding) encode of a UTF-8 string. */
58
69
  function b64urlEncode(s) {
59
70
  return Buffer.from(s, "utf-8").toString("base64url");
@@ -67,12 +78,13 @@ function b64urlDecode(s) {
67
78
  * (`rine:`); we additionally fence our payload with {@link TOKEN_MARKER} so the
68
79
  * decoder can recover it regardless of any prefix the framework adds.
69
80
  */
70
- function encodeReplyToken(conversationId, replyTarget, messageId) {
81
+ function encodeReplyToken(conversationId, replyTarget, messageId, opts = {}) {
71
82
  const payload = {
72
83
  c: conversationId,
73
84
  r: replyTarget
74
85
  };
75
86
  if (messageId) payload.m = messageId;
87
+ if (opts.x402) payload.x = 1;
76
88
  return `${TOKEN_MARKER}${b64urlEncode(JSON.stringify(payload))}`;
77
89
  }
78
90
  /**
@@ -94,7 +106,8 @@ function decodeReplyToken(token) {
94
106
  return {
95
107
  conversationId,
96
108
  replyTarget,
97
- messageId: typeof m === "string" && m.length > 0 ? m : void 0
109
+ messageId: typeof m === "string" && m.length > 0 ? m : void 0,
110
+ x402: payload?.x === 1 ? true : void 0
98
111
  };
99
112
  } catch {
100
113
  return null;
@@ -118,18 +131,134 @@ function messageIdFromWebhook(body) {
118
131
  return typeof id === "string" && id.length > 0 ? id : void 0;
119
132
  }
120
133
  //#endregion
134
+ //#region src/x402.ts
135
+ /**
136
+ * x402 payment-aware inbound re-surfacing for the rine channel (D1 + D2).
137
+ *
138
+ * The three x402 payment frames are first-class message types carrying a verbatim
139
+ * x402 V2 object, NOT a chat turn. v1.0 dropped them wholesale (a stateless gateway
140
+ * that auto-replied prose into a payment handshake would corrupt it). v1.1 re-surfaces
141
+ * them as an agent turn with STRUCTURED payment context so the model can act via the
142
+ * dedicated pay / fulfill tools — while the terminal assistant prose stays suppressed
143
+ * in EVERY case (the `x402` continuation-token flag; the guard hazard never re-arms):
144
+ *
145
+ * - `x402_payment_required` → the quote + guidance to pay via `rine_pay`.
146
+ * - `x402_payment` → the signed authorization + guidance to `rine_fulfill`.
147
+ * - `x402_receipt` → the settlement outcome, informational (no reply).
148
+ *
149
+ * D2 auto-pay (opt-in, DEFAULT OFF): when enabled, a `payment_required` at/below the
150
+ * policy's `autoPayThreshold` is paid with NO LLM turn — still bounded by the policy
151
+ * caps, deny-by-default, journal and reserve-lock (all in rine-core). Any decline
152
+ * falls back to surfacing the quote to the model.
153
+ */
154
+ /** The three x402 frame types this module re-surfaces. */
155
+ const X402_FRAME_TYPES = [
156
+ X402_MESSAGE_TYPE.PAYMENT_REQUIRED,
157
+ X402_MESSAGE_TYPE.PAYMENT,
158
+ X402_MESSAGE_TYPE.RECEIPT
159
+ ];
160
+ const X402_FRAME_SET = new Set(X402_FRAME_TYPES);
161
+ /** Whether a message type is one of the three x402 payment frames. */
162
+ function isX402Frame(type) {
163
+ return X402_FRAME_SET.has(type);
164
+ }
165
+ /** Atomic-unit amount of a requirement (x402 V2 `amount`, else V1 spelling). */
166
+ function requirementAmount(r) {
167
+ return r.amount ?? r.maxAmountRequired ?? "?";
168
+ }
169
+ /** One `accepts[]` requirement as a compact model-readable line. */
170
+ function requirementLine(r) {
171
+ return `${requirementAmount(r)} of ${r.asset} on ${r.network} → ${r.payTo}`;
172
+ }
173
+ /**
174
+ * The structured payment body handed to the model as the turn's primary message.
175
+ * Reads only the decrypted plaintext (the verbatim x402 object) — never ciphertext.
176
+ */
177
+ function renderX402Body(msg) {
178
+ const p = msg.plaintext;
179
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT_REQUIRED) {
180
+ const pr = p ?? {};
181
+ const accepts = Array.isArray(pr.accepts) ? pr.accepts : [];
182
+ const opts = accepts.length > 0 ? accepts.map((r) => ` - ${requirementLine(r)}`).join("\n") : " (no acceptable requirements advertised)";
183
+ const res = pr.resource?.description ?? pr.resource?.url;
184
+ return `x402 payment request. Accepted ways to pay:\n${opts}${res ? `\nfor: ${res}` : ""}`;
185
+ }
186
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT) {
187
+ const pp = p ?? {};
188
+ const acc = pp.accepted;
189
+ return `x402 signed payment authorization from ${pp.payload?.authorization?.from ?? "unknown payer"}${acc ? ` for ${requirementLine(acc)}` : ""}.`;
190
+ }
191
+ const sr = p ?? {};
192
+ return `x402 settlement receipt — ${sr.success ? "settled" : "failed"} (${sr.success ? `tx ${sr.transaction} on ${sr.network}` : `reason: ${sr.errorReason ?? "unknown"}`}).`;
193
+ }
194
+ /** The routing-guidance line: what the model should do, and the do-not-reply rule. */
195
+ function x402GuidanceLine(msg) {
196
+ const who = senderLabel(msg);
197
+ const note = verifiedNote(msg);
198
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT_REQUIRED) return `x402 quote from ${who} (${note}). To pay, call rine_pay with message_id ${msg.id} — the spend is bounded by your local policy (deny-by-default, caps). Do NOT send a text reply into this payment thread.`;
199
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT) return `x402 payment from ${who} (${note}). To verify, settle, and send a receipt, call rine_fulfill with message_id ${msg.id}. Do NOT send a text reply into this payment thread.`;
200
+ return `x402 receipt from ${who} (${note}). This is informational — no reply is expected.`;
201
+ }
202
+ /**
203
+ * D2 auto-pay attempt. Returns `null` when the quote was paid (no session needed),
204
+ * or a one-line decline note when auto-pay was refused or errored (the caller then
205
+ * surfaces the quote to the model). A policy refusal / above-threshold quote is a
206
+ * typed {@link X402Error}; anything else (network, wallet) is reported verbatim.
207
+ */
208
+ async function tryAutoPay(deps, msg) {
209
+ try {
210
+ await deps.client.payments.pay(msg, { autoPay: true });
211
+ return null;
212
+ } catch (err) {
213
+ if (err instanceof X402Error) return `[auto-pay declined (${err.code}): ${err.message}]`;
214
+ return `[auto-pay failed: ${err instanceof Error ? err.message : String(err)}]`;
215
+ }
216
+ }
217
+ /** Surface an x402 frame to the model without arming the prose auto-reply. */
218
+ async function surfaceX402(deps, msg, messageId, note) {
219
+ const replyTarget = msg.group_handle ?? msg.sender_handle ?? senderLabel(msg);
220
+ await deps.client.markDelivered([msg.id]).catch(() => {});
221
+ rememberSeen(deps.seen, messageId);
222
+ const context = [x402GuidanceLine(msg)];
223
+ if (note) context.push(note);
224
+ const token = encodeReplyToken(msg.conversation_id, replyTarget, void 0, { x402: true });
225
+ await deps.send({
226
+ message: renderX402Body(msg),
227
+ context
228
+ }, {
229
+ auth: null,
230
+ continuationToken: token
231
+ });
232
+ return new Response("ok", { status: 200 });
233
+ }
234
+ /**
235
+ * Handle a verified inbound x402 frame (D1). `payment_required` may auto-pay (D2)
236
+ * when enabled and at/below threshold; otherwise, and for `payment` / `receipt`,
237
+ * the frame is surfaced to the model as a payment-aware turn.
238
+ */
239
+ async function handleX402Inbound(deps, msg, messageId) {
240
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT_REQUIRED && deps.autoPay) {
241
+ const decline = await tryAutoPay(deps, msg);
242
+ if (decline === null) {
243
+ await deps.client.markDelivered([msg.id]).catch(() => {});
244
+ rememberSeen(deps.seen, messageId);
245
+ return new Response("auto-paid", { status: 200 });
246
+ }
247
+ return surfaceX402(deps, msg, messageId, decline);
248
+ }
249
+ return surfaceX402(deps, msg, messageId, null);
250
+ }
251
+ //#endregion
121
252
  //#region src/channel-core.ts
122
253
  /**
123
- * Pure inbound/outbound pipeline for the rine channel — no `eve` import, so it is
124
- * unit-testable with fake clients/sends. `channel.ts` wires these to Eve's
125
- * `defineChannel` routes + events.
254
+ * INBOUND half of the pure rine channel pipeline — no `eve` import, so it is
255
+ * unit-testable with fake clients/sends. `channel.ts` wires this to Eve's
256
+ * `defineChannel` route; the OUTBOUND half (`processCompletion`/`processFailure`)
257
+ * lives in `channel-outbound.ts`.
126
258
  *
127
259
  * - INBOUND (`processInbound`): HMAC-verify (transport auth, R5) → parse → dedupe
128
260
  * → `client.read(id)` to HPKE-decrypt + verify the sender's Ed25519 signature
129
261
  * (content auth, R5) → loop-guard → mark delivered → start/resume the session.
130
- * - OUTBOUND (`processCompletion`): on a terminal assistant message (R6) reply
131
- * back over rine IN-PLACE via the reply endpoint, preserving the inbound
132
- * conversation (R7); falls back to `parentConversationId` for legacy tokens.
133
262
  *
134
263
  * TRUST NOTE: `verified === true` cryptographically binds the message's *signer*
135
264
  * (the envelope `kid`), but the SDK exposes only the server-asserted
@@ -138,7 +267,6 @@ function messageIdFromWebhook(body) {
138
267
  * verified signer needs an SDK change to surface `senderKid`; tracked as a
139
268
  * follow-up. Matches the sibling rine connectors' behavior.
140
269
  */
141
- const SEEN_CAP = 5e3;
142
270
  /** The line of context handed to the model so it knows who/where the message is from. */
143
271
  function senderContextLine(msg) {
144
272
  const where = msg.group_handle ? ` in group ${msg.group_handle}` : "";
@@ -173,17 +301,15 @@ async function processInbound(deps) {
173
301
  if (msg.decrypt_error) return new Response("undecryptable", { status: 422 });
174
302
  if (!msg.verified && !deps.acceptUnverified) return new Response("unverified sender signature", { status: 422 });
175
303
  if (deps.ignoreTypes.includes(msg.type)) {
176
- deps.seen?.add(messageId);
304
+ rememberSeen(deps.seen, messageId);
177
305
  return new Response("ignored type", { status: 202 });
178
306
  }
307
+ if (isX402Frame(msg.type)) return handleX402Inbound(deps, msg, messageId);
179
308
  const replyTarget = msg.group_handle ?? msg.sender_handle;
180
309
  if (!replyTarget) return new Response("no reply target", { status: 422 });
181
310
  const token = msg.group_handle ? encodeReplyToken(msg.conversation_id, replyTarget) : encodeReplyToken(msg.conversation_id, replyTarget, msg.id);
182
311
  await deps.client.markDelivered([msg.id]).catch(() => {});
183
- if (deps.seen) {
184
- if (deps.seen.size >= SEEN_CAP) deps.seen.clear();
185
- deps.seen.add(messageId);
186
- }
312
+ rememberSeen(deps.seen, messageId);
187
313
  const context = [senderContextLine(msg)];
188
314
  if (deps.threadContextTokenBudget != null) {
189
315
  const priorTurns = (await deps.client.thread(msg.conversation_id, { limit: THREAD_FETCH_LIMIT }).catch(() => [])).slice(0, -1);
@@ -198,6 +324,19 @@ async function processInbound(deps) {
198
324
  });
199
325
  return new Response("ok", { status: 200 });
200
326
  }
327
+ //#endregion
328
+ //#region src/channel-outbound.ts
329
+ /**
330
+ * OUTBOUND half of the pure rine channel pipeline — no `eve` import, so it is
331
+ * unit-testable with fake clients. `channel.ts` wires these to Eve's
332
+ * `message.completed` / failure events; the INBOUND half lives in `channel-core.ts`.
333
+ *
334
+ * On a terminal assistant message (R6) reply back over rine IN-PLACE via the reply
335
+ * endpoint, preserving the inbound conversation (R7); falls back to
336
+ * `parentConversationId` for legacy tokens. A session started by an x402 frame never
337
+ * prose-replies into the payment thread (the pay / fulfill tools transmit the signed
338
+ * frame in-thread instead) — the `x402` continuation-token flag short-circuits both.
339
+ */
201
340
  /**
202
341
  * Outbound for `message.completed`: every TERMINAL assistant message (any
203
342
  * `finishReason` except the intermediate `tool-calls`, R6) with text becomes a
@@ -213,6 +352,7 @@ async function processCompletion(deps) {
213
352
  if (!deps.message) return;
214
353
  const rc = decodeReplyToken(deps.continuationToken);
215
354
  if (!rc) return;
355
+ if (rc.x402) return;
216
356
  if (rc.messageId) {
217
357
  await deps.client.reply(asMessageUuid(rc.messageId), { text: deps.message }, { type: deps.replyMessageType });
218
358
  return;
@@ -230,6 +370,7 @@ async function processCompletion(deps) {
230
370
  async function processFailure(continuationToken, client) {
231
371
  const rc = decodeReplyToken(continuationToken);
232
372
  if (!rc) return;
373
+ if (rc.x402) return;
233
374
  const text = "Sorry — I hit an internal error handling your request.";
234
375
  if (rc.messageId) {
235
376
  await client.reply(asMessageUuid(rc.messageId), { text }, { type: "rine.v1.error" });
@@ -247,12 +388,25 @@ const DEFAULT_REPLY_TYPE = "rine.v1.task_response";
247
388
  const DEFAULT_MAX_BODY_BYTES = 512 * 1024;
248
389
  /** Default token budget for the push-injected transcript (REQ-CTX-01, ≈8000 chars). */
249
390
  const DEFAULT_THREAD_CONTEXT_TOKEN_BUDGET = 2e3;
250
- /** Message types the connector itself emits — never auto-replied to (loop guard, R2). */
391
+ /**
392
+ * Message types never auto-replied to (loop guard, R2): the connector's OWN
393
+ * outbound types (task_response/error/receipt), so two rine-eve agents don't
394
+ * auto-reply to each other forever.
395
+ *
396
+ * The three x402 payment frames are NOT here: v1.1 re-surfaces them as
397
+ * payment-aware agent turns (D1), routed by `handleX402Inbound` with the prose
398
+ * auto-reply suppressed (the `x402` continuation-token flag), never by the generic
399
+ * assistant reply. Adding them here would revert to the v1.0 silent-drop behavior.
400
+ */
251
401
  const DEFAULT_IGNORE_TYPES = [
252
402
  "rine.v1.task_response",
253
403
  "rine.v1.error",
254
404
  "rine.v1.receipt"
255
405
  ];
406
+ /** Whether an env flag is truthy (`1` / `true`, case-insensitive). */
407
+ function envFlag(value) {
408
+ return value === "1" || value?.toLowerCase() === "true";
409
+ }
256
410
  /** Build the rine channel. Default-export the result from `agent/channels/rine.ts`. */
257
411
  function rineChannel(opts = {}) {
258
412
  const path = opts.path ?? process.env.RINE_INBOUND_PATH ?? DEFAULT_INBOUND_PATH;
@@ -261,6 +415,7 @@ function rineChannel(opts = {}) {
261
415
  const acceptUnverified = opts.acceptUnverified ?? false;
262
416
  const ignoreTypes = opts.ignoreTypes ?? DEFAULT_IGNORE_TYPES;
263
417
  const threadContextTokenBudget = opts.threadContextTokenBudget ?? DEFAULT_THREAD_CONTEXT_TOKEN_BUDGET;
418
+ const autoPay = opts.payments?.autoPay ?? envFlag(process.env.RINE_X402_AUTO_PAY);
264
419
  const seen = /* @__PURE__ */ new Set();
265
420
  const client = () => opts.client ?? getRineClient({
266
421
  agent: opts.agent ?? process.env.RINE_AGENT,
@@ -278,7 +433,8 @@ function rineChannel(opts = {}) {
278
433
  maxBodyBytes,
279
434
  ignoreTypes,
280
435
  seen,
281
- threadContextTokenBudget
436
+ threadContextTokenBudget,
437
+ autoPay
282
438
  }))],
283
439
  events: {
284
440
  "message.completed": async (data, channel) => {
@@ -301,4 +457,4 @@ function rineChannel(opts = {}) {
301
457
  });
302
458
  }
303
459
  //#endregion
304
- export { senderContextLine as a, messageIdFromWebhook as c, processInbound as i, processCompletion as n, decodeReplyToken as o, processFailure as r, encodeReplyToken as s, rineChannel as t };
460
+ export { processInbound as a, handleX402Inbound as c, x402GuidanceLine as d, decodeReplyToken as f, processFailure as i, isX402Frame as l, messageIdFromWebhook as m, rineChannel as n, senderContextLine as o, encodeReplyToken as p, processCompletion as r, X402_FRAME_TYPES as s, DEFAULT_IGNORE_TYPES as t, renderX402Body as u };
@@ -1,14 +1,12 @@
1
1
  /**
2
- * Pure inbound/outbound pipeline for the rine channel — no `eve` import, so it is
3
- * unit-testable with fake clients/sends. `channel.ts` wires these to Eve's
4
- * `defineChannel` routes + events.
2
+ * INBOUND half of the pure rine channel pipeline — no `eve` import, so it is
3
+ * unit-testable with fake clients/sends. `channel.ts` wires this to Eve's
4
+ * `defineChannel` route; the OUTBOUND half (`processCompletion`/`processFailure`)
5
+ * lives in `channel-outbound.ts`.
5
6
  *
6
7
  * - INBOUND (`processInbound`): HMAC-verify (transport auth, R5) → parse → dedupe
7
8
  * → `client.read(id)` to HPKE-decrypt + verify the sender's Ed25519 signature
8
9
  * (content auth, R5) → loop-guard → mark delivered → start/resume the session.
9
- * - OUTBOUND (`processCompletion`): on a terminal assistant message (R6) reply
10
- * back over rine IN-PLACE via the reply endpoint, preserving the inbound
11
- * conversation (R7); falls back to `parentConversationId` for legacy tokens.
12
10
  *
13
11
  * TRUST NOTE: `verified === true` cryptographically binds the message's *signer*
14
12
  * (the envelope `kid`), but the SDK exposes only the server-asserted
@@ -42,6 +40,13 @@ export interface InboundDeps {
42
40
  maxBodyBytes: number;
43
41
  /** Inbound types to ignore (loop guard). */
44
42
  ignoreTypes: readonly string[];
43
+ /**
44
+ * D2 opt-in auto-pay (default OFF). When true, an inbound `x402_payment_required`
45
+ * at/below the policy's `autoPayThreshold` is paid with NO LLM turn — still
46
+ * bounded by the caps, deny-by-default, journal and reserve-lock. Any decline
47
+ * falls back to surfacing the quote to the model.
48
+ */
49
+ autoPay?: boolean;
45
50
  /** Bounded set of already-handled message ids (dedupe across retries/replays). */
46
51
  seen?: Set<string>;
47
52
  /**
@@ -56,27 +61,3 @@ export interface InboundDeps {
56
61
  * `4xx` = terminal drop, `2xx` = accepted/benign-ignore.
57
62
  */
58
63
  export declare function processInbound(deps: InboundDeps): Promise<Response>;
59
- export interface CompletionDeps {
60
- finishReason: string;
61
- message: string | null;
62
- continuationToken: string;
63
- client: AsyncRineClient;
64
- replyMessageType: string;
65
- }
66
- /**
67
- * Outbound for `message.completed`: every TERMINAL assistant message (any
68
- * `finishReason` except the intermediate `tool-calls`, R6) with text becomes a
69
- * reply. When the continuation token carries the inbound message id we reply
70
- * IN-PLACE via the reply endpoint (same conversation, no fork) keeping the
71
- * connector's `replyMessageType` unchanged — type-preserving, so terminal/wake
72
- * semantics are exactly as before. Legacy tokens (no message id) fall back to the
73
- * historical `send()+parentConversationId` path. No-ops for non-rine sessions
74
- * (token not ours) or empty messages.
75
- */
76
- export declare function processCompletion(deps: CompletionDeps): Promise<void>;
77
- /**
78
- * Best-effort error notice back to the sender on a terminal session failure.
79
- * Replies IN-PLACE (reply endpoint, same conversation) keeping the `rine.v1.error`
80
- * type; falls back to `send()+parentConversationId` for legacy tokens.
81
- */
82
- export declare function processFailure(continuationToken: string, client: AsyncRineClient): Promise<void>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OUTBOUND half of the pure rine channel pipeline — no `eve` import, so it is
3
+ * unit-testable with fake clients. `channel.ts` wires these to Eve's
4
+ * `message.completed` / failure events; the INBOUND half lives in `channel-core.ts`.
5
+ *
6
+ * On a terminal assistant message (R6) reply back over rine IN-PLACE via the reply
7
+ * endpoint, preserving the inbound conversation (R7); falls back to
8
+ * `parentConversationId` for legacy tokens. A session started by an x402 frame never
9
+ * prose-replies into the payment thread (the pay / fulfill tools transmit the signed
10
+ * frame in-thread instead) — the `x402` continuation-token flag short-circuits both.
11
+ */
12
+ import type { AsyncRineClient } from "@rine-network/sdk";
13
+ export interface CompletionDeps {
14
+ finishReason: string;
15
+ message: string | null;
16
+ continuationToken: string;
17
+ client: AsyncRineClient;
18
+ replyMessageType: string;
19
+ }
20
+ /**
21
+ * Outbound for `message.completed`: every TERMINAL assistant message (any
22
+ * `finishReason` except the intermediate `tool-calls`, R6) with text becomes a
23
+ * reply. When the continuation token carries the inbound message id we reply
24
+ * IN-PLACE via the reply endpoint (same conversation, no fork) keeping the
25
+ * connector's `replyMessageType` unchanged — type-preserving, so terminal/wake
26
+ * semantics are exactly as before. Legacy tokens (no message id) fall back to the
27
+ * historical `send()+parentConversationId` path. No-ops for non-rine sessions
28
+ * (token not ours) or empty messages.
29
+ */
30
+ export declare function processCompletion(deps: CompletionDeps): Promise<void>;
31
+ /**
32
+ * Best-effort error notice back to the sender on a terminal session failure.
33
+ * Replies IN-PLACE (reply endpoint, same conversation) keeping the `rine.v1.error`
34
+ * type; falls back to `send()+parentConversationId` for legacy tokens.
35
+ */
36
+ export declare function processFailure(continuationToken: string, client: AsyncRineClient): Promise<void>;
package/dist/channel.d.ts CHANGED
@@ -10,8 +10,21 @@
10
10
  * `channel-core.ts` (pure, testable); this file is the `defineChannel` wiring.
11
11
  */
12
12
  import type { AsyncRineClient } from "@rine-network/sdk";
13
- export { processInbound, processCompletion, processFailure, senderContextLine, } from "./channel-core.js";
14
- export type { ChannelSend, InboundDeps, CompletionDeps } from "./channel-core.js";
13
+ export { processInbound, senderContextLine } from "./channel-core.js";
14
+ export { processCompletion, processFailure } from "./channel-outbound.js";
15
+ export type { ChannelSend, InboundDeps } from "./channel-core.js";
16
+ export type { CompletionDeps } from "./channel-outbound.js";
17
+ /**
18
+ * Message types never auto-replied to (loop guard, R2): the connector's OWN
19
+ * outbound types (task_response/error/receipt), so two rine-eve agents don't
20
+ * auto-reply to each other forever.
21
+ *
22
+ * The three x402 payment frames are NOT here: v1.1 re-surfaces them as
23
+ * payment-aware agent turns (D1), routed by `handleX402Inbound` with the prose
24
+ * auto-reply suppressed (the `x402` continuation-token flag), never by the generic
25
+ * assistant reply. Adding them here would revert to the v1.0 silent-drop behavior.
26
+ */
27
+ export declare const DEFAULT_IGNORE_TYPES: string[];
15
28
  /** Options for {@link rineChannel}. All optional; identity defaults to env. */
16
29
  export interface RineChannelOptions {
17
30
  /** Acting agent (handle/name/UUID); defaults to `process.env.RINE_AGENT`. */
@@ -41,6 +54,16 @@ export interface RineChannelOptions {
41
54
  threadContextTokenBudget?: number;
42
55
  /** A pre-built client (tests / advanced use); otherwise env-resolved lazily. */
43
56
  client?: AsyncRineClient;
57
+ /**
58
+ * x402 payment behavior (D2). `autoPay` is OPT-IN and DEFAULT OFF: when true, an
59
+ * inbound `x402_payment_required` at/below the policy's `autoPayThreshold` is paid
60
+ * with no LLM turn (still bounded by the caps, deny-by-default, journal and
61
+ * reserve-lock). Off ⇒ every quote is surfaced to the agent, which pays via
62
+ * `rine_pay`. Falls back to `RINE_X402_AUTO_PAY` (`1`/`true`) when unset.
63
+ */
64
+ payments?: {
65
+ autoPay?: boolean;
66
+ };
44
67
  }
45
68
  /** Build the rine channel. Default-export the result from `agent/channels/rine.ts`. */
46
69
  export declare function rineChannel(opts?: RineChannelOptions): import("eve/channels").Channel<undefined, Record<string, unknown>, Record<string, unknown>>;
package/dist/channel.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as senderContextLine, i as processInbound, n as processCompletion, r as processFailure, t as rineChannel } from "./channel-Bg8l58gg.js";
2
- export { processCompletion, processFailure, processInbound, rineChannel, senderContextLine };
1
+ import { a as processInbound, i as processFailure, n as rineChannel, o as senderContextLine, r as processCompletion, t as DEFAULT_IGNORE_TYPES } from "./channel-b0iIAZTh.js";
2
+ export { DEFAULT_IGNORE_TYPES, processCompletion, processFailure, processInbound, rineChannel, senderContextLine };
package/dist/inbound.d.ts CHANGED
@@ -14,6 +14,15 @@
14
14
  * build carry only `{c,r}` and decode with `messageId === undefined`, so the outbound
15
15
  * handler falls back to the `send()+parentConversationId` broadcast path.
16
16
  */
17
+ /** Bound on the per-runtime `seen` dedupe set before it is cleared wholesale. */
18
+ export declare const SEEN_CAP = 5000;
19
+ /**
20
+ * Record a handled message id in the bounded per-runtime dedupe set. The set is
21
+ * cleared wholesale when it reaches {@link SEEN_CAP} (a coarse but allocation-free
22
+ * eviction — the server-side `delivered_at` ack is the durable dedupe). A no-op
23
+ * when no set is provided.
24
+ */
25
+ export declare function rememberSeen(seen: Set<string> | undefined, id: string): void;
17
26
  /** What a decoded continuation token yields. */
18
27
  export interface ReplyContext {
19
28
  /** The rine conversation id to thread the reply into. */
@@ -25,13 +34,23 @@ export interface ReplyContext {
25
34
  * Absent for legacy 2-field tokens → caller falls back to send()+parent.
26
35
  */
27
36
  readonly messageId?: string;
37
+ /**
38
+ * Marks a session started by an inbound x402 payment frame (D1). The terminal
39
+ * assistant message is NEVER prose-replied into a payment thread — that would
40
+ * corrupt the handshake. The signed payment / receipt is transmitted in-thread
41
+ * by the pay / fulfill tools instead, so `processCompletion` short-circuits when
42
+ * this is set.
43
+ */
44
+ readonly x402?: boolean;
28
45
  }
29
46
  /**
30
47
  * Encode the raw channel-local continuation token. Eve prepends the channel name
31
48
  * (`rine:`); we additionally fence our payload with {@link TOKEN_MARKER} so the
32
49
  * decoder can recover it regardless of any prefix the framework adds.
33
50
  */
34
- export declare function encodeReplyToken(conversationId: string, replyTarget: string, messageId?: string): string;
51
+ export declare function encodeReplyToken(conversationId: string, replyTarget: string, messageId?: string, opts?: {
52
+ x402?: boolean;
53
+ }): string;
35
54
  /**
36
55
  * Decode a continuation token back to its {@link ReplyContext}. Tolerant of a
37
56
  * leading `<channel>:` namespace (or any prefix) and of malformed input —
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  export { rineChannel, processInbound, processCompletion, processFailure, senderContextLine, } from "./channel.js";
17
17
  export type { RineChannelOptions, ChannelSend, InboundDeps, CompletionDeps, } from "./channel.js";
18
18
  export * from "./tools/index.js";
19
+ export { isX402Frame, handleX402Inbound, renderX402Body, x402GuidanceLine, X402_FRAME_TYPES, } from "./x402.js";
19
20
  export { rineSkill, RINE_SKILL_BODY, RINE_SKILL_FILE, RINE_SKILL_DESCRIPTION, } from "./skill.js";
20
21
  export { getRineClient } from "./client.js";
21
22
  export type { RineClientOpts } from "./client.js";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { o as formatError } from "./tool-BC49DldZ.js";
2
- import { a as senderContextLine, c as messageIdFromWebhook, i as processInbound, n as processCompletion, o as decodeReplyToken, r as processFailure, s as encodeReplyToken, t as rineChannel } from "./channel-Bg8l58gg.js";
2
+ import { a as processInbound, c as handleX402Inbound, d as x402GuidanceLine, f as decodeReplyToken, i as processFailure, l as isX402Frame, m as messageIdFromWebhook, n as rineChannel, o as senderContextLine, p as encodeReplyToken, r as processCompletion, s as X402_FRAME_TYPES, u as renderX402Body } from "./channel-b0iIAZTh.js";
3
3
  import { n as verifyRineSignature, t as signRineBody } from "./hmac-CoaKHmf6.js";
4
4
  import { t as getRineClient } from "./client-X_-9CpQT.js";
5
- import { a as rineSendAndWaitTool, c as rineGroupCreateTool, d as rineGroupRemoveTool, f as rineDiscoverTool, i as rineReplyTool, l as rineGroupInspectTool, n as rineCheckInboxTool, o as rineSendTool, p as rineInspectTool, r as rineReadTool, s as rineThreadTool, t as RINE_TOOLS, u as rineGroupInviteTool } from "./tools-Bm9N6tB5.js";
6
- import { t as RINE_TOOL_META } from "./registry-BG7S2XJg.js";
7
- import { a as toolFileContent, c as RINE_SKILL_FILE, i as scaffoldRine, n as envExampleBlock, o as RINE_SKILL_BODY, r as resolveToolSelection, s as RINE_SKILL_DESCRIPTION, t as channelFileContent } from "./scaffold-Dpac1TMU.js";
5
+ import { a as rineReadTool, c as rineSendTool, d as rineGroupInspectTool, f as rineGroupInviteTool, h as rineInspectTool, i as rineCheckInboxTool, l as rineThreadTool, m as rineDiscoverTool, n as rineFulfillTool, o as rineReplyTool, p as rineGroupRemoveTool, r as rinePayTool, s as rineSendAndWaitTool, t as RINE_TOOLS, u as rineGroupCreateTool } from "./tools-BcVm_Onx.js";
6
+ import { t as RINE_TOOL_META } from "./registry-6sWyhOyF.js";
7
+ import { a as toolFileContent, c as RINE_SKILL_FILE, i as scaffoldRine, n as envExampleBlock, o as RINE_SKILL_BODY, r as resolveToolSelection, s as RINE_SKILL_DESCRIPTION, t as channelFileContent } from "./scaffold-0luQyBRK.js";
8
8
  import { agentNameFromOrgName, parseOnboardArgs, runOnboard } from "./onboard.js";
9
9
  import { deleteRineWebhook, registerRineWebhook } from "./webhook.js";
10
10
  import { buildRelayBody, drainOnce, runRelay } from "./relay.js";
@@ -24,4 +24,4 @@ function rineSkill() {
24
24
  });
25
25
  }
26
26
  //#endregion
27
- export { RINE_SKILL_BODY, RINE_SKILL_DESCRIPTION, RINE_SKILL_FILE, RINE_TOOLS, RINE_TOOL_META, agentNameFromOrgName, buildRelayBody, channelFileContent, decodeReplyToken, deleteRineWebhook, drainOnce, encodeReplyToken, envExampleBlock, formatError, getRineClient, messageIdFromWebhook, parseOnboardArgs, processCompletion, processFailure, processInbound, registerRineWebhook, resolveToolSelection, rineChannel, rineCheckInboxTool, rineDiscoverTool, rineGroupCreateTool, rineGroupInspectTool, rineGroupInviteTool, rineGroupRemoveTool, rineInspectTool, rineReadTool, rineReplyTool, rineSendAndWaitTool, rineSendTool, rineSkill, rineThreadTool, runOnboard, runRelay, scaffoldRine, senderContextLine, signRineBody, toolFileContent, verifyRineSignature };
27
+ export { RINE_SKILL_BODY, RINE_SKILL_DESCRIPTION, RINE_SKILL_FILE, RINE_TOOLS, RINE_TOOL_META, X402_FRAME_TYPES, agentNameFromOrgName, buildRelayBody, channelFileContent, decodeReplyToken, deleteRineWebhook, drainOnce, encodeReplyToken, envExampleBlock, formatError, getRineClient, handleX402Inbound, isX402Frame, messageIdFromWebhook, parseOnboardArgs, processCompletion, processFailure, processInbound, registerRineWebhook, renderX402Body, resolveToolSelection, rineChannel, rineCheckInboxTool, rineDiscoverTool, rineFulfillTool, rineGroupCreateTool, rineGroupInspectTool, rineGroupInviteTool, rineGroupRemoveTool, rineInspectTool, rinePayTool, rineReadTool, rineReplyTool, rineSendAndWaitTool, rineSendTool, rineSkill, rineThreadTool, runOnboard, runRelay, scaffoldRine, senderContextLine, signRineBody, toolFileContent, verifyRineSignature, x402GuidanceLine };
@@ -1,7 +1,8 @@
1
1
  //#region src/tools/registry.ts
2
2
  /**
3
3
  * The registry the scaffolder + docs read. Order is the canonical surface order
4
- * (messaging → discovery → groups). `name` === scaffolded filename === tool name.
4
+ * (messaging → discovery → groups → payments). `name` === scaffolded filename ===
5
+ * tool name.
5
6
  */
6
7
  const RINE_TOOL_META = [
7
8
  {
@@ -63,6 +64,16 @@ const RINE_TOOL_META = [
63
64
  name: "rine_group_inspect",
64
65
  factoryName: "rineGroupInspectTool",
65
66
  domain: "groups"
67
+ },
68
+ {
69
+ name: "rine_pay",
70
+ factoryName: "rinePayTool",
71
+ domain: "payments"
72
+ },
73
+ {
74
+ name: "rine_fulfill",
75
+ factoryName: "rineFulfillTool",
76
+ domain: "payments"
66
77
  }
67
78
  ];
68
79
  //#endregion
@@ -1,4 +1,4 @@
1
- import { t as RINE_TOOL_META } from "./registry-BG7S2XJg.js";
1
+ import { t as RINE_TOOL_META } from "./registry-6sWyhOyF.js";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  //#region src/skill-content.ts
@@ -86,6 +86,11 @@ RINE_AGENT=
86
86
  RINE_WEBHOOK_SECRET=
87
87
  # RINE_WEBHOOK_ID=
88
88
  # RINE_INBOUND_PATH=${path}
89
+ # x402 payments (optional). RINE_X402_AUTO_PAY=1 auto-pays quotes at/below your
90
+ # policy's autoPayThreshold with no LLM turn (default OFF). RINE_FACILITATOR is the
91
+ # rine_fulfill facilitator — a preset name (cdp/payai/x402-rs) or a base URL.
92
+ # RINE_X402_AUTO_PAY=
93
+ # RINE_FACILITATOR=
89
94
  `;
90
95
  }
91
96
  /**
package/dist/scaffold.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as toolFileContent, i as scaffoldRine, n as envExampleBlock, r as resolveToolSelection, t as channelFileContent } from "./scaffold-Dpac1TMU.js";
1
+ import { a as toolFileContent, i as scaffoldRine, n as envExampleBlock, r as resolveToolSelection, t as channelFileContent } from "./scaffold-0luQyBRK.js";
2
2
  export { channelFileContent, envExampleBlock, resolveToolSelection, scaffoldRine, toolFileContent };
@@ -20,9 +20,9 @@ export declare const groupCreateInput: z.ZodObject<{
20
20
  description?: string | undefined;
21
21
  }, {
22
22
  name: string;
23
+ description?: string | undefined;
23
24
  enrollment?: "open" | "closed" | "majority" | "unanimity" | undefined;
24
25
  visibility?: "public" | "private" | undefined;
25
- description?: string | undefined;
26
26
  enableMls?: boolean | undefined;
27
27
  }>;
28
28
  export declare const groupInviteInput: z.ZodObject<{
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Zod input schemas for the 2 x402 payment tools (split out of `schemas.ts` to
3
+ * hold the ~200-LOC budget — one file per domain). Same authoring rules: rich
4
+ * `.describe()` on every field, NO identity/credentials in the schema (env-injected,
5
+ * R3). The facilitator is infra config, resolved from the tool factory / env —
6
+ * never a model-visible input.
7
+ */
8
+ import { z } from "zod";
9
+ export declare const payInput: z.ZodObject<{
10
+ messageId: z.ZodString;
11
+ autoPay: z.ZodDefault<z.ZodBoolean>;
12
+ emitMarker: z.ZodDefault<z.ZodBoolean>;
13
+ allowRepay: z.ZodDefault<z.ZodBoolean>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ messageId: string;
16
+ autoPay: boolean;
17
+ emitMarker: boolean;
18
+ allowRepay: boolean;
19
+ }, {
20
+ messageId: string;
21
+ autoPay?: boolean | undefined;
22
+ emitMarker?: boolean | undefined;
23
+ allowRepay?: boolean | undefined;
24
+ }>;
25
+ export declare const fulfillInput: z.ZodObject<{
26
+ messageId: z.ZodString;
27
+ emitMarker: z.ZodDefault<z.ZodBoolean>;
28
+ }, "strip", z.ZodTypeAny, {
29
+ messageId: string;
30
+ emitMarker: boolean;
31
+ }, {
32
+ messageId: string;
33
+ emitMarker?: boolean | undefined;
34
+ }>;
package/dist/schemas.d.ts CHANGED
@@ -103,3 +103,4 @@ export declare const inspectInput: z.ZodObject<{
103
103
  handleOrId: string;
104
104
  }>;
105
105
  export { groupCreateInput, groupInspectInput, groupInviteInput, groupRemoveInput, } from "./schemas-groups.js";
106
+ export { fulfillInput, payInput } from "./schemas-payments.js";
package/dist/tool.d.ts CHANGED
@@ -32,6 +32,15 @@ export interface RineToolOpts extends RineClientOpts {
32
32
  * irreversible network actions. Ignored by read-only tools.
33
33
  */
34
34
  needsApproval?: "always" | "once" | "never";
35
+ /**
36
+ * Facilitator for the payee `rine_fulfill` tool: a preset name
37
+ * (`cdp` / `payai` / `x402-rs`) or an explicit base URL. Falls back to the
38
+ * `RINE_FACILITATOR` env var. Ignored by every other tool. Verify/settle is
39
+ * plain external HTTP to this facilitator — never a rine endpoint.
40
+ */
41
+ facilitator?: string;
42
+ /** Extra HTTP headers (provider auth, e.g. a CDP key) sent to the facilitator. */
43
+ facilitatorHeaders?: Record<string, string>;
35
44
  }
36
45
  /**
37
46
  * Resolve {@link RineToolOpts.needsApproval} to an Eve `needsApproval` callback,
@@ -12,6 +12,7 @@ import { type RineToolMeta } from "./registry.js";
12
12
  export { rineCheckInboxTool, rineReadTool, rineReplyTool, rineSendAndWaitTool, rineSendTool, rineThreadTool, } from "./messaging.js";
13
13
  export { rineDiscoverTool, rineInspectTool } from "./discovery.js";
14
14
  export { rineGroupCreateTool, rineGroupInspectTool, rineGroupInviteTool, rineGroupRemoveTool, } from "./groups.js";
15
+ export { rineFulfillTool, rinePayTool } from "./payments.js";
15
16
  export { RINE_TOOL_META, type RineToolMeta, type RineToolDomain, } from "./registry.js";
16
17
  export type { RineToolOpts } from "../tool.js";
17
18
  /** A rine tool factory: builds an Eve `defineTool` descriptor from options. */
@@ -1,3 +1,3 @@
1
- import { a as rineSendAndWaitTool, c as rineGroupCreateTool, d as rineGroupRemoveTool, f as rineDiscoverTool, i as rineReplyTool, l as rineGroupInspectTool, n as rineCheckInboxTool, o as rineSendTool, p as rineInspectTool, r as rineReadTool, s as rineThreadTool, t as RINE_TOOLS, u as rineGroupInviteTool } from "../tools-Bm9N6tB5.js";
2
- import { t as RINE_TOOL_META } from "../registry-BG7S2XJg.js";
3
- export { RINE_TOOLS, RINE_TOOL_META, rineCheckInboxTool, rineDiscoverTool, rineGroupCreateTool, rineGroupInspectTool, rineGroupInviteTool, rineGroupRemoveTool, rineInspectTool, rineReadTool, rineReplyTool, rineSendAndWaitTool, rineSendTool, rineThreadTool };
1
+ import { a as rineReadTool, c as rineSendTool, d as rineGroupInspectTool, f as rineGroupInviteTool, h as rineInspectTool, i as rineCheckInboxTool, l as rineThreadTool, m as rineDiscoverTool, n as rineFulfillTool, o as rineReplyTool, p as rineGroupRemoveTool, r as rinePayTool, s as rineSendAndWaitTool, t as RINE_TOOLS, u as rineGroupCreateTool } from "../tools-BcVm_Onx.js";
2
+ import { t as RINE_TOOL_META } from "../registry-6sWyhOyF.js";
3
+ export { RINE_TOOLS, RINE_TOOL_META, rineCheckInboxTool, rineDiscoverTool, rineFulfillTool, rineGroupCreateTool, rineGroupInspectTool, rineGroupInviteTool, rineGroupRemoveTool, rineInspectTool, rinePayTool, rineReadTool, rineReplyTool, rineSendAndWaitTool, rineSendTool, rineThreadTool };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The 2 x402 payment tool factories: `rine_pay` (payer) and `rine_fulfill`
3
+ * (payee). Thin adapters over the ts-sdk `client.payments` facade — signing,
4
+ * spend policy, journal, and facilitator verify/settle all live in rine-core; the
5
+ * wallet key is never surfaced. Each returns an Eve `defineTool` descriptor whose
6
+ * `execute` resolves to a TYPED STATUS STRING (never a reject for an expected
7
+ * refusal), so the agent reasons over the outcome.
8
+ *
9
+ * `rine_pay` reuses the shipped `rine_pay` MCP status vocabulary VERBATIM:
10
+ * `payment-submitted` / `no-wallet` / `not-payment-required` / `policy-refused` /
11
+ * `above-auto-pay-threshold` / `already-paid` / `wallet-busy`. `rine_fulfill`
12
+ * reports the PINNED payee vocabulary (`settled` / `settlement-failed` /
13
+ * `verification-failed` / `facilitator-error` / `no-facilitator` / `not-payment`),
14
+ * identical across every surface (CLI/MCP/eve/mastra), with the facilitator's
15
+ * network slug stored VERBATIM (never CAIP-2 string-matched).
16
+ */
17
+ import { type RineToolOpts } from "../tool.js";
18
+ /** `rine_pay` — pay a received x402 quote in-thread (payer). */
19
+ export declare function rinePayTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;
20
+ /** `rine_fulfill` — verify + settle a received payment and send the receipt (payee). */
21
+ export declare function rineFulfillTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;
@@ -6,7 +6,7 @@
6
6
  * attaches the live factories to this metadata for programmatic use.
7
7
  */
8
8
  /** The canonical domains a rine tool belongs to (drives `init --tools`). */
9
- export type RineToolDomain = "messaging" | "discovery" | "groups";
9
+ export type RineToolDomain = "messaging" | "discovery" | "groups" | "payments";
10
10
  /** Scaffold metadata for one tool: canonical name, export name, domain. */
11
11
  export interface RineToolMeta {
12
12
  /** Filename slug + runtime tool name (e.g. `rine_send`). */
@@ -17,6 +17,7 @@ export interface RineToolMeta {
17
17
  }
18
18
  /**
19
19
  * The registry the scaffolder + docs read. Order is the canonical surface order
20
- * (messaging → discovery → groups). `name` === scaffolded filename === tool name.
20
+ * (messaging → discovery → groups → payments). `name` === scaffolded filename ===
21
+ * tool name.
21
22
  */
22
23
  export declare const RINE_TOOL_META: readonly RineToolMeta[];
@@ -1,6 +1,6 @@
1
1
  import { a as GROUP_ON_WAIT_MESSAGE, c as groupIsMls, d as renderInbox, f as renderMessageBody, h as renderThread, i as redactToText, l as renderDiscover, m as renderSingleMessage, n as asRecipient, p as renderProfile, r as makeExecute, s as isGroupUnsupportedOnWait, t as approvalGate, u as renderGroup, v as verifiedNote } from "./tool-BC49DldZ.js";
2
- import { t as RINE_TOOL_META } from "./registry-BG7S2XJg.js";
3
- import { NotFoundError, asAgentUuid, asGroupUuid, asMessageUuid } from "@rine-network/sdk";
2
+ import { t as RINE_TOOL_META } from "./registry-6sWyhOyF.js";
3
+ import { FACILITATOR_PRESET, NotFoundError, X402Error, X402FacilitatorError, X402_ERROR, X402_MESSAGE_TYPE, asAgentUuid, asGroupUuid, asMessageUuid } from "@rine-network/sdk";
4
4
  import { UUID_RE, normalizeHandle, resolveToUuid } from "@rine-network/core";
5
5
  import { z } from "zod";
6
6
  import { defineTool } from "eve/tools";
@@ -48,6 +48,25 @@ const groupRemoveInput = z.object({
48
48
  });
49
49
  const groupInspectInput = z.object({ group: z.string().describe("The group to inspect: a handle (`#name@org`) or a UUID. Reports its E2EE mode and policy.") });
50
50
  //#endregion
51
+ //#region src/schemas-payments.ts
52
+ /**
53
+ * Zod input schemas for the 2 x402 payment tools (split out of `schemas.ts` to
54
+ * hold the ~200-LOC budget — one file per domain). Same authoring rules: rich
55
+ * `.describe()` on every field, NO identity/credentials in the schema (env-injected,
56
+ * R3). The facilitator is infra config, resolved from the tool factory / env —
57
+ * never a model-visible input.
58
+ */
59
+ const payInput = z.object({
60
+ messageId: z.string().describe("The UUID of the received `rine.v1.x402_payment_required` quote to pay. The signed payment is sent back in the same conversation."),
61
+ autoPay: z.boolean().default(false).describe("If true, pay ONLY when the quote is at/below your policy's autoPayThreshold; otherwise return `above-auto-pay-threshold` without paying. Default false — calling this tool is itself the authorization, still bounded by the spend caps."),
62
+ emitMarker: z.boolean().default(true).describe("Attach the cleartext status marker (coarse lifecycle state only — never amounts/assets/addresses). Default true; set false for a fully-sealed payment."),
63
+ allowRepay: z.boolean().default(false).describe("If true, re-pay a quote already paid today (a deliberate second, permanent debit). Default false — a repeat pay of the same messageId returns `already-paid` without spending.")
64
+ });
65
+ const fulfillInput = z.object({
66
+ messageId: z.string().describe("The UUID of the received `rine.v1.x402_payment` (signed authorization) to fulfill. Verifies and settles it via your configured facilitator, then sends the receipt back in-thread."),
67
+ emitMarker: z.boolean().default(true).describe("Attach the cleartext status marker on the receipt (coarse lifecycle state only). Default true.")
68
+ });
69
+ //#endregion
51
70
  //#region src/schemas.ts
52
71
  /**
53
72
  * Shared Zod input schemas for the 12 rine tools.
@@ -345,6 +364,151 @@ function rineReplyTool(opts = {}) {
345
364
  });
346
365
  }
347
366
  //#endregion
367
+ //#region src/tools/payments.ts
368
+ /**
369
+ * The 2 x402 payment tool factories: `rine_pay` (payer) and `rine_fulfill`
370
+ * (payee). Thin adapters over the ts-sdk `client.payments` facade — signing,
371
+ * spend policy, journal, and facilitator verify/settle all live in rine-core; the
372
+ * wallet key is never surfaced. Each returns an Eve `defineTool` descriptor whose
373
+ * `execute` resolves to a TYPED STATUS STRING (never a reject for an expected
374
+ * refusal), so the agent reasons over the outcome.
375
+ *
376
+ * `rine_pay` reuses the shipped `rine_pay` MCP status vocabulary VERBATIM:
377
+ * `payment-submitted` / `no-wallet` / `not-payment-required` / `policy-refused` /
378
+ * `above-auto-pay-threshold` / `already-paid` / `wallet-busy`. `rine_fulfill`
379
+ * reports the PINNED payee vocabulary (`settled` / `settlement-failed` /
380
+ * `verification-failed` / `facilitator-error` / `no-facilitator` / `not-payment`),
381
+ * identical across every surface (CLI/MCP/eve/mastra), with the facilitator's
382
+ * network slug stored VERBATIM (never CAIP-2 string-matched).
383
+ */
384
+ /** The `rine_pay` terminal statuses, reused verbatim from the MCP payer tool. */
385
+ const PAY_STATUS = {
386
+ SUBMITTED: "payment-submitted",
387
+ NO_WALLET: "no-wallet",
388
+ NOT_PAYMENT_REQUIRED: "not-payment-required",
389
+ POLICY_REFUSED: "policy-refused",
390
+ ABOVE_AUTO_PAY_THRESHOLD: "above-auto-pay-threshold",
391
+ ALREADY_PAID: "already-paid",
392
+ WALLET_BUSY: "wallet-busy"
393
+ };
394
+ /**
395
+ * The `rine_fulfill` payee statuses — PINNED across every surface (CLI/MCP/eve/
396
+ * mastra) so an LLM or operator script parses one vocabulary everywhere. Mirrors
397
+ * the MCP `rine_fulfill` reference set; eve emits the subset it can reach (the
398
+ * ts-sdk `fulfill` facade owns decryption, so `no-keys` never surfaces here).
399
+ */
400
+ const FULFILL_STATUS = {
401
+ SETTLED: "settled",
402
+ SETTLEMENT_FAILED: "settlement-failed",
403
+ VERIFICATION_FAILED: "verification-failed",
404
+ FACILITATOR_ERROR: "facilitator-error",
405
+ NO_FACILITATOR: "no-facilitator",
406
+ NOT_PAYMENT: "not-payment"
407
+ };
408
+ /** `<status> — <detail>`: the stable, parseable typed-status line. */
409
+ function status(word, detail) {
410
+ return `${word} — ${detail}`;
411
+ }
412
+ /** Atomic-unit amount of a requirement (x402 V2 `amount`, else V1 spelling). */
413
+ function requirementSummary(r) {
414
+ return `${r.amount ?? r.maxAmountRequired ?? "?"} of ${r.asset} on ${r.network} → ${r.payTo}`;
415
+ }
416
+ /** Map an x402 pay refusal to its typed status line (parity with MCP rine_pay). */
417
+ function mapPayError(err, autoPay) {
418
+ if (err instanceof X402Error) switch (err.code) {
419
+ case X402_ERROR.ALREADY_PAID: return status(PAY_STATUS.ALREADY_PAID, `${err.message}. Pass allowRepay to pay it again.`);
420
+ case X402_ERROR.WALLET_BUSY: return status(PAY_STATUS.WALLET_BUSY, err.message);
421
+ case X402_ERROR.PER_TX_CAP_EXCEEDED:
422
+ if (autoPay) return status(PAY_STATUS.ABOVE_AUTO_PAY_THRESHOLD, `${err.message}. Re-run without autoPay to authorize explicitly.`);
423
+ return status(PAY_STATUS.POLICY_REFUSED, err.message);
424
+ default: return status(PAY_STATUS.POLICY_REFUSED, err.message);
425
+ }
426
+ throw err;
427
+ }
428
+ /** `rine_pay` — pay a received x402 quote in-thread (payer). */
429
+ function rinePayTool(opts = {}) {
430
+ return defineTool({
431
+ description: "Pay a received x402 payment request (rine.v1.x402_payment_required) in-thread. Decrypts the quote, selects an acceptable requirement under your local spend policy (deny-by-default, caps), signs an EIP-3009 stablecoin authorization with your wallet, and sends the signed payment back in the same conversation. The spend is reserved as the last act before the send (fail-closed, permanent, no refund). Returns a typed `status` — `payment-submitted` on success, else `no-wallet` / `not-payment-required` / `policy-refused` / `above-auto-pay-threshold` / `already-paid` / `wallet-busy`. Does NOT wait for settlement; the receipt arrives later as an ordinary inbox message.",
432
+ inputSchema: jsonSchema(payInput),
433
+ outputSchema: STRING_OUTPUT,
434
+ needsApproval: approvalGate(opts),
435
+ execute: makeExecute(payInput, opts, async (client, i) => {
436
+ const msg = await client.read(asMessageUuid(i.messageId));
437
+ if (msg.type !== X402_MESSAGE_TYPE.PAYMENT_REQUIRED) return status(PAY_STATUS.NOT_PAYMENT_REQUIRED, `message ${i.messageId} is type '${msg.type}', not an x402 payment request.`);
438
+ try {
439
+ await client.payments.walletAddress();
440
+ } catch {
441
+ return status(PAY_STATUS.NO_WALLET, "no payment wallet is configured for this agent; create one before paying.");
442
+ }
443
+ try {
444
+ const res = await client.payments.pay(msg, {
445
+ autoPay: i.autoPay,
446
+ emitMarker: i.emitMarker,
447
+ allowRepay: i.allowRepay
448
+ });
449
+ return status(PAY_STATUS.SUBMITTED, `sent x402 payment ${res.payment.id} (${requirementSummary(res.requirement)}). The settlement receipt will arrive as a later inbox message.`);
450
+ } catch (err) {
451
+ return mapPayError(err, i.autoPay);
452
+ }
453
+ })
454
+ });
455
+ }
456
+ /**
457
+ * Resolve the facilitator config from factory opts or `RINE_FACILITATOR`. A
458
+ * `null` return means none is configured (surfaced as `no-facilitator`). An
459
+ * unrecognised reference — neither a preset name nor an `http(s)://` base URL —
460
+ * throws an actionable config error instead of being handed to `fetch` as a
461
+ * bogus URL: parity with the CLI/MCP `resolveFacilitator`, so a typo like
462
+ * `payia` surfaces as a config error naming the bad value, not an opaque
463
+ * `facilitator-error` from a doomed network call.
464
+ */
465
+ function resolveFacilitator(opts) {
466
+ const ref = opts.facilitator ?? process.env.RINE_FACILITATOR;
467
+ if (!ref) return null;
468
+ const headers = opts.facilitatorHeaders;
469
+ const preset = FACILITATOR_PRESET[ref];
470
+ if (preset) return headers ? {
471
+ ...preset,
472
+ headers
473
+ } : preset;
474
+ if (!/^https?:\/\//.test(ref)) throw new Error(`Unknown facilitator '${ref}'. Use a preset (${Object.keys(FACILITATOR_PRESET).join(" | ")}) or an http(s):// base URL.`);
475
+ return headers ? {
476
+ url: ref,
477
+ headers
478
+ } : { url: ref };
479
+ }
480
+ /** Render a settle-first {@link FulfillResult} to a typed outcome line. */
481
+ function renderFulfill(res) {
482
+ if (!res.verification.isValid) return status(FULFILL_STATUS.VERIFICATION_FAILED, `verification failed (${res.verification.invalidReason ?? "verification_failed"}); a failure receipt was sent so the payer reaches a terminal state.`);
483
+ const s = res.settlement;
484
+ if (s?.success) return status(FULFILL_STATUS.SETTLED, `receipt sent; tx ${s.transaction} on ${s.network}${s.payer ? `, payer ${s.payer}` : ""}.`);
485
+ return status(FULFILL_STATUS.SETTLEMENT_FAILED, `settlement did not succeed (${s?.errorReason ?? "unknown"}); receipt sent.`);
486
+ }
487
+ /** `rine_fulfill` — verify + settle a received payment and send the receipt (payee). */
488
+ function rineFulfillTool(opts = {}) {
489
+ return defineTool({
490
+ description: "Fulfill a received x402 payment (rine.v1.x402_payment): verify the signed authorization and settle it on-chain via your configured facilitator, then send the settlement receipt back in-thread. Settle-first — on a failed verification, a `success:false` receipt is sent so the payer reaches a terminal state. Facilitator verify/settle is plain external HTTP, never a rine endpoint. Returns a typed `status` — `settled` on success, else `settlement-failed` / `verification-failed` / `facilitator-error` / `no-facilitator` / `not-payment`.",
491
+ inputSchema: jsonSchema(fulfillInput),
492
+ outputSchema: STRING_OUTPUT,
493
+ needsApproval: approvalGate(opts),
494
+ execute: makeExecute(fulfillInput, opts, async (client, i) => {
495
+ const msg = await client.read(asMessageUuid(i.messageId));
496
+ if (msg.type !== X402_MESSAGE_TYPE.PAYMENT) return status(FULFILL_STATUS.NOT_PAYMENT, `message ${i.messageId} is type '${msg.type}', not an x402 payment authorization.`);
497
+ const facilitator = resolveFacilitator(opts);
498
+ if (!facilitator) return status(FULFILL_STATUS.NO_FACILITATOR, "no facilitator is configured; set RINE_FACILITATOR (a preset name — cdp/payai/x402-rs — or a base URL) to verify and settle payments.");
499
+ try {
500
+ return renderFulfill(await client.payments.fulfill(msg, {
501
+ facilitator,
502
+ emitMarker: i.emitMarker
503
+ }));
504
+ } catch (err) {
505
+ if (err instanceof X402FacilitatorError) return status(FULFILL_STATUS.FACILITATOR_ERROR, err.message);
506
+ throw err;
507
+ }
508
+ })
509
+ });
510
+ }
511
+ //#endregion
348
512
  //#region src/tools/index.ts
349
513
  const FACTORIES = {
350
514
  rineSendTool,
@@ -358,7 +522,9 @@ const FACTORIES = {
358
522
  rineGroupCreateTool,
359
523
  rineGroupInviteTool,
360
524
  rineGroupRemoveTool,
361
- rineGroupInspectTool
525
+ rineGroupInspectTool,
526
+ rinePayTool,
527
+ rineFulfillTool
362
528
  };
363
529
  /** The metadata registry with live factories attached (programmatic use). */
364
530
  const RINE_TOOLS = RINE_TOOL_META.map((m) => ({
@@ -366,4 +532,4 @@ const RINE_TOOLS = RINE_TOOL_META.map((m) => ({
366
532
  factory: FACTORIES[m.factoryName]
367
533
  }));
368
534
  //#endregion
369
- export { rineSendAndWaitTool as a, rineGroupCreateTool as c, rineGroupRemoveTool as d, rineDiscoverTool as f, rineReplyTool as i, rineGroupInspectTool as l, rineCheckInboxTool as n, rineSendTool as o, rineInspectTool as p, rineReadTool as r, rineThreadTool as s, RINE_TOOLS as t, rineGroupInviteTool as u };
535
+ export { rineReadTool as a, rineSendTool as c, rineGroupInspectTool as d, rineGroupInviteTool as f, rineInspectTool as h, rineCheckInboxTool as i, rineThreadTool as l, rineDiscoverTool as m, rineFulfillTool as n, rineReplyTool as o, rineGroupRemoveTool as p, rinePayTool as r, rineSendAndWaitTool as s, RINE_TOOLS as t, rineGroupCreateTool as u };
package/dist/x402.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * x402 payment-aware inbound re-surfacing for the rine channel (D1 + D2).
3
+ *
4
+ * The three x402 payment frames are first-class message types carrying a verbatim
5
+ * x402 V2 object, NOT a chat turn. v1.0 dropped them wholesale (a stateless gateway
6
+ * that auto-replied prose into a payment handshake would corrupt it). v1.1 re-surfaces
7
+ * them as an agent turn with STRUCTURED payment context so the model can act via the
8
+ * dedicated pay / fulfill tools — while the terminal assistant prose stays suppressed
9
+ * in EVERY case (the `x402` continuation-token flag; the guard hazard never re-arms):
10
+ *
11
+ * - `x402_payment_required` → the quote + guidance to pay via `rine_pay`.
12
+ * - `x402_payment` → the signed authorization + guidance to `rine_fulfill`.
13
+ * - `x402_receipt` → the settlement outcome, informational (no reply).
14
+ *
15
+ * D2 auto-pay (opt-in, DEFAULT OFF): when enabled, a `payment_required` at/below the
16
+ * policy's `autoPayThreshold` is paid with NO LLM turn — still bounded by the policy
17
+ * caps, deny-by-default, journal and reserve-lock (all in rine-core). Any decline
18
+ * falls back to surfacing the quote to the model.
19
+ */
20
+ import type { InboundDeps } from "./channel-core.js";
21
+ import type { DecryptedMessage } from "./types.js";
22
+ /** The three x402 frame types this module re-surfaces. */
23
+ export declare const X402_FRAME_TYPES: readonly ["rine.v1.x402_payment_required", "rine.v1.x402_payment", "rine.v1.x402_receipt"];
24
+ /** Whether a message type is one of the three x402 payment frames. */
25
+ export declare function isX402Frame(type: string): boolean;
26
+ /**
27
+ * The structured payment body handed to the model as the turn's primary message.
28
+ * Reads only the decrypted plaintext (the verbatim x402 object) — never ciphertext.
29
+ */
30
+ export declare function renderX402Body(msg: DecryptedMessage): string;
31
+ /** The routing-guidance line: what the model should do, and the do-not-reply rule. */
32
+ export declare function x402GuidanceLine(msg: DecryptedMessage): string;
33
+ /**
34
+ * Handle a verified inbound x402 frame (D1). `payment_required` may auto-pay (D2)
35
+ * when enabled and at/below threshold; otherwise, and for `payment` / `receipt`,
36
+ * the frame is surfaced to the model as a payment-aware turn.
37
+ */
38
+ export declare function handleX402Inbound(deps: InboundDeps, msg: DecryptedMessage, messageId: string): Promise<Response>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rine-network/eve",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Native Vercel Eve connector for the rine network \u2014 a custom channel that makes an Eve agent reachable over E2E-encrypted (HPKE 1:1, MLS groups RFC 9420, PQ-hybrid) agent-to-agent messaging, plus file-discovered rine tools, a skill, and an init/onboard/relay CLI.",
5
5
  "author": "mmmbs <mmmbs@proton.me>",
6
6
  "license": "EUPL-1.2",
@@ -48,8 +48,8 @@
48
48
  "prepublishOnly": "node scripts/check-no-file-deps.mjs"
49
49
  },
50
50
  "dependencies": {
51
- "@rine-network/core": "^0.7.0",
52
- "@rine-network/sdk": "^0.4.0",
51
+ "@rine-network/core": "^0.11.0",
52
+ "@rine-network/sdk": "^0.8.0",
53
53
  "zod": "^3.25.0",
54
54
  "zod-to-json-schema": "^3.24.1"
55
55
  },