@subly_fi/pay 0.4.0 → 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
@@ -5,12 +5,20 @@ Subly client for [x402](https://x402.org)-style HTTP payments funded by
5
5
  deposited USDC, and the principal is never spent. Non-custodial: it signs
6
6
  locally with your own Solana key; Subly never holds it.
7
7
 
8
+ Current payments target standard x402 sellers that offer a Solana USDC `exact`
9
+ rail with facilitator `extra.feePayer` support.
10
+
8
11
  Ships one `pay` dispatcher bin with subcommands, all runnable with `npx` (no clone):
9
12
 
10
13
  - `pay mcp` — an MCP server (Claude Code, Cursor, any MCP client) exposing
11
- the full lifecycle as tools: `deposit_to_subly_vault`,
14
+ the full lifecycle as tools: `create_subly_setup_link` /
15
+ `check_subly_setup` (owner onboarding: the human approves the spending
16
+ mandate + first deposit with one Face ID), `deposit_to_subly_vault`,
12
17
  `get_subly_yield_budget`, `fetch_with_subly_payment`,
13
- `withdraw_from_subly_vault`
18
+ `withdraw_from_subly_vault`. Payments above the owner's approval
19
+ threshold, deposits, and (when the mandate opts in) withdrawals return an
20
+ `approveUrl` to paste into chat; retry with the `approvalId` once the
21
+ human approved.
14
22
  - `pay fetch <url>` — one-shot: pay for a URL, print the receipt (used by the
15
23
  OpenClaw skill)
16
24
  - `pay deposit <amountRawUsdc>` / `pay withdraw <amountRawUsdc>` — vault
@@ -26,11 +34,11 @@ export SUBLY_DEMO_AGENT_KEYPAIR_PATH=~/.subly/agent.json
26
34
  ```
27
35
 
28
36
  Send USDC (Solana mainnet) to the printed address — no SOL needed, fees are
29
- sponsored — then deposit (vault minimum 1 USDC; deposit self-registers the
30
- wallet):
37
+ sponsored — then deposit (vault minimum is just over 1 USDC: share rounding
38
+ refuses exactly 1.000000; deposit self-registers the wallet):
31
39
 
32
40
  ```bash
33
- npx -y @subly_fi/pay deposit 1000000 # 1 USDC
41
+ npx -y @subly_fi/pay deposit 1010000 # 1.01 USDC
34
42
  ```
35
43
 
36
44
  ## Use it
package/dist/deposit.js CHANGED
@@ -993,7 +993,7 @@ var OnboardingError = class extends Error {
993
993
  };
994
994
  async function ensureWalletOnboarded(params) {
995
995
  const fetchImpl = params.fetchImpl ?? fetch;
996
- const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
996
+ const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
997
997
  const post = async (step, path, body) => {
998
998
  const url = `${baseUrl}${path}`;
999
999
  const serialized = JSON.stringify(body);
@@ -1078,14 +1078,18 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1078
1078
 
1079
1079
  // ../../src/client/vault-flows.ts
1080
1080
  var VaultFlowClientError = class extends Error {
1081
- constructor(step, message, detail = null) {
1081
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
1082
1082
  super(message);
1083
1083
  this.step = step;
1084
1084
  this.detail = detail;
1085
+ this.code = code;
1086
+ this.errorDetails = errorDetails;
1085
1087
  this.name = "VaultFlowClientError";
1086
1088
  }
1087
1089
  step;
1088
1090
  detail;
1091
+ code;
1092
+ errorDetails;
1089
1093
  };
1090
1094
  var VaultFlowClient = class {
1091
1095
  baseUrl;
@@ -1095,19 +1099,44 @@ var VaultFlowClient = class {
1095
1099
  pollTimeoutMs;
1096
1100
  pollIntervalMs;
1097
1101
  constructor(config) {
1098
- this.baseUrl = config.facilitatorBaseUrl.replace(/\/$/, "");
1102
+ this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1099
1103
  this.signer = config.signer;
1100
1104
  this.fetchImpl = config.fetchImpl ?? fetch;
1101
1105
  this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1102
1106
  this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1103
1107
  this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1104
1108
  }
1105
- /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1109
+ /**
1110
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
1111
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
1112
+ * without an owner approval; when the caller passes none, an already
1113
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
1114
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
1115
+ * up and used automatically before surfacing deposit_approval_required.
1116
+ */
1106
1117
  async deposit(input) {
1107
- const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1108
- wallet: this.signer.walletAddress,
1109
- amountRawUsdc: input.amountRawUsdc.toString()
1110
- });
1118
+ let approvalId = input.approvalId;
1119
+ let prepared;
1120
+ try {
1121
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1122
+ wallet: this.signer.walletAddress,
1123
+ amountRawUsdc: input.amountRawUsdc.toString(),
1124
+ ...approvalId === void 0 ? {} : { approvalId }
1125
+ });
1126
+ } catch (error) {
1127
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
1128
+ throw error;
1129
+ }
1130
+ approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
1131
+ if (approvalId === void 0) {
1132
+ throw error;
1133
+ }
1134
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1135
+ wallet: this.signer.walletAddress,
1136
+ amountRawUsdc: input.amountRawUsdc.toString(),
1137
+ approvalId
1138
+ });
1139
+ }
1111
1140
  const signed = await this.signer.signDeposit({
1112
1141
  intent: prepared.signingIntent,
1113
1142
  serializedTransaction: prepared.serializedTransaction,
@@ -1146,7 +1175,9 @@ var VaultFlowClient = class {
1146
1175
  {
1147
1176
  wallet: this.signer.walletAddress,
1148
1177
  amountRawUsdc: input.amountRawUsdc.toString(),
1149
- ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1178
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1179
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1180
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1150
1181
  }
1151
1182
  );
1152
1183
  const signed = await this.signer.signWithdrawal({
@@ -1225,6 +1256,70 @@ var VaultFlowClient = class {
1225
1256
  spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1226
1257
  };
1227
1258
  }
1259
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
1260
+ async reportPayment(input) {
1261
+ await this.postJson("submit", "/v1/payments/report", {
1262
+ wallet: this.signer.walletAddress,
1263
+ withdrawalId: input.withdrawalId,
1264
+ paymentTxSignature: input.paymentTxSignature
1265
+ });
1266
+ }
1267
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
1268
+ async listApprovals(status) {
1269
+ const body = await this.getJson(
1270
+ `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
1271
+ );
1272
+ return body.approvals ?? [];
1273
+ }
1274
+ /**
1275
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
1276
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1277
+ */
1278
+ async createSetupSession(input) {
1279
+ return await this.postJson(
1280
+ "prepare",
1281
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1282
+ {
1283
+ ...input.policy === void 0 ? {} : { policy: input.policy },
1284
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1285
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1286
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1287
+ }
1288
+ );
1289
+ }
1290
+ /** Polls a setup session (public capability URL — no auth needed). */
1291
+ async getSetupSession(sessionId) {
1292
+ const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
1293
+ const response = await this.fetchImpl(url);
1294
+ const text = await response.text();
1295
+ if (response.status !== 200) {
1296
+ const parsed = parseRelayerError(text);
1297
+ throw new VaultFlowClientError(
1298
+ "read",
1299
+ parsed.message ?? `setup session read failed with ${response.status}`,
1300
+ text,
1301
+ parsed.code,
1302
+ parsed.details
1303
+ );
1304
+ }
1305
+ return JSON.parse(text);
1306
+ }
1307
+ /**
1308
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
1309
+ * amount — the shape the mandate's initialDeposit approval has.
1310
+ */
1311
+ async findApprovedDepositApproval(amountRawUsdc2) {
1312
+ try {
1313
+ const approvals = await this.listApprovals("approved");
1314
+ const match = approvals.find((approval) => {
1315
+ const binding = approval.binding;
1316
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc2.toString();
1317
+ });
1318
+ return match?.approvalId;
1319
+ } catch {
1320
+ return void 0;
1321
+ }
1322
+ }
1228
1323
  /**
1229
1324
  * Polls the reconciling GET endpoint until the intent leaves "submitted"
1230
1325
  * (each read looks the tx up on-chain) or the timeout elapses.
@@ -1274,10 +1369,13 @@ var VaultFlowClient = class {
1274
1369
  });
1275
1370
  const text = await response.text();
1276
1371
  if (response.status !== 200) {
1372
+ const parsed = parseRelayerError(text);
1277
1373
  throw new VaultFlowClientError(
1278
1374
  step,
1279
- `${path} failed with ${response.status}: ${text}`,
1280
- text
1375
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1376
+ text,
1377
+ parsed.code,
1378
+ parsed.details
1281
1379
  );
1282
1380
  }
1283
1381
  try {
@@ -1290,7 +1388,49 @@ var VaultFlowClient = class {
1290
1388
  );
1291
1389
  }
1292
1390
  }
1391
+ async getJson(path) {
1392
+ const url = `${this.baseUrl}${path}`;
1393
+ const response = await this.fetchImpl(url, {
1394
+ headers: await walletAuthHeaders({
1395
+ signer: this.signer,
1396
+ method: "GET",
1397
+ url
1398
+ })
1399
+ });
1400
+ const text = await response.text();
1401
+ if (response.status !== 200) {
1402
+ const parsed = parseRelayerError(text);
1403
+ throw new VaultFlowClientError(
1404
+ "read",
1405
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1406
+ text,
1407
+ parsed.code,
1408
+ parsed.details
1409
+ );
1410
+ }
1411
+ try {
1412
+ return JSON.parse(text);
1413
+ } catch {
1414
+ throw new VaultFlowClientError(
1415
+ "read",
1416
+ `${path} returned 200 with a non-JSON body`,
1417
+ text
1418
+ );
1419
+ }
1420
+ }
1293
1421
  };
1422
+ function parseRelayerError(text) {
1423
+ try {
1424
+ const parsed = JSON.parse(text);
1425
+ return {
1426
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1427
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1428
+ details: parsed.error?.details ?? null
1429
+ };
1430
+ } catch {
1431
+ return { code: null, message: null, details: null };
1432
+ }
1433
+ }
1294
1434
 
1295
1435
  // ../../src/solana/keys.ts
1296
1436
  import { readFileSync } from "node:fs";
@@ -1353,14 +1493,14 @@ var rpc = createRpc(
1353
1493
  process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
1354
1494
  );
1355
1495
  var vaultFlows = new VaultFlowClient({
1356
- facilitatorBaseUrl: relayerBaseUrl,
1496
+ relayerBaseUrl,
1357
1497
  signer,
1358
1498
  rpc
1359
1499
  });
1360
1500
  console.log(`[deposit] agent wallet: ${signer.walletAddress}`);
1361
1501
  console.log(`[deposit] relayer: ${relayerBaseUrl}`);
1362
1502
  console.log("\n[deposit] step 0: ensure the wallet is registered (self-serve)");
1363
- await ensureWalletOnboarded({ facilitatorBaseUrl: relayerBaseUrl, signer });
1503
+ await ensureWalletOnboarded({ relayerBaseUrl, signer });
1364
1504
  console.log("[deposit] wallet registered and synced");
1365
1505
  console.log(
1366
1506
  `