solverr-agent 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +208 -2
- package/dist/agent-wallet.d.ts +64 -0
- package/dist/agent-wallet.js +194 -0
- package/dist/assets.d.ts +13 -0
- package/dist/assets.js +12 -0
- package/dist/chains/base.d.ts +24 -0
- package/dist/chains/base.js +96 -0
- package/dist/chains/chain-wallet.d.ts +15 -0
- package/dist/chains/chain-wallet.js +1 -0
- package/dist/chains/near.d.ts +28 -0
- package/dist/chains/near.js +96 -0
- package/dist/challenge.d.ts +11 -0
- package/dist/challenge.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +43 -0
- package/dist/coin-selection.d.ts +29 -0
- package/dist/coin-selection.js +60 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +33 -0
- package/dist/env.d.ts +4 -0
- package/dist/env.js +49 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +17 -0
- package/dist/gateway.d.ts +43 -0
- package/dist/gateway.js +24 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/limits.d.ts +18 -0
- package/dist/limits.js +31 -0
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.js +153 -0
- package/dist/payer.d.ts +28 -0
- package/dist/payer.js +157 -0
- package/dist/spend-log.d.ts +38 -0
- package/dist/spend-log.js +162 -0
- package/dist/timeout.d.ts +7 -0
- package/dist/timeout.js +16 -0
- package/dist/url-guard.d.ts +7 -0
- package/dist/url-guard.js +92 -0
- package/package.json +63 -4
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WalletAsset } from "../assets.js";
|
|
2
|
+
export interface TransferResult {
|
|
3
|
+
hash: string;
|
|
4
|
+
}
|
|
5
|
+
/** A key-holding wallet on one chain that can read balances and send exact token amounts. */
|
|
6
|
+
export interface ChainWallet {
|
|
7
|
+
/** 1Click blockchain code. */
|
|
8
|
+
readonly blockchain: string;
|
|
9
|
+
readonly address: string;
|
|
10
|
+
balance(asset: WalletAsset): Promise<bigint>;
|
|
11
|
+
isValidRecipient(address: string): boolean;
|
|
12
|
+
/** Sends exactly `amount` base units of `asset` to `to`. Errors before broadcasting mean no funds left;
|
|
13
|
+
after broadcasting it throws only `PaymentError` with `fundsSent: true`. */
|
|
14
|
+
transfer(asset: WalletAsset, to: string, amount: bigint): Promise<TransferResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { WalletAsset } from "../assets.js";
|
|
2
|
+
import type { ChainWallet, TransferResult } from "./chain-wallet.js";
|
|
3
|
+
interface NearWalletOptions {
|
|
4
|
+
accountId: string;
|
|
5
|
+
/** `ed25519:…` private key. */
|
|
6
|
+
privateKey: string;
|
|
7
|
+
rpcUrl?: string;
|
|
8
|
+
requestTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/** NEAR wallet paying with NEP-141 tokens through `ft_transfer`, registering the recipient's token storage first when needed. */
|
|
11
|
+
export declare class NearWallet implements ChainWallet {
|
|
12
|
+
readonly blockchain = "near";
|
|
13
|
+
readonly address: string;
|
|
14
|
+
private readonly provider;
|
|
15
|
+
private readonly account;
|
|
16
|
+
private readonly requestTimeoutMs;
|
|
17
|
+
private readonly rpcUrl;
|
|
18
|
+
constructor(options: NearWalletOptions);
|
|
19
|
+
isValidRecipient(address: string): boolean;
|
|
20
|
+
balance(asset: WalletAsset): Promise<bigint>;
|
|
21
|
+
transfer(asset: WalletAsset, to: string, amount: bigint): Promise<TransferResult>;
|
|
22
|
+
private contractOf;
|
|
23
|
+
private view;
|
|
24
|
+
/** Awaits an RPC call within `timeoutMs`; its error text names only the RPC origin, since the full URL may carry an API key. */
|
|
25
|
+
private rpc;
|
|
26
|
+
private assertGasBalance;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Account, JsonRpcProvider, actions, baseEncode, teraToGas, yoctoToNear } from "near-api-js";
|
|
3
|
+
import { DEFAULT_NEAR_RPC_URL, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_TRANSFER_TIMEOUT_MS } from "../config.js";
|
|
4
|
+
import { PaymentError, errorSummary } from "../errors.js";
|
|
5
|
+
import { TimeoutError, withTimeout } from "../timeout.js";
|
|
6
|
+
const ACCOUNT_ID_PATTERN = /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/;
|
|
7
|
+
const FUNCTION_CALL_GAS = teraToGas("30");
|
|
8
|
+
const ONE_YOCTO = 1n;
|
|
9
|
+
/** Native NEAR kept back for gas (two 30 TGas calls cost well under 0.01 NEAR). */
|
|
10
|
+
const GAS_RESERVE_YOCTO = 10n ** 22n;
|
|
11
|
+
const STORAGE_PRICE_PER_BYTE = 10n ** 19n;
|
|
12
|
+
const RPC_RETRIES = { retries: 2, wait: 500, backoff: 2 };
|
|
13
|
+
function isNearAccountId(value) {
|
|
14
|
+
return value.length >= 2 && value.length <= 64 && ACCOUNT_ID_PATTERN.test(value);
|
|
15
|
+
}
|
|
16
|
+
/** NEAR wallet paying with NEP-141 tokens through `ft_transfer`, registering the recipient's token storage first when needed. */
|
|
17
|
+
export class NearWallet {
|
|
18
|
+
blockchain = "near";
|
|
19
|
+
address;
|
|
20
|
+
provider;
|
|
21
|
+
account;
|
|
22
|
+
requestTimeoutMs;
|
|
23
|
+
rpcUrl;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
if (!isNearAccountId(options.accountId))
|
|
26
|
+
throw new Error("near.accountId is not a valid NEAR account ID.");
|
|
27
|
+
if (!/^(ed25519|secp256k1):/.test(options.privateKey))
|
|
28
|
+
throw new Error("near.privateKey must start with ed25519: or secp256k1:.");
|
|
29
|
+
this.address = options.accountId;
|
|
30
|
+
this.rpcUrl = options.rpcUrl ?? DEFAULT_NEAR_RPC_URL;
|
|
31
|
+
this.provider = new JsonRpcProvider({ url: this.rpcUrl }, RPC_RETRIES);
|
|
32
|
+
this.account = new Account(options.accountId, this.provider, options.privateKey);
|
|
33
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
34
|
+
}
|
|
35
|
+
isValidRecipient(address) {
|
|
36
|
+
return isNearAccountId(address);
|
|
37
|
+
}
|
|
38
|
+
async balance(asset) {
|
|
39
|
+
const balance = await this.view(this.contractOf(asset), "ft_balance_of", { account_id: this.address });
|
|
40
|
+
return BigInt(balance ?? "0");
|
|
41
|
+
}
|
|
42
|
+
async transfer(asset, to, amount) {
|
|
43
|
+
if (!this.isValidRecipient(to))
|
|
44
|
+
throw new PaymentError("quote_mismatch", `The deposit address ${to} is not a valid NEAR account.`);
|
|
45
|
+
const contract = this.contractOf(asset);
|
|
46
|
+
const registered = await this.view(contract, "storage_balance_of", { account_id: to });
|
|
47
|
+
const transferActions = [];
|
|
48
|
+
let storageCost = 0n;
|
|
49
|
+
if (registered == null) {
|
|
50
|
+
const bounds = await this.view(contract, "storage_balance_bounds", {});
|
|
51
|
+
storageCost = BigInt(bounds?.min ?? "0");
|
|
52
|
+
transferActions.push(actions.functionCall("storage_deposit", { account_id: to, registration_only: true }, FUNCTION_CALL_GAS, storageCost));
|
|
53
|
+
}
|
|
54
|
+
transferActions.push(actions.functionCall("ft_transfer", { receiver_id: to, amount: amount.toString() }, FUNCTION_CALL_GAS, ONE_YOCTO));
|
|
55
|
+
await this.assertGasBalance(storageCost);
|
|
56
|
+
const signed = await this.rpc(this.account.createSignedTransaction({ receiverId: contract, actions: transferActions }), this.requestTimeoutMs, "Preparing the NEAR transaction");
|
|
57
|
+
const hash = baseEncode(createHash("sha256").update(signed.transaction.encode()).digest());
|
|
58
|
+
try {
|
|
59
|
+
const outcome = await this.rpc(this.provider.sendTransactionUntil(signed, "EXECUTED_OPTIMISTIC"), DEFAULT_TRANSFER_TIMEOUT_MS, "The NEAR transfer");
|
|
60
|
+
if (typeof outcome.status === "object" && "Failure" in outcome.status) {
|
|
61
|
+
throw new PaymentError("transfer_failed", `The NEAR transfer ${hash} failed on-chain.`, true);
|
|
62
|
+
}
|
|
63
|
+
return { hash };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof TimeoutError)
|
|
67
|
+
return { hash };
|
|
68
|
+
if (error instanceof PaymentError)
|
|
69
|
+
throw error;
|
|
70
|
+
throw new PaymentError("transfer_failed", `The NEAR transfer ${hash} was not accepted: ${errorSummary(error)}`, true);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
contractOf(asset) {
|
|
74
|
+
if (asset.blockchain !== "near" || !asset.contract_address)
|
|
75
|
+
throw new Error(`${asset.symbol} is not a NEP-141 token on NEAR.`);
|
|
76
|
+
return asset.contract_address;
|
|
77
|
+
}
|
|
78
|
+
view(contractId, method, args) {
|
|
79
|
+
return this.rpc(this.provider.callFunction({ contractId, method, args }), this.requestTimeoutMs, `NEAR ${method}`);
|
|
80
|
+
}
|
|
81
|
+
/** Awaits an RPC call within `timeoutMs`; its error text names only the RPC origin, since the full URL may carry an API key. */
|
|
82
|
+
rpc(call, timeoutMs, label) {
|
|
83
|
+
return withTimeout(call, timeoutMs, label).catch((error) => {
|
|
84
|
+
if (!(error instanceof Error) || !error.message.includes(this.rpcUrl))
|
|
85
|
+
throw error;
|
|
86
|
+
throw new Error(error.message.replaceAll(this.rpcUrl, URL.canParse(this.rpcUrl) ? new URL(this.rpcUrl).origin : "the NEAR RPC"));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async assertGasBalance(storageCost) {
|
|
90
|
+
const account = await this.rpc(this.provider.viewAccount({ accountId: this.address }), this.requestTimeoutMs, "NEAR account lookup");
|
|
91
|
+
const available = account.amount - BigInt(account.storage_usage) * STORAGE_PRICE_PER_BYTE;
|
|
92
|
+
if (available < storageCost + GAS_RESERVE_YOCTO) {
|
|
93
|
+
throw new PaymentError("no_funds", `${this.address} needs at least ${yoctoToNear(storageCost + GAS_RESERVE_YOCTO, 4)} NEAR available for gas and token storage.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SolverrRequirementExtra, type PaymentRequired, type PaymentRequirements } from "solverr-x402";
|
|
2
|
+
/** A validated `solverr` payment requirement from a 402 response. */
|
|
3
|
+
export interface SolverrChallenge {
|
|
4
|
+
paymentRequired: PaymentRequired;
|
|
5
|
+
requirement: PaymentRequirements;
|
|
6
|
+
extra: SolverrRequirementExtra;
|
|
7
|
+
tokensUrl: string;
|
|
8
|
+
}
|
|
9
|
+
/** Returns the `solverr` requirement of a `PAYMENT-REQUIRED` header; `undefined` when the header is missing, undecodable or offers none.
|
|
10
|
+
An incomplete requirement, bad endpoint URLs or an untrusted gateway throw `PaymentError`. */
|
|
11
|
+
export declare function parseChallenge(header: string | null, trustedGateways: readonly string[]): SolverrChallenge | undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { SOLVERR_SCHEME, X402_VERSION, decodePaymentRequiredHeader, readHeader, secureUrl, } from "solverr-x402";
|
|
2
|
+
import { PaymentError } from "./errors.js";
|
|
3
|
+
const INVOICE_ENDPOINT = /^(?<root>(?:\/[^/]+)*?\/api\/v1)\/invoices\/(?<id>[^/]+)\/(?<action>quote|deposit|status)$/;
|
|
4
|
+
const isText = (value) => typeof value === "string" && value.length > 0;
|
|
5
|
+
/** A percent-decoded path segment, or `undefined` when its escapes are malformed. */
|
|
6
|
+
function decodedSegment(segment) {
|
|
7
|
+
try {
|
|
8
|
+
return decodeURIComponent(segment);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function parseEndpoint(value, invoiceId, action) {
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(value);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new PaymentError("invalid_challenge", `The payment challenge has an invalid ${action} URL.`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
secureUrl(url.href, `${action} URL`);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new PaymentError("untrusted_gateway", `The payment challenge's ${action} URL must use https.`);
|
|
27
|
+
}
|
|
28
|
+
const match = INVOICE_ENDPOINT.exec(url.pathname);
|
|
29
|
+
if (!match?.groups || decodedSegment(match.groups.id) !== invoiceId || match.groups.action !== action || url.search || url.hash) {
|
|
30
|
+
throw new PaymentError("invalid_challenge", `The payment challenge's ${action} URL does not point to invoice ${invoiceId}.`);
|
|
31
|
+
}
|
|
32
|
+
return { origin: url.origin, root: `${url.origin}${match.groups.root}` };
|
|
33
|
+
}
|
|
34
|
+
/** Returns the `solverr` requirement of a `PAYMENT-REQUIRED` header; `undefined` when the header is missing, undecodable or offers none.
|
|
35
|
+
An incomplete requirement, bad endpoint URLs or an untrusted gateway throw `PaymentError`. */
|
|
36
|
+
export function parseChallenge(header, trustedGateways) {
|
|
37
|
+
const paymentRequired = readHeader(header, decodePaymentRequiredHeader);
|
|
38
|
+
if (!paymentRequired || paymentRequired.x402Version !== X402_VERSION || !Array.isArray(paymentRequired.accepts))
|
|
39
|
+
return undefined;
|
|
40
|
+
const requirement = paymentRequired.accepts.find((entry) => entry?.scheme === SOLVERR_SCHEME);
|
|
41
|
+
if (!requirement)
|
|
42
|
+
return undefined;
|
|
43
|
+
const extra = requirement.extra;
|
|
44
|
+
const settlement = extra?.settlement;
|
|
45
|
+
if (!extra ||
|
|
46
|
+
!isText(extra.invoice_id) ||
|
|
47
|
+
!isText(extra.mode) ||
|
|
48
|
+
!isText(extra.quote_url) ||
|
|
49
|
+
!isText(extra.deposit_url) ||
|
|
50
|
+
!isText(extra.status_url) ||
|
|
51
|
+
!isText(extra.price) ||
|
|
52
|
+
!isText(extra.currency) ||
|
|
53
|
+
!isText(extra.expires_at) ||
|
|
54
|
+
Number.isNaN(Date.parse(extra.expires_at)) ||
|
|
55
|
+
!settlement ||
|
|
56
|
+
!isText(settlement.asset_id) ||
|
|
57
|
+
!isText(settlement.symbol) ||
|
|
58
|
+
!isText(settlement.blockchain) ||
|
|
59
|
+
!isText(settlement.amount)) {
|
|
60
|
+
throw new PaymentError("invalid_challenge", "The payment challenge is missing invoice details.");
|
|
61
|
+
}
|
|
62
|
+
const endpoints = [
|
|
63
|
+
parseEndpoint(extra.quote_url, extra.invoice_id, "quote"),
|
|
64
|
+
parseEndpoint(extra.deposit_url, extra.invoice_id, "deposit"),
|
|
65
|
+
parseEndpoint(extra.status_url, extra.invoice_id, "status"),
|
|
66
|
+
];
|
|
67
|
+
if (endpoints.some((endpoint) => endpoint.root !== endpoints[0].root)) {
|
|
68
|
+
throw new PaymentError("invalid_challenge", "The payment challenge's URLs point to different servers.");
|
|
69
|
+
}
|
|
70
|
+
const gatewayOrigin = endpoints[0].origin;
|
|
71
|
+
if (!trustedGateways.some((trusted) => new URL(trusted).origin === gatewayOrigin)) {
|
|
72
|
+
throw new PaymentError("untrusted_gateway", `The payment gateway ${gatewayOrigin} is not in the trusted gateway list.`);
|
|
73
|
+
}
|
|
74
|
+
return { paymentRequired, requirement, extra: extra, tokensUrl: `${endpoints[0].root}/tokens` };
|
|
75
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createAgentWallet } from "./agent-wallet.js";
|
|
4
|
+
import { agentOptionsFromEnv } from "./env.js";
|
|
5
|
+
import { errorSummary } from "./errors.js";
|
|
6
|
+
import { normalizeLimits } from "./limits.js";
|
|
7
|
+
import { serveAgentMcp } from "./mcp-server.js";
|
|
8
|
+
const USAGE = `Usage: solverr-agent mcp
|
|
9
|
+
|
|
10
|
+
Starts a local MCP server on stdin/stdout with the tools pay_url, get_balances, get_spending and set_limits.
|
|
11
|
+
|
|
12
|
+
Environment:
|
|
13
|
+
AGENT_NEAR_ACCOUNT, AGENT_NEAR_KEY NEAR account and ed25519: private key
|
|
14
|
+
AGENT_BASE_KEY Base private key (0x…)
|
|
15
|
+
AGENT_LIMIT_PER_PAYMENT_USD Largest single payment in USD (required)
|
|
16
|
+
AGENT_LIMIT_DAILY_USD Largest total over 24 hours in USD (required)
|
|
17
|
+
AGENT_ALLOW_HOSTS Comma-separated hosts the agent may request and pay (required)
|
|
18
|
+
AGENT_TRUSTED_GATEWAYS Comma-separated Solverr URLs to pay through (required)
|
|
19
|
+
AGENT_NEAR_RPC_URL, AGENT_BASE_RPC_URL, AGENT_SPEND_LOG Optional
|
|
20
|
+
AGENT_PAYMENT_TIMEOUT_MS, AGENT_RESOURCE_TIMEOUT_MS Optional timeouts in ms
|
|
21
|
+
`;
|
|
22
|
+
function packageVersion() {
|
|
23
|
+
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
24
|
+
return manifest.version;
|
|
25
|
+
}
|
|
26
|
+
function main(argv) {
|
|
27
|
+
const [command] = argv;
|
|
28
|
+
if (command !== "mcp") {
|
|
29
|
+
process.stderr.write(USAGE);
|
|
30
|
+
process.exitCode = command === "--help" || command === "-h" ? 0 : 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const options = agentOptionsFromEnv();
|
|
34
|
+
const agent = createAgentWallet(options);
|
|
35
|
+
serveAgentMcp(agent, { name: "solverr-agent", version: packageVersion(), ceiling: normalizeLimits(options.limits) });
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
main(process.argv.slice(2));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
process.stderr.write(`solverr-agent: ${errorSummary(error)}\n`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type WalletAsset } from "./assets.js";
|
|
2
|
+
import type { ChainWallet } from "./chains/chain-wallet.js";
|
|
3
|
+
import type { GatewayToken } from "./gateway.js";
|
|
4
|
+
/** A coin the agent holds enough of to try paying with. Lower `tier` is preferred. */
|
|
5
|
+
export interface PaymentOption {
|
|
6
|
+
asset: WalletAsset;
|
|
7
|
+
wallet: ChainWallet;
|
|
8
|
+
priceUsd: number;
|
|
9
|
+
balance: bigint;
|
|
10
|
+
/** Expected amount in base units, with headroom for fees and slippage. */
|
|
11
|
+
estimatedAmount: bigint;
|
|
12
|
+
/** 0: the settlement coin itself, 1: the same coin on another chain, 2: another USD stablecoin, 3: any other coin. */
|
|
13
|
+
tier: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SelectionInput {
|
|
16
|
+
wallets: readonly ChainWallet[];
|
|
17
|
+
assets: readonly WalletAsset[];
|
|
18
|
+
tokens: readonly GatewayToken[];
|
|
19
|
+
settlement: {
|
|
20
|
+
asset_id: string;
|
|
21
|
+
symbol: string;
|
|
22
|
+
};
|
|
23
|
+
amountUsd: number;
|
|
24
|
+
}
|
|
25
|
+
/** Ranks held coins for paying `amountUsd`: same-coin routes first (no swap minimum, lowest fee), then stablecoins, then others. */
|
|
26
|
+
export declare function rankPaymentOptions(input: SelectionInput): Promise<{
|
|
27
|
+
options: PaymentOption[];
|
|
28
|
+
skipped: string[];
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parseUnits } from "viem";
|
|
2
|
+
import { isUsdStablecoin } from "./assets.js";
|
|
3
|
+
import { ESTIMATE_BUFFER } from "./config.js";
|
|
4
|
+
import { errorSummary } from "./errors.js";
|
|
5
|
+
function tierOf(asset, settlement) {
|
|
6
|
+
if (asset.asset_id === settlement.asset_id)
|
|
7
|
+
return 0;
|
|
8
|
+
if (asset.symbol.toUpperCase() === settlement.symbol.toUpperCase())
|
|
9
|
+
return 1;
|
|
10
|
+
return isUsdStablecoin(asset.symbol) ? 2 : 3;
|
|
11
|
+
}
|
|
12
|
+
/** Converts a USD amount to base units of a coin at `priceUsd`, rounded up. */
|
|
13
|
+
function usdToBaseUnits(amountUsd, priceUsd, decimals) {
|
|
14
|
+
return parseUnits((amountUsd / priceUsd).toFixed(decimals), decimals) + 1n;
|
|
15
|
+
}
|
|
16
|
+
/** Ranks held coins for paying `amountUsd`: same-coin routes first (no swap minimum, lowest fee), then stablecoins, then others. */
|
|
17
|
+
export async function rankPaymentOptions(input) {
|
|
18
|
+
const walletByChain = new Map(input.wallets.map((wallet) => [wallet.blockchain, wallet]));
|
|
19
|
+
const prices = new Map(input.tokens.map((token) => [token.asset_id, token]));
|
|
20
|
+
const candidates = [...input.assets];
|
|
21
|
+
const settlementToken = prices.get(input.settlement.asset_id);
|
|
22
|
+
if (settlementToken && walletByChain.has(settlementToken.blockchain) && !candidates.some((asset) => asset.asset_id === settlementToken.asset_id)) {
|
|
23
|
+
const { asset_id, symbol, blockchain, decimals, contract_address } = settlementToken;
|
|
24
|
+
candidates.push({ asset_id, symbol, blockchain, decimals, contract_address });
|
|
25
|
+
}
|
|
26
|
+
const skipped = [];
|
|
27
|
+
const checks = candidates.map(async (asset) => {
|
|
28
|
+
const wallet = walletByChain.get(asset.blockchain);
|
|
29
|
+
const listed = prices.get(asset.asset_id);
|
|
30
|
+
const priceUsd = listed?.price_usd;
|
|
31
|
+
if (!wallet)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!listed || !priceUsd || priceUsd <= 0) {
|
|
34
|
+
skipped.push(`${asset.symbol} on ${asset.blockchain}: no price from the gateway`);
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Amounts and spending limits are worked out with the configured decimals, so a coin set up with the wrong ones is never used. */
|
|
38
|
+
if (listed.decimals !== asset.decimals) {
|
|
39
|
+
skipped.push(`${asset.symbol} on ${asset.blockchain}: set to ${asset.decimals} decimals, but the gateway lists ${listed.decimals}`);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const estimatedAmount = usdToBaseUnits(input.amountUsd * (1 + ESTIMATE_BUFFER), priceUsd, asset.decimals);
|
|
43
|
+
let balance;
|
|
44
|
+
try {
|
|
45
|
+
balance = await wallet.balance(asset);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
skipped.push(`${asset.symbol} on ${asset.blockchain}: balance unavailable (${errorSummary(error, 120)})`);
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (balance < estimatedAmount) {
|
|
52
|
+
skipped.push(`${asset.symbol} on ${asset.blockchain}: balance too low`);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return { asset, wallet, priceUsd, balance, estimatedAmount, tier: tierOf(asset, input.settlement) };
|
|
56
|
+
});
|
|
57
|
+
const options = (await Promise.all(checks)).filter((option) => option !== undefined);
|
|
58
|
+
options.sort((a, b) => a.tier - b.tier);
|
|
59
|
+
return { options, skipped };
|
|
60
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const DEFAULT_NEAR_RPC_URL = "https://free.rpc.fastnear.com";
|
|
2
|
+
export declare const DEFAULT_BASE_RPC_URL = "https://mainnet.base.org";
|
|
3
|
+
/** Timeout for one RPC or Solverr API call. */
|
|
4
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 15000;
|
|
5
|
+
/** Default `resourceTimeoutMs`: the first resource request when the caller passes no `signal`, and every paid attempt, which ignores the
|
|
6
|
+
caller's signal; each includes reading the body. */
|
|
7
|
+
export declare const DEFAULT_RESOURCE_TIMEOUT_MS = 30000;
|
|
8
|
+
/** Total sends of the paid request, the first included (repeated after network errors, 429 or 5xx), and the delay step between them. */
|
|
9
|
+
export declare const PAID_REQUEST_ATTEMPTS = 3;
|
|
10
|
+
export declare const PAID_REQUEST_RETRY_MS = 1000;
|
|
11
|
+
/** Longest wait between paid attempts, even when a 429 or 503 asks for more in `Retry-After`; a paywall's challenge limit counts per minute. */
|
|
12
|
+
export declare const PAID_REQUEST_MAX_WAIT_MS = 60000;
|
|
13
|
+
/** How long to wait for an invoice to become paid after the transfer. */
|
|
14
|
+
export declare const DEFAULT_PAYMENT_TIMEOUT_MS = 180000;
|
|
15
|
+
/** How long to wait for a transfer to be included on-chain before relying on the gateway to detect it. */
|
|
16
|
+
export declare const DEFAULT_TRANSFER_TIMEOUT_MS = 60000;
|
|
17
|
+
export declare const STATUS_POLL_INTERVAL_MS = 3000;
|
|
18
|
+
/** Different coins are tried in turn when a quote is refused, up to this many. */
|
|
19
|
+
export declare const MAX_QUOTE_ATTEMPTS = 3;
|
|
20
|
+
/** Headroom over the settlement value when estimating the amount a coin must cover (fees and slippage). */
|
|
21
|
+
export declare const ESTIMATE_BUFFER = 0.03;
|
|
22
|
+
/** A quote may cost at most this share more than the invoice's USD value (not the buffered estimate) before the payment is refused. */
|
|
23
|
+
export declare const MAX_QUOTE_OVER_ESTIMATE = 0.1;
|
|
24
|
+
/** A quote must stay valid at least this long after it is received. */
|
|
25
|
+
export declare const MIN_QUOTE_VALIDITY_MS = 60000;
|
|
26
|
+
/** Entries older than this are dropped from the spend log. */
|
|
27
|
+
export declare const SPEND_LOG_RETENTION_MS: number;
|
|
28
|
+
/** How often a busy spend log lock is retried, and how long to wait for it in total. */
|
|
29
|
+
export declare const SPEND_LOG_LOCK_RETRY_MS = 50;
|
|
30
|
+
export declare const SPEND_LOG_LOCK_TIMEOUT_MS = 10000;
|
|
31
|
+
/** A lock file older than this was left by a process that stopped while holding it. */
|
|
32
|
+
export declare const SPEND_LOG_STALE_LOCK_MS = 30000;
|
|
33
|
+
export declare const DAY_MS: number;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const DEFAULT_NEAR_RPC_URL = "https://free.rpc.fastnear.com";
|
|
2
|
+
export const DEFAULT_BASE_RPC_URL = "https://mainnet.base.org";
|
|
3
|
+
/** Timeout for one RPC or Solverr API call. */
|
|
4
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
5
|
+
/** Default `resourceTimeoutMs`: the first resource request when the caller passes no `signal`, and every paid attempt, which ignores the
|
|
6
|
+
caller's signal; each includes reading the body. */
|
|
7
|
+
export const DEFAULT_RESOURCE_TIMEOUT_MS = 30_000;
|
|
8
|
+
/** Total sends of the paid request, the first included (repeated after network errors, 429 or 5xx), and the delay step between them. */
|
|
9
|
+
export const PAID_REQUEST_ATTEMPTS = 3;
|
|
10
|
+
export const PAID_REQUEST_RETRY_MS = 1_000;
|
|
11
|
+
/** Longest wait between paid attempts, even when a 429 or 503 asks for more in `Retry-After`; a paywall's challenge limit counts per minute. */
|
|
12
|
+
export const PAID_REQUEST_MAX_WAIT_MS = 60_000;
|
|
13
|
+
/** How long to wait for an invoice to become paid after the transfer. */
|
|
14
|
+
export const DEFAULT_PAYMENT_TIMEOUT_MS = 180_000;
|
|
15
|
+
/** How long to wait for a transfer to be included on-chain before relying on the gateway to detect it. */
|
|
16
|
+
export const DEFAULT_TRANSFER_TIMEOUT_MS = 60_000;
|
|
17
|
+
export const STATUS_POLL_INTERVAL_MS = 3_000;
|
|
18
|
+
/** Different coins are tried in turn when a quote is refused, up to this many. */
|
|
19
|
+
export const MAX_QUOTE_ATTEMPTS = 3;
|
|
20
|
+
/** Headroom over the settlement value when estimating the amount a coin must cover (fees and slippage). */
|
|
21
|
+
export const ESTIMATE_BUFFER = 0.03;
|
|
22
|
+
/** A quote may cost at most this share more than the invoice's USD value (not the buffered estimate) before the payment is refused. */
|
|
23
|
+
export const MAX_QUOTE_OVER_ESTIMATE = 0.1;
|
|
24
|
+
/** A quote must stay valid at least this long after it is received. */
|
|
25
|
+
export const MIN_QUOTE_VALIDITY_MS = 60_000;
|
|
26
|
+
/** Entries older than this are dropped from the spend log. */
|
|
27
|
+
export const SPEND_LOG_RETENTION_MS = 30 * 24 * 60 * 60_000;
|
|
28
|
+
/** How often a busy spend log lock is retried, and how long to wait for it in total. */
|
|
29
|
+
export const SPEND_LOG_LOCK_RETRY_MS = 50;
|
|
30
|
+
export const SPEND_LOG_LOCK_TIMEOUT_MS = 10_000;
|
|
31
|
+
/** A lock file older than this was left by a process that stopped while holding it. */
|
|
32
|
+
export const SPEND_LOG_STALE_LOCK_MS = 30_000;
|
|
33
|
+
export const DAY_MS = 24 * 60 * 60_000;
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentWalletOptions } from "./agent-wallet.js";
|
|
2
|
+
/** Reads AGENT_* variables. Required: AGENT_NEAR_ACCOUNT + AGENT_NEAR_KEY or AGENT_BASE_KEY, AGENT_LIMIT_PER_PAYMENT_USD, AGENT_LIMIT_DAILY_USD,
|
|
3
|
+
AGENT_ALLOW_HOSTS, AGENT_TRUSTED_GATEWAYS; optional: AGENT_NEAR_RPC_URL, AGENT_BASE_RPC_URL, AGENT_SPEND_LOG, AGENT_PAYMENT_TIMEOUT_MS, AGENT_RESOURCE_TIMEOUT_MS. */
|
|
4
|
+
export declare function agentOptionsFromEnv(env?: NodeJS.ProcessEnv): AgentWalletOptions;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function required(env, name) {
|
|
2
|
+
const value = env[name]?.trim();
|
|
3
|
+
if (!value)
|
|
4
|
+
throw new Error(`${name} is required.`);
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function usd(env, name) {
|
|
8
|
+
const value = Number(required(env, name));
|
|
9
|
+
if (!Number.isFinite(value) || value < 0)
|
|
10
|
+
throw new Error(`${name} must be a USD amount, such as 5.`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function milliseconds(env, name) {
|
|
14
|
+
const value = env[name]?.trim();
|
|
15
|
+
if (!value)
|
|
16
|
+
return undefined;
|
|
17
|
+
const ms = Number(value);
|
|
18
|
+
if (!Number.isInteger(ms) || ms <= 0)
|
|
19
|
+
throw new Error(`${name} must be a whole number of milliseconds.`);
|
|
20
|
+
return ms;
|
|
21
|
+
}
|
|
22
|
+
const list = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
23
|
+
/** Reads AGENT_* variables. Required: AGENT_NEAR_ACCOUNT + AGENT_NEAR_KEY or AGENT_BASE_KEY, AGENT_LIMIT_PER_PAYMENT_USD, AGENT_LIMIT_DAILY_USD,
|
|
24
|
+
AGENT_ALLOW_HOSTS, AGENT_TRUSTED_GATEWAYS; optional: AGENT_NEAR_RPC_URL, AGENT_BASE_RPC_URL, AGENT_SPEND_LOG, AGENT_PAYMENT_TIMEOUT_MS, AGENT_RESOURCE_TIMEOUT_MS. */
|
|
25
|
+
export function agentOptionsFromEnv(env = process.env) {
|
|
26
|
+
const nearAccount = env.AGENT_NEAR_ACCOUNT?.trim();
|
|
27
|
+
const nearKey = env.AGENT_NEAR_KEY?.trim();
|
|
28
|
+
if (!!nearAccount !== !!nearKey)
|
|
29
|
+
throw new Error("Set both AGENT_NEAR_ACCOUNT and AGENT_NEAR_KEY to use a NEAR wallet.");
|
|
30
|
+
const baseKey = env.AGENT_BASE_KEY?.trim();
|
|
31
|
+
if (!nearAccount && !baseKey)
|
|
32
|
+
throw new Error("Set AGENT_NEAR_ACCOUNT and AGENT_NEAR_KEY, or AGENT_BASE_KEY.");
|
|
33
|
+
const allow = list(env.AGENT_ALLOW_HOSTS);
|
|
34
|
+
if (allow.length === 0)
|
|
35
|
+
throw new Error("AGENT_ALLOW_HOSTS is required: a comma-separated list of hosts the agent may pay.");
|
|
36
|
+
const trustedGateways = list(env.AGENT_TRUSTED_GATEWAYS);
|
|
37
|
+
if (trustedGateways.length === 0) {
|
|
38
|
+
throw new Error("AGENT_TRUSTED_GATEWAYS is required: a comma-separated list of Solverr URLs the agent may pay through, such as https://pay.example.com.");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...(nearAccount && nearKey ? { near: { accountId: nearAccount, privateKey: nearKey, rpcUrl: env.AGENT_NEAR_RPC_URL?.trim() || undefined } } : {}),
|
|
42
|
+
...(baseKey ? { base: { privateKey: baseKey, rpcUrl: env.AGENT_BASE_RPC_URL?.trim() || undefined } } : {}),
|
|
43
|
+
limits: { perPaymentUsd: usd(env, "AGENT_LIMIT_PER_PAYMENT_USD"), dailyUsd: usd(env, "AGENT_LIMIT_DAILY_USD"), allow },
|
|
44
|
+
spendLogPath: env.AGENT_SPEND_LOG?.trim() || undefined,
|
|
45
|
+
trustedGateways,
|
|
46
|
+
paymentTimeoutMs: milliseconds(env, "AGENT_PAYMENT_TIMEOUT_MS"),
|
|
47
|
+
resourceTimeoutMs: milliseconds(env, "AGENT_RESOURCE_TIMEOUT_MS"),
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type PaymentErrorCode = "host_not_allowed" | "per_payment_limit" | "daily_limit" | "invalid_challenge" | "untrusted_gateway" | "test_invoice" | "invoice_expired" | "no_funds" | "quote_unavailable" | "quote_mismatch" | "transfer_failed" | "payment_failed" | "payment_timeout" | "payment_unconfirmed" | "paid_request_failed" | "body_not_replayable";
|
|
2
|
+
/** First line of an error message, bounded, for logs and tool output. */
|
|
3
|
+
export declare function errorSummary(error: unknown, maxLength?: number): string;
|
|
4
|
+
/** A payment the agent refused or could not complete; `fundsSent` is true once a transfer was broadcast, and `invoiceId` then names its invoice. */
|
|
5
|
+
export declare class PaymentError extends Error {
|
|
6
|
+
readonly code: PaymentErrorCode;
|
|
7
|
+
readonly fundsSent: boolean;
|
|
8
|
+
readonly invoiceId?: string | undefined;
|
|
9
|
+
constructor(code: PaymentErrorCode, message: string, fundsSent?: boolean, invoiceId?: string | undefined);
|
|
10
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** First line of an error message, bounded, for logs and tool output. */
|
|
2
|
+
export function errorSummary(error, maxLength = 300) {
|
|
3
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0].slice(0, maxLength);
|
|
4
|
+
}
|
|
5
|
+
/** A payment the agent refused or could not complete; `fundsSent` is true once a transfer was broadcast, and `invoiceId` then names its invoice. */
|
|
6
|
+
export class PaymentError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
fundsSent;
|
|
9
|
+
invoiceId;
|
|
10
|
+
constructor(code, message, fundsSent = false, invoiceId) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.fundsSent = fundsSent;
|
|
14
|
+
this.invoiceId = invoiceId;
|
|
15
|
+
this.name = "PaymentError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** One coin from `GET /api/v1/tokens`. */
|
|
2
|
+
export interface GatewayToken {
|
|
3
|
+
asset_id: string;
|
|
4
|
+
symbol: string;
|
|
5
|
+
blockchain: string;
|
|
6
|
+
decimals: number;
|
|
7
|
+
price_usd: number;
|
|
8
|
+
contract_address: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** Payment attempt returned by `POST /api/v1/invoices/:id/quote`. */
|
|
11
|
+
export interface GatewayQuote {
|
|
12
|
+
id: string;
|
|
13
|
+
coin: {
|
|
14
|
+
asset_id: string;
|
|
15
|
+
symbol: string;
|
|
16
|
+
blockchain: string;
|
|
17
|
+
decimals: number;
|
|
18
|
+
};
|
|
19
|
+
amount_in: string;
|
|
20
|
+
amount_in_base: string;
|
|
21
|
+
deposit_address: string;
|
|
22
|
+
deposit_memo: string | null;
|
|
23
|
+
refund_address: string;
|
|
24
|
+
deadline: string;
|
|
25
|
+
}
|
|
26
|
+
/** Public checkout status from `GET /api/v1/invoices/:id/status`. */
|
|
27
|
+
export interface GatewayStatus {
|
|
28
|
+
status: string;
|
|
29
|
+
payment: (GatewayQuote & {
|
|
30
|
+
status: string;
|
|
31
|
+
}) | null;
|
|
32
|
+
}
|
|
33
|
+
/** Public Solverr API calls the agent makes for one invoice. URLs come from a validated challenge. */
|
|
34
|
+
export declare class GatewayClient {
|
|
35
|
+
private readonly timeoutMs;
|
|
36
|
+
constructor(timeoutMs: number);
|
|
37
|
+
listTokens(tokensUrl: string): Promise<GatewayToken[]>;
|
|
38
|
+
/** Asks for a deposit address; retried once, since the server returns the same open quote for the same coin and refund address. */
|
|
39
|
+
requestQuote(quoteUrl: string, originAsset: string, refundAddress: string): Promise<GatewayQuote>;
|
|
40
|
+
/** Reports the transfer for the payment it was sent to, so a newer address on the invoice cannot take the report. */
|
|
41
|
+
reportDeposit(depositUrl: string, txHash: string, paymentId: string): Promise<GatewayStatus>;
|
|
42
|
+
getStatus(statusUrl: string): Promise<GatewayStatus>;
|
|
43
|
+
}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requestJson } from "solverr-x402";
|
|
2
|
+
const RETRIES = 2;
|
|
3
|
+
/** Public Solverr API calls the agent makes for one invoice. URLs come from a validated challenge. */
|
|
4
|
+
export class GatewayClient {
|
|
5
|
+
timeoutMs;
|
|
6
|
+
constructor(timeoutMs) {
|
|
7
|
+
this.timeoutMs = timeoutMs;
|
|
8
|
+
}
|
|
9
|
+
async listTokens(tokensUrl) {
|
|
10
|
+
const body = await requestJson({ method: "GET", url: tokensUrl, timeoutMs: this.timeoutMs, retries: RETRIES });
|
|
11
|
+
return body.data;
|
|
12
|
+
}
|
|
13
|
+
/** Asks for a deposit address; retried once, since the server returns the same open quote for the same coin and refund address. */
|
|
14
|
+
requestQuote(quoteUrl, originAsset, refundAddress) {
|
|
15
|
+
return requestJson({ method: "POST", url: quoteUrl, body: { origin_asset: originAsset, refund_address: refundAddress }, timeoutMs: this.timeoutMs, retries: 1 });
|
|
16
|
+
}
|
|
17
|
+
/** Reports the transfer for the payment it was sent to, so a newer address on the invoice cannot take the report. */
|
|
18
|
+
reportDeposit(depositUrl, txHash, paymentId) {
|
|
19
|
+
return requestJson({ method: "POST", url: depositUrl, body: { tx_hash: txHash, payment_id: paymentId }, timeoutMs: this.timeoutMs, retries: RETRIES });
|
|
20
|
+
}
|
|
21
|
+
getStatus(statusUrl) {
|
|
22
|
+
return requestJson({ method: "GET", url: statusUrl, timeoutMs: this.timeoutMs, retries: RETRIES });
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createAgentWallet } from "./agent-wallet.js";
|
|
2
|
+
export type { AgentFetchInit, AgentWallet, AgentWalletOptions, AssetBalance, SpendingSummary } from "./agent-wallet.js";
|
|
3
|
+
export { agentOptionsFromEnv } from "./env.js";
|
|
4
|
+
export { serveAgentMcp } from "./mcp-server.js";
|
|
5
|
+
export type { McpServerOptions } from "./mcp-server.js";
|
|
6
|
+
export { PaymentError } from "./errors.js";
|
|
7
|
+
export type { PaymentErrorCode } from "./errors.js";
|
|
8
|
+
export type { SpendingLimits } from "./limits.js";
|
|
9
|
+
export type { SpendEntry, SpendStatus } from "./spend-log.js";
|
|
10
|
+
export { DEFAULT_ASSETS } from "./assets.js";
|
|
11
|
+
export type { WalletAsset } from "./assets.js";
|
package/dist/index.js
ADDED
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SpendingLimits {
|
|
2
|
+
/** Largest single payment in USD. */
|
|
3
|
+
perPaymentUsd: number;
|
|
4
|
+
/** Largest total in USD over any rolling 24 hours. */
|
|
5
|
+
dailyUsd: number;
|
|
6
|
+
/** Hosts the agent may pay: `api.example.com`, or `*.example.com` for its subdomains. An empty list allows none. */
|
|
7
|
+
allow: string[];
|
|
8
|
+
}
|
|
9
|
+
/** Validates limits and normalizes host patterns to lowercase. */
|
|
10
|
+
export declare function normalizeLimits(limits: SpendingLimits): SpendingLimits;
|
|
11
|
+
export declare function isHostAllowed(hostname: string, allow: readonly string[]): boolean;
|
|
12
|
+
export declare function assertHostAllowed(limits: SpendingLimits, hostname: string): void;
|
|
13
|
+
/** Throws a `PaymentError` when paying `amountUsd` to `hostname` would break a limit. */
|
|
14
|
+
export declare function enforceLimits(limits: SpendingLimits, payment: {
|
|
15
|
+
hostname: string;
|
|
16
|
+
amountUsd: number;
|
|
17
|
+
spentLast24hUsd: number;
|
|
18
|
+
}): void;
|