xytara 3.1.10 → 3.1.12

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/.env.example CHANGED
@@ -17,6 +17,7 @@ XYTARA_X402_TOPUP_UNITS=1000
17
17
  # The runtime mints a fresh CDP Bearer JWT per request when both are set.
18
18
  CDP_API_KEY_ID=
19
19
  CDP_API_KEY_SECRET=
20
+ # (XYTARA_X402_CDP_KEY_ID / XYTARA_X402_CDP_KEY_SECRET are accepted as aliases for the two above)
20
21
  XYTARA_PAYMENT_MAX_AGE_SECONDS=300
21
22
  XYTARA_WALLET_REGISTRY_JSON=[]
22
23
  XYTARA_TREASURY_DESTINATION_ID=
@@ -24,6 +25,9 @@ XYTARA_TREASURY_DESTINATION_KIND=
24
25
  XYTARA_TREASURY_ACCOUNT_ID=
25
26
  XYTARA_TREASURY_EXTERNAL_REF=
26
27
  XYTARA_TREASURY_DESTINATIONS_JSON={}
28
+ # optional file-backed alternatives to the two *_JSON values above (blank = use the JSON values)
29
+ XYTARA_WALLET_REGISTRY_PATH=
30
+ XYTARA_TREASURY_DESTINATIONS_PATH=
27
31
  XYTARA_BSV_TERANODE_NETWORK=mainnet
28
32
  XYTARA_BSV_TERANODE_RPC_URL=
29
33
  XYTARA_BSV_TERANODE_MODE=opreturn
@@ -37,6 +41,18 @@ XYTARA_BSV_TERANODE_SETTLEMENT_RUNTIME_MODE=mock
37
41
  # operator write surfaces fail closed until both tokens are set
38
42
  XYTARA_CREDIT_BRIDGE_BEARER_TOKEN=
39
43
  XYTARA_ACCOUNT_AUTH_BEARER_TOKEN=
44
+ # commerce-bridge write surface (distinct from the credit bridge); also fails closed until set
45
+ XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN=
46
+ # optional: require spend-proof binding on spend-credential use (blank = runtime default)
47
+ XYTARA_REQUIRE_SPEND_PROOF_BINDING=
48
+ # hosted-checkout webhook verification (blank = runtime defaults; set the secret before going live)
49
+ XYTARA_CHECKOUT_WEBHOOK_SECRET=
50
+ XYTARA_HOSTED_CHECKOUT_WEBHOOK_SECRET=
51
+ XYTARA_CHECKOUT_WEBHOOK_VERIFICATION_REQUIRED=
52
+ XYTARA_CHECKOUT_WEBHOOK_MAX_AGE_SECONDS=
53
+ # optional commerce pack map / prep TTL overrides (blank = runtime defaults)
54
+ XYTARA_COMMERCE_PACK_MAP_JSON=
55
+ XYTARA_COMMERCE_PREP_TTL_MS=
40
56
  XYTARA_CALLBACK_DELIVERY_ENABLED=true
41
57
  XYTARA_CALLBACK_SIGNING_SECRET=
42
58
  XYTARA_CALLBACK_TIMEOUT_MS=5000
@@ -47,10 +63,52 @@ XYTARA_SETTLEMENT_REFRESH_INTERVAL_MS=1500
47
63
  XYTARA_RATE_LIMIT_ENABLED=true
48
64
  XYTARA_RATE_LIMIT_WINDOW_MS=60000
49
65
  XYTARA_RATE_LIMIT_MAX_REQUESTS=600
50
- XYTARA_EXTERNAL_EXECUTION_REQUIRED=false
66
+ # optional: scope the rate limit per route instead of globally (blank = runtime default)
67
+ XYTARA_RATE_LIMIT_ROUTE_SCOPED=
68
+ XYTARA_REQUEST_TIMEOUT_MS=30000
69
+ XYTARA_HEADERS_TIMEOUT_MS=15000
51
70
  XYTARA_EXECUTION_TARGETS_JSON=[]
