unbrowse 3.1.0 → 3.2.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.
Files changed (107) hide show
  1. package/dist/cli.js +455 -96
  2. package/dist/index.js +2 -6
  3. package/dist/mcp.js +695 -46
  4. package/dist/server.js +25811 -0
  5. package/package.json +1 -2
  6. package/vendor/kuri/darwin-arm64/kuri +0 -0
  7. package/vendor/kuri/darwin-x64/kuri +0 -0
  8. package/vendor/kuri/linux-arm64/kuri +0 -0
  9. package/vendor/kuri/linux-x64/kuri +0 -0
  10. package/vendor/kuri/manifest.json +7 -10
  11. package/runtime-src/agent-outcome.ts +0 -166
  12. package/runtime-src/analytics-session.ts +0 -55
  13. package/runtime-src/api/browse-index.ts +0 -317
  14. package/runtime-src/api/browse-session.ts +0 -572
  15. package/runtime-src/api/browse-submit-prereqs.ts +0 -48
  16. package/runtime-src/api/browse-submit.ts +0 -1184
  17. package/runtime-src/api/routes.ts +0 -1823
  18. package/runtime-src/auth/browser-cookies.ts +0 -423
  19. package/runtime-src/auth/index.ts +0 -535
  20. package/runtime-src/auth/runtime.ts +0 -116
  21. package/runtime-src/browser/index.ts +0 -659
  22. package/runtime-src/browser/types.ts +0 -41
  23. package/runtime-src/build-info.generated.ts +0 -6
  24. package/runtime-src/capture/index.ts +0 -1794
  25. package/runtime-src/capture/prefetch.ts +0 -95
  26. package/runtime-src/capture/rsc.ts +0 -45
  27. package/runtime-src/cli/shortcuts.ts +0 -273
  28. package/runtime-src/cli.ts +0 -1572
  29. package/runtime-src/client/graph-client.ts +0 -100
  30. package/runtime-src/client/index.ts +0 -1425
  31. package/runtime-src/debug-trace.ts +0 -18
  32. package/runtime-src/domain.ts +0 -38
  33. package/runtime-src/execution/index.ts +0 -3397
  34. package/runtime-src/execution/retry.ts +0 -46
  35. package/runtime-src/execution/robots.ts +0 -167
  36. package/runtime-src/execution/search-forms.ts +0 -188
  37. package/runtime-src/extraction/index.ts +0 -1507
  38. package/runtime-src/foundry/publish-bundle.ts +0 -392
  39. package/runtime-src/graph/agent-augment.ts +0 -315
  40. package/runtime-src/graph/index.ts +0 -1524
  41. package/runtime-src/graph/local-fixtures.ts +0 -393
  42. package/runtime-src/graph/local-harness.ts +0 -646
  43. package/runtime-src/graph/planner.ts +0 -411
  44. package/runtime-src/graph/session.ts +0 -294
  45. package/runtime-src/graph/trace-store.ts +0 -136
  46. package/runtime-src/index.ts +0 -24
  47. package/runtime-src/indexer/index.ts +0 -465
  48. package/runtime-src/intent-match.ts +0 -1515
  49. package/runtime-src/kuri/client.ts +0 -1839
  50. package/runtime-src/logger.ts +0 -30
  51. package/runtime-src/marketplace/index.ts +0 -103
  52. package/runtime-src/mcp.ts +0 -1747
  53. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  54. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  55. package/runtime-src/orchestrator/dag-feedback.ts +0 -257
  56. package/runtime-src/orchestrator/first-pass-action.ts +0 -403
  57. package/runtime-src/orchestrator/index.ts +0 -4480
  58. package/runtime-src/orchestrator/passive-publish.ts +0 -187
  59. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  60. package/runtime-src/payments/cascade.ts +0 -137
  61. package/runtime-src/payments/index.ts +0 -270
  62. package/runtime-src/payments/lobster-pay.ts +0 -182
  63. package/runtime-src/payments/wallet.ts +0 -98
  64. package/runtime-src/publish/review-context.ts +0 -93
  65. package/runtime-src/publish/sanitize.ts +0 -197
  66. package/runtime-src/publish/schema-review.ts +0 -192
  67. package/runtime-src/publish-admission.ts +0 -388
  68. package/runtime-src/ratelimit/index.ts +0 -23
  69. package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
  70. package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
  71. package/runtime-src/reverse-engineer/index.ts +0 -1551
  72. package/runtime-src/router.ts +0 -17
  73. package/runtime-src/routing-telemetry.ts +0 -395
  74. package/runtime-src/runtime/browser-access.ts +0 -11
  75. package/runtime-src/runtime/browser-auth.ts +0 -12
  76. package/runtime-src/runtime/browser-host.ts +0 -48
  77. package/runtime-src/runtime/lifecycle.ts +0 -17
  78. package/runtime-src/runtime/local-server.ts +0 -311
  79. package/runtime-src/runtime/paths.ts +0 -99
  80. package/runtime-src/runtime/setup.ts +0 -251
  81. package/runtime-src/runtime/supervisor.ts +0 -69
  82. package/runtime-src/runtime/update-hints.ts +0 -351
  83. package/runtime-src/server.ts +0 -100
  84. package/runtime-src/session-logs.ts +0 -142
  85. package/runtime-src/settings.ts +0 -221
  86. package/runtime-src/single-binary.ts +0 -143
  87. package/runtime-src/site-policy.ts +0 -54
  88. package/runtime-src/stale-cleanup-runner.ts +0 -144
  89. package/runtime-src/stale-cleanup.ts +0 -133
  90. package/runtime-src/telemetry-attribution.ts +0 -120
  91. package/runtime-src/telemetry.ts +0 -253
  92. package/runtime-src/template-params.ts +0 -141
  93. package/runtime-src/transform/drift.ts +0 -60
  94. package/runtime-src/transform/index.ts +0 -277
  95. package/runtime-src/types/index.ts +0 -1
  96. package/runtime-src/types/skill.ts +0 -912
  97. package/runtime-src/vault/index.ts +0 -196
  98. package/runtime-src/verification/auth-gate.ts +0 -8
  99. package/runtime-src/verification/candidates.ts +0 -27
  100. package/runtime-src/verification/index.ts +0 -120
  101. package/runtime-src/verification/matrix.ts +0 -30
  102. package/runtime-src/version.ts +0 -148
  103. package/runtime-src/workflow/artifact.ts +0 -161
  104. package/runtime-src/workflow/compile.ts +0 -808
  105. package/runtime-src/workflow/publish.ts +0 -225
  106. package/runtime-src/workflow/runtime.ts +0 -213
  107. package/vendor/kuri/win-x64/kuri.exe +0 -0
