@elisym/cli 0.22.5 → 0.23.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/index.js +1451 -7
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/skills-examples/README.md +2 -0
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
|
|
5
|
-
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport,
|
|
6
|
-
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, signerFromSecretKeyBase58, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, generateSolanaWallet, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, NATIVE_SOL } from '@elisym/sdk';
|
|
6
|
+
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
7
7
|
import { isAddress, createSolanaRpc, address } from '@solana/kit';
|
|
8
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
9
9
|
import YAML from 'yaml';
|
|
@@ -14,10 +14,15 @@ import { createIrohTransport } from '@elisym/sdk/node';
|
|
|
14
14
|
import { lookup } from 'node:dns/promises';
|
|
15
15
|
import { Socket } from 'node:net';
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { readFile, mkdtemp, rm,
|
|
17
|
+
import { mkdir, writeFile, readFile, mkdtemp, rm, rename, stat } from 'node:fs/promises';
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
19
|
import pLimit from 'p-limit';
|
|
20
|
-
import { parseSkillMd, validateSkillFrontmatter, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
20
|
+
import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
21
|
+
import { x402Client, wrapFetchWithPayment } from '@x402/fetch';
|
|
22
|
+
import { ExactSvmScheme } from '@x402/svm';
|
|
23
|
+
import { decodePaymentRequiredHeader } from '@x402/core/http';
|
|
24
|
+
import chalk from 'chalk';
|
|
25
|
+
import Decimal from 'decimal.js-light';
|
|
21
26
|
import { fileURLToPath } from 'node:url';
|
|
22
27
|
|
|
23
28
|
var __defProp = Object.defineProperty;
|
|
@@ -2066,6 +2071,43 @@ var MIME_BY_EXT = {
|
|
|
2066
2071
|
function mimeFromPath(path) {
|
|
2067
2072
|
return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
2068
2073
|
}
|
|
2074
|
+
|
|
2075
|
+
// src/x402/errors.ts
|
|
2076
|
+
var X402PreflightError = class extends Error {
|
|
2077
|
+
/**
|
|
2078
|
+
* Optional customer-facing text. Preflight reasons split in two: input
|
|
2079
|
+
* problems the CUSTOMER can act on (too large for a GET upstream) vs
|
|
2080
|
+
* operator problems (empty float, negative margin) that must stay generic
|
|
2081
|
+
* to avoid leaking billing state.
|
|
2082
|
+
*/
|
|
2083
|
+
customerMessage;
|
|
2084
|
+
constructor(message, customerMessage) {
|
|
2085
|
+
super(message);
|
|
2086
|
+
this.name = "X402PreflightError";
|
|
2087
|
+
this.customerMessage = customerMessage;
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
var X402TransientError = class extends Error {
|
|
2091
|
+
/**
|
|
2092
|
+
* Final upstream HTTP status when the failure was a definitive response
|
|
2093
|
+
* (402 refusal, 429, 5xx); absent for network-level failures. The driver
|
|
2094
|
+
* keys the refund-and-retry decision on `402` here.
|
|
2095
|
+
*/
|
|
2096
|
+
upstreamStatus;
|
|
2097
|
+
constructor(message, upstreamStatus) {
|
|
2098
|
+
super(message);
|
|
2099
|
+
this.name = "X402TransientError";
|
|
2100
|
+
this.upstreamStatus = upstreamStatus;
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
var X402PermanentError = class extends Error {
|
|
2104
|
+
constructor(message) {
|
|
2105
|
+
super(message);
|
|
2106
|
+
this.name = "X402PermanentError";
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
// src/runtime.ts
|
|
2069
2111
|
var payment = new SolanaPaymentStrategy();
|
|
2070
2112
|
var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
2071
2113
|
var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -2550,7 +2592,7 @@ var AgentRuntime = class {
|
|
|
2550
2592
|
const log = this.callbacks.onLog ?? console.log;
|
|
2551
2593
|
log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
|
|
2552
2594
|
const currentStatus = this.ledger.getStatus(job.jobId);
|
|
2553
|
-
const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError || e instanceof PaymentTimeoutError) && currentStatus === "paid";
|
|
2595
|
+
const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError || e instanceof PaymentTimeoutError || e instanceof X402TransientError || e instanceof ExecutionBudgetExceededError && this.skills.route(job.tags)?.mode === "x402") && currentStatus === "paid";
|
|
2554
2596
|
if (currentStatus !== "executed" && !keepPaidForRecovery) {
|
|
2555
2597
|
this.ledger.markFailed(job.jobId);
|
|
2556
2598
|
}
|
|
@@ -2589,6 +2631,37 @@ var AgentRuntime = class {
|
|
|
2589
2631
|
return;
|
|
2590
2632
|
}
|
|
2591
2633
|
}
|
|
2634
|
+
if (matched?.mode === "x402" && job.attachment !== void 0) {
|
|
2635
|
+
const method = matched.x402?.method ?? "POST";
|
|
2636
|
+
const maxInputBytes = matched.x402?.maxInputBytes ?? 0;
|
|
2637
|
+
const attachmentOk = method === "POST" && job.attachment.mime.startsWith("text/") && job.attachment.size <= maxInputBytes;
|
|
2638
|
+
if (!attachmentOk) {
|
|
2639
|
+
log(
|
|
2640
|
+
`[${job.jobId.slice(0, 8)}] Rejecting attachment on x402 skill (mime=${job.attachment.mime}, size=${job.attachment.size})`
|
|
2641
|
+
);
|
|
2642
|
+
await this.transport.sendFeedback(job, {
|
|
2643
|
+
type: "error",
|
|
2644
|
+
message: "This skill accepts inline text input only (or a text attachment within its size limit)."
|
|
2645
|
+
}).catch(() => {
|
|
2646
|
+
});
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (matched?.preflight) {
|
|
2651
|
+
try {
|
|
2652
|
+
await matched.preflight(
|
|
2653
|
+
{ data: job.input, inputType: job.inputType, tags: job.tags, jobId: job.jobId },
|
|
2654
|
+
this.skillCtx
|
|
2655
|
+
);
|
|
2656
|
+
} catch (err) {
|
|
2657
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2658
|
+
log(`[${job.jobId.slice(0, 8)}] Preflight refused job: ${detail}`);
|
|
2659
|
+
const customerMessage = err instanceof X402PreflightError && err.customerMessage !== void 0 ? err.customerMessage : AGENT_UNAVAILABLE_MESSAGE;
|
|
2660
|
+
await this.transport.sendFeedback(job, { type: "error", message: customerMessage }).catch(() => {
|
|
2661
|
+
});
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2592
2665
|
if (job.attachment !== void 0 && resolveJobPrice(job.tags, this.skills) === 0) {
|
|
2593
2666
|
log(`[${job.jobId.slice(0, 8)}] Rejecting file input on a free skill`);
|
|
2594
2667
|
await this.transport.sendFeedback(job, { type: "error", message: "File inputs require a paid skill." }).catch(() => {
|
|
@@ -3332,6 +3405,25 @@ var AgentRuntime = class {
|
|
|
3332
3405
|
throw err;
|
|
3333
3406
|
}
|
|
3334
3407
|
}
|
|
3408
|
+
if (skill.preflight) {
|
|
3409
|
+
try {
|
|
3410
|
+
await skill.preflight(
|
|
3411
|
+
{
|
|
3412
|
+
data: entry.input,
|
|
3413
|
+
inputType: entry.input_type,
|
|
3414
|
+
tags: entry.tags,
|
|
3415
|
+
jobId: entry.job_id
|
|
3416
|
+
},
|
|
3417
|
+
this.skillCtx
|
|
3418
|
+
);
|
|
3419
|
+
} catch (err) {
|
|
3420
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3421
|
+
log(
|
|
3422
|
+
`[${entry.job_id.slice(0, 8)}] Recovery: preflight refused (${detail}); waiting for next tick.`
|
|
3423
|
+
);
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3335
3427
|
this.ledger.incrementRetry(entry.job_id);
|
|
3336
3428
|
if (skill.priceSubunits > 0 && !entry.net_amount) {
|
|
3337
3429
|
if (entry.payment_request) {
|
|
@@ -3716,6 +3808,56 @@ var ScriptSkill = class {
|
|
|
3716
3808
|
}
|
|
3717
3809
|
};
|
|
3718
3810
|
|
|
3811
|
+
// src/skill/x402-skill.ts
|
|
3812
|
+
var X402Skill = class {
|
|
3813
|
+
name;
|
|
3814
|
+
description;
|
|
3815
|
+
capabilities;
|
|
3816
|
+
priceSubunits;
|
|
3817
|
+
asset;
|
|
3818
|
+
mode = "x402";
|
|
3819
|
+
x402;
|
|
3820
|
+
noInput;
|
|
3821
|
+
image;
|
|
3822
|
+
imageFile;
|
|
3823
|
+
dir;
|
|
3824
|
+
constructor(params) {
|
|
3825
|
+
this.name = params.name;
|
|
3826
|
+
this.description = params.description;
|
|
3827
|
+
this.capabilities = params.capabilities;
|
|
3828
|
+
this.priceSubunits = params.priceSubunits;
|
|
3829
|
+
this.asset = params.asset;
|
|
3830
|
+
this.x402 = params.x402;
|
|
3831
|
+
this.noInput = params.noInput;
|
|
3832
|
+
this.image = params.image;
|
|
3833
|
+
this.imageFile = params.imageFile;
|
|
3834
|
+
this.dir = params.dir;
|
|
3835
|
+
}
|
|
3836
|
+
requireDriver(ctx) {
|
|
3837
|
+
if (ctx.x402Driver === void 0) {
|
|
3838
|
+
throw new Error(
|
|
3839
|
+
`Skill "${this.name}": x402 driver not wired into the skill context (internal error)`
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
return ctx.x402Driver;
|
|
3843
|
+
}
|
|
3844
|
+
asJob() {
|
|
3845
|
+
return {
|
|
3846
|
+
skillName: this.name,
|
|
3847
|
+
params: this.x402,
|
|
3848
|
+
priceSubunits: this.priceSubunits,
|
|
3849
|
+
asset: this.asset
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
async preflight(input, ctx) {
|
|
3853
|
+
await this.requireDriver(ctx).preflight(this.asJob(), input);
|
|
3854
|
+
}
|
|
3855
|
+
async execute(input, ctx) {
|
|
3856
|
+
const result = await this.requireDriver(ctx).execute(this.asJob(), input, ctx.signal);
|
|
3857
|
+
return { data: result.data, outputMime: result.outputMime, filePath: result.filePath };
|
|
3858
|
+
}
|
|
3859
|
+
};
|
|
3860
|
+
|
|
3719
3861
|
// src/skill/loader.ts
|
|
3720
3862
|
function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
3721
3863
|
let safeImageFile = parsed.imageFile;
|
|
@@ -3803,6 +3945,26 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3803
3945
|
}) : new StaticScriptSkill(scriptParams);
|
|
3804
3946
|
break;
|
|
3805
3947
|
}
|
|
3948
|
+
case "x402": {
|
|
3949
|
+
if (parsed.x402 === void 0) {
|
|
3950
|
+
throw new Error(
|
|
3951
|
+
`SKILL.md "${parsed.name}": internal error - x402 config missing for mode 'x402'`
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
skill = new X402Skill({
|
|
3955
|
+
name: parsed.name,
|
|
3956
|
+
description: parsed.description,
|
|
3957
|
+
capabilities: parsed.capabilities,
|
|
3958
|
+
priceSubunits: Number(parsed.priceSubunits),
|
|
3959
|
+
asset: parsed.asset,
|
|
3960
|
+
x402: parsed.x402,
|
|
3961
|
+
noInput: parsed.noInput === true,
|
|
3962
|
+
image: parsed.image,
|
|
3963
|
+
imageFile: safeImageFile,
|
|
3964
|
+
dir: entryPath
|
|
3965
|
+
});
|
|
3966
|
+
break;
|
|
3967
|
+
}
|
|
3806
3968
|
}
|
|
3807
3969
|
if (parsed.rateLimit) {
|
|
3808
3970
|
skill.rateLimit = parsed.rateLimit;
|
|
@@ -3834,7 +3996,10 @@ function loadSkillsFromDir(skillsDir, options = {}) {
|
|
|
3834
3996
|
const content = readFileSync(skillMdPath, "utf-8");
|
|
3835
3997
|
const { frontmatter, systemPrompt } = parseSkillMd(content);
|
|
3836
3998
|
const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
|
|
3837
|
-
allowFreeSkills: true
|
|
3999
|
+
allowFreeSkills: true,
|
|
4000
|
+
// The CLI runtime wires an x402 driver into the skill context, so
|
|
4001
|
+
// x402 skills are executable here (SDK-only hosts reject them).
|
|
4002
|
+
allowX402Skills: true
|
|
3838
4003
|
});
|
|
3839
4004
|
skills.push(buildCliSkill(parsed, entryPath, options.scriptEnv));
|
|
3840
4005
|
} catch (e) {
|
|
@@ -4153,6 +4318,788 @@ function startWatchdog(deps) {
|
|
|
4153
4318
|
};
|
|
4154
4319
|
}
|
|
4155
4320
|
|
|
4321
|
+
// src/x402/constants.ts
|
|
4322
|
+
var X402_SOLANA_DEVNET_CAIP2 = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
4323
|
+
var X402_SOLANA_DEVNET_V1 = "solana-devnet";
|
|
4324
|
+
var X402_GET_INPUT_MAX_ENCODED_BYTES = 2048;
|
|
4325
|
+
var X402_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
4326
|
+
var X402_MAX_CHALLENGE_BYTES = 256 * 1024;
|
|
4327
|
+
var X402_PROBE_TTL_MS = 3e4;
|
|
4328
|
+
var X402_MAX_PAID_ATTEMPTS = 2;
|
|
4329
|
+
var X402_MAX_PAYMENT_SIGNATURES = 2 * X402_MAX_PAID_ATTEMPTS;
|
|
4330
|
+
var X402_FREE_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
4331
|
+
var X402_REFUNDED_RETRIES = 1;
|
|
4332
|
+
var X402_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4333
|
+
function requirementAmount(requirement) {
|
|
4334
|
+
const v1Amount = requirement.maxAmountRequired;
|
|
4335
|
+
const raw = requirement.amount ?? v1Amount;
|
|
4336
|
+
if (typeof raw !== "string" || !/^[0-9]+$/.test(raw)) {
|
|
4337
|
+
return null;
|
|
4338
|
+
}
|
|
4339
|
+
return BigInt(raw);
|
|
4340
|
+
}
|
|
4341
|
+
function isAcceptableRequirement(requirement, rule) {
|
|
4342
|
+
if (requirement.scheme !== "exact") {
|
|
4343
|
+
return false;
|
|
4344
|
+
}
|
|
4345
|
+
const network = requirement.network;
|
|
4346
|
+
if (network !== X402_SOLANA_DEVNET_CAIP2 && network !== X402_SOLANA_DEVNET_V1) {
|
|
4347
|
+
return false;
|
|
4348
|
+
}
|
|
4349
|
+
if (USDC_SOLANA_DEVNET.mint === void 0 || requirement.asset !== USDC_SOLANA_DEVNET.mint) {
|
|
4350
|
+
return false;
|
|
4351
|
+
}
|
|
4352
|
+
const amount = requirementAmount(requirement);
|
|
4353
|
+
if (amount === null) {
|
|
4354
|
+
return false;
|
|
4355
|
+
}
|
|
4356
|
+
return amount <= rule.maxUpstreamSubunits;
|
|
4357
|
+
}
|
|
4358
|
+
function selectAcceptableRequirement(accepts, rule) {
|
|
4359
|
+
return accepts.find((requirement) => isAcceptableRequirement(requirement, rule));
|
|
4360
|
+
}
|
|
4361
|
+
function maxAcceptableQuote(accepts, rule) {
|
|
4362
|
+
let worst = null;
|
|
4363
|
+
for (const requirement of accepts) {
|
|
4364
|
+
if (!isAcceptableRequirement(requirement, rule)) {
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
const amount = requirementAmount(requirement);
|
|
4368
|
+
if (amount === null) {
|
|
4369
|
+
continue;
|
|
4370
|
+
}
|
|
4371
|
+
if (worst === null || amount > worst) {
|
|
4372
|
+
worst = amount;
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
return worst;
|
|
4376
|
+
}
|
|
4377
|
+
function buildRequirementsPolicy(rule) {
|
|
4378
|
+
return function filterAcceptableRequirements(_x402Version, paymentRequirements) {
|
|
4379
|
+
return paymentRequirements.filter((requirement) => isAcceptableRequirement(requirement, rule));
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
4383
|
+
var MAX_PROBE_BODY_BYTES = 256 * 1024;
|
|
4384
|
+
var PROBE_CACHE_MAX_ENTRIES = 256;
|
|
4385
|
+
async function readTextCapped(response, maxBytes) {
|
|
4386
|
+
const reader = response.body?.getReader();
|
|
4387
|
+
if (reader === void 0) {
|
|
4388
|
+
const buffer = await response.arrayBuffer();
|
|
4389
|
+
if (buffer.byteLength > maxBytes) {
|
|
4390
|
+
throw new X402ProbeError(`402 body exceeds ${maxBytes} bytes`, 402);
|
|
4391
|
+
}
|
|
4392
|
+
return new TextDecoder().decode(buffer);
|
|
4393
|
+
}
|
|
4394
|
+
const chunks = [];
|
|
4395
|
+
let total = 0;
|
|
4396
|
+
while (true) {
|
|
4397
|
+
const { done, value } = await reader.read();
|
|
4398
|
+
if (done) {
|
|
4399
|
+
break;
|
|
4400
|
+
}
|
|
4401
|
+
total += value.byteLength;
|
|
4402
|
+
if (total > maxBytes) {
|
|
4403
|
+
await reader.cancel().catch(() => {
|
|
4404
|
+
});
|
|
4405
|
+
throw new X402ProbeError(`402 body exceeds ${maxBytes} bytes`, 402);
|
|
4406
|
+
}
|
|
4407
|
+
chunks.push(value);
|
|
4408
|
+
}
|
|
4409
|
+
const combined = new Uint8Array(total);
|
|
4410
|
+
let offset = 0;
|
|
4411
|
+
for (const chunk of chunks) {
|
|
4412
|
+
combined.set(chunk, offset);
|
|
4413
|
+
offset += chunk.byteLength;
|
|
4414
|
+
}
|
|
4415
|
+
return new TextDecoder().decode(combined);
|
|
4416
|
+
}
|
|
4417
|
+
var X402ProbeError = class extends Error {
|
|
4418
|
+
status;
|
|
4419
|
+
constructor(message, status) {
|
|
4420
|
+
super(message);
|
|
4421
|
+
this.name = "X402ProbeError";
|
|
4422
|
+
this.status = status;
|
|
4423
|
+
}
|
|
4424
|
+
};
|
|
4425
|
+
function normalizeV1Requirement(raw) {
|
|
4426
|
+
return {
|
|
4427
|
+
scheme: typeof raw.scheme === "string" ? raw.scheme : "",
|
|
4428
|
+
network: typeof raw.network === "string" ? raw.network : "unknown:unknown",
|
|
4429
|
+
asset: typeof raw.asset === "string" ? raw.asset : "",
|
|
4430
|
+
payTo: typeof raw.payTo === "string" ? raw.payTo : "",
|
|
4431
|
+
amount: typeof raw.maxAmountRequired === "string" ? raw.maxAmountRequired : "",
|
|
4432
|
+
maxTimeoutSeconds: typeof raw.maxTimeoutSeconds === "number" ? raw.maxTimeoutSeconds : 60,
|
|
4433
|
+
extra: raw.extra ?? {}
|
|
4434
|
+
};
|
|
4435
|
+
}
|
|
4436
|
+
function parseV1Body(bodyText) {
|
|
4437
|
+
let parsed;
|
|
4438
|
+
try {
|
|
4439
|
+
parsed = JSON.parse(bodyText);
|
|
4440
|
+
} catch {
|
|
4441
|
+
return null;
|
|
4442
|
+
}
|
|
4443
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
4444
|
+
return null;
|
|
4445
|
+
}
|
|
4446
|
+
const body = parsed;
|
|
4447
|
+
if (body.x402Version !== 1 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
|
|
4448
|
+
return null;
|
|
4449
|
+
}
|
|
4450
|
+
const rawRequirements = body.accepts.filter(
|
|
4451
|
+
(item) => typeof item === "object" && item !== null
|
|
4452
|
+
);
|
|
4453
|
+
if (rawRequirements.length === 0) {
|
|
4454
|
+
return null;
|
|
4455
|
+
}
|
|
4456
|
+
const accepts = rawRequirements.map(normalizeV1Requirement);
|
|
4457
|
+
const first = rawRequirements[0];
|
|
4458
|
+
return {
|
|
4459
|
+
x402Version: 1,
|
|
4460
|
+
accepts,
|
|
4461
|
+
resource: {
|
|
4462
|
+
url: typeof first.resource === "string" ? first.resource : void 0,
|
|
4463
|
+
description: typeof first.description === "string" ? first.description : void 0,
|
|
4464
|
+
mimeType: typeof first.mimeType === "string" ? first.mimeType : void 0
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
async function probePaymentRequired(url, method, signal) {
|
|
4469
|
+
const probeSignal = AbortSignal.timeout(PROBE_TIMEOUT_MS);
|
|
4470
|
+
const response = await fetch(url, { method, signal: probeSignal, redirect: "error" });
|
|
4471
|
+
if (response.status !== 402) {
|
|
4472
|
+
await response.body?.cancel().catch(() => {
|
|
4473
|
+
});
|
|
4474
|
+
throw new X402ProbeError(
|
|
4475
|
+
`expected HTTP 402 from ${url}, got ${response.status} - not an x402-paid endpoint (or it validates the request before the paywall; try the other method)`,
|
|
4476
|
+
response.status
|
|
4477
|
+
);
|
|
4478
|
+
}
|
|
4479
|
+
const headerValue = response.headers.get("PAYMENT-REQUIRED");
|
|
4480
|
+
if (headerValue !== null) {
|
|
4481
|
+
await response.body?.cancel().catch(() => {
|
|
4482
|
+
});
|
|
4483
|
+
try {
|
|
4484
|
+
const paymentRequired = decodePaymentRequiredHeader(headerValue);
|
|
4485
|
+
return {
|
|
4486
|
+
x402Version: paymentRequired.x402Version,
|
|
4487
|
+
accepts: paymentRequired.accepts,
|
|
4488
|
+
resource: paymentRequired.resource
|
|
4489
|
+
};
|
|
4490
|
+
} catch (error) {
|
|
4491
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4492
|
+
throw new X402ProbeError(`could not decode PAYMENT-REQUIRED header: ${message}`, 402);
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
const bodyText = await readTextCapped(response, MAX_PROBE_BODY_BYTES).catch((error) => {
|
|
4496
|
+
if (error instanceof X402ProbeError) {
|
|
4497
|
+
throw error;
|
|
4498
|
+
}
|
|
4499
|
+
return "";
|
|
4500
|
+
});
|
|
4501
|
+
const v1 = parseV1Body(bodyText);
|
|
4502
|
+
if (v1 !== null) {
|
|
4503
|
+
return v1;
|
|
4504
|
+
}
|
|
4505
|
+
throw new X402ProbeError(
|
|
4506
|
+
"got HTTP 402 but neither a v2 PAYMENT-REQUIRED header nor a v1 JSON body was parseable",
|
|
4507
|
+
402
|
|
4508
|
+
);
|
|
4509
|
+
}
|
|
4510
|
+
var PROBE_CACHE = /* @__PURE__ */ new Map();
|
|
4511
|
+
async function probePaymentRequiredCached(url, method, ttlMs) {
|
|
4512
|
+
const key = `${method} ${url}`;
|
|
4513
|
+
const cached = PROBE_CACHE.get(key);
|
|
4514
|
+
const now = Date.now();
|
|
4515
|
+
if (cached !== void 0 && now - cached.at < ttlMs) {
|
|
4516
|
+
return cached.result;
|
|
4517
|
+
}
|
|
4518
|
+
const result = await probePaymentRequired(url, method);
|
|
4519
|
+
PROBE_CACHE.delete(key);
|
|
4520
|
+
PROBE_CACHE.set(key, { at: now, result });
|
|
4521
|
+
while (PROBE_CACHE.size > PROBE_CACHE_MAX_ENTRIES) {
|
|
4522
|
+
const oldest = PROBE_CACHE.keys().next().value;
|
|
4523
|
+
if (oldest === void 0) {
|
|
4524
|
+
break;
|
|
4525
|
+
}
|
|
4526
|
+
PROBE_CACHE.delete(oldest);
|
|
4527
|
+
}
|
|
4528
|
+
return result;
|
|
4529
|
+
}
|
|
4530
|
+
var X402_JOBS_FILE = ".x402-jobs.json";
|
|
4531
|
+
var X402_RESULTS_DIR = ".x402-results";
|
|
4532
|
+
function sanitizeJobId(jobId) {
|
|
4533
|
+
return jobId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
|
|
4534
|
+
}
|
|
4535
|
+
var X402JobStore = class {
|
|
4536
|
+
jobsPath;
|
|
4537
|
+
resultsDir;
|
|
4538
|
+
queue = Promise.resolve();
|
|
4539
|
+
constructor(agentDir) {
|
|
4540
|
+
this.jobsPath = join(agentDir, X402_JOBS_FILE);
|
|
4541
|
+
this.resultsDir = join(agentDir, X402_RESULTS_DIR);
|
|
4542
|
+
}
|
|
4543
|
+
/** Serialize read-modify-write cycles; a failed task must not wedge the queue. */
|
|
4544
|
+
runExclusive(task) {
|
|
4545
|
+
const scheduled = this.queue.then(task, task);
|
|
4546
|
+
this.queue = scheduled.then(
|
|
4547
|
+
function ignoreResult() {
|
|
4548
|
+
},
|
|
4549
|
+
function ignoreError() {
|
|
4550
|
+
}
|
|
4551
|
+
);
|
|
4552
|
+
return scheduled;
|
|
4553
|
+
}
|
|
4554
|
+
async load() {
|
|
4555
|
+
let raw;
|
|
4556
|
+
try {
|
|
4557
|
+
raw = await readFile(this.jobsPath, "utf-8");
|
|
4558
|
+
} catch (error) {
|
|
4559
|
+
if (error.code === "ENOENT") {
|
|
4560
|
+
return {};
|
|
4561
|
+
}
|
|
4562
|
+
throw error;
|
|
4563
|
+
}
|
|
4564
|
+
const parsed = JSON.parse(raw);
|
|
4565
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4566
|
+
throw new Error(`x402 store ${this.jobsPath} is not a JSON object`);
|
|
4567
|
+
}
|
|
4568
|
+
return parsed;
|
|
4569
|
+
}
|
|
4570
|
+
async save(file) {
|
|
4571
|
+
const tempPath = `${this.jobsPath}.tmp`;
|
|
4572
|
+
await writeFile(tempPath, JSON.stringify(file, null, 2), "utf-8");
|
|
4573
|
+
await rename(tempPath, this.jobsPath);
|
|
4574
|
+
}
|
|
4575
|
+
resultFilePath(jobId) {
|
|
4576
|
+
return join(this.resultsDir, sanitizeJobId(jobId));
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Atomically claim one paid attempt: increment iff still under BOTH caps
|
|
4580
|
+
* (durable attempts and the monotonic signature count), and FLUSH before
|
|
4581
|
+
* returning. The whole check-and-increment runs inside the serialization
|
|
4582
|
+
* queue so concurrent `execute()` flows for the same jobId (dedup failure,
|
|
4583
|
+
* crash-recovery racing the original) can never each read a stale count
|
|
4584
|
+
* and all pass the budget gate - the money bound holds. The flush ordering
|
|
4585
|
+
* errs on "an attempt happened", since the caller sends the payment
|
|
4586
|
+
* immediately after a grant.
|
|
4587
|
+
*/
|
|
4588
|
+
async claimPaidAttempt(jobId, maxAttempts, maxSignatures) {
|
|
4589
|
+
return this.runExclusive(async () => {
|
|
4590
|
+
const file = await this.load();
|
|
4591
|
+
const now = Date.now();
|
|
4592
|
+
const record = file[jobId] ?? { attempts: 0, created_at: now, updated_at: now };
|
|
4593
|
+
const signatures = record.signatures ?? record.attempts;
|
|
4594
|
+
if (signatures >= maxSignatures) {
|
|
4595
|
+
return { granted: false, attempts: record.attempts, signatures, refusedBy: "signatures" };
|
|
4596
|
+
}
|
|
4597
|
+
if (record.attempts >= maxAttempts) {
|
|
4598
|
+
return { granted: false, attempts: record.attempts, signatures, refusedBy: "attempts" };
|
|
4599
|
+
}
|
|
4600
|
+
record.attempts += 1;
|
|
4601
|
+
record.signatures = signatures + 1;
|
|
4602
|
+
record.updated_at = now;
|
|
4603
|
+
file[jobId] = record;
|
|
4604
|
+
await this.save(file);
|
|
4605
|
+
return { granted: true, attempts: record.attempts, signatures: record.signatures };
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
4608
|
+
/**
|
|
4609
|
+
* Return one durable attempt slot after the upstream DEFINITIVELY refused
|
|
4610
|
+
* the signed payment with a fresh 402 (an honest upstream did not settle,
|
|
4611
|
+
* so no money moved). The monotonic `signatures` counter is intentionally
|
|
4612
|
+
* NOT decremented: it is the adversarial bound against an upstream that
|
|
4613
|
+
* settles the payment and lies with a 402 (see `claimPaidAttempt`).
|
|
4614
|
+
*/
|
|
4615
|
+
async refundPaidAttempt(jobId) {
|
|
4616
|
+
await this.runExclusive(async () => {
|
|
4617
|
+
const file = await this.load();
|
|
4618
|
+
const record = file[jobId];
|
|
4619
|
+
if (record === void 0 || record.attempts === 0) {
|
|
4620
|
+
return;
|
|
4621
|
+
}
|
|
4622
|
+
const signatures = record.signatures ?? record.attempts;
|
|
4623
|
+
record.attempts -= 1;
|
|
4624
|
+
record.signatures = signatures;
|
|
4625
|
+
record.updated_at = Date.now();
|
|
4626
|
+
await this.save(file);
|
|
4627
|
+
});
|
|
4628
|
+
}
|
|
4629
|
+
/** Serialized point-in-time attempt count (test/inspection helper). */
|
|
4630
|
+
async paidAttempts(jobId) {
|
|
4631
|
+
return this.runExclusive(async () => {
|
|
4632
|
+
const file = await this.load();
|
|
4633
|
+
return file[jobId]?.attempts ?? 0;
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
/** Serialized point-in-time signed-payment count (test/inspection helper). */
|
|
4637
|
+
async paymentSignatures(jobId) {
|
|
4638
|
+
return this.runExclusive(async () => {
|
|
4639
|
+
const file = await this.load();
|
|
4640
|
+
const record = file[jobId];
|
|
4641
|
+
return record === void 0 ? 0 : record.signatures ?? record.attempts;
|
|
4642
|
+
});
|
|
4643
|
+
}
|
|
4644
|
+
async saveTextResult(jobId, data) {
|
|
4645
|
+
await this.runExclusive(async () => {
|
|
4646
|
+
const file = await this.load();
|
|
4647
|
+
const now = Date.now();
|
|
4648
|
+
const record = file[jobId] ?? { attempts: 0, created_at: now, updated_at: now };
|
|
4649
|
+
record.result = { kind: "text", data };
|
|
4650
|
+
record.updated_at = now;
|
|
4651
|
+
file[jobId] = record;
|
|
4652
|
+
await this.save(file);
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
/** Binary result: bytes hit disk BEFORE the record flush (crash-safe ordering). */
|
|
4656
|
+
async saveFileResult(jobId, mime, bytes) {
|
|
4657
|
+
const filePath = this.resultFilePath(jobId);
|
|
4658
|
+
await mkdir(this.resultsDir, { recursive: true });
|
|
4659
|
+
await writeFile(filePath, bytes);
|
|
4660
|
+
await this.runExclusive(async () => {
|
|
4661
|
+
const file = await this.load();
|
|
4662
|
+
const now = Date.now();
|
|
4663
|
+
const record = file[jobId] ?? { attempts: 0, created_at: now, updated_at: now };
|
|
4664
|
+
record.result = { kind: "file", mime };
|
|
4665
|
+
record.updated_at = now;
|
|
4666
|
+
file[jobId] = record;
|
|
4667
|
+
await this.save(file);
|
|
4668
|
+
});
|
|
4669
|
+
return filePath;
|
|
4670
|
+
}
|
|
4671
|
+
/**
|
|
4672
|
+
* Completed result for this job, or null. A file record whose file went
|
|
4673
|
+
* missing degrades to attempt-without-result (the budget still applies).
|
|
4674
|
+
*/
|
|
4675
|
+
async getResult(jobId) {
|
|
4676
|
+
const file = await this.load();
|
|
4677
|
+
const result = file[jobId]?.result;
|
|
4678
|
+
if (result === void 0) {
|
|
4679
|
+
return null;
|
|
4680
|
+
}
|
|
4681
|
+
if (result.kind === "text") {
|
|
4682
|
+
return { data: result.data };
|
|
4683
|
+
}
|
|
4684
|
+
const filePath = this.resultFilePath(jobId);
|
|
4685
|
+
try {
|
|
4686
|
+
const fileStat = await stat(filePath);
|
|
4687
|
+
if (!fileStat.isFile()) {
|
|
4688
|
+
return null;
|
|
4689
|
+
}
|
|
4690
|
+
} catch {
|
|
4691
|
+
return null;
|
|
4692
|
+
}
|
|
4693
|
+
return { data: "", outputMime: result.mime, filePath };
|
|
4694
|
+
}
|
|
4695
|
+
/** Drop records (and their result files) older than the cache TTL. */
|
|
4696
|
+
async sweepExpired(now = Date.now()) {
|
|
4697
|
+
await this.runExclusive(async () => {
|
|
4698
|
+
const file = await this.load();
|
|
4699
|
+
let changed = false;
|
|
4700
|
+
for (const [jobId, record] of Object.entries(file)) {
|
|
4701
|
+
if (now - record.updated_at <= X402_CACHE_TTL_MS) {
|
|
4702
|
+
continue;
|
|
4703
|
+
}
|
|
4704
|
+
delete file[jobId];
|
|
4705
|
+
changed = true;
|
|
4706
|
+
await rm(this.resultFilePath(jobId), { force: true }).catch(() => {
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
if (changed) {
|
|
4710
|
+
await this.save(file);
|
|
4711
|
+
}
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
};
|
|
4715
|
+
|
|
4716
|
+
// src/x402/driver.ts
|
|
4717
|
+
var CUSTOMER_INPUT_TOO_LARGE = "Input too large for this skill.";
|
|
4718
|
+
function jsonContentType(text) {
|
|
4719
|
+
const trimmed = text.trimStart();
|
|
4720
|
+
if (trimmed.length === 0 || trimmed[0] !== "{" && trimmed[0] !== "[") {
|
|
4721
|
+
return false;
|
|
4722
|
+
}
|
|
4723
|
+
try {
|
|
4724
|
+
JSON.parse(text);
|
|
4725
|
+
return true;
|
|
4726
|
+
} catch {
|
|
4727
|
+
return false;
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
function isTextContentType(contentType) {
|
|
4731
|
+
const mime = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
4732
|
+
return mime.startsWith("text/") || mime === "application/json" || mime.endsWith("+json");
|
|
4733
|
+
}
|
|
4734
|
+
async function readBodyCapped(response, maxBytes) {
|
|
4735
|
+
const reader = response.body?.getReader();
|
|
4736
|
+
if (reader === void 0) {
|
|
4737
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
4738
|
+
if (buffer.byteLength > maxBytes) {
|
|
4739
|
+
throw new X402PermanentError(`upstream response too large (> ${maxBytes} bytes)`);
|
|
4740
|
+
}
|
|
4741
|
+
return buffer;
|
|
4742
|
+
}
|
|
4743
|
+
const chunks = [];
|
|
4744
|
+
let total = 0;
|
|
4745
|
+
while (true) {
|
|
4746
|
+
const { done, value } = await reader.read();
|
|
4747
|
+
if (done) {
|
|
4748
|
+
break;
|
|
4749
|
+
}
|
|
4750
|
+
total += value.byteLength;
|
|
4751
|
+
if (total > maxBytes) {
|
|
4752
|
+
await reader.cancel().catch(() => {
|
|
4753
|
+
});
|
|
4754
|
+
throw new X402PermanentError(`upstream response too large (> ${maxBytes} bytes)`);
|
|
4755
|
+
}
|
|
4756
|
+
chunks.push(value);
|
|
4757
|
+
}
|
|
4758
|
+
const combined = new Uint8Array(total);
|
|
4759
|
+
let offset = 0;
|
|
4760
|
+
for (const chunk of chunks) {
|
|
4761
|
+
combined.set(chunk, offset);
|
|
4762
|
+
offset += chunk.byteLength;
|
|
4763
|
+
}
|
|
4764
|
+
return combined;
|
|
4765
|
+
}
|
|
4766
|
+
async function capChallengeResponse(response) {
|
|
4767
|
+
if (response.status !== 402) {
|
|
4768
|
+
return response;
|
|
4769
|
+
}
|
|
4770
|
+
const bytes = await readBodyCapped(response, X402_MAX_CHALLENGE_BYTES);
|
|
4771
|
+
return new Response(bytes, {
|
|
4772
|
+
status: response.status,
|
|
4773
|
+
statusText: response.statusText,
|
|
4774
|
+
headers: response.headers
|
|
4775
|
+
});
|
|
4776
|
+
}
|
|
4777
|
+
function abortableDelay(ms, signal) {
|
|
4778
|
+
return new Promise((resolve3) => {
|
|
4779
|
+
if (signal?.aborted) {
|
|
4780
|
+
resolve3(false);
|
|
4781
|
+
return;
|
|
4782
|
+
}
|
|
4783
|
+
const onAbort = () => {
|
|
4784
|
+
clearTimeout(timer);
|
|
4785
|
+
resolve3(false);
|
|
4786
|
+
};
|
|
4787
|
+
const timer = setTimeout(() => {
|
|
4788
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4789
|
+
resolve3(true);
|
|
4790
|
+
}, ms);
|
|
4791
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
var X402Driver = class {
|
|
4795
|
+
constructor(options) {
|
|
4796
|
+
this.options = options;
|
|
4797
|
+
this.store = new X402JobStore(options.agentDir);
|
|
4798
|
+
this.rpc = createSolanaRpc(options.rpcUrl);
|
|
4799
|
+
this.store.sweepExpired().catch(() => {
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
store;
|
|
4803
|
+
rpc;
|
|
4804
|
+
signerPromise = null;
|
|
4805
|
+
repriceHintLogged = /* @__PURE__ */ new Set();
|
|
4806
|
+
log(message) {
|
|
4807
|
+
(this.options.log ?? console.log)(`[x402] ${message}`);
|
|
4808
|
+
}
|
|
4809
|
+
getSigner() {
|
|
4810
|
+
const secret = this.options.solanaSecretKeyBase58;
|
|
4811
|
+
if (secret === void 0 || secret.length === 0) {
|
|
4812
|
+
return Promise.reject(
|
|
4813
|
+
new X402PreflightError(
|
|
4814
|
+
"bridge wallet missing: .secrets.json has no solana_secret_key (run `npx @elisym/cli x402 add` to set it up)"
|
|
4815
|
+
)
|
|
4816
|
+
);
|
|
4817
|
+
}
|
|
4818
|
+
if (this.signerPromise === null) {
|
|
4819
|
+
this.signerPromise = signerFromSecretKeyBase58(secret);
|
|
4820
|
+
}
|
|
4821
|
+
return this.signerPromise;
|
|
4822
|
+
}
|
|
4823
|
+
/** The single-wallet invariant: revenue lands where the upstream payer spends from. */
|
|
4824
|
+
async assertWalletInvariant() {
|
|
4825
|
+
const signer = await this.getSigner();
|
|
4826
|
+
const paymentsAddress = this.options.paymentsAddress;
|
|
4827
|
+
if (paymentsAddress === void 0 || paymentsAddress.length === 0) {
|
|
4828
|
+
throw new X402PreflightError(
|
|
4829
|
+
"elisym.yaml has no payments[] entry: revenue would not refill the bridge wallet (run `npx @elisym/cli x402 add` to fix)"
|
|
4830
|
+
);
|
|
4831
|
+
}
|
|
4832
|
+
if (signer.address !== paymentsAddress) {
|
|
4833
|
+
throw new X402PreflightError(
|
|
4834
|
+
`wallet invariant violated: payments[].address (${paymentsAddress}) differs from the solana_secret_key address (${signer.address}); revenue would accumulate in one wallet while jobs drain the other - align them (re-run \`npx @elisym/cli x402 add\` or fix elisym.yaml)`
|
|
4835
|
+
);
|
|
4836
|
+
}
|
|
4837
|
+
return signer;
|
|
4838
|
+
}
|
|
4839
|
+
assertInputRules(job, input) {
|
|
4840
|
+
const { params } = job;
|
|
4841
|
+
if (params.method === "GET" && params.queryParam !== void 0) {
|
|
4842
|
+
const encodedBytes = Buffer.byteLength(encodeURIComponent(input.data), "utf8");
|
|
4843
|
+
if (encodedBytes > X402_GET_INPUT_MAX_ENCODED_BYTES) {
|
|
4844
|
+
throw new X402PreflightError(
|
|
4845
|
+
`GET input too large: ${encodedBytes} percent-encoded bytes (max ${X402_GET_INPUT_MAX_ENCODED_BYTES})`,
|
|
4846
|
+
CUSTOMER_INPUT_TOO_LARGE
|
|
4847
|
+
);
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
const inputBytes = Buffer.byteLength(input.data, "utf8");
|
|
4851
|
+
if (inputBytes > params.maxInputBytes) {
|
|
4852
|
+
throw new X402PreflightError(
|
|
4853
|
+
`input too large: ${inputBytes} bytes (skill x402_max_input_bytes=${params.maxInputBytes})`,
|
|
4854
|
+
CUSTOMER_INPUT_TOO_LARGE
|
|
4855
|
+
);
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
async preflight(job, input) {
|
|
4859
|
+
const cached = await this.store.getResult(input.jobId);
|
|
4860
|
+
if (cached !== null) {
|
|
4861
|
+
return;
|
|
4862
|
+
}
|
|
4863
|
+
const signer = await this.assertWalletInvariant();
|
|
4864
|
+
if (job.asset.mint !== USDC_SOLANA_DEVNET.mint || USDC_SOLANA_DEVNET.mint === void 0) {
|
|
4865
|
+
throw new X402PreflightError(
|
|
4866
|
+
`x402 skills must be priced in devnet USDC (skill "${job.skillName}" is priced in ${job.asset.symbol}); the margin check compares the elisym price to the upstream USDC quote - re-run \`npx @elisym/cli x402 add\``
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4869
|
+
this.assertInputRules(job, input);
|
|
4870
|
+
const rule = { maxUpstreamSubunits: job.params.maxUpstreamSubunits };
|
|
4871
|
+
let probe;
|
|
4872
|
+
try {
|
|
4873
|
+
probe = await probePaymentRequiredCached(
|
|
4874
|
+
job.params.url,
|
|
4875
|
+
job.params.method,
|
|
4876
|
+
X402_PROBE_TTL_MS
|
|
4877
|
+
);
|
|
4878
|
+
} catch (error) {
|
|
4879
|
+
if (error instanceof X402ProbeError) {
|
|
4880
|
+
throw new X402PreflightError(`upstream probe failed: ${error.message}`);
|
|
4881
|
+
}
|
|
4882
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4883
|
+
throw new X402PreflightError(`upstream unreachable: ${message}`);
|
|
4884
|
+
}
|
|
4885
|
+
const quote = maxAcceptableQuote(probe.accepts, rule);
|
|
4886
|
+
if (quote === null) {
|
|
4887
|
+
throw new X402PreflightError(
|
|
4888
|
+
"no acceptable upstream payment requirement: network/asset/scheme mismatch or the live quote exceeds x402_max_upstream (if the upstream repriced, re-run `npx @elisym/cli x402 add` to regenerate the skill)"
|
|
4889
|
+
);
|
|
4890
|
+
}
|
|
4891
|
+
if (quote < job.params.maxUpstreamSubunits && !this.repriceHintLogged.has(job.skillName)) {
|
|
4892
|
+
this.repriceHintLogged.add(job.skillName);
|
|
4893
|
+
this.log(
|
|
4894
|
+
`upstream for "${job.skillName}" now quotes ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)} (your price was computed from ${formatAssetAmount(USDC_SOLANA_DEVNET, job.params.maxUpstreamSubunits)}); re-run \`npx @elisym/cli x402 add\` to reprice`
|
|
4895
|
+
);
|
|
4896
|
+
}
|
|
4897
|
+
const balance = await fetchUsdcBalance(this.rpc, address(signer.address));
|
|
4898
|
+
if (balance < quote) {
|
|
4899
|
+
throw new X402PreflightError(
|
|
4900
|
+
`bridge float insufficient: ${formatAssetAmount(USDC_SOLANA_DEVNET, balance)} USDC in ${signer.address}, upstream quotes ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)} - top up the wallet`
|
|
4901
|
+
);
|
|
4902
|
+
}
|
|
4903
|
+
const feeBps = await this.options.getFeeBps();
|
|
4904
|
+
const fee = calculateProtocolFee(job.priceSubunits, feeBps);
|
|
4905
|
+
const net = BigInt(job.priceSubunits - fee);
|
|
4906
|
+
if (net < quote) {
|
|
4907
|
+
throw new X402PreflightError(
|
|
4908
|
+
`negative margin: net revenue ${formatAssetAmount(USDC_SOLANA_DEVNET, net)} (price minus ${feeBps} bps protocol fee) is below the upstream quote ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)} - re-run \`npx @elisym/cli x402 add\` to reprice`
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
/**
|
|
4913
|
+
* Instrumented fetch injected into the payment wrapper. The wrapper calls
|
|
4914
|
+
* it for the initial unpaid request (no headers) and, after signing, for
|
|
4915
|
+
* the paid retry (carrying `PAYMENT-SIGNATURE`). ONLY the paid request
|
|
4916
|
+
* claims a budget slot, and it does so ATOMICALLY: `claimPaidAttempt`
|
|
4917
|
+
* increments-or-refuses inside the store's serialization queue, so no set
|
|
4918
|
+
* of concurrent `execute()` flows for one jobId can each pass a stale
|
|
4919
|
+
* check and pay past the budget caps. A refusal throws BEFORE
|
|
4920
|
+
* `globalThis.fetch` so no payment is sent.
|
|
4921
|
+
*/
|
|
4922
|
+
buildInstrumentedFetch(jobId, attemptState) {
|
|
4923
|
+
const store = this.store;
|
|
4924
|
+
return async function instrumentedFetch(info, init) {
|
|
4925
|
+
const request = new Request(info, init);
|
|
4926
|
+
if (request.headers.has("PAYMENT-SIGNATURE") || request.headers.has("X-PAYMENT")) {
|
|
4927
|
+
const claim = await store.claimPaidAttempt(
|
|
4928
|
+
jobId,
|
|
4929
|
+
X402_MAX_PAID_ATTEMPTS,
|
|
4930
|
+
X402_MAX_PAYMENT_SIGNATURES
|
|
4931
|
+
);
|
|
4932
|
+
if (!claim.granted) {
|
|
4933
|
+
throw new X402PermanentError(
|
|
4934
|
+
claim.refusedBy === "signatures" ? `signed payment budget exhausted (${claim.signatures}/${X402_MAX_PAYMENT_SIGNATURES} payments signed for this job) - refusing to sign another` : `paid attempt budget exhausted (${claim.attempts}/${X402_MAX_PAID_ATTEMPTS}) - refusing to pay the upstream again`
|
|
4935
|
+
);
|
|
4936
|
+
}
|
|
4937
|
+
attemptState.paymentSent = true;
|
|
4938
|
+
}
|
|
4939
|
+
const response = await globalThis.fetch(new Request(request, { redirect: "error" }));
|
|
4940
|
+
return capChallengeResponse(response);
|
|
4941
|
+
};
|
|
4942
|
+
}
|
|
4943
|
+
buildClient(signer, rule) {
|
|
4944
|
+
const scheme = new ExactSvmScheme(signer, { rpcUrl: this.options.rpcUrl });
|
|
4945
|
+
return x402Client.fromConfig({
|
|
4946
|
+
schemes: [{ network: X402_SOLANA_DEVNET_CAIP2, client: scheme }],
|
|
4947
|
+
policies: [buildRequirementsPolicy(rule)]
|
|
4948
|
+
}).registerV1(X402_SOLANA_DEVNET_V1, scheme);
|
|
4949
|
+
}
|
|
4950
|
+
classifyWrapperError(error) {
|
|
4951
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
4952
|
+
return error;
|
|
4953
|
+
}
|
|
4954
|
+
if (error instanceof X402PermanentError || error instanceof X402TransientError) {
|
|
4955
|
+
return error;
|
|
4956
|
+
}
|
|
4957
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4958
|
+
if (message.includes("Failed to create payment payload")) {
|
|
4959
|
+
return new X402PermanentError(
|
|
4960
|
+
`payment refused by policy (upstream likely repriced above x402_max_upstream): ${message}`
|
|
4961
|
+
);
|
|
4962
|
+
}
|
|
4963
|
+
return new X402TransientError(`upstream request failed: ${message}`);
|
|
4964
|
+
}
|
|
4965
|
+
classifyStatus(status) {
|
|
4966
|
+
if (status === 402) {
|
|
4967
|
+
return new X402TransientError("upstream still returned 402 after a payment attempt", status);
|
|
4968
|
+
}
|
|
4969
|
+
if (status === 429 || status >= 500) {
|
|
4970
|
+
return new X402TransientError(`upstream returned ${status}`, status);
|
|
4971
|
+
}
|
|
4972
|
+
return new X402PermanentError(`upstream returned ${status}`);
|
|
4973
|
+
}
|
|
4974
|
+
async execute(job, input, signal) {
|
|
4975
|
+
const cached = await this.store.getResult(input.jobId);
|
|
4976
|
+
if (cached !== null) {
|
|
4977
|
+
this.log(`job ${input.jobId.slice(0, 8)}: delivering cached upstream result (no re-payment)`);
|
|
4978
|
+
return cached;
|
|
4979
|
+
}
|
|
4980
|
+
if (input.filePath !== void 0) {
|
|
4981
|
+
throw new X402PermanentError(
|
|
4982
|
+
"file inputs are not supported by x402 bridge skills (attachment exceeded the text re-inline cap)"
|
|
4983
|
+
);
|
|
4984
|
+
}
|
|
4985
|
+
const inputBytes = Buffer.byteLength(input.data, "utf8");
|
|
4986
|
+
if (inputBytes > job.params.maxInputBytes) {
|
|
4987
|
+
throw new X402PermanentError(
|
|
4988
|
+
`input exceeds x402_max_input_bytes after materialization: ${inputBytes} > ${job.params.maxInputBytes}`
|
|
4989
|
+
);
|
|
4990
|
+
}
|
|
4991
|
+
const requestUrl = new URL(job.params.url);
|
|
4992
|
+
const headers = {};
|
|
4993
|
+
let body;
|
|
4994
|
+
if (job.params.method === "GET") {
|
|
4995
|
+
if (job.params.queryParam !== void 0 && input.data.length > 0) {
|
|
4996
|
+
requestUrl.searchParams.set(job.params.queryParam, input.data);
|
|
4997
|
+
}
|
|
4998
|
+
} else {
|
|
4999
|
+
body = input.data;
|
|
5000
|
+
headers["content-type"] = jsonContentType(input.data) ? "application/json" : "text/plain";
|
|
5001
|
+
}
|
|
5002
|
+
const signer = await this.assertWalletInvariant();
|
|
5003
|
+
const rule = { maxUpstreamSubunits: job.params.maxUpstreamSubunits };
|
|
5004
|
+
const client = this.buildClient(signer, rule);
|
|
5005
|
+
const jobTag = input.jobId.slice(0, 8);
|
|
5006
|
+
let freeRetriesUsed = 0;
|
|
5007
|
+
let refundedRetriesUsed = 0;
|
|
5008
|
+
while (true) {
|
|
5009
|
+
const attemptState = { paymentSent: false };
|
|
5010
|
+
try {
|
|
5011
|
+
return await this.attemptUpstream(
|
|
5012
|
+
job,
|
|
5013
|
+
requestUrl,
|
|
5014
|
+
headers,
|
|
5015
|
+
body,
|
|
5016
|
+
input.jobId,
|
|
5017
|
+
client,
|
|
5018
|
+
attemptState,
|
|
5019
|
+
signal
|
|
5020
|
+
);
|
|
5021
|
+
} catch (error) {
|
|
5022
|
+
if (!(error instanceof X402TransientError)) {
|
|
5023
|
+
throw error;
|
|
5024
|
+
}
|
|
5025
|
+
if (attemptState.paymentSent) {
|
|
5026
|
+
if (error.upstreamStatus !== 402) {
|
|
5027
|
+
throw error;
|
|
5028
|
+
}
|
|
5029
|
+
try {
|
|
5030
|
+
await this.store.refundPaidAttempt(input.jobId);
|
|
5031
|
+
} catch (refundError) {
|
|
5032
|
+
const message = refundError instanceof Error ? refundError.message : String(refundError);
|
|
5033
|
+
this.log(
|
|
5034
|
+
`job ${jobTag}: could not refund the refused payment attempt (${message}) - keeping the slot consumed`
|
|
5035
|
+
);
|
|
5036
|
+
throw error;
|
|
5037
|
+
}
|
|
5038
|
+
if (refundedRetriesUsed >= X402_REFUNDED_RETRIES) {
|
|
5039
|
+
throw error;
|
|
5040
|
+
}
|
|
5041
|
+
refundedRetriesUsed += 1;
|
|
5042
|
+
this.log(
|
|
5043
|
+
`job ${jobTag}: upstream refused the signed payment with a fresh 402 (likely expired blockhash) - attempt slot refunded, retrying with a fresh payment`
|
|
5044
|
+
);
|
|
5045
|
+
continue;
|
|
5046
|
+
}
|
|
5047
|
+
const freeRetryDelays = this.options.freeRetryDelaysMs ?? X402_FREE_RETRY_DELAYS_MS;
|
|
5048
|
+
const delayMs = freeRetryDelays[freeRetriesUsed];
|
|
5049
|
+
if (delayMs === void 0) {
|
|
5050
|
+
throw error;
|
|
5051
|
+
}
|
|
5052
|
+
freeRetriesUsed += 1;
|
|
5053
|
+
this.log(
|
|
5054
|
+
`job ${jobTag}: transient failure before any payment (${error.message}) - inline retry ${freeRetriesUsed}/${freeRetryDelays.length} in ${delayMs}ms`
|
|
5055
|
+
);
|
|
5056
|
+
const slept = await abortableDelay(delayMs, signal);
|
|
5057
|
+
if (!slept) {
|
|
5058
|
+
throw error;
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
/** One full payment-wrapper pass against the upstream; throws classified errors. */
|
|
5064
|
+
async attemptUpstream(job, requestUrl, headers, body, jobId, client, attemptState, signal) {
|
|
5065
|
+
const fetchWithPayment = wrapFetchWithPayment(
|
|
5066
|
+
this.buildInstrumentedFetch(jobId, attemptState),
|
|
5067
|
+
client
|
|
5068
|
+
);
|
|
5069
|
+
let response;
|
|
5070
|
+
try {
|
|
5071
|
+
response = await fetchWithPayment(requestUrl, {
|
|
5072
|
+
method: job.params.method,
|
|
5073
|
+
headers,
|
|
5074
|
+
body,
|
|
5075
|
+
signal
|
|
5076
|
+
});
|
|
5077
|
+
} catch (error) {
|
|
5078
|
+
throw this.classifyWrapperError(error);
|
|
5079
|
+
}
|
|
5080
|
+
if (!response.ok) {
|
|
5081
|
+
await response.body?.cancel().catch(() => {
|
|
5082
|
+
});
|
|
5083
|
+
throw this.classifyStatus(response.status);
|
|
5084
|
+
}
|
|
5085
|
+
let bytes;
|
|
5086
|
+
try {
|
|
5087
|
+
bytes = await readBodyCapped(response, X402_MAX_RESPONSE_BYTES);
|
|
5088
|
+
} catch (error) {
|
|
5089
|
+
throw this.classifyWrapperError(error);
|
|
5090
|
+
}
|
|
5091
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
5092
|
+
if (isTextContentType(contentType)) {
|
|
5093
|
+
const data = new TextDecoder().decode(bytes);
|
|
5094
|
+
await this.store.saveTextResult(jobId, data);
|
|
5095
|
+
return { data };
|
|
5096
|
+
}
|
|
5097
|
+
const mime = contentType.split(";")[0]?.trim() ?? "application/octet-stream";
|
|
5098
|
+
const filePath = await this.store.saveFileResult(jobId, mime, bytes);
|
|
5099
|
+
return { data: "", outputMime: mime, filePath };
|
|
5100
|
+
}
|
|
5101
|
+
};
|
|
5102
|
+
|
|
4156
5103
|
// src/commands/start.ts
|
|
4157
5104
|
async function cmdStart(nameArg, options = {}) {
|
|
4158
5105
|
const cwd = process.cwd();
|
|
@@ -4418,6 +5365,52 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4418
5365
|
agentName,
|
|
4419
5366
|
agentDescription: loaded.yaml.description ?? ""
|
|
4420
5367
|
};
|
|
5368
|
+
const x402Skills = allSkills.filter((skill) => skill.mode === "x402");
|
|
5369
|
+
let x402InvariantBroken;
|
|
5370
|
+
if (x402Skills.length > 0) {
|
|
5371
|
+
const solanaSecretKey = loaded.secrets.solana_secret_key;
|
|
5372
|
+
if (solanaSecretKey === void 0 || solanaSecretKey.length === 0) {
|
|
5373
|
+
x402InvariantBroken = "no solana_secret_key in .secrets.json";
|
|
5374
|
+
} else if (!solanaAddress) {
|
|
5375
|
+
x402InvariantBroken = "no payments[] entry in elisym.yaml";
|
|
5376
|
+
} else {
|
|
5377
|
+
const bridgeSigner = await signerFromSecretKeyBase58(solanaSecretKey);
|
|
5378
|
+
if (bridgeSigner.address !== solanaAddress) {
|
|
5379
|
+
x402InvariantBroken = `payments[].address (${solanaAddress}) differs from the solana_secret_key address (${bridgeSigner.address})`;
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
for (const skill of x402Skills) {
|
|
5383
|
+
if (skill.asset.mint !== USDC_SOLANA_DEVNET.mint) {
|
|
5384
|
+
console.warn(
|
|
5385
|
+
` ! x402 skill "${skill.name}" is priced in ${skill.asset.symbol}, not devnet USDC - every job will be refused at preflight. Re-run \`npx @elisym/cli x402 add\`.`
|
|
5386
|
+
);
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
if (x402InvariantBroken !== void 0) {
|
|
5390
|
+
console.warn(` ! x402 wallet invariant broken: ${x402InvariantBroken}.`);
|
|
5391
|
+
console.warn(
|
|
5392
|
+
" ! x402 skills will NOT be advertised and their jobs will be refused before payment."
|
|
5393
|
+
);
|
|
5394
|
+
console.warn(" ! Fix: re-run `npx @elisym/cli x402 add` or align elisym.yaml with the key.");
|
|
5395
|
+
}
|
|
5396
|
+
const x402RpcUrl = getRpcUrl();
|
|
5397
|
+
async function fetchLiveFeeBps() {
|
|
5398
|
+
const config = await getProtocolConfig(
|
|
5399
|
+
createSolanaRpc(x402RpcUrl),
|
|
5400
|
+
getProtocolProgramId("devnet"),
|
|
5401
|
+
{ forceRefresh: true }
|
|
5402
|
+
);
|
|
5403
|
+
return config.feeBps;
|
|
5404
|
+
}
|
|
5405
|
+
skillCtx.x402Driver = new X402Driver({
|
|
5406
|
+
agentDir: loaded.dir,
|
|
5407
|
+
paymentsAddress: solanaAddress,
|
|
5408
|
+
solanaSecretKeyBase58: loaded.secrets.solana_secret_key,
|
|
5409
|
+
rpcUrl: x402RpcUrl,
|
|
5410
|
+
getFeeBps: fetchLiveFeeBps,
|
|
5411
|
+
log: (message) => console.log(` ${message}`)
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
4421
5414
|
console.log(" Connecting to relays and publishing capabilities...");
|
|
4422
5415
|
const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
4423
5416
|
const relays = loaded.yaml.relays.length > 0 ? loaded.yaml.relays : [...RELAYS];
|
|
@@ -4564,7 +5557,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4564
5557
|
}
|
|
4565
5558
|
const kinds = [jobRequestKind(DEFAULT_KIND_OFFSET)];
|
|
4566
5559
|
function buildCard(skill) {
|
|
4567
|
-
const isStatic = skill.mode === "static-file" || skill.mode === "static-script";
|
|
5560
|
+
const isStatic = skill.mode === "static-file" || skill.mode === "static-script" || skill.noInput === true;
|
|
4568
5561
|
return {
|
|
4569
5562
|
name: skill.name,
|
|
4570
5563
|
description: skill.description,
|
|
@@ -4608,6 +5601,14 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4608
5601
|
);
|
|
4609
5602
|
continue;
|
|
4610
5603
|
}
|
|
5604
|
+
if (skill.mode === "x402" && x402InvariantBroken !== void 0) {
|
|
5605
|
+
console.warn(` ! Not advertising "${skill.name}" - x402 wallet invariant broken.`);
|
|
5606
|
+
logger.warn(
|
|
5607
|
+
{ event: "publish_skipped_x402_invariant", kind: 31990, skill: skill.name },
|
|
5608
|
+
"capability not advertised - x402 wallet invariant broken"
|
|
5609
|
+
);
|
|
5610
|
+
continue;
|
|
5611
|
+
}
|
|
4611
5612
|
try {
|
|
4612
5613
|
await client.discovery.publishCapability(identity, buildCard(skill), kinds);
|
|
4613
5614
|
cardsPublished += 1;
|
|
@@ -4661,6 +5662,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4661
5662
|
const ledger = new JobLedger(paths.jobs);
|
|
4662
5663
|
const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
|
|
4663
5664
|
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
5665
|
+
if (x402Skills.length > 0) {
|
|
5666
|
+
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
5667
|
+
}
|
|
4664
5668
|
const runtimeConfig = {
|
|
4665
5669
|
paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
|
|
4666
5670
|
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
|
@@ -4923,6 +5927,431 @@ async function cmdWallet(name) {
|
|
|
4923
5927
|
console.log(` USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}
|
|
4924
5928
|
`);
|
|
4925
5929
|
}
|
|
5930
|
+
var BPS_DENOMINATOR = 1e4;
|
|
5931
|
+
var DEFAULT_MARGIN_BPS = 1e3;
|
|
5932
|
+
var SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
5933
|
+
var CIRCLE_FAUCET_URL = "https://faucet.circle.com";
|
|
5934
|
+
function fail(message) {
|
|
5935
|
+
console.error(`
|
|
5936
|
+
! ${message}
|
|
5937
|
+
`);
|
|
5938
|
+
process.exit(1);
|
|
5939
|
+
}
|
|
5940
|
+
function sanitizeUpstreamText(raw, maxLength) {
|
|
5941
|
+
let withoutControls = "";
|
|
5942
|
+
for (const char of raw) {
|
|
5943
|
+
const code = char.codePointAt(0) ?? 0;
|
|
5944
|
+
withoutControls += code < 32 || code === 127 ? " " : char;
|
|
5945
|
+
}
|
|
5946
|
+
return withoutControls.replace(/—/g, "-").replace(/\s+/g, " ").trim().slice(0, maxLength).trim();
|
|
5947
|
+
}
|
|
5948
|
+
function slugFromUrl(url) {
|
|
5949
|
+
const pathPart = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? "";
|
|
5950
|
+
const host = url.hostname.replace(/^www\./, "").split(".")[0] ?? "x402";
|
|
5951
|
+
return pathPart.length > 0 ? `${host}-${pathPart}` : host;
|
|
5952
|
+
}
|
|
5953
|
+
function parseMarginBps(raw) {
|
|
5954
|
+
if (raw === void 0) {
|
|
5955
|
+
return DEFAULT_MARGIN_BPS;
|
|
5956
|
+
}
|
|
5957
|
+
const value = Number(raw);
|
|
5958
|
+
if (!Number.isInteger(value) || value < 0 || value > 1e5) {
|
|
5959
|
+
fail(`--margin-bps must be a non-negative integer (bps, 100 = 1%); got "${raw}"`);
|
|
5960
|
+
}
|
|
5961
|
+
return value;
|
|
5962
|
+
}
|
|
5963
|
+
async function parseImportedSecret(input) {
|
|
5964
|
+
const trimmed = input.trim();
|
|
5965
|
+
if (existsSync(trimmed)) {
|
|
5966
|
+
const content = await readFile(trimmed, "utf-8");
|
|
5967
|
+
let parsed;
|
|
5968
|
+
try {
|
|
5969
|
+
parsed = JSON.parse(content);
|
|
5970
|
+
} catch {
|
|
5971
|
+
throw new Error(`${trimmed} is not a valid solana-keygen JSON file`);
|
|
5972
|
+
}
|
|
5973
|
+
if (!Array.isArray(parsed) || parsed.length !== 64 || parsed.some(
|
|
5974
|
+
(byte) => typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255
|
|
5975
|
+
)) {
|
|
5976
|
+
throw new Error(`${trimmed} must contain a JSON array of 64 bytes (solana-keygen format)`);
|
|
5977
|
+
}
|
|
5978
|
+
return encodeSecretKeyBase58(new Uint8Array(parsed));
|
|
5979
|
+
}
|
|
5980
|
+
try {
|
|
5981
|
+
await signerFromSecretKeyBase58(trimmed);
|
|
5982
|
+
} catch {
|
|
5983
|
+
throw new Error("not a readable file and not a valid base58-encoded 64-byte secret key");
|
|
5984
|
+
}
|
|
5985
|
+
return trimmed;
|
|
5986
|
+
}
|
|
5987
|
+
function computeBridgePriceSubunits(quote, marginBps, feeBps) {
|
|
5988
|
+
if (feeBps >= BPS_DENOMINATOR) {
|
|
5989
|
+
throw new Error(`feeBps ${feeBps} >= ${BPS_DENOMINATOR} - refusing zero/negative denominator`);
|
|
5990
|
+
}
|
|
5991
|
+
return BigInt(
|
|
5992
|
+
new Decimal(quote.toString()).mul(BPS_DENOMINATOR + marginBps).div(BPS_DENOMINATOR - feeBps).toDecimalPlaces(0, Decimal.ROUND_CEIL).toString()
|
|
5993
|
+
);
|
|
5994
|
+
}
|
|
5995
|
+
function buildSkillMd(input) {
|
|
5996
|
+
const frontmatter = {
|
|
5997
|
+
name: input.skillName,
|
|
5998
|
+
description: input.description,
|
|
5999
|
+
capabilities: input.capabilities,
|
|
6000
|
+
price: input.priceDisplay,
|
|
6001
|
+
token: "usdc",
|
|
6002
|
+
mode: "x402",
|
|
6003
|
+
x402_url: input.url,
|
|
6004
|
+
x402_method: input.method,
|
|
6005
|
+
...input.queryParam !== void 0 ? { x402_query_param: input.queryParam } : {},
|
|
6006
|
+
x402_max_upstream: Number(input.quote),
|
|
6007
|
+
// GET input is dominated by the ~2KB percent-encoded query-string limit,
|
|
6008
|
+
// so the byte cap is written for POST bridges only.
|
|
6009
|
+
...input.method === "POST" ? { x402_max_input_bytes: DEFAULT_X402_MAX_INPUT_BYTES } : {}
|
|
6010
|
+
};
|
|
6011
|
+
const bodyLines = [
|
|
6012
|
+
`# ${input.skillName}`,
|
|
6013
|
+
"",
|
|
6014
|
+
"Operator notes for this x402 bridge skill (the executor ignores this body):",
|
|
6015
|
+
"",
|
|
6016
|
+
`- Upstream: ${input.url} (${input.method})`,
|
|
6017
|
+
`- Upstream quote at generation time: ${formatAssetAmount(USDC_SOLANA_DEVNET, input.quote)} (= x402_max_upstream, the signing ceiling)`,
|
|
6018
|
+
`- Price: ${input.priceDisplay} USDC (margin ${input.marginPercent}%, protocol fee ${input.feePercent}% at generation time)`,
|
|
6019
|
+
"- To reprice after an upstream change, re-run:",
|
|
6020
|
+
"",
|
|
6021
|
+
"```bash",
|
|
6022
|
+
`npx @elisym/cli x402 add ${input.url}`,
|
|
6023
|
+
"```"
|
|
6024
|
+
];
|
|
6025
|
+
return `---
|
|
6026
|
+
${YAML.stringify(frontmatter)}---
|
|
6027
|
+
|
|
6028
|
+
${bodyLines.join("\n")}
|
|
6029
|
+
`;
|
|
6030
|
+
}
|
|
6031
|
+
function scanExistingSkills(skillsDir, candidateDTag, url) {
|
|
6032
|
+
const scan = {};
|
|
6033
|
+
let entries;
|
|
6034
|
+
try {
|
|
6035
|
+
entries = readdirSync(skillsDir);
|
|
6036
|
+
} catch {
|
|
6037
|
+
return scan;
|
|
6038
|
+
}
|
|
6039
|
+
for (const entry of entries) {
|
|
6040
|
+
const skillMdPath = join(skillsDir, entry, "SKILL.md");
|
|
6041
|
+
try {
|
|
6042
|
+
if (!statSync(join(skillsDir, entry)).isDirectory()) {
|
|
6043
|
+
continue;
|
|
6044
|
+
}
|
|
6045
|
+
const { frontmatter } = parseSkillMd(readFileSync(skillMdPath, "utf-8"));
|
|
6046
|
+
if (typeof frontmatter.name !== "string") {
|
|
6047
|
+
continue;
|
|
6048
|
+
}
|
|
6049
|
+
if (typeof frontmatter.x402_url === "string" && frontmatter.x402_url === url) {
|
|
6050
|
+
scan.sameUrlDir = join(skillsDir, entry);
|
|
6051
|
+
scan.sameUrlName = frontmatter.name;
|
|
6052
|
+
continue;
|
|
6053
|
+
}
|
|
6054
|
+
let existingDTag;
|
|
6055
|
+
try {
|
|
6056
|
+
existingDTag = toDTag(frontmatter.name);
|
|
6057
|
+
} catch {
|
|
6058
|
+
continue;
|
|
6059
|
+
}
|
|
6060
|
+
if (existingDTag === candidateDTag) {
|
|
6061
|
+
scan.dTagCollision = frontmatter.name;
|
|
6062
|
+
}
|
|
6063
|
+
} catch {
|
|
6064
|
+
continue;
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
return scan;
|
|
6068
|
+
}
|
|
6069
|
+
function describeUnacceptableAccepts(probe) {
|
|
6070
|
+
const networks = [...new Set(probe.accepts.map((requirement) => String(requirement.network)))];
|
|
6071
|
+
if (networks.length > 0 && networks.every((network) => network.startsWith("eip155:"))) {
|
|
6072
|
+
return `the service accepts only EVM networks (${networks.join(", ")}) - not supported yet`;
|
|
6073
|
+
}
|
|
6074
|
+
if (networks.some((network) => network === SOLANA_MAINNET_CAIP2 || network === "solana")) {
|
|
6075
|
+
return "the service settles on Solana mainnet - elisym currently runs on devnet (mainnet is on the roadmap)";
|
|
6076
|
+
}
|
|
6077
|
+
return `no exact-scheme devnet-USDC requirement found (service accepts: ${networks.join(", ") || "nothing parseable"})`;
|
|
6078
|
+
}
|
|
6079
|
+
async function cmdX402Add(url, agentName, options) {
|
|
6080
|
+
const cwd = process.cwd();
|
|
6081
|
+
const { default: inquirer } = await import('inquirer');
|
|
6082
|
+
let parsedUrl;
|
|
6083
|
+
try {
|
|
6084
|
+
parsedUrl = new URL(url);
|
|
6085
|
+
} catch {
|
|
6086
|
+
fail(`"${url}" is not a valid URL`);
|
|
6087
|
+
}
|
|
6088
|
+
const isLoopback = ["localhost", "127.0.0.1", "[::1]"].includes(parsedUrl.hostname);
|
|
6089
|
+
if (parsedUrl.protocol !== "https:" && !(parsedUrl.protocol === "http:" && isLoopback)) {
|
|
6090
|
+
fail("the upstream must be an https:// URL (plain http is allowed for localhost only)");
|
|
6091
|
+
}
|
|
6092
|
+
const methodRaw = (options.method ?? "POST").toUpperCase();
|
|
6093
|
+
if (methodRaw !== "GET" && methodRaw !== "POST") {
|
|
6094
|
+
fail(`--method must be GET or POST; got "${options.method}"`);
|
|
6095
|
+
}
|
|
6096
|
+
const method = methodRaw;
|
|
6097
|
+
const marginBps = parseMarginBps(options.marginBps);
|
|
6098
|
+
if (!agentName) {
|
|
6099
|
+
const agents = await listAgents(cwd);
|
|
6100
|
+
if (agents.length === 0) {
|
|
6101
|
+
fail("no agents found - create one first: npx @elisym/cli init");
|
|
6102
|
+
}
|
|
6103
|
+
if (options.yes) {
|
|
6104
|
+
fail("--yes needs an explicit agent argument: elisym x402 add <url> <agent> --yes");
|
|
6105
|
+
}
|
|
6106
|
+
const { selected } = await inquirer.prompt([
|
|
6107
|
+
{
|
|
6108
|
+
type: "list",
|
|
6109
|
+
name: "selected",
|
|
6110
|
+
message: "Add the bridge skill to which agent?",
|
|
6111
|
+
choices: agents.map((agent) => ({
|
|
6112
|
+
name: `${agent.name} (${agent.source})`,
|
|
6113
|
+
value: agent.name
|
|
6114
|
+
}))
|
|
6115
|
+
}
|
|
6116
|
+
]);
|
|
6117
|
+
agentName = selected;
|
|
6118
|
+
}
|
|
6119
|
+
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
6120
|
+
const loaded = await loadAgent(agentName, cwd, passphrase);
|
|
6121
|
+
console.log(`
|
|
6122
|
+
Probing ${url} (${method}, unpaid)...`);
|
|
6123
|
+
let probe;
|
|
6124
|
+
try {
|
|
6125
|
+
probe = await probePaymentRequired(url, method);
|
|
6126
|
+
} catch (error) {
|
|
6127
|
+
if (error instanceof X402ProbeError) {
|
|
6128
|
+
fail(error.message);
|
|
6129
|
+
}
|
|
6130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6131
|
+
fail(`upstream unreachable: ${message}`);
|
|
6132
|
+
}
|
|
6133
|
+
const anyCeiling = { maxUpstreamSubunits: BigInt(Number.MAX_SAFE_INTEGER) };
|
|
6134
|
+
const requirement = selectAcceptableRequirement(probe.accepts, anyCeiling);
|
|
6135
|
+
if (requirement === void 0) {
|
|
6136
|
+
fail(describeUnacceptableAccepts(probe));
|
|
6137
|
+
}
|
|
6138
|
+
const quote = requirementAmount(requirement);
|
|
6139
|
+
if (quote === null || quote <= 0n) {
|
|
6140
|
+
fail("the upstream quote is not a positive integer amount");
|
|
6141
|
+
}
|
|
6142
|
+
let solanaSecretKey = loaded.secrets.solana_secret_key;
|
|
6143
|
+
if (solanaSecretKey === void 0 || solanaSecretKey.length === 0) {
|
|
6144
|
+
let choice;
|
|
6145
|
+
if (options.yes || options.generateWallet) {
|
|
6146
|
+
if (!options.generateWallet) {
|
|
6147
|
+
fail(
|
|
6148
|
+
"this agent has no Solana wallet key. Run interactively to generate/import one, or pass --generate-wallet (a money-holding key is never created silently under --yes)."
|
|
6149
|
+
);
|
|
6150
|
+
}
|
|
6151
|
+
choice = "generate";
|
|
6152
|
+
} else {
|
|
6153
|
+
const { walletChoice } = await inquirer.prompt([
|
|
6154
|
+
{
|
|
6155
|
+
type: "list",
|
|
6156
|
+
name: "walletChoice",
|
|
6157
|
+
message: "The bridge pays the upstream from the agent wallet, but .secrets.json has no solana_secret_key:",
|
|
6158
|
+
choices: [
|
|
6159
|
+
{ name: "Generate a new wallet", value: "generate" },
|
|
6160
|
+
{ name: "Import an existing one (solana-keygen JSON file or base58)", value: "import" }
|
|
6161
|
+
]
|
|
6162
|
+
}
|
|
6163
|
+
]);
|
|
6164
|
+
choice = walletChoice;
|
|
6165
|
+
}
|
|
6166
|
+
if (choice === "generate") {
|
|
6167
|
+
const wallet = await generateSolanaWallet();
|
|
6168
|
+
solanaSecretKey = wallet.secretKeyBase58;
|
|
6169
|
+
} else {
|
|
6170
|
+
const { imported } = await inquirer.prompt([
|
|
6171
|
+
{
|
|
6172
|
+
// Masked: a pasted base58 secret key must not echo to the terminal
|
|
6173
|
+
// scrollback (matches every other secret prompt in the CLI). A file
|
|
6174
|
+
// path can still be pasted/drag-dropped under the mask.
|
|
6175
|
+
type: "password",
|
|
6176
|
+
mask: "*",
|
|
6177
|
+
name: "imported",
|
|
6178
|
+
message: "Path to a solana-keygen JSON file (or paste a base58 secret key):"
|
|
6179
|
+
}
|
|
6180
|
+
]);
|
|
6181
|
+
try {
|
|
6182
|
+
solanaSecretKey = await parseImportedSecret(imported);
|
|
6183
|
+
} catch (error) {
|
|
6184
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
await writeSecrets(
|
|
6188
|
+
loaded.dir,
|
|
6189
|
+
{ ...loaded.secrets, solana_secret_key: solanaSecretKey },
|
|
6190
|
+
passphrase
|
|
6191
|
+
);
|
|
6192
|
+
console.log(" Wallet key saved to .secrets.json");
|
|
6193
|
+
}
|
|
6194
|
+
const signer = await signerFromSecretKeyBase58(solanaSecretKey);
|
|
6195
|
+
const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
|
|
6196
|
+
if (solPayment !== void 0 && solPayment.address !== signer.address) {
|
|
6197
|
+
fail(
|
|
6198
|
+
`wallet invariant violated: elisym.yaml payments[].address (${solPayment.address}) differs from the solana_secret_key address (${signer.address}).
|
|
6199
|
+
Revenue must land in the wallet the bridge spends from, or the float never refills.
|
|
6200
|
+
Fix: set payments[].address to the key address, or import the matching key.`
|
|
6201
|
+
);
|
|
6202
|
+
}
|
|
6203
|
+
if (solPayment === void 0) {
|
|
6204
|
+
console.log(
|
|
6205
|
+
` elisym.yaml has no Solana payments entry; adding address ${signer.address} (devnet).`
|
|
6206
|
+
);
|
|
6207
|
+
await writeYaml(loaded.dir, {
|
|
6208
|
+
...loaded.yaml,
|
|
6209
|
+
payments: [
|
|
6210
|
+
...loaded.yaml.payments,
|
|
6211
|
+
{ chain: "solana", network: "devnet", address: signer.address }
|
|
6212
|
+
]
|
|
6213
|
+
});
|
|
6214
|
+
}
|
|
6215
|
+
const rpc = createSolanaRpc(getRpcUrl());
|
|
6216
|
+
const balance = await fetchUsdcBalance(rpc, address(signer.address));
|
|
6217
|
+
let feeBps;
|
|
6218
|
+
try {
|
|
6219
|
+
const protocolConfig = await getProtocolConfig(rpc, getProtocolProgramId("devnet"), {
|
|
6220
|
+
forceRefresh: true
|
|
6221
|
+
});
|
|
6222
|
+
feeBps = protocolConfig.feeBps;
|
|
6223
|
+
} catch (error) {
|
|
6224
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6225
|
+
fail(`could not read the on-chain protocol config (fee): ${message}`);
|
|
6226
|
+
}
|
|
6227
|
+
if (feeBps >= BPS_DENOMINATOR) {
|
|
6228
|
+
fail(
|
|
6229
|
+
`protocol fee is ${feeBps} bps (>= 100%) - price math would divide by zero or go negative`
|
|
6230
|
+
);
|
|
6231
|
+
}
|
|
6232
|
+
const priceSubunits = computeBridgePriceSubunits(quote, marginBps, feeBps);
|
|
6233
|
+
const priceDisplay = new Decimal(priceSubunits.toString()).div(new Decimal(10).pow(USDC_SOLANA_DEVNET.decimals)).toString();
|
|
6234
|
+
const protocolFee = calculateProtocolFee(Number(priceSubunits), feeBps);
|
|
6235
|
+
const netMargin = priceSubunits - BigInt(protocolFee) - quote;
|
|
6236
|
+
const fallbackName = slugFromUrl(parsedUrl);
|
|
6237
|
+
const rawName = options.name ?? probe.resource?.serviceName ?? fallbackName;
|
|
6238
|
+
const skillName = sanitizeUpstreamText(rawName, LIMITS.MAX_AGENT_NAME_LENGTH);
|
|
6239
|
+
if (!/[a-zA-Z0-9]/.test(skillName)) {
|
|
6240
|
+
fail(
|
|
6241
|
+
`the upstream service name ("${rawName}") has no ASCII letters or digits - pass an explicit --name <skill-name>`
|
|
6242
|
+
);
|
|
6243
|
+
}
|
|
6244
|
+
const candidateDTag = toDTag(skillName);
|
|
6245
|
+
const description = sanitizeUpstreamText(
|
|
6246
|
+
probe.resource?.description ?? `x402 bridge to ${parsedUrl.hostname}`,
|
|
6247
|
+
LIMITS.MAX_DESCRIPTION_LENGTH
|
|
6248
|
+
);
|
|
6249
|
+
const capabilities = [candidateDTag];
|
|
6250
|
+
const skillsDir = join(loaded.dir, "skills");
|
|
6251
|
+
const scan = scanExistingSkills(skillsDir, candidateDTag, url);
|
|
6252
|
+
let targetDir = join(skillsDir, candidateDTag);
|
|
6253
|
+
if (scan.sameUrlDir !== void 0) {
|
|
6254
|
+
if (!options.yes) {
|
|
6255
|
+
const { update } = await inquirer.prompt([
|
|
6256
|
+
{
|
|
6257
|
+
type: "confirm",
|
|
6258
|
+
name: "update",
|
|
6259
|
+
message: `Skill "${scan.sameUrlName}" already bridges this URL - regenerate it with the fresh quote?`,
|
|
6260
|
+
default: true
|
|
6261
|
+
}
|
|
6262
|
+
]);
|
|
6263
|
+
if (!update) {
|
|
6264
|
+
fail("aborted");
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
targetDir = scan.sameUrlDir;
|
|
6268
|
+
} else if (scan.dTagCollision !== void 0) {
|
|
6269
|
+
fail(
|
|
6270
|
+
`the generated name "${skillName}" collides with existing skill "${scan.dTagCollision}" (both map to d-tag "${candidateDTag}", which would replace its live discovery card) - pass a distinct --name`
|
|
6271
|
+
);
|
|
6272
|
+
} else if (existsSync(targetDir)) {
|
|
6273
|
+
fail(`skill directory ${targetDir} already exists - pass a distinct --name`);
|
|
6274
|
+
}
|
|
6275
|
+
let queryParam = options.queryParam;
|
|
6276
|
+
if (method === "GET" && queryParam === void 0 && !options.yes) {
|
|
6277
|
+
const { param } = await inquirer.prompt([
|
|
6278
|
+
{
|
|
6279
|
+
type: "input",
|
|
6280
|
+
name: "param",
|
|
6281
|
+
message: "GET upstream: which query parameter should carry the buyer input? (empty = the skill takes no input)"
|
|
6282
|
+
}
|
|
6283
|
+
]);
|
|
6284
|
+
queryParam = param.trim() === "" ? void 0 : param.trim();
|
|
6285
|
+
}
|
|
6286
|
+
if (queryParam !== void 0 && parsedUrl.searchParams.has(queryParam)) {
|
|
6287
|
+
fail(`--query-param "${queryParam}" collides with a parameter already present in the URL`);
|
|
6288
|
+
}
|
|
6289
|
+
if (method === "POST" && queryParam !== void 0) {
|
|
6290
|
+
fail("--query-param only applies to GET upstreams (POST maps the input to the request body)");
|
|
6291
|
+
}
|
|
6292
|
+
const noInput = method === "GET" && queryParam === void 0;
|
|
6293
|
+
const feePercent = new Decimal(feeBps).div(100).toString();
|
|
6294
|
+
const marginPercent = new Decimal(marginBps).div(100).toString();
|
|
6295
|
+
console.log("");
|
|
6296
|
+
console.log(` ${chalk.bold(skillName)}`);
|
|
6297
|
+
console.log(` ${description}`);
|
|
6298
|
+
console.log("");
|
|
6299
|
+
console.log(
|
|
6300
|
+
` Upstream ${url} (${method}${queryParam !== void 0 ? `, input in ?${queryParam}=` : ""}${noInput ? ", no buyer input" : ""})`
|
|
6301
|
+
);
|
|
6302
|
+
console.log(` Quote ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)}`);
|
|
6303
|
+
console.log(
|
|
6304
|
+
` Your price ${formatAssetAmount(USDC_SOLANA_DEVNET, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
|
|
6305
|
+
);
|
|
6306
|
+
console.log(` Net margin ${formatAssetAmount(USDC_SOLANA_DEVNET, netMargin)} per job`);
|
|
6307
|
+
console.log(
|
|
6308
|
+
` Float ${formatAssetAmount(USDC_SOLANA_DEVNET, balance)} in ${signer.address}`
|
|
6309
|
+
);
|
|
6310
|
+
if (balance < quote) {
|
|
6311
|
+
console.log(
|
|
6312
|
+
chalk.yellow(
|
|
6313
|
+
` ! Float below one upstream quote - fund the wallet with devnet USDC: ${CIRCLE_FAUCET_URL}`
|
|
6314
|
+
)
|
|
6315
|
+
);
|
|
6316
|
+
}
|
|
6317
|
+
console.log(` Skill dir ${targetDir}`);
|
|
6318
|
+
console.log("");
|
|
6319
|
+
if (!options.yes) {
|
|
6320
|
+
const { confirmed } = await inquirer.prompt([
|
|
6321
|
+
{ type: "confirm", name: "confirmed", message: "Write the skill?", default: true }
|
|
6322
|
+
]);
|
|
6323
|
+
if (!confirmed) {
|
|
6324
|
+
fail("aborted");
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
const content = buildSkillMd({
|
|
6328
|
+
skillName,
|
|
6329
|
+
description,
|
|
6330
|
+
capabilities,
|
|
6331
|
+
priceDisplay,
|
|
6332
|
+
url,
|
|
6333
|
+
method,
|
|
6334
|
+
queryParam,
|
|
6335
|
+
quote,
|
|
6336
|
+
marginPercent,
|
|
6337
|
+
feePercent
|
|
6338
|
+
});
|
|
6339
|
+
const roundTrip = parseSkillMd(content);
|
|
6340
|
+
validateSkillFrontmatter(roundTrip.frontmatter, roundTrip.systemPrompt, {
|
|
6341
|
+
allowFreeSkills: false,
|
|
6342
|
+
allowX402Skills: true
|
|
6343
|
+
});
|
|
6344
|
+
await mkdir(targetDir, { recursive: true });
|
|
6345
|
+
await writeFile(join(targetDir, "SKILL.md"), content, "utf-8");
|
|
6346
|
+
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
6347
|
+
console.log(`
|
|
6348
|
+
Wrote ${join(targetDir, "SKILL.md")}`);
|
|
6349
|
+
if (balance < quote) {
|
|
6350
|
+
console.log(` Next: fund the float (devnet USDC): ${CIRCLE_FAUCET_URL}`);
|
|
6351
|
+
}
|
|
6352
|
+
console.log(` Start the agent: npx @elisym/cli start ${agentName}
|
|
6353
|
+
`);
|
|
6354
|
+
}
|
|
4926
6355
|
function readPackageVersion() {
|
|
4927
6356
|
try {
|
|
4928
6357
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -4996,6 +6425,21 @@ program.command("list").description("List all agents (project-local and home-glo
|
|
|
4996
6425
|
})
|
|
4997
6426
|
);
|
|
4998
6427
|
program.command("wallet [name]").description("Show wallet balance").action(safe(cmdWallet));
|
|
6428
|
+
var x402 = program.command("x402").description("Bridge x402-paid HTTP services into elisym skills");
|
|
6429
|
+
x402.command("add <url> [agent]").description("Generate a bridge skill from a live x402 endpoint (probes its 402 challenge)").option("--method <method>", "HTTP method for the upstream call: GET or POST (default POST)").option(
|
|
6430
|
+
"--query-param <name>",
|
|
6431
|
+
"GET only: query parameter carrying the buyer input (omitting it on GET makes a no-input skill)"
|
|
6432
|
+
).option(
|
|
6433
|
+
"--margin-bps <bps>",
|
|
6434
|
+
"Operator margin over the upstream quote in basis points (default 1000 = 10%)"
|
|
6435
|
+
).option("--name <skill-name>", "Override the generated skill name (also the discovery d-tag)").option(
|
|
6436
|
+
"--generate-wallet",
|
|
6437
|
+
"Non-interactive runs only: generate a new Solana wallet key when the agent has none"
|
|
6438
|
+
).option("--yes", "Skip confirmation prompts (requires an explicit agent argument)").action(
|
|
6439
|
+
safe(async (url, agent, options) => {
|
|
6440
|
+
await cmdX402Add(url, agent, options);
|
|
6441
|
+
})
|
|
6442
|
+
);
|
|
4999
6443
|
program.parse();
|
|
5000
6444
|
//# sourceMappingURL=index.js.map
|
|
5001
6445
|
//# sourceMappingURL=index.js.map
|