71
+ # External execution (env targets + registered providers). Cold-start ride-out retries ONLY on
72
+ # not-processed signals (408/429/503, connection refused/DNS). The budget is the TOTAL wall-clock for
73
+ # one buyer call across all retries AND failover candidates, kept inside the ~60s x402 authorization
74
+ # window so a ridden-out call still settles. Per-target timeout_ms is capped at 30000 for the same reason.
75
+ XYTARA_EXTERNAL_EXECUTION_RETRY_WINDOW_MS=45000
76
+ XYTARA_EXTERNAL_EXECUTION_MAX_RETRIES=29
77
+ XYTARA_EXTERNAL_EXECUTION_BUDGET_MS=50000
78
+ # Open execution-provider network (lib/provider_registry.js): providers self-register an endpoint +
79
+ # the capabilities they serve; the runtime routes paid calls to ACTIVE providers and splits revenue.
80
+ # Registration FAILS CLOSED until the bearer is set, UNLESS open registration is turned on. Every
81
+ # provider still lands pending until an operator activates it (the vetting gate). See the live
82
+ # machine contract at GET /v1/provider-network/contract.
83
+ XYTARA_PROVIDER_REGISTRY_BEARER_TOKEN=
84
+ # 1 = permissionless self-registration (no bearer); still rate-limited + pending-capped.
85
+ XYTARA_PROVIDER_OPEN_REGISTRATION=0
86
+ XYTARA_PROVIDER_REGISTRATION_RATE_PER_MIN=5
87
+ XYTARA_PROVIDER_REGISTRATION_RATE_GLOBAL_PER_MIN=60
88
+ XYTARA_PROVIDER_MAX_PENDING=100
89
+ # provider endpoint policy: strict (default when RENDER is set) = https only + no loopback/private/
90
+ # link-local/metadata hosts, re-checked by DNS at dispatch; permissive (default locally) for dev/tests
91
+ XYTARA_PROVIDER_ENDPOINT_POLICY=
92
+ # cap on a provider's revenue share, basis points (10000 = 100%)
93
+ XYTARA_PROVIDER_MAX_REVENUE_SHARE_BPS=10000
94
+ # health/failover: consecutive failures -> unhealthy; cooldown before a half-open retry; max candidates tried
95
+ XYTARA_PROVIDER_UNHEALTHY_THRESHOLD=3
96
+ XYTARA_PROVIDER_UNHEALTHY_COOLDOWN_MS=60000
97
+ XYTARA_PROVIDER_FAILOVER_MAX=3
98
+ # routing reputation recency decay, factor in (0,1): fades older observations so recent performance
99
+ # weighs more. 1 (default) = no decay (lifetime counts). e.g. 0.95 once the network has real history.
100
+ XYTARA_PROVIDER_REPUTATION_DECAY=1
101
+ # Provider payout auto-execution via an EXTERNAL signer (the runtime stays key-less). OFF unless the
102
+ # URL is set; otherwise payouts are recorded and the operator sends + confirms them. See services/payout-signer.
103
+ XYTARA_PAYOUT_SIGNER_URL=
104
+ XYTARA_PAYOUT_SIGNER_BEARER=
105
+ XYTARA_PAYOUT_SIGNER_TIMEOUT_MS=20000
52
106
  XYTARA_L402_ENABLED=false
53
107
  XYTARA_L402_SHARED_SECRET=
108
+ # optional L402 tuning (blank = runtime defaults)
109
+ XYTARA_L402_MAX_AGE_SECONDS=
110
+ XYTARA_L402_INVOICE_RATE_PER_MIN=
111
+ XYTARA_L402_INVOICE_RATE_GLOBAL_PER_MIN=
54
112
  XYTARA_BOLT_ENABLED=false
55
113
  XYTARA_BOLT_SHARED_SECRET=
56
114
  XYTARA_BOLT_NETWORK=mainnet
@@ -61,6 +119,8 @@ XYTARA_MPP_SHARED_SECRET=
61
119
  XYTARA_MPP_PROVIDER=stripe_tempo_compatible
62
120
  XYTARA_MPP_SETTLEMENT_FAMILY=cards_or_stablecoin_or_lightning
63
121
  XYTARA_MPP_MAX_AGE_SECONDS=300
122
+ # optional minimum MPP amount in minor units (blank = runtime default)
123
+ XYTARA_MPP_MIN_AMOUNT_MINOR=
64
124
  XYTARA_NETWORK_DNS_RESULT_ORDER=verbatim
65
125
  XYTARA_NETWORK_PREFERRED_IP_FAMILY=auto
66
126
  XYTARA_EXTERNAL_EXECUTION_IP_FAMILY=auto
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xytara
2
2
 
