kompass-sdk 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/matching.d.ts.map +1 -1
- package/dist/matching.js +8 -0
- package/dist/matching.js.map +1 -1
- package/dist/sources/acp.js +1 -1
- package/dist/sources/acp.js.map +1 -1
- package/dist/sources/bankr.d.ts +8 -0
- package/dist/sources/bankr.d.ts.map +1 -0
- package/dist/sources/bankr.js +79 -0
- package/dist/sources/bankr.js.map +1 -0
- package/dist/sources/index.d.ts.map +1 -1
- package/dist/sources/index.js +4 -0
- package/dist/sources/index.js.map +1 -1
- package/dist/sources/locus.d.ts +12 -0
- package/dist/sources/locus.d.ts.map +1 -0
- package/dist/sources/locus.js +134 -0
- package/dist/sources/locus.js.map +1 -0
- package/dist/sources/mcp-registry.d.ts +2 -2
- package/dist/sources/mcp-registry.d.ts.map +1 -1
- package/dist/sources/mcp-registry.js +89 -81
- package/dist/sources/mcp-registry.js.map +1 -1
- package/dist/sources/openserv.d.ts +8 -0
- package/dist/sources/openserv.d.ts.map +1 -0
- package/dist/sources/openserv.js +76 -0
- package/dist/sources/openserv.js.map +1 -0
- package/dist/sources/types.d.ts +1 -1
- package/dist/sources/types.d.ts.map +1 -1
- package/dist/wallet/bridge.d.ts +1 -0
- package/dist/wallet/bridge.d.ts.map +1 -1
- package/dist/wallet/bridge.js +31 -5
- package/dist/wallet/bridge.js.map +1 -1
- package/dist/wallet/handlers/acp.d.ts.map +1 -1
- package/dist/wallet/handlers/acp.js +59 -3
- package/dist/wallet/handlers/acp.js.map +1 -1
- package/dist/wallet/handlers/bankr.d.ts +13 -0
- package/dist/wallet/handlers/bankr.d.ts.map +1 -0
- package/dist/wallet/handlers/bankr.js +88 -0
- package/dist/wallet/handlers/bankr.js.map +1 -0
- package/dist/wallet/handlers/locus.d.ts +21 -0
- package/dist/wallet/handlers/locus.d.ts.map +1 -0
- package/dist/wallet/handlers/locus.js +144 -0
- package/dist/wallet/handlers/locus.js.map +1 -0
- package/dist/wallet/payment-router.d.ts.map +1 -1
- package/dist/wallet/payment-router.js +12 -0
- package/dist/wallet/payment-router.js.map +1 -1
- package/package.json +1 -1
- package/src/matching.ts +9 -0
- package/src/sources/acp.ts +1 -1
- package/src/sources/bankr.ts +87 -0
- package/src/sources/index.ts +4 -0
- package/src/sources/locus.ts +156 -0
- package/src/sources/mcp-registry.ts +108 -99
- package/src/sources/types.ts +3 -1
- package/src/wallet/bridge.ts +33 -6
- package/src/wallet/handlers/acp.ts +69 -3
- package/src/wallet/handlers/bankr.ts +108 -0
- package/src/wallet/handlers/locus.ts +170 -0
- package/src/wallet/payment-router.ts +14 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Registry Source Adapter
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Fetches servers from registry.modelcontextprotocol.io
|
|
4
|
+
* Uses local cache + text search against descriptions (not just names)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { SourceAdapter, UnifiedAgent, SourceSearchOptions } from "./types.js";
|
|
@@ -13,9 +13,83 @@ interface McpServer {
|
|
|
13
13
|
description?: string;
|
|
14
14
|
version?: string;
|
|
15
15
|
repo?: string;
|
|
16
|
+
repository?: { url?: string };
|
|
17
|
+
websiteUrl?: string;
|
|
16
18
|
website?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
remotes?: { type: string; url: string; headers?: { name: string; isRequired?: boolean }[] }[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// In-memory cache of all servers — fetched once, reused
|
|
23
|
+
let serverCache: McpServer[] | null = null;
|
|
24
|
+
let cacheTime = 0;
|
|
25
|
+
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
|
|
26
|
+
|
|
27
|
+
async function fetchAllServers(timeout: number): Promise<McpServer[]> {
|
|
28
|
+
// Return cache if fresh
|
|
29
|
+
if (serverCache && Date.now() - cacheTime < CACHE_TTL) {
|
|
30
|
+
return serverCache;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const allServers: McpServer[] = [];
|
|
34
|
+
let cursor: string | undefined;
|
|
35
|
+
|
|
36
|
+
// Paginate through all servers (max 3 pages of 96)
|
|
37
|
+
for (let page = 0; page < 3; page++) {
|
|
38
|
+
const url = new URL("/v0.1/servers", BASE_URL);
|
|
39
|
+
url.searchParams.set("limit", "96");
|
|
40
|
+
url.searchParams.set("version", "latest");
|
|
41
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
42
|
+
|
|
43
|
+
const res = await fetch(url.toString(), { signal: AbortSignal.timeout(timeout) });
|
|
44
|
+
if (!res.ok) break;
|
|
45
|
+
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
const servers = (data.servers ?? []).map((item: any) => item.server ?? item);
|
|
48
|
+
allServers.push(...servers);
|
|
49
|
+
|
|
50
|
+
cursor = data.metadata?.nextCursor ?? data.nextCursor;
|
|
51
|
+
if (!cursor) break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
serverCache = allServers;
|
|
55
|
+
cacheTime = Date.now();
|
|
56
|
+
return allServers;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Search servers locally by matching query against name + description.
|
|
61
|
+
* Much better than the registry's keyword search which only matches names.
|
|
62
|
+
*/
|
|
63
|
+
function searchLocally(servers: McpServer[], query: string, limit: number): McpServer[] {
|
|
64
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
|
|
65
|
+
|
|
66
|
+
return servers
|
|
67
|
+
.map((server) => {
|
|
68
|
+
const text = `${server.name} ${server.description ?? ""}`.toLowerCase();
|
|
69
|
+
let score = 0;
|
|
70
|
+
|
|
71
|
+
for (const term of queryTerms) {
|
|
72
|
+
// Exact word match in description
|
|
73
|
+
if (text.includes(term)) score += 3;
|
|
74
|
+
// Name match is stronger
|
|
75
|
+
if (server.name.toLowerCase().includes(term)) score += 5;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Bonus for having remote endpoints (actually callable)
|
|
79
|
+
if (server.remotes && server.remotes.length > 0) score += 2;
|
|
80
|
+
|
|
81
|
+
// Bonus for no auth required
|
|
82
|
+
const noAuth = server.remotes?.some((r) =>
|
|
83
|
+
!r.headers?.some((h) => h.isRequired && h.name.toLowerCase() === "authorization")
|
|
84
|
+
);
|
|
85
|
+
if (noAuth) score += 1;
|
|
86
|
+
|
|
87
|
+
return { server, score };
|
|
88
|
+
})
|
|
89
|
+
.filter((s) => s.score > 0)
|
|
90
|
+
.sort((a, b) => b.score - a.score)
|
|
91
|
+
.slice(0, limit)
|
|
92
|
+
.map((s) => s.server);
|
|
19
93
|
}
|
|
20
94
|
|
|
21
95
|
export const mcpRegistryAdapter: SourceAdapter = {
|
|
@@ -23,47 +97,30 @@ export const mcpRegistryAdapter: SourceAdapter = {
|
|
|
23
97
|
displayName: "MCP Registry (Anthropic)",
|
|
24
98
|
|
|
25
99
|
async search(query: string, options?: SourceSearchOptions): Promise<UnifiedAgent[]> {
|
|
26
|
-
const limit = options?.limit ??
|
|
27
|
-
const timeout = options?.timeout ??
|
|
28
|
-
|
|
29
|
-
// MCP Registry uses keyword search, not semantic.
|
|
30
|
-
// Expand user intent into multiple keyword searches to improve recall.
|
|
31
|
-
const searchTerms = expandQueryToKeywords(query);
|
|
32
|
-
const allServers: McpServer[] = [];
|
|
33
|
-
const seenNames = new Set<string>();
|
|
100
|
+
const limit = options?.limit ?? 20;
|
|
101
|
+
const timeout = options?.timeout ?? 15000;
|
|
34
102
|
|
|
35
103
|
try {
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const url = new URL("/v0.1/servers", BASE_URL);
|
|
40
|
-
url.searchParams.set("limit", String(Math.min(limit, 20)));
|
|
41
|
-
url.searchParams.set("search", term);
|
|
42
|
-
url.searchParams.set("version", "latest");
|
|
43
|
-
|
|
44
|
-
const res = await fetch(url.toString(), { signal: AbortSignal.timeout(timeout) });
|
|
45
|
-
if (!res.ok) return [];
|
|
46
|
-
|
|
47
|
-
const data = await res.json();
|
|
48
|
-
const rawServers = data.servers ?? data.data ?? [];
|
|
49
|
-
return rawServers.map((item: any) => item.server ?? item) as McpServer[];
|
|
50
|
-
})
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
for (const result of results) {
|
|
54
|
-
if (result.status === "fulfilled") {
|
|
55
|
-
for (const server of result.value) {
|
|
56
|
-
if (!seenNames.has(server.name)) {
|
|
57
|
-
seenNames.add(server.name);
|
|
58
|
-
allServers.push(server);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return allServers.slice(0, limit).map((server) => mapMcpServer(server));
|
|
104
|
+
const allServers = await fetchAllServers(timeout);
|
|
105
|
+
const matched = searchLocally(allServers, query, limit);
|
|
106
|
+
return matched.map(mapMcpServer);
|
|
65
107
|
} catch {
|
|
66
|
-
|
|
108
|
+
// Fallback to direct API search if cache fetch fails
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL("/v0.1/servers", BASE_URL);
|
|
111
|
+
url.searchParams.set("limit", String(limit));
|
|
112
|
+
if (query) url.searchParams.set("search", query.split(/\s+/)[0]); // Single word fallback
|
|
113
|
+
url.searchParams.set("version", "latest");
|
|
114
|
+
|
|
115
|
+
const res = await fetch(url.toString(), { signal: AbortSignal.timeout(timeout) });
|
|
116
|
+
if (!res.ok) return [];
|
|
117
|
+
|
|
118
|
+
const data = await res.json();
|
|
119
|
+
const servers = (data.servers ?? []).map((item: any) => item.server ?? item);
|
|
120
|
+
return servers.slice(0, limit).map(mapMcpServer);
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
67
124
|
}
|
|
68
125
|
},
|
|
69
126
|
|
|
@@ -80,7 +137,10 @@ export const mcpRegistryAdapter: SourceAdapter = {
|
|
|
80
137
|
};
|
|
81
138
|
|
|
82
139
|
function mapMcpServer(server: McpServer): UnifiedAgent {
|
|
83
|
-
const
|
|
140
|
+
const remote = server.remotes?.find((r) =>
|
|
141
|
+
!r.headers?.some((h) => h.isRequired && h.name.toLowerCase() === "authorization")
|
|
142
|
+
) ?? server.remotes?.[0];
|
|
143
|
+
|
|
84
144
|
const categories = extractCategories(server.description ?? server.name);
|
|
85
145
|
|
|
86
146
|
return {
|
|
@@ -93,68 +153,15 @@ function mapMcpServer(server: McpServer): UnifiedAgent {
|
|
|
93
153
|
source: "mcp-registry",
|
|
94
154
|
protocol: "mcp",
|
|
95
155
|
endpoints: {
|
|
96
|
-
mcp:
|
|
97
|
-
http: server.website ?? server.repo,
|
|
156
|
+
mcp: remote?.url,
|
|
157
|
+
http: server.websiteUrl ?? server.website ?? (server.repository as any)?.url ?? server.repo,
|
|
98
158
|
},
|
|
99
159
|
pricing: { model: "free" },
|
|
100
|
-
verified: !!
|
|
160
|
+
verified: !!remote,
|
|
101
161
|
raw: server,
|
|
102
162
|
};
|
|
103
163
|
}
|
|
104
164
|
|
|
105
|
-
/**
|
|
106
|
-
* Expand natural language query into MCP Registry keyword searches.
|
|
107
|
-
* "search the web for DeFi news" → ["web search", "exa", "DeFi", "news"]
|
|
108
|
-
* "query postgres database" → ["postgres", "database", "sql"]
|
|
109
|
-
*/
|
|
110
|
-
function expandQueryToKeywords(query: string): string[] {
|
|
111
|
-
const terms: string[] = [];
|
|
112
|
-
const lower = query.toLowerCase();
|
|
113
|
-
|
|
114
|
-
// Always include the raw query words (2+ chars)
|
|
115
|
-
const words = lower.split(/\s+/).filter((w) => w.length > 2);
|
|
116
|
-
terms.push(...words.slice(0, 3));
|
|
117
|
-
|
|
118
|
-
// Intent → tool name mappings (what users say → what MCP servers are called)
|
|
119
|
-
const expansions: Record<string, string[]> = {
|
|
120
|
-
"search": ["exa", "web search", "tavily", "search"],
|
|
121
|
-
"web": ["exa", "web", "browser", "scrape"],
|
|
122
|
-
"scrape": ["scrape", "crawl", "browser", "firecrawl"],
|
|
123
|
-
"database": ["postgres", "mysql", "sqlite", "database", "sql"],
|
|
124
|
-
"sql": ["postgres", "mysql", "sql", "database"],
|
|
125
|
-
"postgres": ["postgres"],
|
|
126
|
-
"weather": ["weather"],
|
|
127
|
-
"email": ["email", "gmail", "mail"],
|
|
128
|
-
"github": ["github", "git"],
|
|
129
|
-
"slack": ["slack"],
|
|
130
|
-
"file": ["file", "filesystem", "s3"],
|
|
131
|
-
"image": ["image", "dalle", "vision"],
|
|
132
|
-
"code": ["code", "github", "lint"],
|
|
133
|
-
"defi": ["defi", "exa", "blockchain"],
|
|
134
|
-
"crypto": ["crypto", "blockchain", "exa"],
|
|
135
|
-
"news": ["news", "exa", "search"],
|
|
136
|
-
"data": ["data", "analytics"],
|
|
137
|
-
"deploy": ["deploy", "vercel", "aws"],
|
|
138
|
-
"document": ["document", "pdf", "markdown"],
|
|
139
|
-
"translate": ["translate", "language"],
|
|
140
|
-
"calendar": ["calendar", "google"],
|
|
141
|
-
"map": ["map", "location", "geo"],
|
|
142
|
-
"payment": ["payment", "stripe"],
|
|
143
|
-
"ai": ["openai", "anthropic", "ai"],
|
|
144
|
-
"research": ["exa", "search", "research"],
|
|
145
|
-
"analyze": ["analytics", "data", "chart"],
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
for (const word of words) {
|
|
149
|
-
if (expansions[word]) {
|
|
150
|
-
terms.push(...expansions[word]);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Deduplicate
|
|
155
|
-
return [...new Set(terms)];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
165
|
function extractCategories(text: string): string[] {
|
|
159
166
|
const lower = text.toLowerCase();
|
|
160
167
|
const cats: string[] = [];
|
|
@@ -165,6 +172,8 @@ function extractCategories(text: string): string[] {
|
|
|
165
172
|
file: "tools", browser: "tools", web: "tools",
|
|
166
173
|
ai: "ai", llm: "ai", model: "ai",
|
|
167
174
|
image: "media", video: "media", audio: "media",
|
|
175
|
+
trading: "trading", market: "trading", price: "trading",
|
|
176
|
+
email: "communication", slack: "communication", chat: "communication",
|
|
168
177
|
};
|
|
169
178
|
for (const [keyword, cat] of Object.entries(keywords)) {
|
|
170
179
|
if (lower.includes(keyword) && !cats.includes(cat)) cats.push(cat);
|
package/src/sources/types.ts
CHANGED
|
@@ -14,7 +14,9 @@ export type AgentSource =
|
|
|
14
14
|
| "adp"
|
|
15
15
|
| "a2a-wellknown"
|
|
16
16
|
| "l402-directory"
|
|
17
|
-
| "skills-registry"
|
|
17
|
+
| "skills-registry"
|
|
18
|
+
| "bankr"
|
|
19
|
+
| "locus";
|
|
18
20
|
|
|
19
21
|
export type AgentProtocol = "acp" | "mcp" | "x402" | "a2a" | "http" | "l402" | "onchain" | "skill";
|
|
20
22
|
|
package/src/wallet/bridge.ts
CHANGED
|
@@ -19,6 +19,10 @@ const USDC_ADDRESSES: Record<string, Address> = {
|
|
|
19
19
|
"base-sepolia": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
// Kompass treasury address — receives platform fee on payments
|
|
23
|
+
const KOMPASS_TREASURY: Address = "0x2aB73f8CCee4ABE601cE71eCC82Bd9eC375FeBbA";
|
|
24
|
+
const PLATFORM_FEE_BPS = 200; // 2% = 200 basis points
|
|
25
|
+
|
|
22
26
|
// ── ACP Wallet Topup ─────────────────────────────────────
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -29,12 +33,17 @@ export async function topupACPWallet(
|
|
|
29
33
|
wallet: KompassWallet,
|
|
30
34
|
acpWalletAddress: Address,
|
|
31
35
|
amountUSDC: number
|
|
32
|
-
): Promise<{ txHash: Hex; amount: string }> {
|
|
36
|
+
): Promise<{ txHash: Hex; amount: string; fee?: string }> {
|
|
33
37
|
const network = wallet.getNetwork();
|
|
34
38
|
const usdcAddress = USDC_ADDRESSES[network];
|
|
35
39
|
if (!usdcAddress) throw new Error(`No USDC address for network: ${network}`);
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
// Calculate platform fee (2%)
|
|
42
|
+
const feeUSDC = amountUSDC * PLATFORM_FEE_BPS / 10000;
|
|
43
|
+
const netAmount = amountUSDC - feeUSDC;
|
|
44
|
+
const totalNeeded = parseUnits(String(amountUSDC), 6);
|
|
45
|
+
const netAmountRaw = parseUnits(String(netAmount.toFixed(6)), 6);
|
|
46
|
+
const feeAmountRaw = totalNeeded - netAmountRaw;
|
|
38
47
|
|
|
39
48
|
// Check balance first
|
|
40
49
|
const balance = await wallet.getPublicClient().readContract({
|
|
@@ -44,25 +53,43 @@ export async function topupACPWallet(
|
|
|
44
53
|
args: [wallet.getAddress()],
|
|
45
54
|
});
|
|
46
55
|
|
|
47
|
-
if (balance <
|
|
56
|
+
if (balance < totalNeeded) {
|
|
48
57
|
throw new Error(
|
|
49
58
|
`Insufficient USDC: have ${formatUnits(balance, 6)}, need ${amountUSDC}. ` +
|
|
50
59
|
`Fund your Kompass wallet at ${wallet.getAddress()} on ${network}.`
|
|
51
60
|
);
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
// Transfer net amount to ACP wallet
|
|
54
64
|
const txHash = await (wallet.getWalletClient() as any).writeContract({
|
|
55
65
|
address: usdcAddress,
|
|
56
66
|
abi: USDC_ABI,
|
|
57
67
|
functionName: "transfer",
|
|
58
|
-
args: [acpWalletAddress,
|
|
68
|
+
args: [acpWalletAddress, netAmountRaw],
|
|
59
69
|
});
|
|
60
70
|
|
|
61
71
|
await wallet.getPublicClient().waitForTransactionReceipt({ hash: txHash });
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
// Transfer platform fee to Kompass treasury (skip if treasury == ACP wallet)
|
|
74
|
+
if (feeAmountRaw > 0n && KOMPASS_TREASURY.toLowerCase() !== acpWalletAddress.toLowerCase()) {
|
|
75
|
+
try {
|
|
76
|
+
const feeTx = await (wallet.getWalletClient() as any).writeContract({
|
|
77
|
+
address: usdcAddress,
|
|
78
|
+
abi: USDC_ABI,
|
|
79
|
+
functionName: "transfer",
|
|
80
|
+
args: [KOMPASS_TREASURY, feeAmountRaw],
|
|
81
|
+
});
|
|
82
|
+
await wallet.getPublicClient().waitForTransactionReceipt({ hash: feeTx });
|
|
83
|
+
console.log(`[Kompass Bridge] Platform fee: $${feeUSDC.toFixed(4)} USDC → treasury | tx: ${feeTx}`);
|
|
84
|
+
} catch (feeErr) {
|
|
85
|
+
// Fee transfer failure shouldn't block the job
|
|
86
|
+
console.log(`[Kompass Bridge] Fee transfer failed (non-blocking): ${feeErr instanceof Error ? feeErr.message : feeErr}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`[Kompass Bridge] Transferred ${netAmount.toFixed(4)} USDC to ACP wallet ${acpWalletAddress.slice(0, 10)}... | tx: ${txHash}`);
|
|
64
91
|
|
|
65
|
-
return { txHash, amount: String(
|
|
92
|
+
return { txHash, amount: String(netAmount.toFixed(4)), fee: feeUSDC.toFixed(4) };
|
|
66
93
|
}
|
|
67
94
|
|
|
68
95
|
// ── Lightning Swap (USDC → sats) ─────────────────────────
|
|
@@ -40,9 +40,10 @@ export async function handleACPPayment(
|
|
|
40
40
|
// Job creation (REQUEST) and acceptance (NEGOTIATION) are FREE.
|
|
41
41
|
// We topup only after confirming an agent accepted.
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
43
|
+
// Format requirements to match offering's expected schema.
|
|
44
|
+
// If the offering has a requirement template/schema, try to conform to it.
|
|
45
|
+
// Otherwise fall back to {description: task}.
|
|
46
|
+
const requirements = JSON.stringify(formatRequirements(task, offering));
|
|
46
47
|
|
|
47
48
|
const { stdout } = await execFileAsync(
|
|
48
49
|
"npx",
|
|
@@ -157,3 +158,68 @@ export async function handleACPPayment(
|
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format task requirements to match the offering's expected schema.
|
|
164
|
+
*
|
|
165
|
+
* ACP agents reject jobs when requirements don't match their offering's
|
|
166
|
+
* expected input format. This function:
|
|
167
|
+
* 1. Checks if the offering has a requirement schema/template
|
|
168
|
+
* 2. Tries to extract relevant fields from the natural language task
|
|
169
|
+
* 3. Falls back to {description: task} if no schema exists
|
|
170
|
+
*/
|
|
171
|
+
function formatRequirements(task: string, offering: any): Record<string, unknown> {
|
|
172
|
+
const schema = offering.requirement ?? offering.requirementSchema ?? offering.inputSchema;
|
|
173
|
+
|
|
174
|
+
// No schema defined — send description (most flexible)
|
|
175
|
+
if (!schema || typeof schema === "string") {
|
|
176
|
+
return { description: task };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Schema is an object — try to populate its fields from the task
|
|
180
|
+
if (typeof schema === "object") {
|
|
181
|
+
const formatted: Record<string, unknown> = {};
|
|
182
|
+
const taskLower = task.toLowerCase();
|
|
183
|
+
|
|
184
|
+
for (const [key, spec] of Object.entries(schema)) {
|
|
185
|
+
const keyLower = key.toLowerCase();
|
|
186
|
+
|
|
187
|
+
// Try to extract Ethereum addresses for address-type fields
|
|
188
|
+
if (keyLower.includes("address") || keyLower.includes("token") || keyLower.includes("contract")) {
|
|
189
|
+
const addrMatch = task.match(/0x[a-fA-F0-9]{40}/);
|
|
190
|
+
if (addrMatch) {
|
|
191
|
+
formatted[key] = addrMatch[0];
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Try to extract chain/network
|
|
197
|
+
if (keyLower.includes("chain") || keyLower.includes("network")) {
|
|
198
|
+
const chains = ["base", "ethereum", "polygon", "arbitrum", "optimism", "bsc"];
|
|
199
|
+
const found = chains.find((c) => taskLower.includes(c));
|
|
200
|
+
formatted[key] = found ?? "base";
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Try to extract numeric amounts
|
|
205
|
+
if (keyLower.includes("amount") || keyLower.includes("quantity") || keyLower.includes("limit")) {
|
|
206
|
+
const numMatch = task.match(/\d+\.?\d*/);
|
|
207
|
+
if (numMatch) {
|
|
208
|
+
formatted[key] = parseFloat(numMatch[0]);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// For any other field, use the task description as the value
|
|
214
|
+
if (typeof spec === "string") {
|
|
215
|
+
formatted[key] = task; // Use full task as the value
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Always include description for context
|
|
220
|
+
formatted.description = task;
|
|
221
|
+
return formatted;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { description: task };
|
|
225
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bankr Payment Handler
|
|
3
|
+
* Routes trading tasks through Bankr's LLM Gateway
|
|
4
|
+
* POST prompt → poll for result
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { KompassWallet } from "../index.js";
|
|
8
|
+
import type { UnifiedAgent } from "../../sources/types.js";
|
|
9
|
+
|
|
10
|
+
const BANKR_API = "https://api.bankr.bot";
|
|
11
|
+
|
|
12
|
+
export async function handleBankrPayment(
|
|
13
|
+
wallet: KompassWallet,
|
|
14
|
+
agent: UnifiedAgent,
|
|
15
|
+
task: string,
|
|
16
|
+
): Promise<{ success: boolean; deliverable?: unknown; jobId?: string }> {
|
|
17
|
+
const apiKey = process.env.BANKR_API_KEY;
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
deliverable: {
|
|
22
|
+
error: "BANKR_API_KEY not configured. Set the BANKR_API_KEY environment variable.",
|
|
23
|
+
hint: "Get an API key from https://bankr.bot",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"X-API-Key": apiKey,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Step 1: Submit prompt
|
|
35
|
+
const submitRes = await fetch(`${BANKR_API}/agent/prompt`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers,
|
|
38
|
+
body: JSON.stringify({ prompt: task }),
|
|
39
|
+
signal: AbortSignal.timeout(30000),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!submitRes.ok) {
|
|
43
|
+
const errBody = await submitRes.text().catch(() => "");
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
deliverable: { error: `Bankr API error: ${submitRes.status}`, details: errBody },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const submitData = await submitRes.json();
|
|
51
|
+
const jobId = submitData.jobId ?? submitData.job_id ?? submitData.id;
|
|
52
|
+
|
|
53
|
+
// If the response is immediate (no job polling needed)
|
|
54
|
+
if (!jobId || submitData.result || submitData.response) {
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
deliverable: submitData.result ?? submitData.response ?? submitData,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 2: Poll for result
|
|
62
|
+
const maxAttempts = 30;
|
|
63
|
+
const pollInterval = 2000;
|
|
64
|
+
|
|
65
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
67
|
+
|
|
68
|
+
const pollRes = await fetch(`${BANKR_API}/agent/job/${jobId}`, {
|
|
69
|
+
headers,
|
|
70
|
+
signal: AbortSignal.timeout(10000),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!pollRes.ok) continue;
|
|
74
|
+
|
|
75
|
+
const pollData = await pollRes.json();
|
|
76
|
+
const status = pollData.status?.toLowerCase();
|
|
77
|
+
|
|
78
|
+
if (status === "completed" || status === "done" || status === "success") {
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
deliverable: pollData.result ?? pollData.response ?? pollData,
|
|
82
|
+
jobId,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (status === "failed" || status === "error") {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
deliverable: { error: pollData.error ?? "Job failed", details: pollData },
|
|
90
|
+
jobId,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Still pending, continue polling
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
deliverable: { error: "Job timed out after polling", jobId },
|
|
100
|
+
jobId,
|
|
101
|
+
};
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
deliverable: { error: err instanceof Error ? err.message : String(err) },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|