@absol-labs/agent 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 +196 -0
- package/dist/discovery/registry.d.ts +68 -0
- package/dist/discovery/registry.d.ts.map +1 -0
- package/dist/discovery/registry.js +148 -0
- package/dist/discovery/registry.js.map +1 -0
- package/dist/frameworks/agentkit.d.ts +158 -0
- package/dist/frameworks/agentkit.d.ts.map +1 -0
- package/dist/frameworks/agentkit.js +339 -0
- package/dist/frameworks/agentkit.js.map +1 -0
- package/dist/frameworks/crewai.d.ts +19 -0
- package/dist/frameworks/crewai.d.ts.map +1 -0
- package/dist/frameworks/crewai.js +73 -0
- package/dist/frameworks/crewai.js.map +1 -0
- package/dist/frameworks/eliza.d.ts +393 -0
- package/dist/frameworks/eliza.d.ts.map +1 -0
- package/dist/frameworks/eliza.js +542 -0
- package/dist/frameworks/eliza.js.map +1 -0
- package/dist/frameworks/langchain.d.ts +31 -0
- package/dist/frameworks/langchain.d.ts.map +1 -0
- package/dist/frameworks/langchain.js +206 -0
- package/dist/frameworks/langchain.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/mandates/env.d.ts +51 -0
- package/dist/mandates/env.d.ts.map +1 -0
- package/dist/mandates/env.js +148 -0
- package/dist/mandates/env.js.map +1 -0
- package/dist/mandates/mandate.d.ts +214 -0
- package/dist/mandates/mandate.d.ts.map +1 -0
- package/dist/mandates/mandate.js +172 -0
- package/dist/mandates/mandate.js.map +1 -0
- package/dist/mcp/http-server.d.ts +2 -0
- package/dist/mcp/http-server.d.ts.map +1 -0
- package/dist/mcp/http-server.js +32 -0
- package/dist/mcp/http-server.js.map +1 -0
- package/dist/mcp/http.d.ts +114 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/http.js +428 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/server.d.ts +119 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +679 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.d.ts.map +1 -0
- package/dist/mcp/stdio.js +6 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/sdk/client.d.ts +78 -0
- package/dist/sdk/client.d.ts.map +1 -0
- package/dist/sdk/client.js +99 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/wallet/provider.d.ts +51 -0
- package/dist/wallet/provider.d.ts.map +1 -0
- package/dist/wallet/provider.js +107 -0
- package/dist/wallet/provider.js.map +1 -0
- package/dist/x402/facilitator.d.ts +315 -0
- package/dist/x402/facilitator.d.ts.map +1 -0
- package/dist/x402/facilitator.js +194 -0
- package/dist/x402/facilitator.js.map +1 -0
- package/dist/zktls/reclaim.d.ts +137 -0
- package/dist/zktls/reclaim.d.ts.map +1 -0
- package/dist/zktls/reclaim.js +245 -0
- package/dist/zktls/reclaim.js.map +1 -0
- package/package.json +86 -0
- package/src/discovery/registry.ts +255 -0
- package/src/frameworks/agentkit.ts +537 -0
- package/src/frameworks/crewai.ts +102 -0
- package/src/frameworks/eliza.ts +776 -0
- package/src/frameworks/langchain.ts +325 -0
- package/src/index.ts +179 -0
- package/src/mandates/env.ts +222 -0
- package/src/mandates/mandate.ts +280 -0
- package/src/mcp/http-server.ts +38 -0
- package/src/mcp/http.ts +620 -0
- package/src/mcp/server.ts +1006 -0
- package/src/mcp/stdio.ts +8 -0
- package/src/sdk/client.ts +222 -0
- package/src/wallet/provider.ts +197 -0
- package/src/x402/facilitator.ts +336 -0
- package/src/zktls/reclaim.ts +424 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
HireComputeResult,
|
|
5
|
+
StreamProofClientConfig,
|
|
6
|
+
} from "@absol-labs/sdk";
|
|
7
|
+
import {
|
|
8
|
+
ActionProvider,
|
|
9
|
+
CreateAction,
|
|
10
|
+
WalletProvider,
|
|
11
|
+
type Network,
|
|
12
|
+
} from "@coinbase/agentkit";
|
|
13
|
+
import { isAddress, isHex } from "viem";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
discoverServices as defaultDiscoverServices,
|
|
18
|
+
type DiscoverServicesOptions,
|
|
19
|
+
type ServiceListing,
|
|
20
|
+
} from "../discovery/registry.js";
|
|
21
|
+
import { checkMandate, type SignedSpendMandate } from "../mandates/mandate.js";
|
|
22
|
+
import {
|
|
23
|
+
MandateDeniedError,
|
|
24
|
+
VerifiedStreamAgentClient,
|
|
25
|
+
type AgentSdkClientFactory,
|
|
26
|
+
type MandateAuthorizedStreamActionInput,
|
|
27
|
+
type OpenVerifiedStreamInput,
|
|
28
|
+
type ReclaimAuthorizedStreamInput,
|
|
29
|
+
type ReclaimVerifiedStreamResult,
|
|
30
|
+
type StreamProofTransactionResult,
|
|
31
|
+
type StreamStatusView,
|
|
32
|
+
} from "../sdk/client.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Base Sepolia is the only settlement network Metrik supports today (Model A,
|
|
36
|
+
* single-chain USDC on Base). Every action fails closed on any other network.
|
|
37
|
+
*/
|
|
38
|
+
export const METRIK_BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
39
|
+
|
|
40
|
+
const addressSchema = z
|
|
41
|
+
.string()
|
|
42
|
+
.refine((value) => isAddress(value), "must be an EVM address");
|
|
43
|
+
const bytes32Schema = z
|
|
44
|
+
.string()
|
|
45
|
+
.refine(
|
|
46
|
+
(value) => isHex(value, { strict: true }) && value.length === 66,
|
|
47
|
+
"must be bytes32 hex",
|
|
48
|
+
);
|
|
49
|
+
const decimalStringSchema = z.preprocess(
|
|
50
|
+
(value) =>
|
|
51
|
+
typeof value === "bigint" || typeof value === "number"
|
|
52
|
+
? value.toString()
|
|
53
|
+
: value,
|
|
54
|
+
z
|
|
55
|
+
.string()
|
|
56
|
+
.regex(
|
|
57
|
+
/^(0|[1-9]\d*)$/,
|
|
58
|
+
"must be a base-10 integer string (USDC atomic units, 6 decimals)",
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export const discoverServicesSchema = z.object({
|
|
63
|
+
category: z
|
|
64
|
+
.string()
|
|
65
|
+
.min(1)
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Optional marketplace category to filter listings by."),
|
|
68
|
+
limit: z
|
|
69
|
+
.number()
|
|
70
|
+
.int()
|
|
71
|
+
.positive()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Optional cap on the number of verified listings to return."),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const hireVerifiedServiceSchema = z.object({
|
|
77
|
+
operator: addressSchema.describe(
|
|
78
|
+
"Seller payout wallet (EVM address) that receives USDC for verified delivery.",
|
|
79
|
+
),
|
|
80
|
+
serviceRef: bytes32Schema.describe(
|
|
81
|
+
"bytes32 service identifier the oracle verifies delivery against.",
|
|
82
|
+
),
|
|
83
|
+
budgetUsdc: decimalStringSchema.describe(
|
|
84
|
+
"Total USDC to escrow for this stream, in atomic units (6 decimals).",
|
|
85
|
+
),
|
|
86
|
+
ratePerSecondUsdc: decimalStringSchema.describe(
|
|
87
|
+
"Accrual rate to the seller per verified second, in USDC atomic units.",
|
|
88
|
+
),
|
|
89
|
+
durationSeconds: z
|
|
90
|
+
.number()
|
|
91
|
+
.int()
|
|
92
|
+
.positive()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe(
|
|
95
|
+
"Optional max stream lifetime in seconds. Defaults to the mandate's max duration.",
|
|
96
|
+
),
|
|
97
|
+
publicUrl: z
|
|
98
|
+
.string()
|
|
99
|
+
.url()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe("Optional service endpoint URL, recorded in the human summary."),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const checkStreamStatusSchema = z.object({
|
|
105
|
+
streamId: bytes32Schema.describe(
|
|
106
|
+
"The stream id returned by hire_verified_service.",
|
|
107
|
+
),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const reclaimUnspentSchema = z.object({
|
|
111
|
+
streamId: bytes32Schema.describe(
|
|
112
|
+
"The stream id to reclaim unspent USDC from.",
|
|
113
|
+
),
|
|
114
|
+
closeFirst: z
|
|
115
|
+
.boolean()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe(
|
|
118
|
+
"Close the stream before reclaiming. Use when the stream is still active.",
|
|
119
|
+
),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const closeStreamSchema = z.object({
|
|
123
|
+
streamId: bytes32Schema.describe("The stream id to close."),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolves the mutable, per-owner mandate accounting the guard needs: how much
|
|
128
|
+
* of the total cap has already been spent, and which mandate ids are revoked.
|
|
129
|
+
*/
|
|
130
|
+
export interface MetrikMandateStateResolver {
|
|
131
|
+
getSpentSoFarUsdc(owner: `0x${string}`): Promise<bigint>;
|
|
132
|
+
getRevokedMandateIds(owner: `0x${string}`): Promise<readonly `0x${string}`[]>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DEFAULT_MANDATE_STATE_RESOLVER: MetrikMandateStateResolver = {
|
|
136
|
+
getSpentSoFarUsdc: async () => 0n,
|
|
137
|
+
getRevokedMandateIds: async () => [],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The subset of the Metrik SDK the action provider drives. `VerifiedStreamAgentClient`
|
|
142
|
+
* satisfies this structurally; tests inject a lightweight double so the provider
|
|
143
|
+
* is exercisable with no CDP creds and no RPC.
|
|
144
|
+
*/
|
|
145
|
+
export interface MetrikAgentKitClient {
|
|
146
|
+
openVerifiedStream(
|
|
147
|
+
input: OpenVerifiedStreamInput,
|
|
148
|
+
): Promise<HireComputeResult>;
|
|
149
|
+
getStreamStatus(streamId: `0x${string}`): Promise<StreamStatusView>;
|
|
150
|
+
reclaimStream(
|
|
151
|
+
input: ReclaimAuthorizedStreamInput,
|
|
152
|
+
): Promise<ReclaimVerifiedStreamResult>;
|
|
153
|
+
closeStream(
|
|
154
|
+
input: MandateAuthorizedStreamActionInput,
|
|
155
|
+
): Promise<StreamProofTransactionResult>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface MetrikActionProviderConfig {
|
|
159
|
+
/**
|
|
160
|
+
* The owner-signed spend mandate that bounds every hire (per-stream + total
|
|
161
|
+
* budget, rate, duration, allowed operators). Configured once at the
|
|
162
|
+
* agent's construction — the LLM never supplies or edits it.
|
|
163
|
+
*/
|
|
164
|
+
readonly signedMandate: SignedSpendMandate;
|
|
165
|
+
/** Settlement chain id. Defaults to Base Sepolia (84532); any other value fails closed. */
|
|
166
|
+
readonly chainId?: number;
|
|
167
|
+
/** Injected Metrik client (test path). Mutually exclusive with `sdkConfig`. */
|
|
168
|
+
readonly agentClient?: MetrikAgentKitClient;
|
|
169
|
+
/**
|
|
170
|
+
* Metrik SDK config (escrow address, chain, rpc, signing account) used to
|
|
171
|
+
* BUILD a `VerifiedStreamAgentClient` when no `agentClient` is injected. The
|
|
172
|
+
* signing account comes from the CDP wallet in `createMetrikAgentKit`.
|
|
173
|
+
*/
|
|
174
|
+
readonly sdkConfig?: StreamProofClientConfig;
|
|
175
|
+
/** Optional SDK client factory override, forwarded to the built client. */
|
|
176
|
+
readonly createSdkClient?: AgentSdkClientFactory;
|
|
177
|
+
/** Mandate accounting source. Defaults to fresh state (0 spent, none revoked). */
|
|
178
|
+
readonly mandateStateResolver?: Partial<MetrikMandateStateResolver>;
|
|
179
|
+
/** Clock injection for deterministic tests. Defaults to wall-clock seconds. */
|
|
180
|
+
readonly now?: () => number;
|
|
181
|
+
/**
|
|
182
|
+
* Discovery driver for the `discover_services` action. Defaults to the live
|
|
183
|
+
* registry-backed `discoverServices`; tests inject a double.
|
|
184
|
+
*/
|
|
185
|
+
readonly discoverServices?: (
|
|
186
|
+
opts?: DiscoverServicesOptions,
|
|
187
|
+
) => Promise<ServiceListing[]>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const warnedAgentKitGaps = new Set<string>();
|
|
191
|
+
|
|
192
|
+
/** Emits a stderr warning at most once per process for a given key. */
|
|
193
|
+
function warnAgentKitOnce(key: string, message: string): void {
|
|
194
|
+
if (warnedAgentKitGaps.has(key)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
warnedAgentKitGaps.add(key);
|
|
198
|
+
console.error(message);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class MetrikActionProviderConfigError extends Error {
|
|
202
|
+
constructor(message: string) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.name = "MetrikActionProviderConfigError";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Coinbase AgentKit action provider that lets any AgentKit-based agent hire and
|
|
210
|
+
* pay third-party services through Metrik's verified USDC escrow. Every action
|
|
211
|
+
* is fail-closed and buyer-favoring; a hire is refused before any transaction if
|
|
212
|
+
* it would breach the configured spend mandate.
|
|
213
|
+
*/
|
|
214
|
+
export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
215
|
+
readonly #client: MetrikAgentKitClient;
|
|
216
|
+
readonly #signedMandate: SignedSpendMandate;
|
|
217
|
+
readonly #resolver: MetrikMandateStateResolver;
|
|
218
|
+
readonly #now: () => number;
|
|
219
|
+
readonly #chainId: number;
|
|
220
|
+
readonly #discover: (
|
|
221
|
+
opts?: DiscoverServicesOptions,
|
|
222
|
+
) => Promise<ServiceListing[]>;
|
|
223
|
+
|
|
224
|
+
constructor(config: MetrikActionProviderConfig) {
|
|
225
|
+
super("metrik", []);
|
|
226
|
+
|
|
227
|
+
this.#chainId = config.chainId ?? METRIK_BASE_SEPOLIA_CHAIN_ID;
|
|
228
|
+
if (this.#chainId !== METRIK_BASE_SEPOLIA_CHAIN_ID) {
|
|
229
|
+
// Fail closed: Model A settles only on Base Sepolia today.
|
|
230
|
+
throw new MetrikActionProviderConfigError(
|
|
231
|
+
`unsupported chainId ${this.#chainId}: Metrik settles only on Base Sepolia (${METRIK_BASE_SEPOLIA_CHAIN_ID})`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.#client = resolveClient(config);
|
|
236
|
+
this.#signedMandate = config.signedMandate;
|
|
237
|
+
this.#resolver = {
|
|
238
|
+
...DEFAULT_MANDATE_STATE_RESOLVER,
|
|
239
|
+
...config.mandateStateResolver,
|
|
240
|
+
};
|
|
241
|
+
// Fail-loud on the two mandate controls that require external state. The
|
|
242
|
+
// action provider holds no stream registry, so without a wired resolver the
|
|
243
|
+
// cumulative cap (maxTotalUsdc) and revocation are not enforced. Warn once.
|
|
244
|
+
if (config.mandateStateResolver?.getSpentSoFarUsdc === undefined) {
|
|
245
|
+
warnAgentKitOnce(
|
|
246
|
+
"agentkit-spent",
|
|
247
|
+
"[metrik] AgentKit provider: no getSpentSoFarUsdc wired — the mandate cumulative cap (maxTotalUsdc) is NOT enforced across hires (per-stream/rate/duration/operator/expiry still are).",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (config.mandateStateResolver?.getRevokedMandateIds === undefined) {
|
|
251
|
+
warnAgentKitOnce(
|
|
252
|
+
"agentkit-revocation",
|
|
253
|
+
"[metrik] AgentKit provider: no getRevokedMandateIds wired — revoked mandates will NOT be rejected until you wire a revocation store.",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
this.#now = config.now ?? (() => Math.floor(Date.now() / 1000));
|
|
257
|
+
this.#discover = config.discoverServices ?? defaultDiscoverServices;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@CreateAction({
|
|
261
|
+
name: "discover_services",
|
|
262
|
+
description:
|
|
263
|
+
"Discover verified third-party services on the Metrik marketplace. Returns each seller's payout operator, bytes32 serviceRef, and endpoint URL — but only for listings whose operator signature is cryptographically verified, so the agent can go straight from discovery to hire_verified_service without pre-known addresses.",
|
|
264
|
+
schema: discoverServicesSchema,
|
|
265
|
+
})
|
|
266
|
+
async discoverServices(
|
|
267
|
+
_walletProvider: WalletProvider,
|
|
268
|
+
args: z.infer<typeof discoverServicesSchema>,
|
|
269
|
+
): Promise<string> {
|
|
270
|
+
const listings = await this.#discover({
|
|
271
|
+
...(args.category === undefined ? {} : { category: args.category }),
|
|
272
|
+
...(args.limit === undefined ? {} : { limit: args.limit }),
|
|
273
|
+
});
|
|
274
|
+
if (listings.length === 0) {
|
|
275
|
+
return "No verified Metrik services discovered.";
|
|
276
|
+
}
|
|
277
|
+
const lines = listings.map(
|
|
278
|
+
(listing) =>
|
|
279
|
+
`- ${listing.serviceRef} operator=${listing.operator} url=${listing.publicUrl}${listing.category ? ` category=${listing.category}` : ""}`,
|
|
280
|
+
);
|
|
281
|
+
return (
|
|
282
|
+
`Discovered ${listings.length} verified Metrik service${listings.length === 1 ? "" : "s"} ` +
|
|
283
|
+
`(each operator signature verified). Hire one with hire_verified_service using its operator + serviceRef:\n` +
|
|
284
|
+
lines.join("\n")
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@CreateAction({
|
|
289
|
+
name: "hire_verified_service",
|
|
290
|
+
description:
|
|
291
|
+
"Hire a third-party service through Metrik: escrow USDC on Base for a per-second verified-delivery stream to the seller. Payment accrues only for delivery the oracle proves, and unspent funds are reclaimable. Refuses before any transaction if the hire would exceed the signed spend mandate.",
|
|
292
|
+
schema: hireVerifiedServiceSchema,
|
|
293
|
+
})
|
|
294
|
+
async hireVerifiedService(
|
|
295
|
+
walletProvider: WalletProvider,
|
|
296
|
+
args: z.infer<typeof hireVerifiedServiceSchema>,
|
|
297
|
+
): Promise<string> {
|
|
298
|
+
this.#assertWalletNetwork(walletProvider);
|
|
299
|
+
|
|
300
|
+
const owner = this.#signedMandate.owner as `0x${string}`;
|
|
301
|
+
const nowSeconds = this.#now();
|
|
302
|
+
const [spentSoFarUsdc, revokedMandateIds] = await Promise.all([
|
|
303
|
+
this.#resolver.getSpentSoFarUsdc(owner),
|
|
304
|
+
this.#resolver.getRevokedMandateIds(owner),
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
const operator = args.operator as `0x${string}`;
|
|
308
|
+
const serviceRef = args.serviceRef as `0x${string}`;
|
|
309
|
+
const budgetUsdc = BigInt(args.budgetUsdc);
|
|
310
|
+
const ratePerSecondUsdc = BigInt(args.ratePerSecondUsdc);
|
|
311
|
+
const maxDurationSeconds =
|
|
312
|
+
args.durationSeconds ?? this.#signedMandate.mandate.maxDurationSeconds;
|
|
313
|
+
|
|
314
|
+
// Mandate guard runs BEFORE any transaction — fail closed on breach.
|
|
315
|
+
const decision = await checkMandate(
|
|
316
|
+
this.#signedMandate,
|
|
317
|
+
{
|
|
318
|
+
operator,
|
|
319
|
+
budgetUsdc,
|
|
320
|
+
ratePerSecondUsdc,
|
|
321
|
+
maxDurationSeconds,
|
|
322
|
+
},
|
|
323
|
+
spentSoFarUsdc,
|
|
324
|
+
{ nowSeconds, revokedMandateIds },
|
|
325
|
+
);
|
|
326
|
+
if (!decision.allowed) {
|
|
327
|
+
throw new MandateDeniedError(decision.reason ?? "mandate-expired");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result = await this.#client.openVerifiedStream({
|
|
331
|
+
operator,
|
|
332
|
+
serviceRef,
|
|
333
|
+
budgetUsdc,
|
|
334
|
+
ratePerSecondUsdc,
|
|
335
|
+
maxDurationSeconds,
|
|
336
|
+
signedMandate: this.#signedMandate,
|
|
337
|
+
spentSoFarUsdc,
|
|
338
|
+
nowSeconds,
|
|
339
|
+
revokedMandateIds,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const urlLine = args.publicUrl ? ` Endpoint: ${args.publicUrl}.` : "";
|
|
343
|
+
return (
|
|
344
|
+
`Hired verified service via Metrik. Stream ${result.streamId} opened on Base Sepolia (escrow ${result.handle.escrow}). ` +
|
|
345
|
+
`Escrowed ${formatUsdc(budgetUsdc)} USDC at ${formatUsdc(ratePerSecondUsdc)} USDC/sec to operator ${operator} for service ${serviceRef}, ` +
|
|
346
|
+
`max ${maxDurationSeconds}s. Payment accrues only for oracle-verified delivery; unspent USDC is reclaimable.${urlLine} ` +
|
|
347
|
+
`tx=${result.txHash}${result.approveTxHash ? ` approve=${result.approveTxHash}` : ""}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@CreateAction({
|
|
352
|
+
name: "check_stream_status",
|
|
353
|
+
description:
|
|
354
|
+
"Read a Metrik stream's status: verified accrued amount, whether it is active or paused (two failed checks auto-pause), claimable-by-seller, and reclaimable-by-buyer USDC.",
|
|
355
|
+
schema: checkStreamStatusSchema,
|
|
356
|
+
})
|
|
357
|
+
async checkStreamStatus(
|
|
358
|
+
walletProvider: WalletProvider,
|
|
359
|
+
args: z.infer<typeof checkStreamStatusSchema>,
|
|
360
|
+
): Promise<string> {
|
|
361
|
+
this.#assertWalletNetwork(walletProvider);
|
|
362
|
+
|
|
363
|
+
const streamId = args.streamId as `0x${string}`;
|
|
364
|
+
const { stream, claimable, reclaimable } =
|
|
365
|
+
await this.#client.getStreamStatus(streamId);
|
|
366
|
+
const paused = stream.status !== "active";
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
`Stream ${streamId} is ${stream.status}${paused ? " (payment halted)" : ""}. ` +
|
|
370
|
+
`Verified accrued: ${formatUsdc(stream.accrued)} USDC of ${formatUsdc(stream.deposit)} deposited. ` +
|
|
371
|
+
`Claimable by seller: ${formatUsdc(claimable)} USDC. Reclaimable by buyer: ${formatUsdc(reclaimable)} USDC. ` +
|
|
372
|
+
`Consecutive failed checks: ${stream.consecutiveFailures}.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
@CreateAction({
|
|
377
|
+
name: "reclaim_unspent",
|
|
378
|
+
description:
|
|
379
|
+
"Reclaim unspent escrowed USDC from a Metrik stream back to the buyer. Set closeFirst to close an active stream before reclaiming. Runs under the same signed mandate controls.",
|
|
380
|
+
schema: reclaimUnspentSchema,
|
|
381
|
+
})
|
|
382
|
+
async reclaimUnspent(
|
|
383
|
+
walletProvider: WalletProvider,
|
|
384
|
+
args: z.infer<typeof reclaimUnspentSchema>,
|
|
385
|
+
): Promise<string> {
|
|
386
|
+
this.#assertWalletNetwork(walletProvider);
|
|
387
|
+
|
|
388
|
+
const result = await this.#client.reclaimStream(
|
|
389
|
+
await this.#authorizedStreamAction(args.streamId as `0x${string}`, {
|
|
390
|
+
...(args.closeFirst === undefined
|
|
391
|
+
? {}
|
|
392
|
+
: { closeFirst: args.closeFirst }),
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
`Reclaimed unspent USDC from stream ${args.streamId} to the buyer. ` +
|
|
398
|
+
`${result.closeResult ? `Closed first (tx=${result.closeResult.txHash}). ` : ""}` +
|
|
399
|
+
`reclaim tx=${result.reclaimResult.txHash}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@CreateAction({
|
|
404
|
+
name: "close_stream",
|
|
405
|
+
description:
|
|
406
|
+
"Close a Metrik stream, finalizing accrual and stopping further payment. Runs under the same signed mandate controls.",
|
|
407
|
+
schema: closeStreamSchema,
|
|
408
|
+
})
|
|
409
|
+
async closeStream(
|
|
410
|
+
walletProvider: WalletProvider,
|
|
411
|
+
args: z.infer<typeof closeStreamSchema>,
|
|
412
|
+
): Promise<string> {
|
|
413
|
+
this.#assertWalletNetwork(walletProvider);
|
|
414
|
+
|
|
415
|
+
const result = await this.#client.closeStream(
|
|
416
|
+
await this.#authorizedStreamAction(args.streamId as `0x${string}`, {}),
|
|
417
|
+
);
|
|
418
|
+
return `Closed stream ${args.streamId}. tx=${result.txHash}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
supportsNetwork(network: Network): boolean {
|
|
422
|
+
// Fail closed: EVM Base Sepolia only.
|
|
423
|
+
return (
|
|
424
|
+
network.protocolFamily === "evm" &&
|
|
425
|
+
network.chainId === String(this.#chainId)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#assertWalletNetwork(walletProvider: WalletProvider): void {
|
|
430
|
+
const network = walletProvider.getNetwork();
|
|
431
|
+
if (!this.supportsNetwork(network)) {
|
|
432
|
+
throw new MetrikActionProviderConfigError(
|
|
433
|
+
`unsupported network ${network.protocolFamily}:${network.chainId ?? "unknown"}: Metrik requires Base Sepolia (evm:${this.#chainId})`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async #authorizedStreamAction<TExtra extends object>(
|
|
439
|
+
streamId: `0x${string}`,
|
|
440
|
+
extra: TExtra,
|
|
441
|
+
): Promise<MandateAuthorizedStreamActionInput & TExtra> {
|
|
442
|
+
const owner = this.#signedMandate.owner as `0x${string}`;
|
|
443
|
+
const nowSeconds = this.#now();
|
|
444
|
+
const [spentSoFarUsdc, revokedMandateIds] = await Promise.all([
|
|
445
|
+
this.#resolver.getSpentSoFarUsdc(owner),
|
|
446
|
+
this.#resolver.getRevokedMandateIds(owner),
|
|
447
|
+
]);
|
|
448
|
+
return {
|
|
449
|
+
streamId,
|
|
450
|
+
signedMandate: this.#signedMandate,
|
|
451
|
+
spentSoFarUsdc,
|
|
452
|
+
nowSeconds,
|
|
453
|
+
revokedMandateIds,
|
|
454
|
+
...extra,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Factory mirroring AgentKit's `xxxActionProvider()` convention.
|
|
461
|
+
*/
|
|
462
|
+
export function metrikActionProvider(
|
|
463
|
+
config: MetrikActionProviderConfig,
|
|
464
|
+
): MetrikActionProvider {
|
|
465
|
+
return new MetrikActionProvider(config);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface CreateMetrikAgentKitOptions {
|
|
469
|
+
/**
|
|
470
|
+
* A concrete AgentKit wallet provider (e.g. `CdpEvmWalletProvider` or
|
|
471
|
+
* `CdpSmartWalletProvider`) built from the CDP creds. Constructing it reaches
|
|
472
|
+
* CDP and therefore needs live creds at runtime; the action provider itself is
|
|
473
|
+
* independently unit-testable without any creds.
|
|
474
|
+
*/
|
|
475
|
+
readonly walletProvider: WalletProvider;
|
|
476
|
+
readonly metrik: MetrikActionProviderConfig;
|
|
477
|
+
/** Additional AgentKit action providers to compose alongside Metrik. */
|
|
478
|
+
readonly additionalActionProviders?: readonly ActionProvider[];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Convenience that composes Metrik into an AgentKit instance alongside the CDP
|
|
483
|
+
* wallet provider. From the returned `AgentKit` you can expose Metrik natively
|
|
484
|
+
* via `getLangChainTools`, `getVercelAITools`, or the AgentKit MCP server.
|
|
485
|
+
*/
|
|
486
|
+
export async function createMetrikAgentKit(
|
|
487
|
+
options: CreateMetrikAgentKitOptions,
|
|
488
|
+
) {
|
|
489
|
+
const { AgentKit } = await import("@coinbase/agentkit");
|
|
490
|
+
return AgentKit.from({
|
|
491
|
+
walletProvider: options.walletProvider,
|
|
492
|
+
actionProviders: [
|
|
493
|
+
metrikActionProvider(options.metrik),
|
|
494
|
+
...(options.additionalActionProviders ?? []),
|
|
495
|
+
],
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function resolveClient(
|
|
500
|
+
config: MetrikActionProviderConfig,
|
|
501
|
+
): MetrikAgentKitClient {
|
|
502
|
+
if (config.agentClient !== undefined) {
|
|
503
|
+
if (config.sdkConfig !== undefined) {
|
|
504
|
+
throw new MetrikActionProviderConfigError(
|
|
505
|
+
"provide either agentClient or sdkConfig, not both",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return config.agentClient;
|
|
509
|
+
}
|
|
510
|
+
if (config.sdkConfig !== undefined) {
|
|
511
|
+
return new VerifiedStreamAgentClient(
|
|
512
|
+
config.sdkConfig,
|
|
513
|
+
config.createSdkClient === undefined
|
|
514
|
+
? {}
|
|
515
|
+
: { createSdkClient: config.createSdkClient },
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
throw new MetrikActionProviderConfigError(
|
|
519
|
+
"MetrikActionProvider requires either an injected agentClient or an sdkConfig (escrow address, chain, rpc, signing account)",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const USDC_DECIMALS = 6n;
|
|
524
|
+
const USDC_SCALE = 10n ** USDC_DECIMALS;
|
|
525
|
+
|
|
526
|
+
function formatUsdc(atomic: bigint): string {
|
|
527
|
+
const whole = atomic / USDC_SCALE;
|
|
528
|
+
const fraction = atomic % USDC_SCALE;
|
|
529
|
+
if (fraction === 0n) {
|
|
530
|
+
return whole.toString();
|
|
531
|
+
}
|
|
532
|
+
const fractionStr = fraction
|
|
533
|
+
.toString()
|
|
534
|
+
.padStart(Number(USDC_DECIMALS), "0")
|
|
535
|
+
.replace(/0+$/, "");
|
|
536
|
+
return `${whole.toString()}.${fractionStr}`;
|
|
537
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export const CREWAI_VERIFIED_STREAM_TOOL_NAMES = [
|
|
2
|
+
"discover_services",
|
|
3
|
+
"hire_verified_service",
|
|
4
|
+
"check_stream_status",
|
|
5
|
+
"reclaim_unspent",
|
|
6
|
+
"list_streams",
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
export interface CrewAiVerifiedStreamMcpConfig {
|
|
10
|
+
readonly transport: "stdio";
|
|
11
|
+
readonly command: string;
|
|
12
|
+
readonly args: readonly string[];
|
|
13
|
+
readonly env: Readonly<Record<string, string>>;
|
|
14
|
+
readonly allowedToolNames: readonly string[];
|
|
15
|
+
readonly cacheToolsList: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CrewAiVerifiedStreamMcpConfigOptions {
|
|
19
|
+
readonly command?: string;
|
|
20
|
+
readonly args?: readonly string[];
|
|
21
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
22
|
+
readonly allowedToolNames?: readonly string[];
|
|
23
|
+
readonly cacheToolsList?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createCrewAiVerifiedStreamMcpConfig(
|
|
27
|
+
options: CrewAiVerifiedStreamMcpConfigOptions = {},
|
|
28
|
+
): CrewAiVerifiedStreamMcpConfig {
|
|
29
|
+
return {
|
|
30
|
+
transport: "stdio",
|
|
31
|
+
command: options.command ?? "pnpm",
|
|
32
|
+
args: options.args ?? ["mcp:stdio"],
|
|
33
|
+
env: options.env ?? {},
|
|
34
|
+
allowedToolNames:
|
|
35
|
+
options.allowedToolNames ?? CREWAI_VERIFIED_STREAM_TOOL_NAMES,
|
|
36
|
+
cacheToolsList: options.cacheToolsList ?? true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderCrewAiVerifiedStreamPythonExample(
|
|
41
|
+
options: CrewAiVerifiedStreamMcpConfigOptions = {},
|
|
42
|
+
): string {
|
|
43
|
+
const config = createCrewAiVerifiedStreamMcpConfig(options);
|
|
44
|
+
const envEntries = Object.entries(config.env)
|
|
45
|
+
.map(
|
|
46
|
+
([key, value]) =>
|
|
47
|
+
` "${escapePythonString(key)}": "${escapePythonString(value)}",`,
|
|
48
|
+
)
|
|
49
|
+
.join("\n");
|
|
50
|
+
const toolNames = config.allowedToolNames
|
|
51
|
+
.map((name) => ` "${escapePythonString(name)}",`)
|
|
52
|
+
.join("\n");
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
"from crewai import Agent, Crew, Task",
|
|
56
|
+
"from crewai.mcp import MCPServerStdio",
|
|
57
|
+
"from crewai.mcp.filters import create_static_tool_filter",
|
|
58
|
+
"",
|
|
59
|
+
"metrik_mcp = MCPServerStdio(",
|
|
60
|
+
` command="${escapePythonString(config.command)}",`,
|
|
61
|
+
` args=${renderPythonList(config.args)},`,
|
|
62
|
+
" env={",
|
|
63
|
+
envEntries,
|
|
64
|
+
" },",
|
|
65
|
+
" tool_filter=create_static_tool_filter(",
|
|
66
|
+
" allowed_tool_names=[",
|
|
67
|
+
toolNames,
|
|
68
|
+
" ]",
|
|
69
|
+
" ),",
|
|
70
|
+
` cache_tools_list=${config.cacheToolsList ? "True" : "False"},`,
|
|
71
|
+
")",
|
|
72
|
+
"",
|
|
73
|
+
"buyer_agent = Agent(",
|
|
74
|
+
' role="Verified Compute Buyer",',
|
|
75
|
+
' goal="Hire a verified service and stop work when the stream pauses.",',
|
|
76
|
+
' backstory="An operator that only pays for oracle-verified delivery.",',
|
|
77
|
+
" mcps=[metrik_mcp],",
|
|
78
|
+
")",
|
|
79
|
+
"",
|
|
80
|
+
"task = Task(",
|
|
81
|
+
' description="Use hire_verified_service to open a stream, then call check_stream_status to monitor it.",',
|
|
82
|
+
' expected_output="A stream id plus the current verified stream status.",',
|
|
83
|
+
" agent=buyer_agent,",
|
|
84
|
+
")",
|
|
85
|
+
"",
|
|
86
|
+
"crew = Crew(agents=[buyer_agent], tasks=[task])",
|
|
87
|
+
"result = crew.kickoff()",
|
|
88
|
+
"print(result)",
|
|
89
|
+
"",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderPythonList(values: readonly string[]): string {
|
|
94
|
+
const rendered = values
|
|
95
|
+
.map((value) => `"${escapePythonString(value)}"`)
|
|
96
|
+
.join(", ");
|
|
97
|
+
return `[${rendered}]`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function escapePythonString(value: string): string {
|
|
101
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
102
|
+
}
|