3
- [![smithery badge](https://smithery.ai/badge/naxytra/xytara-mcp-executor)](https://smithery.ai/servers/naxytra/xytara-mcp-executor)
3
+ Listed on [Smithery](https://smithery.ai/server/naxytra/xytara-mcp-executor) · [Glama](https://glama.ai/mcp/servers/naxytra/xytara-mcp-executor) · the official MCP registry (`com.naxytra/xytara-mcp-executor`).
4
4
 
5
5
  **x402 pay-per-call agent-commerce runtime** — the payment and execution layer of the naxytra
6
6
  machine stack. An agent discovers a capability, pays once with x402 (gasless USDC on Base), and gets
package/RELEASE_NOTES.md CHANGED
@@ -1,7 +1,5 @@
1
- `xytara` 3.1.10 adds a free-tier cold-start ride-out and a reference payout signer, and refreshes the public collateral.
1
+ `xytara` 3.1.12 makes the public adapter-depth surface report open-network providers truthfully.
2
2
 
3
- Highlights:
4
-
5
- - **Cold-start ride-out** — external execution now retries only not-processed signals (408/429/503 and connection-refused) within the x402 settlement window, so a cold free-tier provider warms up mid-retry instead of failing an unsettled call. No always-on keepalive is required. An ambiguous timeout or reset is never retried, so there is no double-execution.
6
- - **Reference payout signer** (`services/payout-signer`) — a separately-deployed service that sends provider payouts on Base while the runtime stays key-less. Fail-closed and double-pay-safe, with an optional durable (Supabase) idempotency store so a free-tier restart cannot cause a re-send.
7
- - Refreshed README and package metadata to lead with the proven, mainnet-demonstrated capabilities.
3
+ - `/v1/adapter-depth` and `/v1/adapter-depth/summary` now count ACTIVE open-network providers that declared an adapter task as provider-live paths, alongside operator env targets. Each is labelled `routing: open_provider_registry`, and the pack lists its `depth_sources`. A pending provider never counts, and a provider is shown as shadowed (not counted) when an operator env target pins the whole lane, because env targets are routed first.
4
+ - Previously the surface only looked at env targets, so it reported `provider_wiring_required` even while paid MCP calls were being served by the live registry provider. It now reports `initial_provider_live_path` for the task that provider serves.
5
+ - Internal callers without runtime state keep the env-only view, so no other surface changes.
@@ -3,6 +3,7 @@
3
3
  const packageJson = require("../package.json");
4
4
  const { catalog } = require("../fixtures/catalog");
5
5
  const { loadExecutionTargetRegistry } = require("./external_execution_runtime");
6
+ const { listProviders } = require("./provider_registry");
6
7
 
7
8
  function normalizeString(value, fallback = "") {
8
9
  const raw = typeof value === "string" ? value.trim() : "";
@@ -59,10 +60,60 @@ function summarizeTarget(target) {
59
60
  };
60
61
  }
61
62
 
62
- function summarizeAdapterTaskDepth(task) {
63
- const targets = listTargetsForTask(task.task_ref).map(summarizeTarget);
63
+ // ACTIVE open-network providers that declared this task_ref. Env targets resolve before the
64
+ // registry at call time, so a provider is shadowed (not routed) when an enabled env target pins
65
+ // the WHOLE lane (task_ref with no tool/integration selector); it is then listed but not counted.
66
+ function listRegistryProvidersForTask(state, taskRef, envTargets) {
67
+ if (!state) return [];
68
+ const normalizedTaskRef = normalizeString(taskRef);
69
+ const wholeLanePinned = envTargets.some((target) =>
70
+ target.enabled !== false
71
+ && normalizeString(target.task_ref) === normalizedTaskRef
72
+ && !normalizeString(target.tool_name)
73
+ && !normalizeString(target.integration_id)
74
+ );
75
+ return listProviders(state, { status: "active" })
76
+ .filter((provider) => Array.isArray(provider.task_refs) && provider.task_refs.includes(normalizedTaskRef))
77
+ .map((provider) => {
78
+ const health = (provider.health && provider.health.status) || "healthy";
79
+ return {
80
+ target_id: `registry:${provider.provider_id}`,
81
+ provider: provider.provider_id,
82
+ routing: "open_provider_registry",
83
+ task_ref: normalizedTaskRef,
84
+ tool_name: null,
85
+ tool_names: Array.isArray(provider.tool_names) ? provider.tool_names.slice() : [],
86
+ integration_id: null,
87
+ enabled: !wholeLanePinned,
88
+ shadowed_by_env_target: wholeLanePinned,
89
+ health_status: health,
90
+ timeout_ms: provider.timeout_ms,
91
+ endpoint_configured: Boolean(normalizeString(provider.endpoint_url)),
92
+ auth_configured: Boolean(normalizeString(provider.auth_token)),
93
+ operational_coverage: {
94
+ execute_endpoint: Boolean(normalizeString(provider.endpoint_url)),
95
+ bearer_auth: Boolean(normalizeString(provider.auth_token)),
96
+ bounded_timeout: Number(provider.timeout_ms || 0) >= 500,
97
+ // The provider-network execution contract fixes the deliverable shape
98
+ // (result.output / output_payload), and activation is operator-vetted against it.
99
+ proof_fact_compatible: true,
100
+ deterministic_retry_surface: true,
101
+ tool_level_binding: Array.isArray(provider.tool_names) && provider.tool_names.length > 0,
102
+ operator_health_surface: "runtime_tracked_provider_health",
103
+ operator_tool_catalog_surface: null
104
+ }
105
+ };
106
+ });
107
+ }
108
+
109
+ function summarizeAdapterTaskDepth(task, state) {
110
+ const envTargets = listTargetsForTask(task.task_ref).map(summarizeTarget);
111
+ const registryTargets = listRegistryProvidersForTask(state, task.task_ref, loadExecutionTargetRegistry());
112
+ const targets = envTargets.concat(registryTargets);
64
113
  const enabledTargets = targets.filter((target) => target.enabled);
65
- const toolNames = Array.from(new Set(enabledTargets.map((target) => target.tool_name).filter(Boolean))).sort();
114
+ const toolNames = Array.from(new Set(enabledTargets
115
+ .flatMap((target) => [target.tool_name].concat(target.tool_names || []))
116
+ .filter(Boolean))).sort();
66
117
  const allTargetsCovered = enabledTargets.length > 0 && enabledTargets.every((target) =>
67
118
  target.operational_coverage.execute_endpoint
68
119
  && target.operational_coverage.bounded_timeout
@@ -80,6 +131,8 @@ function summarizeAdapterTaskDepth(task) {
80
131
  provider_live: enabledTargets.length > 0,
81
132
  configured_target_count: targets.length,
82
133
  enabled_target_count: enabledTargets.length,
134
+ env_target_count: envTargets.length,
135
+ registry_provider_count: registryTargets.filter((target) => target.enabled).length,
83
136
  configured_tool_count: toolNames.length,
84
137
  configured_tools: toolNames,
85
138
  execution_targets: targets,
@@ -89,11 +142,17 @@ function summarizeAdapterTaskDepth(task) {
89
142
  };
90
143
  }
91
144
 
92
- function buildAdapterDepthPack() {
93
- const adapterTasks = listAdapterTasks().map(summarizeAdapterTaskDepth);
145
+ // `state` (optional): the live runtime state. When given, ACTIVE open-network providers count as
146
+ // provider-live paths alongside operator env targets (each labelled routing:open_provider_registry).
147
+ // Without it the pack is env-target-only, exactly as before.
148
+ function buildAdapterDepthPack(state) {
149
+ const adapterTasks = listAdapterTasks().map((task) => summarizeAdapterTaskDepth(task, state));
94
150
  const providerLiveTasks = adapterTasks.filter((task) => task.provider_live);
95
151
  const deepenedTasks = adapterTasks.filter((task) => task.provider_depth_state === "provider_live_path_deepened");
96
152
  const configuredTargets = loadExecutionTargetRegistry().filter((target) => target && target.enabled !== false);
153
+ const registryProviderIds = new Set(adapterTasks.flatMap((task) =>
154
+ task.execution_targets.filter((target) => target.routing === "open_provider_registry" && target.enabled).map((target) => target.provider)
155
+ ));
97
156
  return {
98
157
  ok: true,
99
158
  product: packageJson.name,
@@ -110,8 +169,10 @@ function buildAdapterDepthPack() {
110
169
  deepened_provider_live_task_count: deepenedTasks.length,
111
170
  configured_execution_target_count: configuredTargets.length,
112
171
  configured_provider_count: new Set(configuredTargets.map((target) => normalizeString(target.provider))).size,
113
- configured_tool_count: new Set(configuredTargets.map((target) => normalizeString(target.tool_name)).filter(Boolean)).size
172
+ configured_tool_count: new Set(configuredTargets.map((target) => normalizeString(target.tool_name)).filter(Boolean)).size,
173
+ registry_provider_count: registryProviderIds.size
114
174
  },
175
+ depth_sources: state ? ["operator_env_targets", "open_provider_registry"] : ["operator_env_targets"],
115
176
  adapter_tasks: adapterTasks,
116
177
  production_depth_requirements: [
117
178
  "at_least_one_enabled_execution_target_for_provider_backed_task",
@@ -136,8 +197,8 @@ function buildAdapterDepthPack() {
136
197
  };
137
198
  }
138
199
 
139
- function summarizeAdapterDepthPack() {
140
- const pack = buildAdapterDepthPack();
200
+ function summarizeAdapterDepthPack(state) {
201
+ const pack = buildAdapterDepthPack(state);
141
202
  return {
142
203
  ok: true,
143
204
  product: pack.product,
@@ -145,6 +206,7 @@ function summarizeAdapterDepthPack() {
145
206
  summary_version: "xytara-adapter-depth-summary-v1",
146
207
  adapter_depth_state: pack.adapter_depth_state,
147
208
  counts: pack.counts,
209
+ depth_sources: pack.depth_sources,
148
210
  public_claim_boundary: pack.public_claim_boundary,
149
211
  linked_surfaces: pack.linked_surfaces
150
212
  };
@@ -108,6 +108,9 @@ function classifyTask(task, settlementReadiness) {
108
108
  }
109
109
 
110
110
  if (normalizeString(task && task.public_category) === "adapter") {
111
+ // Only ENABLED targets make an adapter lane live — a bound-but-disabled target must not be
112
+ // reported as provider_live (adapter-depth filters the same way; the two surfaces must agree).
113
+ const enabledTargets = providerTargets.filter((target) => target.enabled);
111
114
  return {
112
115
  task_ref: task.public_task_ref,
113
116
  internal_task_ref: task.task_ref,
@@ -115,10 +118,11 @@ function classifyTask(task, settlementReadiness) {
115
118
  public_category: task.public_category,
116
119
  execution_mode: task.execution_mode,
117
120
  latency_class: task.latency_class,
118
- execution_truth: providerTargets.length > 0 ? "provider_live" : "provider_wiring_required",
119
- execution_origin: providerTargets.length > 0 ? "external_execution_target" : "adapter_lane_without_bound_target",
120
- commercialization_posture: providerTargets.length > 0 ? "sellable_live_now" : "sellable_by_integration",
121
- matching_execution_target_count: providerTargets.length,
121
+ execution_truth: enabledTargets.length > 0 ? "provider_live" : "provider_wiring_required",
122
+ execution_origin: enabledTargets.length > 0 ? "external_execution_target" : "adapter_lane_without_bound_target",
123
+ commercialization_posture: enabledTargets.length > 0 ? "sellable_live_now" : "sellable_by_integration",
124
+ matching_execution_target_count: enabledTargets.length,
125
+ disabled_execution_target_count: providerTargets.length - enabledTargets.length,
122
126
  matching_execution_targets: providerTargets,
123
127
  requirements: {
124
128
  configured_provider_target_required: true
@@ -10,6 +10,7 @@ const { resolveSpendCredential, evaluateAuthorityBindingAction } = require("./ac
10
10
  const { validateIntegrationSelection } = require("../integrations/dispatch");
11
11
  const { markPaymentAccepted, settleDeferredPayment, verifyPaymentAcceptance } = require("./payment_verification");
12
12
  const { executeExternalTarget, resolveExecutionTarget } = require("./external_execution_runtime");
13
+ const { externalExecutionBudgetMs } = require("./external_execution_policy");
13
14
  const { resolveRegisteredTargets, hasActiveMatchingProvider, recordProviderHealth, recordProviderRouting, recordProviderSettlement } = require("./provider_registry");
14
15
  const { claimDeferredExecution, releaseDeferredExecution } = require("./execution_dedupe");
15
16
 
@@ -94,12 +95,36 @@ function buildCreditRejectionResponse(creditDecision, quote) {
94
95
  };
95
96
  }
96
97
 
98
+ // What a provider is allowed to see of the buyer's payment context: the verification outcome and
99
+ // scalar payment facts only. NEVER the signed x402 authorization / deferred-settlement material
100
+ // (that would hand a third-party provider the buyer's signature before settlement), and never
101
+ // signature-, secret- or raw-looking keys or nested objects from any lane's payload.
102
+ const SENSITIVE_CONTEXT_KEY = /signature|secret|private|authorization|raw|base64|header|token/i;
103
+ function sanitizeAuthorizationContextForProvider(context) {
104
+ if (!context || typeof context !== "object") return null;
105
+ const payload = context.payment_payload && typeof context.payment_payload === "object" ? context.payment_payload : null;
106
+ const safePayload = payload
107
+ ? Object.fromEntries(Object.entries(payload).filter(([key, value]) => !SENSITIVE_CONTEXT_KEY.test(key) && (value === null || typeof value !== "object")))
108
+ : null;
109
+ return {
110
+ ok: context.ok === true,
111
+ verification_mode: context.verification_mode || null,
112
+ wallet_id: context.wallet_id || null,
113
+ age_seconds: Number.isFinite(Number(context.age_seconds)) ? Number(context.age_seconds) : null,
114
+ payment_payload: safePayload
115
+ };
116
+ }
117
+
97
118
  async function buildExternalExecutionOptions(state, payload, quote, authorizationContext) {
119
+ // One wall-clock budget for the whole call (every retry of every candidate), inside the x402
120
+ // authorization window — see external_execution_policy.js.
121
+ const executionOptions = { deadline_ms: Date.now() + externalExecutionBudgetMs() };
122
+ const forwardedContext = sanitizeAuthorizationContextForProvider(authorizationContext);
98
123
  // Operator-pinned env target resolves first and is used as-is (an explicit single target,
99
124
  // no failover). Empty env + empty registry -> {} (unchanged behavior).
100
125
  const envTarget = resolveExecutionTarget(payload, quote);
101
126
  if (envTarget) {
102
- const execution = await executeExternalTarget(envTarget, payload, quote, authorizationContext);
127
+ const execution = await executeExternalTarget(envTarget, payload, quote, forwardedContext, executionOptions);
103
128
  if (!execution || !execution.ok) {
104
129
  return { error: execution || { ok: false, status: 502, reason: "external_execution_failed" } };
105
130
  }
@@ -122,7 +147,12 @@ async function buildExternalExecutionOptions(state, payload, quote, authorizatio
122
147
  }
123
148
  let lastError = null;
124
149
  for (const target of candidates) {
125
- const execution = await executeExternalTarget(target, payload, quote, authorizationContext);
150
+ if (Date.now() >= executionOptions.deadline_ms) {
151
+ // Budget spent on earlier candidates: stop here rather than start a call that cannot settle.
152
+ lastError = lastError || { ok: false, status: 504, reason: "external_execution_budget_exhausted" };
153
+ break;
154
+ }
155
+ const execution = await executeExternalTarget(target, payload, quote, forwardedContext, executionOptions);
126
156
  try { recordProviderRouting(state, target.registered_provider_id); } catch (_) { /* non-fatal */ }
127
157
  if (execution && execution.ok) {
128
158
  try { recordProviderHealth(state, target.registered_provider_id, { ok: true }); } catch (_) { /* non-fatal */ }
@@ -139,9 +169,38 @@ async function buildExternalExecutionOptions(state, payload, quote, authorizatio
139
169
  return { error: lastError };
140
170
  }
141
171
 
172
+ // Assets the x402 lane can settle in, with their on-chain decimals (needed to turn settled base
173
+ // units into the ledger's cents). Anything else cannot be reconciled and must not accrue on the quote.
174
+ const X402_ASSET_DECIMALS = new Map([
175
+ ["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", 6], // USDC, Base mainnet (eip155:8453)
176
+ ["0x036cbd53842c5426634e7929541ec2318f3dcf7e", 6] // USDC, Base Sepolia (eip155:84532)
177
+ ]);
178
+
179
+ // The amount a provider's revenue share is computed from. The quote is what we ASKED for; on the
180
+ // x402 lane the facilitator settles exactly the advertised `accepts` amount, which may differ from
181
+ // the catalog quote — so accrue on min(quote, settled) and never on money that was not collected.
182
+ // Other lanes bind the paid amount to the quote at verification time (local_signed) or charge the
183
+ // quote directly (credits / hosted checkout), so the quote is the settled amount there.
184
+ function reconcileProviderSettlementAmount(quote, verification) {
185
+ const quoted = quote && Number.isFinite(Number(quote.amount_minor)) ? Math.max(0, Math.round(Number(quote.amount_minor))) : 0;
186
+ const mode = verification && typeof verification.verification_mode === "string" ? verification.verification_mode : null;
187
+ if (mode !== "x402_facilitator") {
188
+ return { amount_minor: quoted, quoted_minor: quoted, settled_minor: null, settlement_basis: mode ? `${mode}_quote` : "quote", reconciled: true };
189
+ }
190
+ const paymentPayload = (verification && verification.payment_payload) || {};
191
+ const decimals = X402_ASSET_DECIMALS.get(String(paymentPayload.x402_asset || "").toLowerCase());
192
+ const baseUnits = /^\d{1,30}$/.test(String(paymentPayload.x402_amount)) ? Number(paymentPayload.x402_amount) : NaN;
193
+ const centsCurrency = /^USD(_CENTS)?$/i.test(String((quote && quote.currency) || "USD_CENTS"));
194
+ if (decimals === undefined || !Number.isFinite(baseUnits) || !centsCurrency) {
195
+ return { amount_minor: 0, quoted_minor: quoted, settled_minor: null, settlement_basis: "x402_unreconciled_asset", reconciled: false };
196
+ }
197
+ const settledCents = Math.floor(baseUnits / Math.pow(10, decimals - 2));
198
+ return { amount_minor: Math.min(quoted, settledCents), quoted_minor: quoted, settled_minor: settledCents, settlement_basis: "x402_settled_min_quote", reconciled: true };
199
+ }
200
+
142
201
  // Records the provider revenue-share split for a settled call routed to a registered
143
202
  // provider. Fail-safe: metering must NEVER break or reverse a settled/charged response.
144
- function attributeProviderSettlement(state, externalExecutionOptions, payload, quote, extra) {
203
+ function attributeProviderSettlement(state, externalExecutionOptions, payload, quote, extra, verification) {
145
204
  const execution = externalExecutionOptions && externalExecutionOptions.externalExecution;
146
205
  const providerId = execution && execution.registered_provider_id;
147
206
  if (!providerId) return;
@@ -149,9 +208,14 @@ function attributeProviderSettlement(state, externalExecutionOptions, payload, q
149
208
  const taskRef = (payload && payload.task_ref)
150
209
  || (Array.isArray(payload && payload.tasks) && payload.tasks[0] && payload.tasks[0].task_ref)
151
210
  || null;
211
+ const reconciled = reconcileProviderSettlementAmount(quote, verification);
152
212
  recordProviderSettlement(state, {
153
213
  provider_id: providerId,
154
- amount_minor: quote && Number.isFinite(Number(quote.amount_minor)) ? Number(quote.amount_minor) : 0,
214
+ amount_minor: reconciled.amount_minor,
215
+ quoted_minor: reconciled.quoted_minor,
216
+ settled_minor: reconciled.settled_minor,
217
+ settlement_basis: reconciled.settlement_basis,
218
+ reconciled: reconciled.reconciled,
155
219
  currency: quote && quote.currency ? quote.currency : null,
156
220
  transaction_id: (extra && extra.transaction_id) || (quote && quote.quote_id) || null,
157
221
  settlement_ref: (extra && extra.settlement_ref) || null,
@@ -299,7 +363,7 @@ async function executeCommandRequest(state, payload, headers, options) {
299
363
  };
300
364
  attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
301
365
  transaction_id: creditSpend.credit_spend.credit_spend_id
302
- });
366
+ }, paymentVerification);
303
367
  return {
304
368
  status: 200,
305
369
  payload: createCommandResult(state, {
@@ -486,7 +550,7 @@ async function executeCommandRequest(state, payload, headers, options) {
486
550
 
487
551
  attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
488
552
  settlement_ref: (settledVerification.settlement && (settledVerification.settlement.transaction || settledVerification.settlement.txid)) || null
489
- });
553
+ }, settledVerification);
490
554
  return {
491
555
  status: 200,
492
556
  payload: createCommandResult(state, payload, quote, {
@@ -544,7 +608,7 @@ async function executeAuthorizedCommandRequest(state, payload, authorizationCont
544
608
  };
545
609
  }
546
610
 
547
- attributeProviderSettlement(state, externalExecutionOptions, payload, quote);
611
+ attributeProviderSettlement(state, externalExecutionOptions, payload, quote, undefined, paymentVerification);
548
612
  return {
549
613
  status: 200,
550
614
  payload: createCommandResult(state, payload, quote, {
@@ -557,5 +621,7 @@ async function executeAuthorizedCommandRequest(state, payload, authorizationCont
557
621
  module.exports = {
558
622
  executeAuthorizedCommandRequest,
559
623
  executeCommandRequest,
560
- buildExternalExecutionOptions
624
+ buildExternalExecutionOptions,
625
+ reconcileProviderSettlementAmount,
626
+ sanitizeAuthorizationContextForProvider
561
627
  };
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+
3
+ const dns = require("dns").promises;
4
+ const net = require("net");
5
+
6
+ // Upper bound on a single provider call. The x402 (EIP-3009) authorization a buyer signs is valid
7
+ // for roughly a minute, so a provider allowed to run longer than this could do real work for a
8
+ // payment that then fails to settle (nobody pays). Applies to env targets and registered providers.
9
+ const MAX_TARGET_TIMEOUT_MS = 30000;
10
+ const DEFAULT_TARGET_TIMEOUT_MS = 10000;
11
+ const MIN_TARGET_TIMEOUT_MS = 500;
12
+
13
+ // Total wall-clock budget for ONE buyer call across cold-start ride-out retries AND failover
14
+ // candidates, shared so three candidates can never each consume a full window. Kept inside the
15
+ // x402 settlement window (~60s) with headroom for the settle call itself.
16
+ const DEFAULT_EXECUTION_BUDGET_MS = 50000;
17
+ const MAX_EXECUTION_BUDGET_MS = 55000;
18
+
19
+ function clampTargetTimeoutMs(value) {
20
+ const n = Number(value || DEFAULT_TARGET_TIMEOUT_MS) || DEFAULT_TARGET_TIMEOUT_MS;
21
+ return Math.min(MAX_TARGET_TIMEOUT_MS, Math.max(MIN_TARGET_TIMEOUT_MS, Math.round(n)));
22
+ }
23
+
24
+ function externalExecutionBudgetMs() {
25
+ const raw = process.env.XYTARA_EXTERNAL_EXECUTION_BUDGET_MS;
26
+ if (raw === undefined || raw === "") return DEFAULT_EXECUTION_BUDGET_MS;
27
+ const n = Number(raw);
28
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_EXECUTION_BUDGET_MS;
29
+ return Math.min(MAX_EXECUTION_BUDGET_MS, Math.max(1000, Math.round(n)));
30
+ }
31
+
32
+ // Endpoint policy. "strict" (default on a hosted deploy, i.e. when RENDER is set) requires https
33
+ // and refuses endpoints at loopback / private / link-local / metadata addresses, so a registered
34
+ // provider cannot turn the runtime into a request proxy into its own network (SSRF). "permissive"
35
+ // (default locally) keeps http://127.0.0.1-style endpoints usable for tests and development.
36
+ function endpointPolicyMode() {
37
+ const raw = String(process.env.XYTARA_PROVIDER_ENDPOINT_POLICY || "").trim().toLowerCase();
38
+ if (raw === "strict" || raw === "permissive") return raw;
39
+ return process.env.RENDER ? "strict" : "permissive";
40
+ }
41
+
42
+ function isPrivateIp(ip) {
43
+ const family = net.isIP(ip);
44
+ if (family === 4) {
45
+ const p = ip.split(".").map(Number);
46
+ if (p[0] === 10 || p[0] === 127 || p[0] === 0) return true;
47
+ if (p[0] === 169 && p[1] === 254) return true; // link-local + cloud metadata
48
+ if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true;
49
+ if (p[0] === 192 && p[1] === 168) return true;
50
+ if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true; // carrier-grade NAT
51
+ if (p[0] >= 224) return true; // multicast / reserved / broadcast
52
+ return false;
53
+ }
54
+ if (family === 6) {
55
+ const lower = ip.toLowerCase();
56
+ if (lower === "::" || lower === "::1") return true;
57
+ if (lower.startsWith("fe80:") || lower.startsWith("fc") || lower.startsWith("fd")) return true; // link-local, ULA
58
+ if (lower.startsWith("::ffff:")) return isPrivateIp(lower.slice(7)); // v4-mapped
59
+ if (lower.startsWith("ff")) return true; // multicast
60
+ return false;
61
+ }
62
+ return false;
63
+ }
64
+
65
+ function isBlockedHostname(hostname) {
66
+ const h = String(hostname || "").toLowerCase().replace(/\.$/, "");
67
+ if (!h || h === "localhost" || h.endsWith(".localhost")) return true;
68
+ if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localdomain")) return true;
69
+ if (h === "metadata" || h === "metadata.google.internal") return true;
70
+ return false;
71
+ }
72
+
73
+ function hostOf(parsedUrl) {
74
+ return String(parsedUrl.hostname || "").replace(/^\[|\]$/g, "");
75
+ }
76
+
77
+ // Synchronous shape check (used at registration): protocol + literal-host rules. No DNS here.
78
+ function checkEndpointUrlShape(endpointUrl) {
79
+ const policy = endpointPolicyMode();
80
+ let parsed;
81
+ try {
82
+ parsed = new URL(String(endpointUrl || ""));
83
+ } catch (_) {
84
+ return { ok: false, reason: "endpoint_url_invalid", policy };
85
+ }
86
+ if (parsed.username || parsed.password) {
87
+ return { ok: false, reason: "endpoint_url_credentials_not_allowed", policy };
88
+ }
89
+ if (policy !== "strict") return { ok: true, policy };
90
+ if (parsed.protocol !== "https:") {
91
+ return { ok: false, reason: "endpoint_url_insecure", detail: "https:// is required for provider endpoints", policy };
92
+ }
93
+ const host = hostOf(parsed);
94
+ if (isBlockedHostname(host) || (net.isIP(host) && isPrivateIp(host))) {
95
+ return { ok: false, reason: "endpoint_url_not_public", detail: "loopback, private, link-local and metadata hosts are refused", policy };
96
+ }
97
+ return { ok: true, policy };
98
+ }
99
+
100
+ // Dispatch-time check: re-validates the shape AND resolves the host, refusing any address in a
101
+ // private range (defeats DNS rebinding to an internal address after registration). Results are
102
+ // cached briefly so the hot path adds at most one lookup per host per minute.
103
+ const resolveCache = new Map();
104
+ const RESOLVE_CACHE_TTL_MS = 60000;
105
+
106
+ async function checkEndpointBeforeDispatch(endpointUrl) {
107
+ const shape = checkEndpointUrlShape(endpointUrl);
108
+ if (!shape.ok || shape.policy !== "strict") return shape;
109
+ let host;
110
+ try {
111
+ host = hostOf(new URL(String(endpointUrl)));
112
+ } catch (_) {
113
+ return { ok: false, reason: "endpoint_url_invalid", policy: shape.policy };
114
+ }
115
+ if (net.isIP(host)) return shape; // literal already checked above
116
+ const cached = resolveCache.get(host);
117
+ if (cached && cached.expires_at > Date.now()) return cached.result;
118
+ let result;
119
+ try {
120
+ const addresses = await dns.lookup(host, { all: true, verbatim: true });
121
+ const bad = addresses.find((entry) => isPrivateIp(entry.address));
122
+ result = bad
123
+ ? { ok: false, reason: "endpoint_url_not_public", detail: "endpoint resolves to a non-public address", policy: shape.policy }
124
+ : { ok: true, policy: shape.policy };
125
+ } catch (_) {
126
+ // Unresolvable host: let the fetch surface ENOTFOUND (a not-processed, failover-safe signal).
127
+ result = { ok: true, policy: shape.policy, unresolved: true };
128
+ }
129
+ resolveCache.set(host, { result, expires_at: Date.now() + RESOLVE_CACHE_TTL_MS });
130
+ return result;
131
+ }
132
+
133
+ module.exports = {
134
+ MAX_TARGET_TIMEOUT_MS,
135
+ DEFAULT_TARGET_TIMEOUT_MS,
136
+ DEFAULT_EXECUTION_BUDGET_MS,
137
+ clampTargetTimeoutMs,
138
+ externalExecutionBudgetMs,
139
+ endpointPolicyMode,
140
+ isPrivateIp,
141
+ checkEndpointUrlShape,
142
+ checkEndpointBeforeDispatch
143
+ };