@@ -1,187 +0,0 @@
1
- import { cachePublishedSkill, validateManifest, publishGraphEdges } from "../client/index.js";
2
- import {
3
- formatMarketplacePublishSelection,
4
- selectMarketplacePublishClosure,
5
- } from "../publish-admission.js";
6
- import { attributeLifecycle, type LifecycleEvent } from "../runtime/lifecycle.js";
7
- import { publishSkill } from "../marketplace/index.js";
8
- import type { SkillManifest } from "../types/index.js";
9
- import { readWorkflowArtifact } from "../workflow/artifact.js";
10
- import { buildWorkflowPublishArtifact, writeWorkflowPublishArtifact } from "../workflow/publish.js";
11
-
12
- type PassivePublishDeps = {
13
- cachePublishedSkill: typeof cachePublishedSkill;
14
- publishSkill: typeof publishSkill;
15
- validateManifest: typeof validateManifest;
16
- };
17
-
18
- export type PassiveParityVerdict = "pass" | "fail" | "skip";
19
-
20
- type PassivePublishOptions = {
21
- deps?: PassivePublishDeps;
22
- parity?: PassiveParityVerdict | Promise<PassiveParityVerdict> | (() => Promise<PassiveParityVerdict>);
23
- };
24
-
25
- const defaultDeps: PassivePublishDeps = {
26
- cachePublishedSkill,
27
- publishSkill,
28
- validateManifest,
29
- };
30
-
31
- const passivePublishInFlight = new Map<string, Promise<void>>();
32
-
33
- function mergeBackendDescriptions(
34
- localSkill: SkillManifest,
35
- publishedSkill: SkillManifest,
36
- ): SkillManifest["endpoints"] {
37
- return localSkill.endpoints.map((endpoint) => {
38
- const backendEndpoint = publishedSkill.endpoints.find(
39
- (candidate) =>
40
- candidate.endpoint_id === endpoint.endpoint_id ||
41
- (candidate.method === endpoint.method && candidate.url_template === endpoint.url_template),
42
- );
43
- if (!backendEndpoint?.description) return endpoint;
44
- return {
45
- ...endpoint,
46
- description: backendEndpoint.description,
47
- };
48
- });
49
- }
50
-
51
- export function queuePassiveSkillPublish(
52
- skill: SkillManifest,
53
- options: PassivePublishOptions = {},
54
- ): Promise<void> {
55
- const deps = options.deps ?? defaultDeps;
56
- const existing = passivePublishInFlight.get(skill.skill_id);
57
- if (existing) return existing;
58
-
59
- const job = (async () => {
60
- if (skill.execution_type !== "http") return;
61
-
62
- const parityVerdict =
63
- typeof options.parity === "function"
64
- ? await options.parity()
65
- : options.parity instanceof Promise
66
- ? await options.parity
67
- : options.parity;
68
- if (parityVerdict === "fail") {
69
- console.warn(`[publish] passive publish skipped for ${skill.skill_id}: parity_failed`);
70
- return;
71
- }
72
-
73
- const selection = selectMarketplacePublishClosure(skill);
74
- if (selection.endpoints.length === 0) {
75
- console.warn(
76
- `[publish] passive publish skipped for ${skill.skill_id}: no admitted endpoints (${formatMarketplacePublishSelection(selection)})`,
77
- );
78
- return;
79
- }
80
-
81
- const { operation_graph: _graph, ...publishBase } = skill;
82
- const publishDraft: SkillManifest = {
83
- ...publishBase,
84
- endpoints: selection.endpoints,
85
- };
86
-
87
- const validation = await deps.validateManifest({ ...publishDraft, skill_id: "__validate__" });
88
- if (!validation.valid) {
89
- writeWorkflowPublishArtifact(buildWorkflowPublishArtifact(
90
- skill,
91
- readWorkflowArtifact(skill.skill_id),
92
- {
93
- publishStatus: "blocked-validation",
94
- validationErrors: validation.hardErrors,
95
- endpointIds: selection.closure_endpoint_ids,
96
- rootEndpointIds: selection.root_endpoint_ids,
97
- },
98
- ));
99
- console.warn(
100
- `[publish] passive publish skipped for ${skill.skill_id}: ${validation.hardErrors.join("; ") || "validation failed"}`,
101
- );
102
- return;
103
- }
104
-
105
- const publishStart = Date.now();
106
- const published = await deps.publishSkill(publishDraft);
107
- const publishMs = Date.now() - publishStart;
108
- console.log(
109
- `[publish] passive publish admitted ${selection.endpoints.length}/${selection.stats.total} endpoint(s) for ${skill.skill_id} (${selection.root_endpoint_ids.length} roots, ${selection.endpoints.length - selection.root_endpoint_ids.length} closure) (${formatMarketplacePublishSelection(selection)})`,
110
- );
111
- deps.cachePublishedSkill({
112
- ...skill,
113
- ...published,
114
- endpoints: mergeBackendDescriptions(skill, published),
115
- operation_graph: skill.operation_graph,
116
- ...(skill.auth_profile_ref ? { auth_profile_ref: skill.auth_profile_ref } : {}),
117
- });
118
- writeWorkflowPublishArtifact(buildWorkflowPublishArtifact(
119
- skill,
120
- readWorkflowArtifact(skill.skill_id),
121
- {
122
- publishStatus: "published",
123
- publishedAt: new Date().toISOString(),
124
- endpointIds: selection.closure_endpoint_ids,
125
- rootEndpointIds: selection.root_endpoint_ids,
126
- },
127
- ));
128
-
129
- // Publish graph edges via dedicated endpoint (fire-and-forget)
130
- if (skill.operation_graph?.operations) {
131
- for (const op of skill.operation_graph.operations) {
132
- if (!selection.closure_operation_ids.includes(op.operation_id)) continue;
133
- const opEdges = (skill.operation_graph.edges ?? [])
134
- .filter(e => e.from_operation_id === op.operation_id && selection.closure_operation_ids.includes(e.to_operation_id))
135
- .map(e => ({
136
- target_endpoint_id: skill.operation_graph!.operations.find(
137
- t => t.operation_id === e.to_operation_id
138
- )?.endpoint_id ?? e.to_operation_id,
139
- kind: e.kind,
140
- confidence: e.confidence,
141
- }));
142
- if (opEdges.length > 0) {
143
- publishGraphEdges(skill.domain, {
144
- endpoint_id: op.endpoint_id,
145
- method: op.method,
146
- url_template: op.url_template,
147
- }, opEdges).catch(() => {});
148
- }
149
- }
150
- }
151
-
152
- const publishEvent: LifecycleEvent = {
153
- phase: "publish",
154
- skill_id: skill.skill_id,
155
- timestamp: new Date().toISOString(),
156
- duration_ms: publishMs,
157
- source: "marketplace",
158
- };
159
- const totals = attributeLifecycle([publishEvent]);
160
- console.log(`[lifecycle] publish=${totals.get("publish")}ms for ${skill.skill_id}`);
161
- console.log(`[publish] passive publish succeeded for ${skill.skill_id}`);
162
- })()
163
- .catch((err) => {
164
- console.error(
165
- `[publish] passive publish failed for ${skill.skill_id}: ${err instanceof Error ? err.message : String(err)}`,
166
- );
167
- })
168
- .finally(() => {
169
- passivePublishInFlight.delete(skill.skill_id);
170
- });
171
-
172
- passivePublishInFlight.set(skill.skill_id, job);
173
- return job;
174
- }
175
-
176
- /** Await all in-flight passive publish jobs. Call before process exit. */
177
- export async function drainPendingPassivePublishes(): Promise<void> {
178
- const pending = [...passivePublishInFlight.values()];
179
- if (pending.length === 0) return;
180
- console.log(`[publish] draining ${pending.length} pending passive publish(es)...`);
181
- await Promise.allSettled(pending);
182
- console.log(`[publish] all passive publishes drained`);
183
- }
184
-
185
- export function resetPassivePublishQueueForTests(): void {
186
- passivePublishInFlight.clear();
187
- }
@@ -1,80 +0,0 @@
1
- import type { SkillManifest } from "../types/index.js";
2
-
3
- export const DEFAULT_CAPTURE_MS = 22_000;
4
- export const DEFAULT_CAPTURE_TOKENS = 30_000;
5
- export const CHARS_PER_TOKEN = 4;
6
- export const TOKEN_COST_PER_MILLION_USD = 3;
7
- export const TOKEN_COST_UC = Math.round(TOKEN_COST_PER_MILLION_USD);
8
-
9
- const SAVINGS_SOURCES = new Set([
10
- "marketplace",
11
- "route-cache",
12
- "first-pass",
13
- "direct-fetch",
14
- "browser-action",
15
- ]);
16
-
17
- export interface TimingEconomics {
18
- response_bytes: number;
19
- response_tokens: number;
20
- tokens_saved: number;
21
- tokens_saved_pct: number;
22
- time_saved_pct: number;
23
- baseline_total_ms?: number;
24
- time_saved_ms?: number;
25
- baseline_cost_uc?: number;
26
- actual_cost_uc: number;
27
- cost_saved_uc?: number;
28
- baseline_source?: "real" | "estimated";
29
- }
30
-
31
- export function computeTimingEconomics({
32
- source,
33
- totalMs,
34
- result,
35
- skill,
36
- paidSearchUc = 0,
37
- paidExecutionUc = 0,
38
- }: {
39
- source: string;
40
- totalMs: number;
41
- result: unknown;
42
- skill?: Pick<SkillManifest, "discovery_cost">;
43
- paidSearchUc?: number;
44
- paidExecutionUc?: number;
45
- }): TimingEconomics {
46
- const resultStr = typeof result === "string" ? result : JSON.stringify(result ?? "");
47
- const responseBytes = resultStr.length;
48
- const responseTokens = Math.ceil(responseBytes / CHARS_PER_TOKEN);
49
- const actualCostUc = (responseTokens * TOKEN_COST_UC) + paidSearchUc + paidExecutionUc;
50
-
51
- const economics: TimingEconomics = {
52
- response_bytes: responseBytes,
53
- response_tokens: responseTokens,
54
- tokens_saved: 0,
55
- tokens_saved_pct: 0,
56
- time_saved_pct: 0,
57
- actual_cost_uc: actualCostUc,
58
- };
59
-
60
- if (!SAVINGS_SOURCES.has(source)) return economics;
61
-
62
- const baselineTokens = skill?.discovery_cost?.capture_tokens ?? DEFAULT_CAPTURE_TOKENS;
63
- const baselineMs = skill?.discovery_cost?.capture_ms ?? DEFAULT_CAPTURE_MS;
64
- const baselineCostUc = baselineTokens * TOKEN_COST_UC;
65
-
66
- economics.tokens_saved = Math.max(0, baselineTokens - responseTokens);
67
- economics.tokens_saved_pct = baselineTokens > 0
68
- ? Math.round((economics.tokens_saved / baselineTokens) * 100)
69
- : 0;
70
- economics.time_saved_pct = baselineMs > 0
71
- ? Math.round((Math.max(0, baselineMs - totalMs) / baselineMs) * 100)
72
- : 0;
73
- economics.baseline_total_ms = baselineMs;
74
- economics.time_saved_ms = Math.max(0, baselineMs - totalMs);
75
- economics.baseline_cost_uc = baselineCostUc;
76
- economics.cost_saved_uc = Math.max(0, baselineCostUc - actualCostUc);
77
- economics.baseline_source = skill?.discovery_cost ? "real" : "estimated";
78
-
79
- return economics;
80
- }
@@ -1,137 +0,0 @@
1
- import { createHash } from "crypto";
2
- import bs58 from "bs58";
3
- import type { SkillManifest } from "../types/index.js";
4
-
5
- type Contributor = NonNullable<SkillManifest["contributors"]>[number];
6
-
7
- type CascadeSdk = {
8
- createSplitsClient(args: {
9
- rpc: unknown;
10
- rpcSubscriptions: unknown;
11
- signer: unknown;
12
- }): {
13
- ensureSplit(args: {
14
- recipients: Array<{ address: string; share: number }>;
15
- uniqueId?: unknown;
16
- }): Promise<{
17
- status: "created" | "updated" | "no_change" | "blocked" | "failed";
18
- splitConfig?: string;
19
- message?: string;
20
- reason?: string;
21
- }>;
22
- };
23
- labelToSeed(label: string): unknown;
24
- };
25
-
26
- type SolanaKit = {
27
- createSolanaRpc(url: string): unknown;
28
- createSolanaRpcSubscriptions(url: string): unknown;
29
- createKeyPairSignerFromBytes(secretKey: Uint8Array): Promise<unknown>;
30
- };
31
-
32
- type CascadeDeps = {
33
- loadSdk?: () => Promise<CascadeSdk>;
34
- loadKit?: () => Promise<SolanaKit>;
35
- env?: Record<string, string | undefined>;
36
- };
37
-
38
- export type CascadeProvisionResult = {
39
- split_config?: string;
40
- warning?: string;
41
- source?: "env" | "sdk" | "existing";
42
- };
43
-
44
- function payableContributors(skill: Pick<SkillManifest, "contributors">): Contributor[] {
45
- return (skill.contributors ?? []).filter((c): c is Contributor => !!c.wallet_address?.trim());
46
- }
47
-
48
- function cascadeLabel(skillId: string): string {
49
- const digest = createHash("sha256").update(skillId).digest("hex");
50
- return `ubr-${digest.slice(0, 23)}`;
51
- }
52
-
53
- function decodeSecretKey(raw: string): Uint8Array {
54
- const trimmed = raw.trim();
55
- if (!trimmed) throw new Error("empty signer secret");
56
-
57
- if (trimmed.startsWith("[")) {
58
- return Uint8Array.from(JSON.parse(trimmed) as number[]);
59
- }
60
-
61
- if (/^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed)) {
62
- return Uint8Array.from(bs58.decode(trimmed));
63
- }
64
-
65
- return Uint8Array.from(Buffer.from(trimmed, "base64"));
66
- }
67
-
68
- function recipientsForSkill(
69
- skill: Pick<SkillManifest, "contributors">,
70
- platformWallet: string,
71
- ): Array<{ address: string; share: number }> {
72
- return [
73
- { address: platformWallet, share: 10 },
74
- ...payableContributors(skill).map((c) => ({
75
- address: c.wallet_address!.trim(),
76
- share: c.share,
77
- })),
78
- ];
79
- }
80
-
81
- export async function ensureCascadeSplitForSkill(
82
- skill: Pick<SkillManifest, "skill_id" | "contributors" | "split_config">,
83
- deps: CascadeDeps = {},
84
- ): Promise<CascadeProvisionResult> {
85
- const env = deps.env ?? process.env;
86
- if (skill.split_config?.trim()) {
87
- return { split_config: skill.split_config.trim(), source: "existing" };
88
- }
89
-
90
- const contributors = payableContributors(skill);
91
- if (contributors.length <= 1) return {};
92
-
93
- const explicitSplitConfig = env.UNBROWSE_CASCADE_SPLIT_ADDRESS?.trim()
94
- || env.UNBROWSE_CASCADE_SPLIT_CONFIG?.trim();
95
- if (explicitSplitConfig) {
96
- return { split_config: explicitSplitConfig, source: "env" };
97
- }
98
-
99
- const platformWallet = env.UNBROWSE_CASCADE_PLATFORM_WALLET?.trim()
100
- || env.PAYMENT_RECIPIENT?.trim();
101
- const secretKey = env.UNBROWSE_CASCADE_SIGNER_SECRET_KEY?.trim();
102
- const rpcUrl = env.UNBROWSE_CASCADE_RPC_URL?.trim();
103
- const rpcWsUrl = env.UNBROWSE_CASCADE_RPC_WS_URL?.trim();
104
-
105
- if (!platformWallet || !secretKey || !rpcUrl || !rpcWsUrl) {
106
- return {
107
- warning: "cascade_split_not_configured",
108
- };
109
- }
110
-
111
- const loadSdk = deps.loadSdk ?? (async () => await import("@cascade-fyi/splits-sdk") as unknown as CascadeSdk);
112
- const loadKit = deps.loadKit ?? (async () => await import("@solana/kit") as unknown as SolanaKit);
113
- const [sdk, kit] = await Promise.all([loadSdk(), loadKit()]);
114
-
115
- const signer = await kit.createKeyPairSignerFromBytes(decodeSecretKey(secretKey));
116
- const splits = sdk.createSplitsClient({
117
- rpc: kit.createSolanaRpc(rpcUrl),
118
- rpcSubscriptions: kit.createSolanaRpcSubscriptions(rpcWsUrl),
119
- signer,
120
- });
121
-
122
- const result = await splits.ensureSplit({
123
- recipients: recipientsForSkill(skill, platformWallet),
124
- uniqueId: sdk.labelToSeed(cascadeLabel(skill.skill_id)),
125
- });
126
-
127
- if (result.status === "created" || result.status === "updated" || result.status === "no_change") {
128
- return {
129
- split_config: result.splitConfig,
130
- source: "sdk",
131
- };
132
- }
133
-
134
- return {
135
- warning: result.message || `cascade_split_${result.status}`,
136
- };
137
- }
@@ -1,270 +0,0 @@
1
- import { DEFAULT_BACKEND_URL } from "../version.js";
2
-
3
- /**
4
- * Payment integration — lobster.cash compatible.
5
- *
6
- * This module describes payment INTENT and requirements.
7
- * It does NOT implement wallet internals, generate wallets,
8
- * or hardcode wallet provider action names.
9
- *
10
- * Delegation boundary:
11
- * - Unbrowse owns: use-case intent, amount, recipient, memo
12
- * - Wallet provider (lobster.cash) owns: provisioning, signing, broadcast, status
13
- *
14
- * @see https://lobster.cash/docs/skill-compatibility-guide
15
- */
16
-
17
- // ---------------------------------------------------------------------------
18
- // Payment requirement — what unbrowse tells the agent/wallet
19
- // ---------------------------------------------------------------------------
20
-
21
- export interface PaymentRequirement {
22
- required: boolean;
23
- amount: string;
24
- currency: string;
25
- reason: string;
26
- recipient?: string;
27
- memo?: string;
28
- }
29
-
30
- export type PaymentStatus =
31
- | "paid"
32
- | "payment_required"
33
- | "wallet_not_configured"
34
- | "insufficient_balance"
35
- | "payment_failed"
36
- | "awaiting_confirmation"
37
- | "indexing_fallback"
38
- | "free";
39
-
40
- export interface PaymentGateResult {
41
- status: PaymentStatus;
42
- requirement?: PaymentRequirement;
43
- message: string;
44
- next_step?: string;
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // X402 configuration — Solana + Base, USDC via corbits.dev
49
- // ---------------------------------------------------------------------------
50
-
51
- export const X402_CONFIG = {
52
- chains: {
53
- solana: {
54
- network: "solana",
55
- currency: "USDC",
56
- testnet: "solana-devnet",
57
- mainnet: "solana-mainnet",
58
- },
59
- base: {
60
- network: "base",
61
- currency: "USDC",
62
- testnet: "base-sepolia",
63
- mainnet: "base-mainnet",
64
- },
65
- },
66
- facilitator: "https://facilitator.corbits.dev",
67
- supports_pda_wallets: true,
68
- } as const;
69
-
70
- // ---------------------------------------------------------------------------
71
- // Dynamic pricing — fetch real route price from the backend
72
- // ---------------------------------------------------------------------------
73
-
74
- /** Backend API base URL for pricing queries. */
75
- const PRICING_API_URL = process.env.UNBROWSE_BACKEND_URL ?? DEFAULT_BACKEND_URL;
76
-
77
- /** Maximum time (ms) to wait for dynamic price before falling back. */
78
- const PRICING_TIMEOUT_MS = 2_000;
79
-
80
- /** Default fallback price when backend is unreachable or slow (USD). */
81
- const DEFAULT_PRICE_USD = "0.001";
82
-
83
- /**
84
- * Fetch the dynamic route price for a skill from the backend.
85
- *
86
- * Calls GET /v1/skills/:id/price with a 2 s timeout.
87
- * Returns the price as a USD string, or null if the backend is
88
- * unavailable, slow, or returns an unexpected shape.
89
- *
90
- * Non-blocking: callers should fall back to DEFAULT_PRICE_USD on null.
91
- */
92
- export async function fetchDynamicPrice(skillId: string): Promise<string | null> {
93
- const controller = new AbortController();
94
- const timer = setTimeout(() => controller.abort(), PRICING_TIMEOUT_MS);
95
- try {
96
- const res = await fetch(
97
- `${PRICING_API_URL}/v1/skills/${encodeURIComponent(skillId)}/price`,
98
- {
99
- method: "GET",
100
- headers: { Accept: "application/json" },
101
- signal: controller.signal,
102
- },
103
- );
104
- if (!res.ok) return null;
105
- const body = (await res.json()) as Record<string, unknown>;
106
- const price = body?.price_usd;
107
- if (typeof price === "number" && price > 0) return String(price);
108
- if (typeof price === "string" && parseFloat(price) > 0) return price;
109
- return null;
110
- } catch {
111
- // Network error, timeout, or JSON parse failure — non-fatal.
112
- return null;
113
- } finally {
114
- clearTimeout(timer);
115
- }
116
- }
117
-
118
- // ---------------------------------------------------------------------------
119
- // Payment gate — determines if execution requires payment
120
- // ---------------------------------------------------------------------------
121
-
122
- /**
123
- * Check if a skill execution requires payment.
124
- * Returns a PaymentGateResult describing what the agent needs to do.
125
- *
126
- * Attempts to fetch the real dynamic price from the backend via
127
- * GET /v1/skills/:id/price. Falls back to DEFAULT_PRICE_USD ("0.001")
128
- * when the backend is unreachable, slow (>2 s), or returns an error.
129
- *
130
- * This function does NOT execute payments — it describes requirements.
131
- * The agent's wallet plugin (lobster.cash) handles actual payment.
132
- */
133
- export async function checkPaymentRequirement(
134
- skillId: string,
135
- endpointId: string,
136
- options?: {
137
- price_usd?: string;
138
- skip_payment?: boolean;
139
- wallet_configured?: boolean;
140
- },
141
- ): Promise<PaymentGateResult> {
142
- if (options?.skip_payment || process.env.UNBROWSE_SKIP_PAYMENT === "1") {
143
- return { status: "free", message: "Payment skipped." };
144
- }
145
-
146
- if (skillId.startsWith("local:") || process.env.UNBROWSE_FREE_TIER === "1") {
147
- return { status: "free", message: "No payment required for local skills." };
148
- }
149
-
150
- // Resolve price: explicit override > dynamic backend price > hardcoded default
151
- let amount = options?.price_usd ?? null;
152
- if (amount === null) {
153
- const dynamic = await fetchDynamicPrice(skillId).catch(() => null);
154
- amount = dynamic ?? DEFAULT_PRICE_USD;
155
- }
156
-
157
- if (parseFloat(amount) <= 0) {
158
- return { status: "free", message: "No payment required." };
159
- }
160
-
161
- const requirement: PaymentRequirement = {
162
- required: true,
163
- amount,
164
- currency: "USDC",
165
- reason: `Per-query fee for ${skillId}/${endpointId}`,
166
- recipient: X402_CONFIG.facilitator,
167
- memo: `unbrowse:${skillId}:${endpointId}`,
168
- };
169
-
170
- if (options?.wallet_configured === false) {
171
- return {
172
- status: "wallet_not_configured",
173
- requirement,
174
- message: "No agent wallet configured. Set up a wallet like lobster.cash to use paid skills.",
175
- next_step: "Complete wallet setup before proceeding with this skill execution.",
176
- };
177
- }
178
-
179
- return {
180
- status: "payment_required",
181
- requirement,
182
- message: `This execution requires ${amount} USDC. Transaction execution and final status are handled by your wallet provider.`,
183
- next_step: "If a wallet step is required and wallet context is missing, complete wallet setup first.",
184
- };
185
- }
186
-
187
- /**
188
- * Interpret a payment result from the agent's wallet.
189
- * Maps wallet outcomes to gate decisions.
190
- */
191
- /**
192
- * Interpret a payment result from the agent's wallet.
193
- * Maps wallet outcomes to gate decisions.
194
- *
195
- * When the wallet reports insufficient balance or no wallet,
196
- * the caller should use resolveUnpaidAccess() to determine
197
- * whether indexing fallback is available.
198
- */
199
- export function interpretPaymentResult(
200
- walletStatus: string,
201
- requirement: PaymentRequirement,
202
- ): PaymentGateResult {
203
- switch (walletStatus) {
204
- case "confirmed":
205
- case "success":
206
- case "paid":
207
- return { status: "paid", requirement, message: "Payment confirmed. Proceeding with execution." };
208
-
209
- case "pending":
210
- case "processing":
211
- return {
212
- status: "awaiting_confirmation",
213
- requirement,
214
- message: "Payment is being processed. Wait for your wallet to report the final transaction status before continuing.",
215
- };
216
-
217
- case "insufficient_balance":
218
- case "insufficient_funds":
219
- return {
220
- status: "insufficient_balance",
221
- requirement,
222
- message: `Insufficient balance. Fund your wallet with at least ${requirement.amount} ${requirement.currency}, or fall back to indexing mode.`,
223
- next_step: "Fund your agent wallet and retry, or use resolveUnpaidAccess() to contribute indexing instead.",
224
- };
225
-
226
- case "wallet_not_configured":
227
- case "no_wallet":
228
- return {
229
- status: "wallet_not_configured",
230
- requirement,
231
- message: "No agent wallet configured. Falling back to indexing mode — you can contribute route indexing instead of paying.",
232
- next_step: "Set up lobster.cash to unlock paid execution, or continue in indexing mode.",
233
- };
234
-
235
- default:
236
- return {
237
- status: "payment_failed",
238
- requirement,
239
- message: `Payment failed: ${walletStatus}. You may retry or fall back to indexing mode.`,
240
- next_step: "Retry the payment, or use resolveUnpaidAccess() to contribute indexing instead.",
241
- };
242
- }
243
- }
244
-
245
- /**
246
- * Determine what an unpaid user can do.
247
- *
248
- * Users who can't pay (no wallet, insufficient balance, payment failed)
249
- * fall back to indexing mode: they can capture, reverse-engineer, and
250
- * index routes into the marketplace — contributing to the network
251
- * instead of consuming paid executions.
252
- *
253
- * This is the default behavior when payment is required but not available.
254
- */
255
- export function resolveUnpaidAccess(
256
- gateResult: PaymentGateResult,
257
- ): PaymentGateResult {
258
- const cantPay = gateResult.status === "wallet_not_configured"
259
- || gateResult.status === "insufficient_balance"
260
- || gateResult.status === "payment_failed";
261
-
262
- if (!cantPay) return gateResult;
263
-
264
- return {
265
- status: "indexing_fallback",
266
- requirement: gateResult.requirement,
267
- message: "Indexing mode: you can capture and index routes for the marketplace. Paid execution requires a funded wallet.",
268
- next_step: "Use resolve with --force-capture to contribute new routes. Set up lobster.cash to unlock paid execution.",
269
- };
270
- }