@piprail/sdk 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -737,6 +737,17 @@ function makeEvmNetwork(resolved) {
737
737
  async recipientReady() {
738
738
  return { ready: "n/a" };
739
739
  },
740
+ // Discovery only (ownership proofs / SIWX) — never the payment path. Signs
741
+ // through the wallet client so it works for both { privateKey } (local) and
742
+ // bring-your-own { walletClient } (JSON-RPC) accounts. eip191 → recoverable
743
+ // with viem's recoverMessageAddress (how x402scan verifies origin ownership).
744
+ discoverySigner(wallet) {
745
+ const a = wallet._native;
746
+ return {
747
+ address: a.account.address,
748
+ signMessage: (message) => a.walletClient.signMessage({ account: a.account, message })
749
+ };
750
+ },
740
751
  async verify(ref, accept) {
741
752
  return verifyEvm({
742
753
  publicClient,
@@ -888,6 +899,334 @@ async function resolveNetwork2(opts) {
888
899
  return resolveNetwork(opts);
889
900
  }
890
901
 
902
+ // src/indexes.ts
903
+ var BAZAAR_URL = "https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources";
904
+ var INDEX402_SEARCH = "https://402index.io/api/v1/services";
905
+ var INDEX402_REGISTER = "https://402index.io/api/v1/register";
906
+ var X402SCAN_REGISTER = "https://www.x402scan.com/api/x402/registry/register";
907
+ var USER_AGENT = "@piprail/sdk (+https://piprail.com)";
908
+ function clientHeaders(extra = {}) {
909
+ return { "user-agent": USER_AGENT, ...extra };
910
+ }
911
+ var SLUG_TO_CAIP2 = {
912
+ // EVM (the common index-reported slugs; others fall through to net.supports)
913
+ ethereum: "eip155:1",
914
+ base: "eip155:8453",
915
+ polygon: "eip155:137",
916
+ arbitrum: "eip155:42161",
917
+ optimism: "eip155:10",
918
+ avalanche: "eip155:43114",
919
+ bnb: "eip155:56",
920
+ bsc: "eip155:56",
921
+ // non-EVM families — values mirror each driver's bound caip2 exactly
922
+ solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
923
+ ton: "ton:-239",
924
+ tron: "tron:mainnet",
925
+ near: "near:mainnet",
926
+ sui: "sui:mainnet",
927
+ aptos: "aptos:1",
928
+ algorand: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k",
929
+ stellar: "stellar:pubnet",
930
+ xrpl: "xrpl:0"
931
+ };
932
+ function normalizeNetwork(network) {
933
+ if (network.includes(":")) return network;
934
+ return SLUG_TO_CAIP2[network.toLowerCase()] ?? network;
935
+ }
936
+ async function searchOpenIndexes(opts = {}) {
937
+ const sources = opts.sources ?? ["bazaar", "402index"];
938
+ const limit = opts.limit ?? 20;
939
+ const results = await Promise.all(
940
+ sources.map((source) => {
941
+ if (source === "bazaar") return safeSearch(() => searchBazaar(opts.query, limit, opts.signal));
942
+ if (source === "402index") return safeSearch(() => search402Index(opts.query, limit, opts.signal));
943
+ return Promise.resolve([]);
944
+ })
945
+ );
946
+ return dedupeByResource(results.flat());
947
+ }
948
+ async function safeSearch(run) {
949
+ try {
950
+ return await run();
951
+ } catch {
952
+ return [];
953
+ }
954
+ }
955
+ function dedupeByResource(items) {
956
+ const seen = /* @__PURE__ */ new Set();
957
+ const out = [];
958
+ for (const it of items) {
959
+ const key = it.resource;
960
+ if (!key || seen.has(key)) continue;
961
+ seen.add(key);
962
+ out.push(it);
963
+ }
964
+ return out;
965
+ }
966
+ async function searchBazaar(query, limit, signal) {
967
+ const res = await fetch(`${BAZAAR_URL}?limit=${encodeURIComponent(String(limit))}`, {
968
+ headers: clientHeaders({ accept: "application/json" }),
969
+ ...signal ? { signal } : {}
970
+ });
971
+ if (!res.ok) return [];
972
+ const body = await res.json();
973
+ const items = Array.isArray(body.items) ? body.items : [];
974
+ const mapped = items.map(mapBazaarItem).filter((r) => r !== null);
975
+ return query ? mapped.filter((r) => matchesQuery(r, query)) : mapped;
976
+ }
977
+ function mapBazaarItem(raw) {
978
+ if (!raw || typeof raw !== "object") return null;
979
+ const o = raw;
980
+ const resource = pickString(o, "resource", "url", "endpoint");
981
+ if (!resource) return null;
982
+ const meta = o.metadata && typeof o.metadata === "object" ? o.metadata : {};
983
+ return {
984
+ resource,
985
+ source: "bazaar",
986
+ rails: mapRails(o.accepts),
987
+ ...optionalString("name", pickString(meta, "name", "title")),
988
+ ...optionalString("description", pickString(meta, "description") ?? pickString(o, "description")),
989
+ ...optionalString("category", pickString(meta, "category"))
990
+ };
991
+ }
992
+ async function search402Index(query, limit, signal) {
993
+ const qs = new URLSearchParams({ limit: String(limit) });
994
+ if (query) qs.set("q", query);
995
+ const res = await fetch(`${INDEX402_SEARCH}?${qs.toString()}`, {
996
+ headers: clientHeaders({ accept: "application/json" }),
997
+ ...signal ? { signal } : {}
998
+ });
999
+ if (!res.ok) return [];
1000
+ const body = await res.json();
1001
+ const list = firstArray(body, "services", "results", "items", "data");
1002
+ return list.map(map402IndexItem).filter((r) => r !== null).filter((r) => r.rails.length > 0);
1003
+ }
1004
+ function map402IndexItem(raw) {
1005
+ if (!raw || typeof raw !== "object") return null;
1006
+ const o = raw;
1007
+ const resource = pickString(o, "url", "resource", "endpoint");
1008
+ if (!resource) return null;
1009
+ const protocol = (pickString(o, "protocol") ?? "x402").toLowerCase();
1010
+ if (protocol !== "x402") return null;
1011
+ const rails = Array.isArray(o.accepts) ? mapRails(o.accepts) : railFrom402IndexFields(o);
1012
+ const priceUsd = pickNumber(o, "price_usd", "priceUsd", "price");
1013
+ return {
1014
+ resource,
1015
+ source: "402index",
1016
+ rails,
1017
+ ...priceUsd !== void 0 ? { priceUsd } : {},
1018
+ ...optionalString("name", pickString(o, "name", "title")),
1019
+ ...optionalString("description", pickString(o, "description")),
1020
+ ...optionalString("category", pickString(o, "category", "tag"))
1021
+ };
1022
+ }
1023
+ function railFrom402IndexFields(o) {
1024
+ const network = pickString(o, "payment_network", "network");
1025
+ const asset = pickString(o, "payment_asset", "asset", "token");
1026
+ if (!network && !asset) return [];
1027
+ return [
1028
+ {
1029
+ scheme: "exact",
1030
+ network: network ?? "unknown",
1031
+ ...asset ? { asset } : {},
1032
+ ...optionalString("symbol", asset)
1033
+ }
1034
+ ];
1035
+ }
1036
+ async function register402Index(input) {
1037
+ try {
1038
+ const payload = {
1039
+ url: input.url,
1040
+ name: input.name ?? hostOf(input.url),
1041
+ protocol: "x402",
1042
+ ...input.description ? { description: input.description } : {},
1043
+ ...typeof input.priceUsd === "number" ? { price_usd: input.priceUsd } : {},
1044
+ ...input.asset ? { payment_asset: input.asset } : {},
1045
+ ...input.network ? { payment_network: input.network } : {},
1046
+ ...input.method ? { http_method: input.method.toUpperCase() } : {},
1047
+ ...input.attribution ? { via: "@piprail/sdk" } : {}
1048
+ };
1049
+ const res = await fetch(INDEX402_REGISTER, {
1050
+ method: "POST",
1051
+ headers: clientHeaders({ "content-type": "application/json", accept: "application/json" }),
1052
+ body: JSON.stringify(payload)
1053
+ });
1054
+ if (res.ok) {
1055
+ return { source: "402index", ok: true, status: res.status, detail: "Listed on 402 Index (searchable at 402index.io)." };
1056
+ }
1057
+ const why = await readIndexError(res);
1058
+ return {
1059
+ source: "402index",
1060
+ ok: false,
1061
+ status: res.status,
1062
+ detail: why ? `402 Index rejected it (HTTP ${res.status}): ${why}` : `402 Index returned HTTP ${res.status}.`
1063
+ };
1064
+ } catch (err) {
1065
+ return { source: "402index", ok: false, detail: errMsg(err) };
1066
+ }
1067
+ }
1068
+ async function readIndexError(res) {
1069
+ try {
1070
+ const body = await res.json();
1071
+ const parts = [body.error, body.detail, body.message].filter(
1072
+ (p) => typeof p === "string" && p.length > 0
1073
+ );
1074
+ return parts.length ? [...new Set(parts)].join(" \u2014 ") : void 0;
1075
+ } catch {
1076
+ return void 0;
1077
+ }
1078
+ }
1079
+ async function registerX402Scan(input, signer) {
1080
+ try {
1081
+ const challengeRes = await fetch(X402SCAN_REGISTER, {
1082
+ method: "POST",
1083
+ headers: clientHeaders({ "content-type": "application/json", accept: "application/json" }),
1084
+ body: JSON.stringify({ url: input.url })
1085
+ });
1086
+ if (challengeRes.status !== 402) {
1087
+ return {
1088
+ source: "x402scan",
1089
+ ok: challengeRes.ok,
1090
+ status: challengeRes.status,
1091
+ detail: challengeRes.ok ? "Listed on x402scan." : `x402scan returned HTTP ${challengeRes.status} (expected a SIWX 402 challenge).`
1092
+ };
1093
+ }
1094
+ const info = await readSiwxInfo(challengeRes);
1095
+ if (!info) {
1096
+ return { source: "x402scan", ok: false, status: 402, detail: "x402scan SIWX challenge was unparseable." };
1097
+ }
1098
+ const resolvedInfo = { ...info, issuedAt: info.issuedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
1099
+ const message = formatSiweMessage(resolvedInfo, signer.address);
1100
+ const signature = await signer.signMessage(message);
1101
+ const header = encodeBase642(
1102
+ JSON.stringify({ ...resolvedInfo, address: signer.address, type: "eip191", message, signature })
1103
+ );
1104
+ const res = await fetch(X402SCAN_REGISTER, {
1105
+ method: "POST",
1106
+ headers: clientHeaders({
1107
+ "content-type": "application/json",
1108
+ accept: "application/json",
1109
+ "sign-in-with-x": header
1110
+ }),
1111
+ body: JSON.stringify({ url: input.url })
1112
+ });
1113
+ if (res.ok) {
1114
+ return { source: "x402scan", ok: true, status: res.status, detail: "Listed on x402scan (SIWX)." };
1115
+ }
1116
+ const why = await readIndexError(res);
1117
+ return {
1118
+ source: "x402scan",
1119
+ ok: false,
1120
+ status: res.status,
1121
+ detail: why ? `x402scan rejected it (HTTP ${res.status}): ${why}` : `x402scan returned HTTP ${res.status} after signing.`
1122
+ };
1123
+ } catch (err) {
1124
+ return { source: "x402scan", ok: false, detail: errMsg(err) };
1125
+ }
1126
+ }
1127
+ async function readSiwxInfo(res) {
1128
+ try {
1129
+ const body = await res.json();
1130
+ const ext = body.extensions;
1131
+ const siwx = ext?.["sign-in-with-x"];
1132
+ const info = siwx?.info ?? siwx;
1133
+ if (info && typeof info.domain === "string" && info.domain.length > 0 && typeof info.nonce === "string" && info.nonce.length > 0 && typeof info.uri === "string" && info.uri.length > 0) {
1134
+ return info;
1135
+ }
1136
+ return null;
1137
+ } catch {
1138
+ return null;
1139
+ }
1140
+ }
1141
+ function formatSiweMessage(info, address) {
1142
+ const chainId = info.chainId ? caip2ToChainId(info.chainId) : 1;
1143
+ const statement = info.statement && info.statement.trim() ? info.statement : void 0;
1144
+ const lines = [
1145
+ `${info.domain} wants you to sign in with your Ethereum account:`,
1146
+ address,
1147
+ "",
1148
+ ...statement ? [statement, ""] : [""],
1149
+ `URI: ${info.uri}`,
1150
+ "Version: 1",
1151
+ `Chain ID: ${chainId}`,
1152
+ `Nonce: ${info.nonce}`,
1153
+ `Issued At: ${info.issuedAt}`,
1154
+ ...info.expirationTime ? [`Expiration Time: ${info.expirationTime}`] : []
1155
+ ];
1156
+ return lines.join("\n");
1157
+ }
1158
+ function caip2ToChainId(caip2) {
1159
+ const m = /^eip155:(\d+)$/.exec(caip2);
1160
+ const n = m ? Number(m[1]) : Number(caip2);
1161
+ return Number.isSafeInteger(n) && n > 0 ? n : 1;
1162
+ }
1163
+ function mapRails(accepts) {
1164
+ if (!Array.isArray(accepts)) return [];
1165
+ const out = [];
1166
+ for (const raw of accepts) {
1167
+ if (!raw || typeof raw !== "object") continue;
1168
+ const a = raw;
1169
+ const network = pickString(a, "network");
1170
+ if (!network) continue;
1171
+ const extra = a.extra && typeof a.extra === "object" ? a.extra : {};
1172
+ out.push({
1173
+ scheme: pickString(a, "scheme") ?? "exact",
1174
+ network,
1175
+ ...optionalString("asset", pickString(a, "asset")),
1176
+ ...optionalString("amount", pickString(a, "amount", "maxAmountRequired")),
1177
+ ...optionalString("payTo", pickString(a, "payTo")),
1178
+ ...optionalString("symbol", pickString(extra, "symbol"))
1179
+ });
1180
+ }
1181
+ return out;
1182
+ }
1183
+ function matchesQuery(r, query) {
1184
+ const q = query.toLowerCase();
1185
+ return r.resource.toLowerCase().includes(q) || (r.name?.toLowerCase().includes(q) ?? false) || (r.description?.toLowerCase().includes(q) ?? false);
1186
+ }
1187
+ function pickString(o, ...keys) {
1188
+ for (const k of keys) {
1189
+ const v = o[k];
1190
+ if (typeof v === "string" && v.length > 0) return v;
1191
+ }
1192
+ return void 0;
1193
+ }
1194
+ function pickNumber(o, ...keys) {
1195
+ for (const k of keys) {
1196
+ const v = o[k];
1197
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1198
+ if (typeof v === "string" && /^\d+(\.\d+)?$/.test(v.trim())) {
1199
+ const n = Number(v.trim());
1200
+ if (Number.isFinite(n)) return n;
1201
+ }
1202
+ }
1203
+ return void 0;
1204
+ }
1205
+ function optionalString(field, value) {
1206
+ return value !== void 0 ? { [field]: value } : {};
1207
+ }
1208
+ function firstArray(o, ...keys) {
1209
+ for (const k of keys) {
1210
+ if (Array.isArray(o[k])) return o[k];
1211
+ }
1212
+ return Array.isArray(o) ? o : [];
1213
+ }
1214
+ function hostOf(url) {
1215
+ try {
1216
+ return new URL(url).hostname;
1217
+ } catch {
1218
+ return url;
1219
+ }
1220
+ }
1221
+ function errMsg(err) {
1222
+ return err instanceof Error ? err.message : String(err);
1223
+ }
1224
+ function encodeBase642(str) {
1225
+ if (typeof btoa === "function") return btoa(str);
1226
+ if (typeof Buffer !== "undefined") return Buffer.from(str, "utf8").toString("base64");
1227
+ throw new Error("No base64 encoder available in this runtime.");
1228
+ }
1229
+
891
1230
  // src/policy.ts
892
1231
  var ALLOW = { allowed: true };
893
1232
  var deny = (reason) => ({ allowed: false, reason });
@@ -923,7 +1262,12 @@ function evaluatePolicy(intent, policy, spentForAssetBase) {
923
1262
  }
924
1263
  if (policy.tokens) {
925
1264
  const sym = intent.recognized ? intent.symbol : void 0;
926
- if (!sym || !policy.tokens.some((t) => t.toUpperCase() === sym.toUpperCase())) {
1265
+ const isNative = intent.asset === "native";
1266
+ const matches = policy.tokens.some((t) => {
1267
+ if (isNative && t.toUpperCase() === "NATIVE") return true;
1268
+ return sym ? t.toUpperCase() === sym.toUpperCase() : false;
1269
+ });
1270
+ if (!matches) {
927
1271
  return deny(
928
1272
  `token ${intent.symbol ?? intent.asset} is not in the allowed set (policy.tokens).`
929
1273
  );
@@ -1149,6 +1493,100 @@ var PipRailClient = class {
1149
1493
  const plan = await this.planPayment(url, init);
1150
1494
  return plan == null ? true : plan.payable;
1151
1495
  }
1496
+ /* ------------------------- discovery (find + list) ------------------------- */
1497
+ /**
1498
+ * Find payable resources on the OPEN x402 indexes — WITHOUT paying. Reads the
1499
+ * free indexes (CDP Bazaar + 402 Index by default), merges + dedupes them, and
1500
+ * by default returns only resources payable on THIS client's chain
1501
+ * (`network: 'self'`). Each result carries its advertised `rails[]`; feed a
1502
+ * chosen `resource` straight into `quote()` → `planPayment()` → `fetch()`.
1503
+ *
1504
+ * Nothing PipRail-hosted: these are third-party open directories. Never throws
1505
+ * for a read problem — an index that's down or changed simply contributes
1506
+ * nothing. Honest caveat: index results are cross-scheme (mostly the
1507
+ * mainstream `exact` scheme); `fetch()` pays only `onchain-proof` rails
1508
+ * directly (pay `exact` resources with the experimental `drivers/evm/exact.ts`).
1509
+ */
1510
+ async discover(opts = {}) {
1511
+ const found = await searchOpenIndexes({
1512
+ ...opts.query !== void 0 ? { query: opts.query } : {},
1513
+ ...opts.sources ? { sources: opts.sources } : {},
1514
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {}
1515
+ });
1516
+ const scope = opts.network ?? "self";
1517
+ let out = found;
1518
+ if (scope === "self") {
1519
+ const { net } = await this.ensure();
1520
+ out = out.filter((r) => r.rails.some((rail) => railOnNetwork(rail, (n) => net.supports(n))));
1521
+ } else if (scope !== "any") {
1522
+ const target = normalizeNetwork(scope);
1523
+ out = out.filter((r) => r.rails.some((rail) => railOnNetwork(rail, (n) => n === target)));
1524
+ }
1525
+ if (opts.maxPrice !== void 0) {
1526
+ const max = opts.maxPrice;
1527
+ out = out.filter((r) => r.priceUsd === void 0 || r.priceUsd <= max);
1528
+ }
1529
+ return out;
1530
+ }
1531
+ /**
1532
+ * List a resource you run on the OPEN x402 registries, so agents can find it.
1533
+ * Default target is **402 Index** — one POST, no auth, no signature, no payment
1534
+ * (searchable within seconds). Add `'x402scan'` to also register via SIWX (one
1535
+ * wallet signature; EVM + a Base/Solana rail). Returns one {@link RegisterOutcome}
1536
+ * per target — a target the chain can't satisfy comes back `{ ok:false, detail }`,
1537
+ * never a throw. An explicit, developer-invoked action; it moves no funds, and
1538
+ * nothing is PipRail-hosted — you're listing on third-party open directories.
1539
+ */
1540
+ async register(url, opts = {}) {
1541
+ const targets = opts.targets ?? ["402index"];
1542
+ const networkSlug = opts.network ?? (typeof this.opts.chain === "string" ? this.opts.chain : void 0);
1543
+ const outcomes = [];
1544
+ for (const target of targets) {
1545
+ if (target === "402index") {
1546
+ outcomes.push(
1547
+ await register402Index({
1548
+ url,
1549
+ ...opts.name ? { name: opts.name } : {},
1550
+ ...opts.description ? { description: opts.description } : {},
1551
+ ...opts.priceUsd !== void 0 ? { priceUsd: opts.priceUsd } : {},
1552
+ ...opts.asset ? { asset: opts.asset } : {},
1553
+ ...networkSlug ? { network: networkSlug } : {},
1554
+ ...opts.method ? { method: opts.method } : {},
1555
+ ...opts.attribution ? { attribution: true } : {}
1556
+ })
1557
+ );
1558
+ } else if (target === "x402scan") {
1559
+ const signer = await this.discoverySigner();
1560
+ if (!signer) {
1561
+ outcomes.push({
1562
+ source: "x402scan",
1563
+ ok: false,
1564
+ detail: "x402scan registration needs an EVM signer; this chain family has no discoverySigner. Use 402 Index (the default), which needs no signature."
1565
+ });
1566
+ continue;
1567
+ }
1568
+ outcomes.push(await registerX402Scan({ url }, signer));
1569
+ } else {
1570
+ outcomes.push({
1571
+ source: "bazaar",
1572
+ ok: false,
1573
+ detail: "CDP Bazaar has no register endpoint \u2014 it catalogs a resource only when its facilitator settles a payment (PipRail uses no facilitator). List on 402 Index / x402scan instead."
1574
+ });
1575
+ }
1576
+ }
1577
+ return outcomes;
1578
+ }
1579
+ /**
1580
+ * The discovery signer for the bound wallet (its address + a message signer),
1581
+ * or `null` if the chain family doesn't support it (EVM does today). For
1582
+ * discovery only — ownership proofs (sign the bare origin string and pass it to
1583
+ * `buildOpenApi({ ownershipProofs })`) and SIWX registration. Never signs a
1584
+ * payment.
1585
+ */
1586
+ async discoverySigner() {
1587
+ const { net, wallet } = await this.ensure();
1588
+ return net.discoverySigner ? net.discoverySigner(wallet) : null;
1589
+ }
1152
1590
  /**
1153
1591
  * Lower-level: drive any HTTP method through the 402 flow.
1154
1592
  *
@@ -1332,7 +1770,7 @@ var PipRailClient = class {
1332
1770
  const symbol = described?.symbol ?? accept.extra.symbol;
1333
1771
  const amountFormatted = formatUnits(amountBase, decimals);
1334
1772
  const intent = {
1335
- host: hostOf(url),
1773
+ host: hostOf2(url),
1336
1774
  chain: this.opts.chain,
1337
1775
  network: accept.network,
1338
1776
  asset: accept.asset,
@@ -1395,7 +1833,7 @@ var PipRailClient = class {
1395
1833
  this.ledger.record(
1396
1834
  {
1397
1835
  url: quote.url,
1398
- host: hostOf(quote.url),
1836
+ host: hostOf2(quote.url),
1399
1837
  network: quote.network,
1400
1838
  asset: quote.asset,
1401
1839
  amountBase: quote.amount,
@@ -1552,7 +1990,11 @@ async function planAcross(clients, url, init) {
1552
1990
  fundingHint: best ? null : live.map((p) => p.fundingHint).find(Boolean) ?? null
1553
1991
  };
1554
1992
  }
1555
- function hostOf(url) {
1993
+ function railOnNetwork(rail, matches) {
1994
+ const n = normalizeNetwork(rail.network);
1995
+ return !n.includes(":") || matches(n);
1996
+ }
1997
+ function hostOf2(url) {
1556
1998
  try {
1557
1999
  return new URL(url).hostname;
1558
2000
  } catch {
@@ -1595,6 +2037,42 @@ async function readBody(res) {
1595
2037
  }
1596
2038
  function paymentTools(client) {
1597
2039
  return [
2040
+ {
2041
+ name: "piprail_discover",
2042
+ description: `Find x402 payment-gated resources on the OPEN indexes (a phone book of payable APIs) WITHOUT paying. Use it to answer "what can I buy?" \u2014 search by topic, then quote/plan/pay a chosen one. By default returns only resources payable on your wallet's chain (network='self'); pass 'any' for every chain. Results are cross-scheme: ALWAYS call piprail_quote_payment on a chosen resource (it re-checks the live price) before piprail_pay_request.`,
2043
+ parameters: {
2044
+ type: "object",
2045
+ properties: {
2046
+ query: { type: "string", description: "Free-text topic to search for (optional)." },
2047
+ network: {
2048
+ type: "string",
2049
+ description: "CAIP-2 id, 'self' (your chain \u2014 default), or 'any' (all chains)."
2050
+ },
2051
+ maxPrice: { type: "number", description: "Drop results advertised above this USD price." },
2052
+ limit: { type: "number", description: "Max results per index (default 20)." }
2053
+ },
2054
+ additionalProperties: false
2055
+ },
2056
+ invoke: async (args) => {
2057
+ const opts = {};
2058
+ if (typeof args.query === "string") opts.query = args.query;
2059
+ if (typeof args.network === "string") opts.network = args.network;
2060
+ if (typeof args.maxPrice === "number") opts.maxPrice = args.maxPrice;
2061
+ if (typeof args.limit === "number") opts.limit = args.limit;
2062
+ const found = await client.discover(opts);
2063
+ return {
2064
+ count: found.length,
2065
+ resources: found.map((r) => ({
2066
+ resource: r.resource,
2067
+ name: r.name,
2068
+ description: r.description,
2069
+ source: r.source,
2070
+ priceUsd: r.priceUsd,
2071
+ networks: [...new Set(r.rails.map((rail) => rail.network))]
2072
+ }))
2073
+ };
2074
+ }
2075
+ },
1598
2076
  {
1599
2077
  name: "piprail_quote_payment",
1600
2078
  description: "Get the price of an x402 payment-gated URL WITHOUT paying. Returns the amount, token, chain, recipient, and whether it is within the spend policy. Returns { gated: false } when the URL needs no payment. Call this first to decide whether a resource is worth buying.",
@@ -1698,6 +2176,29 @@ function paymentTools(client) {
1698
2176
  throw err;
1699
2177
  }
1700
2178
  }
2179
+ },
2180
+ {
2181
+ name: "piprail_register",
2182
+ description: "List an x402 payment-gated resource YOU run on the open indexes so other agents can discover it. Default target is 402 Index \u2014 no auth, no signature, no payment; searchable within seconds. Returns one outcome per index ({ source, ok, detail }); a step the chain can't satisfy comes back ok:false with the reason. Moves no funds; nothing is PipRail-hosted.",
2183
+ parameters: {
2184
+ type: "object",
2185
+ properties: {
2186
+ url: { type: "string", description: "Full URL of the resource to list." },
2187
+ name: { type: "string", description: "Display name (defaults to the host)." },
2188
+ description: { type: "string", description: "What the resource offers." },
2189
+ priceUsd: { type: "number", description: "Advertised price in USD (metadata)." }
2190
+ },
2191
+ required: ["url"],
2192
+ additionalProperties: false
2193
+ },
2194
+ invoke: async (args) => {
2195
+ const opts = {};
2196
+ if (typeof args.name === "string") opts.name = args.name;
2197
+ if (typeof args.description === "string") opts.description = args.description;
2198
+ if (typeof args.priceUsd === "number") opts.priceUsd = args.priceUsd;
2199
+ const outcomes = await client.register(String(args.url), opts);
2200
+ return { outcomes };
2201
+ }
1701
2202
  }
1702
2203
  ];
1703
2204
  }
@@ -1800,6 +2301,25 @@ function createPaymentGate(options) {
1800
2301
  const { challenge: c, requiredHeader } = await challenge();
1801
2302
  return { kind: "challenge", challenge: c, requiredHeader, statusCode: 402 };
1802
2303
  }
2304
+ async function describe(resourceUrl = "") {
2305
+ const specs = await ready();
2306
+ const accepts = specs.map((s) => ({
2307
+ scheme: "onchain-proof",
2308
+ network: s.net.network,
2309
+ asset: s.asset,
2310
+ payTo: s.payTo,
2311
+ amount: s.amountBase.toString(),
2312
+ amountFormatted: s.amountFormatted,
2313
+ decimals: s.decimals,
2314
+ maxTimeoutSeconds,
2315
+ ...s.symbol ? { symbol: s.symbol } : {}
2316
+ }));
2317
+ return {
2318
+ url: resourceUrl,
2319
+ ...options.description ? { description: options.description } : {},
2320
+ accepts
2321
+ };
2322
+ }
1803
2323
  async function verify(paymentSignature) {
1804
2324
  const raw = normaliseHeader(paymentSignature);
1805
2325
  if (!raw) return asChallenge();
@@ -1851,7 +2371,7 @@ function createPaymentGate(options) {
1851
2371
  receiptHeader: buildReceiptHeader(result.receipt)
1852
2372
  };
1853
2373
  }
1854
- return { challenge, verify };
2374
+ return { challenge, verify, describe };
1855
2375
  }
1856
2376
  function requirePayment(options) {
1857
2377
  const gate = createPaymentGate(options);
@@ -1981,11 +2501,67 @@ function encodeXPaymentHeader(input) {
1981
2501
  };
1982
2502
  return base64(JSON.stringify(payload));
1983
2503
  }
2504
+
2505
+ // src/discovery.ts
2506
+ var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
2507
+ function pathOf(url) {
2508
+ try {
2509
+ return new URL(url).pathname || "/";
2510
+ } catch {
2511
+ return url.startsWith("/") ? url : `/${url}`;
2512
+ }
2513
+ }
2514
+ function buildOpenApi(input) {
2515
+ const paths = {};
2516
+ for (const r of input.resources) {
2517
+ const path = pathOf(r.url);
2518
+ const method = (r.method ?? "GET").toLowerCase();
2519
+ const op = {
2520
+ ...r.description ? { summary: r.description } : {},
2521
+ responses: {
2522
+ "200": { description: "Paid \u2014 the resource." },
2523
+ "402": { description: "Payment required (x402)." }
2524
+ },
2525
+ "x-payment-info": {
2526
+ x402Version: 2,
2527
+ accepts: r.accepts,
2528
+ bazaar: { discoverable: true }
2529
+ }
2530
+ };
2531
+ paths[path] = { ...paths[path] ?? {}, [method]: op };
2532
+ }
2533
+ return {
2534
+ openapi: "3.1.0",
2535
+ info: { title: input.title ?? "PipRail x402 resources", version: input.version ?? "1.0.0" },
2536
+ servers: [{ url: input.origin }],
2537
+ paths,
2538
+ // "Built with @piprail/sdk" — default on (opt out with attribution:false). At the
2539
+ // document ROOT so `info` stays exactly { title, version }.
2540
+ ...input.attribution === false ? {} : { "x-generator": GENERATOR },
2541
+ ...input.ownershipProofs && input.ownershipProofs.length > 0 ? { "x-agentcash-provenance": { ownershipProofs: input.ownershipProofs } } : {}
2542
+ };
2543
+ }
2544
+ function buildWellKnownX402(input) {
2545
+ return {
2546
+ version: 1,
2547
+ resources: input.resources.map((r) => r.url),
2548
+ ...input.ownershipProofs && input.ownershipProofs.length > 0 ? { ownershipProofs: input.ownershipProofs } : {}
2549
+ };
2550
+ }
2551
+ function buildX402DnsTxt(input) {
2552
+ const descriptor = input.descriptor ? `descriptor=${input.descriptor};` : "";
2553
+ return {
2554
+ name: `_x402.${input.host}`,
2555
+ type: "TXT",
2556
+ value: `v=x4021;${descriptor}url=${input.discoveryUrl}`
2557
+ };
2558
+ }
1984
2559
  export {
1985
2560
  CHAINS,
1986
2561
  ConfirmationTimeoutError,
1987
2562
  EIP3009_TYPES,
1988
2563
  EXACT_NETWORK_SLUGS,
2564
+ GENERATOR,
1989
2565
  InsufficientFundsError,
1990
2566
  InvalidEnvelopeError,
1991
2567
  MaxRetriesExceededError,
@@ -2003,12 +2579,16 @@ export {
2003
2579
  WrongFamilyError,
2004
2580
  buildChallengeHeader,
2005
2581
  buildExactAuthorization,
2582
+ buildOpenApi,
2006
2583
  buildReceiptHeader,
2007
2584
  buildSignatureHeader,
2585
+ buildWellKnownX402,
2586
+ buildX402DnsTxt,
2008
2587
  chainIdForExactNetwork,
2009
2588
  createPaymentGate,
2010
2589
  encodeXPaymentHeader,
2011
2590
  evaluatePolicy,
2591
+ normalizeNetwork,
2012
2592
  parseChallenge,
2013
2593
  parseExactRequirements,
2014
2594
  parseReceipt,
@@ -2016,9 +2596,12 @@ export {
2016
2596
  paymentTools,
2017
2597
  pickAccept,
2018
2598
  planAcross,
2599
+ register402Index,
2019
2600
  registerDriver,
2601
+ registerX402Scan,
2020
2602
  requirePayment,
2021
2603
  resolveChain,
2604
+ searchOpenIndexes,
2022
2605
  toInsufficientFundsError,
2023
2606
  toInvalidBody
2024
2607
  };