@subly_fi/pay 0.2.0 → 0.4.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
@@ -7,7 +7,10 @@ locally with your own Solana key; Subly never holds it.
7
7
 
8
8
  Ships one `pay` dispatcher bin with subcommands, all runnable with `npx` (no clone):
9
9
 
10
- - `pay mcp` — an MCP server (Claude Code, Cursor, any MCP client)
10
+ - `pay mcp` — an MCP server (Claude Code, Cursor, any MCP client) exposing
11
+ the full lifecycle as tools: `deposit_to_subly_vault`,
12
+ `get_subly_yield_budget`, `fetch_with_subly_payment`,
13
+ `withdraw_from_subly_vault`
11
14
  - `pay fetch <url>` — one-shot: pay for a URL, print the receipt (used by the
12
15
  OpenClaw skill)
13
16
  - `pay deposit <amountRawUsdc>` / `pay withdraw <amountRawUsdc>` — vault
@@ -45,10 +48,14 @@ npx -y @subly_fi/pay fetch https://seller.example.com/api/premium
45
48
  | Var | Required | Default |
46
49
  |---|---|---|
47
50
  | `SUBLY_DEMO_AGENT_KEYPAIR_PATH` | yes (or `SUBLY_DEMO_AGENT_KEYPAIR` base58) | — |
48
- | `SUBLY_FACILITATOR_URL` | no | `https://api.demo.sublyfi.com` |
51
+ | `SUBLY_RELAYER_URL` | no | `https://api.demo.sublyfi.com` |
49
52
  | `SOLANA_RPC_URL` | no | public mainnet RPC |
50
53
  | `SUBLY_MCP_MAX_AMOUNT_RAW_USDC` | no | `10000` (0.01 USDC) per-payment cap |
51
54
 
52
55
  Requests authenticate with a signature from your wallet key — there is no API
53
- token. The cap and the facilitator's yield-budget check both bound spending;
54
- the principal is never touched.
56
+ token. `SUBLY_FACILITATOR_URL` is still accepted as a legacy fallback for
57
+ `SUBLY_RELAYER_URL`. Spending is bounded twice: the client cap, and the
58
+ relayer's server-side guard that refuses to realize anything beyond the
59
+ spendable yield — the deposited principal is never touched by a payment.
60
+ (A plain `pay withdraw` is the exit path and may of course move principal
61
+ back to your wallet.)
package/dist/deposit.js CHANGED
@@ -1,8 +1,8 @@
1
- // src/client/agent-wallet-signer.ts
1
+ // ../../src/client/agent-wallet-signer.ts
2
2
  import { signBytes } from "@solana/kit";
3
3
  import bs584 from "bs58";
4
4
 
5
- // src/solana/tx.ts
5
+ // ../../src/solana/tx.ts
6
6
  import bs58 from "bs58";
