@piprail/sdk 1.6.0 → 1.8.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/CHANGELOG.md +77 -0
- package/DISCOVERY.md +420 -0
- package/ERRORS.md +9 -0
- package/README.md +86 -4
- package/STANDARDS.md +4 -4
- package/dist/index.cjs +647 -27
- package/dist/index.d.cts +392 -2
- package/dist/index.d.ts +392 -2
- package/dist/index.js +624 -4
- package/package.json +2 -1
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 });
|
|
@@ -1154,6 +1493,100 @@ var PipRailClient = class {
|
|
|
1154
1493
|
const plan = await this.planPayment(url, init);
|
|
1155
1494
|
return plan == null ? true : plan.payable;
|
|
1156
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
|
+
}
|
|
1157
1590
|
/**
|
|
1158
1591
|
* Lower-level: drive any HTTP method through the 402 flow.
|
|
1159
1592
|
*
|
|
@@ -1337,7 +1770,7 @@ var PipRailClient = class {
|
|
|
1337
1770
|
const symbol = described?.symbol ?? accept.extra.symbol;
|
|
1338
1771
|
const amountFormatted = formatUnits(amountBase, decimals);
|
|
1339
1772
|
const intent = {
|
|
1340
|
-
host:
|
|
1773
|
+
host: hostOf2(url),
|
|
1341
1774
|
chain: this.opts.chain,
|
|
1342
1775
|
network: accept.network,
|
|
1343
1776
|
asset: accept.asset,
|
|
@@ -1400,7 +1833,7 @@ var PipRailClient = class {
|
|
|
1400
1833
|
this.ledger.record(
|
|
1401
1834
|
{
|
|
1402
1835
|
url: quote.url,
|
|
1403
|
-
host:
|
|
1836
|
+
host: hostOf2(quote.url),
|
|
1404
1837
|
network: quote.network,
|
|
1405
1838
|
asset: quote.asset,
|
|
1406
1839
|
amountBase: quote.amount,
|
|
@@ -1557,7 +1990,11 @@ async function planAcross(clients, url, init) {
|
|
|
1557
1990
|
fundingHint: best ? null : live.map((p) => p.fundingHint).find(Boolean) ?? null
|
|
1558
1991
|
};
|
|
1559
1992
|
}
|
|
1560
|
-
function
|
|
1993
|
+
function railOnNetwork(rail, matches) {
|
|
1994
|
+
const n = normalizeNetwork(rail.network);
|
|
1995
|
+
return !n.includes(":") || matches(n);
|
|
1996
|
+
}
|
|
1997
|
+
function hostOf2(url) {
|
|
1561
1998
|
try {
|
|
1562
1999
|
return new URL(url).hostname;
|
|
1563
2000
|
} catch {
|
|
@@ -1600,9 +2037,59 @@ async function readBody(res) {
|
|
|
1600
2037
|
}
|
|
1601
2038
|
function paymentTools(client) {
|
|
1602
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
|
+
annotations: {
|
|
2044
|
+
title: "Discover payable x402 APIs",
|
|
2045
|
+
readOnlyHint: true,
|
|
2046
|
+
// reads the open indexes only; never pays
|
|
2047
|
+
openWorldHint: true
|
|
2048
|
+
// reaches external indexes (402 Index, CDP Bazaar)
|
|
2049
|
+
},
|
|
2050
|
+
parameters: {
|
|
2051
|
+
type: "object",
|
|
2052
|
+
properties: {
|
|
2053
|
+
query: { type: "string", description: "Free-text topic to search for (optional)." },
|
|
2054
|
+
network: {
|
|
2055
|
+
type: "string",
|
|
2056
|
+
description: "CAIP-2 id, 'self' (your chain \u2014 default), or 'any' (all chains)."
|
|
2057
|
+
},
|
|
2058
|
+
maxPrice: { type: "number", description: "Drop results advertised above this USD price." },
|
|
2059
|
+
limit: { type: "number", description: "Max results per index (default 20)." }
|
|
2060
|
+
},
|
|
2061
|
+
additionalProperties: false
|
|
2062
|
+
},
|
|
2063
|
+
invoke: async (args) => {
|
|
2064
|
+
const opts = {};
|
|
2065
|
+
if (typeof args.query === "string") opts.query = args.query;
|
|
2066
|
+
if (typeof args.network === "string") opts.network = args.network;
|
|
2067
|
+
if (typeof args.maxPrice === "number") opts.maxPrice = args.maxPrice;
|
|
2068
|
+
if (typeof args.limit === "number") opts.limit = args.limit;
|
|
2069
|
+
const found = await client.discover(opts);
|
|
2070
|
+
return {
|
|
2071
|
+
count: found.length,
|
|
2072
|
+
resources: found.map((r) => ({
|
|
2073
|
+
resource: r.resource,
|
|
2074
|
+
name: r.name,
|
|
2075
|
+
description: r.description,
|
|
2076
|
+
source: r.source,
|
|
2077
|
+
priceUsd: r.priceUsd,
|
|
2078
|
+
networks: [...new Set(r.rails.map((rail) => rail.network))]
|
|
2079
|
+
}))
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
},
|
|
1603
2083
|
{
|
|
1604
2084
|
name: "piprail_quote_payment",
|
|
1605
2085
|
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.",
|
|
2086
|
+
annotations: {
|
|
2087
|
+
title: "Quote an x402 price",
|
|
2088
|
+
readOnlyHint: true,
|
|
2089
|
+
// reads the 402 challenge; never pays
|
|
2090
|
+
openWorldHint: true
|
|
2091
|
+
// fetches an arbitrary URL
|
|
2092
|
+
},
|
|
1606
2093
|
parameters: {
|
|
1607
2094
|
type: "object",
|
|
1608
2095
|
properties: {
|
|
@@ -1619,6 +2106,13 @@ function paymentTools(client) {
|
|
|
1619
2106
|
{
|
|
1620
2107
|
name: "piprail_plan_payment",
|
|
1621
2108
|
description: "Check whether you CAN pay an x402-gated URL before paying. Reads your wallet balance, native gas, and whether the recipient can receive \u2014 across every rail the URL offers on your chain \u2014 and returns { gated, payable, best, options, fundingHint }. payable:false means do NOT attempt the payment; fundingHint says exactly what to top up. Call this before piprail_pay_request so you never commit to a payment you cannot finish. Returns { gated: false } when no payment is needed.",
|
|
2109
|
+
annotations: {
|
|
2110
|
+
title: "Plan an x402 payment",
|
|
2111
|
+
readOnlyHint: true,
|
|
2112
|
+
// reads balances + the challenge; never pays
|
|
2113
|
+
openWorldHint: true
|
|
2114
|
+
// fetches a URL and reads chain state
|
|
2115
|
+
},
|
|
1622
2116
|
parameters: {
|
|
1623
2117
|
type: "object",
|
|
1624
2118
|
properties: {
|
|
@@ -1657,6 +2151,17 @@ function paymentTools(client) {
|
|
|
1657
2151
|
{
|
|
1658
2152
|
name: "piprail_pay_request",
|
|
1659
2153
|
description: "Fetch an x402 payment-gated URL, automatically paying the required on-chain payment if needed (subject to the spend policy + approval hook). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
|
|
2154
|
+
annotations: {
|
|
2155
|
+
title: "Pay an x402 request",
|
|
2156
|
+
readOnlyHint: false,
|
|
2157
|
+
// this is the one tool that MOVES FUNDS
|
|
2158
|
+
destructiveHint: true,
|
|
2159
|
+
// an on-chain payment is value-moving and not reversible
|
|
2160
|
+
idempotentHint: false,
|
|
2161
|
+
// paying twice = two payments
|
|
2162
|
+
openWorldHint: true
|
|
2163
|
+
// fetches a URL and settles on-chain
|
|
2164
|
+
},
|
|
1660
2165
|
parameters: {
|
|
1661
2166
|
type: "object",
|
|
1662
2167
|
properties: {
|
|
@@ -1703,6 +2208,39 @@ function paymentTools(client) {
|
|
|
1703
2208
|
throw err;
|
|
1704
2209
|
}
|
|
1705
2210
|
}
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
name: "piprail_register",
|
|
2214
|
+
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.",
|
|
2215
|
+
annotations: {
|
|
2216
|
+
title: "Register an x402 endpoint",
|
|
2217
|
+
readOnlyHint: false,
|
|
2218
|
+
// writes a listing to an external index
|
|
2219
|
+
destructiveHint: false,
|
|
2220
|
+
// adds a listing; nothing is destroyed and no funds move
|
|
2221
|
+
openWorldHint: true
|
|
2222
|
+
// posts to external indexes (402 Index)
|
|
2223
|
+
// idempotentHint intentionally omitted — index dedup behaviour isn't guaranteed.
|
|
2224
|
+
},
|
|
2225
|
+
parameters: {
|
|
2226
|
+
type: "object",
|
|
2227
|
+
properties: {
|
|
2228
|
+
url: { type: "string", description: "Full URL of the resource to list." },
|
|
2229
|
+
name: { type: "string", description: "Display name (defaults to the host)." },
|
|
2230
|
+
description: { type: "string", description: "What the resource offers." },
|
|
2231
|
+
priceUsd: { type: "number", description: "Advertised price in USD (metadata)." }
|
|
2232
|
+
},
|
|
2233
|
+
required: ["url"],
|
|
2234
|
+
additionalProperties: false
|
|
2235
|
+
},
|
|
2236
|
+
invoke: async (args) => {
|
|
2237
|
+
const opts = {};
|
|
2238
|
+
if (typeof args.name === "string") opts.name = args.name;
|
|
2239
|
+
if (typeof args.description === "string") opts.description = args.description;
|
|
2240
|
+
if (typeof args.priceUsd === "number") opts.priceUsd = args.priceUsd;
|
|
2241
|
+
const outcomes = await client.register(String(args.url), opts);
|
|
2242
|
+
return { outcomes };
|
|
2243
|
+
}
|
|
1706
2244
|
}
|
|
1707
2245
|
];
|
|
1708
2246
|
}
|
|
@@ -1805,6 +2343,25 @@ function createPaymentGate(options) {
|
|
|
1805
2343
|
const { challenge: c, requiredHeader } = await challenge();
|
|
1806
2344
|
return { kind: "challenge", challenge: c, requiredHeader, statusCode: 402 };
|
|
1807
2345
|
}
|
|
2346
|
+
async function describe(resourceUrl = "") {
|
|
2347
|
+
const specs = await ready();
|
|
2348
|
+
const accepts = specs.map((s) => ({
|
|
2349
|
+
scheme: "onchain-proof",
|
|
2350
|
+
network: s.net.network,
|
|
2351
|
+
asset: s.asset,
|
|
2352
|
+
payTo: s.payTo,
|
|
2353
|
+
amount: s.amountBase.toString(),
|
|
2354
|
+
amountFormatted: s.amountFormatted,
|
|
2355
|
+
decimals: s.decimals,
|
|
2356
|
+
maxTimeoutSeconds,
|
|
2357
|
+
...s.symbol ? { symbol: s.symbol } : {}
|
|
2358
|
+
}));
|
|
2359
|
+
return {
|
|
2360
|
+
url: resourceUrl,
|
|
2361
|
+
...options.description ? { description: options.description } : {},
|
|
2362
|
+
accepts
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
1808
2365
|
async function verify(paymentSignature) {
|
|
1809
2366
|
const raw = normaliseHeader(paymentSignature);
|
|
1810
2367
|
if (!raw) return asChallenge();
|
|
@@ -1856,7 +2413,7 @@ function createPaymentGate(options) {
|
|
|
1856
2413
|
receiptHeader: buildReceiptHeader(result.receipt)
|
|
1857
2414
|
};
|
|
1858
2415
|
}
|
|
1859
|
-
return { challenge, verify };
|
|
2416
|
+
return { challenge, verify, describe };
|
|
1860
2417
|
}
|
|
1861
2418
|
function requirePayment(options) {
|
|
1862
2419
|
const gate = createPaymentGate(options);
|
|
@@ -1986,11 +2543,67 @@ function encodeXPaymentHeader(input) {
|
|
|
1986
2543
|
};
|
|
1987
2544
|
return base64(JSON.stringify(payload));
|
|
1988
2545
|
}
|
|
2546
|
+
|
|
2547
|
+
// src/discovery.ts
|
|
2548
|
+
var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
|
|
2549
|
+
function pathOf(url) {
|
|
2550
|
+
try {
|
|
2551
|
+
return new URL(url).pathname || "/";
|
|
2552
|
+
} catch {
|
|
2553
|
+
return url.startsWith("/") ? url : `/${url}`;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function buildOpenApi(input) {
|
|
2557
|
+
const paths = {};
|
|
2558
|
+
for (const r of input.resources) {
|
|
2559
|
+
const path = pathOf(r.url);
|
|
2560
|
+
const method = (r.method ?? "GET").toLowerCase();
|
|
2561
|
+
const op = {
|
|
2562
|
+
...r.description ? { summary: r.description } : {},
|
|
2563
|
+
responses: {
|
|
2564
|
+
"200": { description: "Paid \u2014 the resource." },
|
|
2565
|
+
"402": { description: "Payment required (x402)." }
|
|
2566
|
+
},
|
|
2567
|
+
"x-payment-info": {
|
|
2568
|
+
x402Version: 2,
|
|
2569
|
+
accepts: r.accepts,
|
|
2570
|
+
bazaar: { discoverable: true }
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
paths[path] = { ...paths[path] ?? {}, [method]: op };
|
|
2574
|
+
}
|
|
2575
|
+
return {
|
|
2576
|
+
openapi: "3.1.0",
|
|
2577
|
+
info: { title: input.title ?? "PipRail x402 resources", version: input.version ?? "1.0.0" },
|
|
2578
|
+
servers: [{ url: input.origin }],
|
|
2579
|
+
paths,
|
|
2580
|
+
// "Built with @piprail/sdk" — default on (opt out with attribution:false). At the
|
|
2581
|
+
// document ROOT so `info` stays exactly { title, version }.
|
|
2582
|
+
...input.attribution === false ? {} : { "x-generator": GENERATOR },
|
|
2583
|
+
...input.ownershipProofs && input.ownershipProofs.length > 0 ? { "x-agentcash-provenance": { ownershipProofs: input.ownershipProofs } } : {}
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
function buildWellKnownX402(input) {
|
|
2587
|
+
return {
|
|
2588
|
+
version: 1,
|
|
2589
|
+
resources: input.resources.map((r) => r.url),
|
|
2590
|
+
...input.ownershipProofs && input.ownershipProofs.length > 0 ? { ownershipProofs: input.ownershipProofs } : {}
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
function buildX402DnsTxt(input) {
|
|
2594
|
+
const descriptor = input.descriptor ? `descriptor=${input.descriptor};` : "";
|
|
2595
|
+
return {
|
|
2596
|
+
name: `_x402.${input.host}`,
|
|
2597
|
+
type: "TXT",
|
|
2598
|
+
value: `v=x4021;${descriptor}url=${input.discoveryUrl}`
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
1989
2601
|
export {
|
|
1990
2602
|
CHAINS,
|
|
1991
2603
|
ConfirmationTimeoutError,
|
|
1992
2604
|
EIP3009_TYPES,
|
|
1993
2605
|
EXACT_NETWORK_SLUGS,
|
|
2606
|
+
GENERATOR,
|
|
1994
2607
|
InsufficientFundsError,
|
|
1995
2608
|
InvalidEnvelopeError,
|
|
1996
2609
|
MaxRetriesExceededError,
|
|
@@ -2008,12 +2621,16 @@ export {
|
|
|
2008
2621
|
WrongFamilyError,
|
|
2009
2622
|
buildChallengeHeader,
|
|
2010
2623
|
buildExactAuthorization,
|
|
2624
|
+
buildOpenApi,
|
|
2011
2625
|
buildReceiptHeader,
|
|
2012
2626
|
buildSignatureHeader,
|
|
2627
|
+
buildWellKnownX402,
|
|
2628
|
+
buildX402DnsTxt,
|
|
2013
2629
|
chainIdForExactNetwork,
|
|
2014
2630
|
createPaymentGate,
|
|
2015
2631
|
encodeXPaymentHeader,
|
|
2016
2632
|
evaluatePolicy,
|
|
2633
|
+
normalizeNetwork,
|
|
2017
2634
|
parseChallenge,
|
|
2018
2635
|
parseExactRequirements,
|
|
2019
2636
|
parseReceipt,
|
|
@@ -2021,9 +2638,12 @@ export {
|
|
|
2021
2638
|
paymentTools,
|
|
2022
2639
|
pickAccept,
|
|
2023
2640
|
planAcross,
|
|
2641
|
+
register402Index,
|
|
2024
2642
|
registerDriver,
|
|
2643
|
+
registerX402Scan,
|
|
2025
2644
|
requirePayment,
|
|
2026
2645
|
resolveChain,
|
|
2646
|
+
searchOpenIndexes,
|
|
2027
2647
|
toInsufficientFundsError,
|
|
2028
2648
|
toInvalidBody
|
|
2029
2649
|
};
|