7
7
  import {
8
8
  appendTransactionMessageInstructions,
@@ -17,7 +17,7 @@ import {
17
17
  setTransactionMessageLifetimeUsingBlockhash
18
18
  } from "@solana/kit";
19
19
 
20
- // src/lib/hash.ts
20
+ // ../../src/lib/hash.ts
21
21
  import { createHash } from "node:crypto";
22
22
  function sha256TaggedHex(data) {
23
23
  return `sha256-${createHash("sha256").update(data).digest("hex")}`;
@@ -42,16 +42,16 @@ function hashStableJson(value) {
42
42
  return sha256TaggedHex(stableStringify(value));
43
43
  }
44
44
 
45
- // src/solana/tx.ts
45
+ // ../../src/solana/tx.ts
46
46
  function decodeSerializedTransaction(serializedBase64) {
47
47
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
48
48
  }
49
49
  async function addSignaturesToSerializedTransaction(params) {
50
50
  const decoded = decodeSerializedTransaction(params.serializedBase64);
51
- const signed2 = await partiallySignTransaction(params.signers, decoded);
51
+ const signed = await partiallySignTransaction(params.signers, decoded);
52
52
  return {
53
- serializedBase64: getBase64EncodedWireTransaction(signed2),
54
- transaction: signed2
53
+ serializedBase64: getBase64EncodedWireTransaction(signed),
54
+ transaction: signed
55
55
  };
56
56
  }
57
57
  function signatureBase58ForSigner(transaction, signer2) {
@@ -62,11 +62,11 @@ function signatureBase58ForSigner(transaction, signer2) {
62
62
  return bs58.encode(signature);
63
63
  }
64
64
 
65
- // src/client/transaction-intent-validator.ts
65
+ // ../../src/client/transaction-intent-validator.ts
66
66
  import bs583 from "bs58";
67
67
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
68
68
 
69
- // src/config/constants.ts
69
+ // ../../src/config/constants.ts
70
70
  var PAYMENT_SCHEME = "subly-yield-exact";
71
71
  var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
72
72
  var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
@@ -82,7 +82,7 @@ var SUBLY_VAULT = {
82
82
  };
83
83
  var USDC_DECIMALS = 6;
84
84
 
85
- // src/domain/request-binding.ts
85
+ // ../../src/domain/request-binding.ts
86
86
  function computeRequestBindingHash(fields) {
87
87
  return hashStableJson({
88
88
  sellerRequestId: fields.sellerRequestId,
@@ -97,7 +97,7 @@ function computeRequestBindingHash(fields) {
97
97
  });
98
98
  }
99
99
 
100
- // src/lib/associated-token-account.ts
100
+ // ../../src/lib/associated-token-account.ts
101
101
  import { createHash as createHash2 } from "node:crypto";
102
102
  import bs582 from "bs58";
103
103
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
@@ -189,7 +189,7 @@ function modPow(base, exponent, modulus) {
189
189
  return result;
190
190
  }
191
191
 
192
- // src/client/transaction-intent-validator.ts
192
+ // ../../src/client/transaction-intent-validator.ts
193
193
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
194
194
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
195
195
  var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
@@ -875,7 +875,7 @@ function readShortVec(bytes, startOffset) {
875
875
  return null;
876
876
  }
877
877
 
878
- // src/client/agent-wallet-signer.ts
878
+ // ../../src/client/agent-wallet-signer.ts
879
879
  var LocalKeypairAgentWalletSigner = class {
880
880
  validationMode = "structured_intent_transaction";
881
881
  keyPairSigner;
@@ -945,7 +945,7 @@ var LocalKeypairAgentWalletSigner = class {
945
945
  }
946
946
  };
947
947
 
948
- // src/api/wallet-auth.ts
948
+ // ../../src/api/wallet-auth.ts
949
949
  import { createHash as createHash3 } from "node:crypto";
950
950
  import bs585 from "bs58";
951
951
  import nacl from "tweetnacl";
@@ -963,7 +963,7 @@ function walletAuthMessage(params) {
963
963
  );
964
964
  }
965
965
 
966
- // src/client/wallet-auth-headers.ts
966
+ // ../../src/client/wallet-auth-headers.ts
967
967
  async function walletAuthHeaders(params) {
968
968
  const signedAtMs = String(Date.now());
969
969
  const message = walletAuthMessage({
@@ -979,7 +979,7 @@ async function walletAuthHeaders(params) {
979
979
  };
980
980
  }
981
981
 
982
- // src/client/onboarding.ts
982
+ // ../../src/client/onboarding.ts
983
983
  var SELF_SERVE_POLICY_ID = "self-serve";
984
984
  var OnboardingError = class extends Error {
985
985
  constructor(step, message, detail = null) {
@@ -1036,7 +1036,7 @@ async function ensureWalletOnboarded(params) {
1036
1036
  await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1037
1037
  }
1038
1038
 
1039
- // src/client/lookup-tables.ts
1039
+ // ../../src/client/lookup-tables.ts
1040
1040
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1041
1041
  import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1042
1042
  function lookupTableAddressesForTransaction(serializedTransaction) {
@@ -1076,7 +1076,223 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1076
1076
  return result;
1077
1077
  }
1078
1078
 
1079
- // src/solana/keys.ts
1079
+ // ../../src/client/vault-flows.ts
1080
+ var VaultFlowClientError = class extends Error {
1081
+ constructor(step, message, detail = null) {
1082
+ super(message);
1083
+ this.step = step;
1084
+ this.detail = detail;
1085
+ this.name = "VaultFlowClientError";
1086
+ }
1087
+ step;
1088
+ detail;
1089
+ };
1090
+ var VaultFlowClient = class {
1091
+ baseUrl;
1092
+ signer;
1093
+ fetchImpl;
1094
+ lookupTablesFor;
1095
+ pollTimeoutMs;
1096
+ pollIntervalMs;
1097
+ constructor(config) {
1098
+ this.baseUrl = config.facilitatorBaseUrl.replace(/\/$/, "");
1099
+ this.signer = config.signer;
1100
+ this.fetchImpl = config.fetchImpl ?? fetch;
1101
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1102
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1103
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1104
+ }
1105
+ /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1106
+ async deposit(input) {
1107
+ const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1108
+ wallet: this.signer.walletAddress,
1109
+ amountRawUsdc: input.amountRawUsdc.toString()
1110
+ });
1111
+ const signed = await this.signer.signDeposit({
1112
+ intent: prepared.signingIntent,
1113
+ serializedTransaction: prepared.serializedTransaction,
1114
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1115
+ });
1116
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1117
+ depositId: prepared.depositId,
1118
+ serializedTransaction: signed.serializedTransaction,
1119
+ agentSignature: signed.agentSignature
1120
+ });
1121
+ if (outcome.status === "submitted") {
1122
+ outcome = await this.pollUntilTerminal(
1123
+ `/v1/deposits/${prepared.depositId}`,
1124
+ outcome
1125
+ );
1126
+ }
1127
+ return {
1128
+ depositId: prepared.depositId,
1129
+ status: outcome.status,
1130
+ txSignature: outcome.txSignature ?? null,
1131
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1132
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1133
+ errorCode: outcome.errorCode ?? null
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1138
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1139
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1140
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1141
+ */
1142
+ async withdraw(input) {
1143
+ const prepared = await this.postJson(
1144
+ "prepare",
1145
+ "/v1/withdrawals/prepare",
1146
+ {
1147
+ wallet: this.signer.walletAddress,
1148
+ amountRawUsdc: input.amountRawUsdc.toString(),
1149
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1150
+ }
1151
+ );
1152
+ const signed = await this.signer.signWithdrawal({
1153
+ intent: prepared.signingIntent,
1154
+ serializedTransaction: prepared.serializedTransaction,
1155
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1156
+ });
1157
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1158
+ withdrawalId: prepared.withdrawalId,
1159
+ serializedTransaction: signed.serializedTransaction,
1160
+ agentSignature: signed.agentSignature
1161
+ });
1162
+ if (outcome.status === "submitted") {
1163
+ outcome = await this.pollUntilTerminal(
1164
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1165
+ outcome
1166
+ );
1167
+ }
1168
+ return {
1169
+ withdrawalId: prepared.withdrawalId,
1170
+ status: outcome.status,
1171
+ txSignature: outcome.txSignature ?? null,
1172
+ destinationUsdcAta: prepared.destinationUsdcAta,
1173
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1174
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1175
+ errorCode: outcome.errorCode ?? null
1176
+ };
1177
+ }
1178
+ /**
1179
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1180
+ * yield accrued since the last sync shows up); the sync is best-effort and
1181
+ * on failure the last-synced view is returned.
1182
+ */
1183
+ async getBudget(options = {}) {
1184
+ if (options.refreshFromChain !== false) {
1185
+ try {
1186
+ await this.postJson(
1187
+ "sync",
1188
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1189
+ { source: "chain" }
1190
+ );
1191
+ } catch {
1192
+ }
1193
+ }
1194
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1195
+ const response = await this.fetchImpl(url, {
1196
+ headers: await walletAuthHeaders({
1197
+ signer: this.signer,
1198
+ method: "GET",
1199
+ url
1200
+ })
1201
+ });
1202
+ const text = await response.text();
1203
+ if (response.status !== 200) {
1204
+ throw new VaultFlowClientError(
1205
+ "budget",
1206
+ `budget endpoint returned ${response.status}: ${text}`
1207
+ );
1208
+ }
1209
+ let parsed;
1210
+ try {
1211
+ parsed = JSON.parse(text);
1212
+ } catch {
1213
+ throw new VaultFlowClientError(
1214
+ "budget",
1215
+ "budget endpoint returned 200 with a non-JSON body",
1216
+ text
1217
+ );
1218
+ }
1219
+ const body = parsed;
1220
+ return {
1221
+ wallet: this.signer.walletAddress,
1222
+ principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1223
+ positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1224
+ grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
1225
+ spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1226
+ };
1227
+ }
1228
+ /**
1229
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1230
+ * (each read looks the tx up on-chain) or the timeout elapses.
1231
+ */
1232
+ async pollUntilTerminal(path, last) {
1233
+ const deadline = Date.now() + this.pollTimeoutMs;
1234
+ let latest = last;
1235
+ while (Date.now() < deadline) {
1236
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1237
+ const url = `${this.baseUrl}${path}`;
1238
+ const response = await this.fetchImpl(url, {
1239
+ headers: await walletAuthHeaders({
1240
+ signer: this.signer,
1241
+ method: "GET",
1242
+ url
1243
+ })
1244
+ });
1245
+ if (response.status !== 200) {
1246
+ continue;
1247
+ }
1248
+ try {
1249
+ latest = await response.json();
1250
+ } catch {
1251
+ continue;
1252
+ }
1253
+ if (latest.status !== "submitted") {
1254
+ return latest;
1255
+ }
1256
+ }
1257
+ return latest;
1258
+ }
1259
+ async postJson(step, path, body) {
1260
+ const url = `${this.baseUrl}${path}`;
1261
+ const serialized = JSON.stringify(body);
1262
+ const response = await this.fetchImpl(url, {
1263
+ method: "POST",
1264
+ headers: {
1265
+ ...await walletAuthHeaders({
1266
+ signer: this.signer,
1267
+ method: "POST",
1268
+ url,
1269
+ body: serialized
1270
+ }),
1271
+ "content-type": "application/json"
1272
+ },
1273
+ body: serialized
1274
+ });
1275
+ const text = await response.text();
1276
+ if (response.status !== 200) {
1277
+ throw new VaultFlowClientError(
1278
+ step,
1279
+ `${path} failed with ${response.status}: ${text}`,
1280
+ text
1281
+ );
1282
+ }
1283
+ try {
1284
+ return JSON.parse(text);
1285
+ } catch {
1286
+ throw new VaultFlowClientError(
1287
+ step,
1288
+ `${path} returned 200 with a non-JSON body`,
1289
+ text
1290
+ );
1291
+ }
1292
+ }
1293
+ };
1294
+
1295
+ // ../../src/solana/keys.ts
1080
1296
  import { readFileSync } from "node:fs";
1081
1297
  import bs586 from "bs58";
1082
1298
  import {
@@ -1101,13 +1317,13 @@ async function loadKeyPairSigner(params) {
1101
1317
  throw new Error(`${label} keypair is not configured`);
1102
1318
  }
1103
1319
 
1104
- // src/solana/rpc.ts
1320
+ // ../../src/solana/rpc.ts
1105
1321
  import { createSolanaRpc } from "@solana/kit";
1106
1322
  function createRpc(url) {
1107
1323
  return createSolanaRpc(url);
1108
1324
  }
1109
1325
 
1110
- // demo/shared.ts
1326
+ // ../../demo/shared.ts
1111
1327
  function fail(message) {
1112
1328
  console.error(message);
1113
1329
  process.exit(1);
@@ -1119,8 +1335,8 @@ function formatRawUsdc(raw) {
1119
1335
  return `${whole}.${fraction}`;
1120
1336
  }
1121
1337
 
1122
- // demo/deposit.ts
1123
- var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1338
+ // ../../demo/deposit.ts
1339
+ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1124
1340
  var amountRawUsdc = process.argv[2];
1125
1341
  if (amountRawUsdc === void 0 || !/^[1-9]\d*$/.test(amountRawUsdc)) {
1126
1342
  fail(
@@ -1136,69 +1352,44 @@ var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1136
1352
  var rpc = createRpc(
1137
1353
  process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
1138
1354
  );
1139
- async function postJson(path, body) {
1140
- const url = `${facilitatorBaseUrl}${path}`;
1141
- const serialized = JSON.stringify(body);
1142
- const response = await fetch(url, {
1143
- method: "POST",
1144
- headers: {
1145
- ...await walletAuthHeaders({
1146
- signer,
1147
- method: "POST",
1148
- url,
1149
- body: serialized
1150
- }),
1151
- "content-type": "application/json"
1152
- },
1153
- body: serialized
1154
- });
1155
- const text = await response.text();
1156
- if (response.status !== 200) {
1157
- fail(`[deposit] ${path} failed with ${response.status}: ${text}`);
1158
- }
1159
- return JSON.parse(text);
1160
- }
1355
+ var vaultFlows = new VaultFlowClient({
1356
+ facilitatorBaseUrl: relayerBaseUrl,
1357
+ signer,
1358
+ rpc
1359
+ });
1161
1360
  console.log(`[deposit] agent wallet: ${signer.walletAddress}`);
1162
- console.log(`[deposit] facilitator: ${facilitatorBaseUrl}`);
1361
+ console.log(`[deposit] relayer: ${relayerBaseUrl}`);
1163
1362
  console.log("\n[deposit] step 0: ensure the wallet is registered (self-serve)");
1164
- await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
1363
+ await ensureWalletOnboarded({ facilitatorBaseUrl: relayerBaseUrl, signer });
1165
1364
  console.log("[deposit] wallet registered and synced");
1166
1365
  console.log(
1167
1366
  `
1168
- [deposit] step 1: prepare deposit of ${formatRawUsdc(amountRawUsdc)} USDC`
1367
+ [deposit] step 1: deposit ${formatRawUsdc(amountRawUsdc)} USDC (prepare -> validate intent + sign locally -> sponsor submits)`
1169
1368
  );
1170
- var prepared = await postJson("/v1/deposits/prepare", {
1171
- wallet: signer.walletAddress,
1172
- amountRawUsdc
1173
- });
1174
- console.log(`[deposit] prepared (depositId=${prepared.depositId})`);
1175
- console.log(
1176
- "\n[deposit] step 2: validate the signing intent against the transaction, sign locally"
1177
- );
1178
- var signed = await signer.signDeposit({
1179
- intent: prepared.signingIntent,
1180
- serializedTransaction: prepared.serializedTransaction,
1181
- lookupTables: await fetchLookupTablesForTransaction(
1182
- rpc,
1183
- prepared.serializedTransaction
1184
- )
1185
- });
1186
- console.log("[deposit] agent signature attached");
1187
- console.log("\n[deposit] step 3: submit (sponsor co-signs and broadcasts)");
1188
- var submitted = await postJson("/v1/deposits/submit", {
1189
- depositId: prepared.depositId,
1190
- serializedTransaction: signed.serializedTransaction,
1191
- agentSignature: signed.agentSignature
1192
- });
1369
+ var submitted;
1370
+ try {
1371
+ submitted = await vaultFlows.deposit({ amountRawUsdc: BigInt(amountRawUsdc) });
1372
+ } catch (error) {
1373
+ if (error instanceof VaultFlowClientError) {
1374
+ fail(`[deposit] ${error.step} failed: ${error.message}`);
1375
+ }
1376
+ throw error;
1377
+ }
1378
+ console.log(`[deposit] depositId: ${submitted.depositId}`);
1193
1379
  console.log(`[deposit] status: ${submitted.status}`);
1194
1380
  if (submitted.txSignature !== null) {
1195
1381
  console.log(
1196
1382
  `[deposit] transaction: https://solscan.io/tx/${submitted.txSignature}`
1197
1383
  );
1198
1384
  }
1385
+ if (submitted.status === "submitted") {
1386
+ fail(
1387
+ `[deposit] broadcast but not yet confirmed \u2014 it may still land. Do NOT resubmit; check GET /v1/deposits/${submitted.depositId} (or the tx link above) first`
1388
+ );
1389
+ }
1199
1390
  if (submitted.status !== "confirmed") {
1200
1391
  fail(
1201
- `[deposit] not confirmed (errorCode=${submitted.errorCode}); check GET /v1/deposits/${prepared.depositId} and the facilitator logs`
1392
+ `[deposit] not confirmed (errorCode=${submitted.errorCode}); check GET /v1/deposits/${submitted.depositId} and the relayer logs`
1202
1393
  );
1203
1394
  }
1204
1395
  console.log(