@agentpayments/edge 0.1.0 → 0.1.1
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/README.md +6 -0
- package/index.js +101 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,12 @@ export default { fetch: (req, env, ctx) => gate(req, env, ctx) };
|
|
|
82
82
|
| `SOLANA_RPC_URL` | No | Custom Solana RPC endpoint. Defaults by debug flag. |
|
|
83
83
|
| `USDC_MINT` | No | Custom USDC mint address. Defaults by debug flag. |
|
|
84
84
|
| `DEBUG` | No | `"true"` = devnet. `"false"` = mainnet (default varies by adapter). |
|
|
85
|
+
| `AGENTPAYMENTS_API_KEY` | No | AgentPayments hosted-platform API key (`ap_live_...`). When set, agent keys are issued and metered via the platform instead of self-signed locally. See **Hosted Platform Mode** below. |
|
|
86
|
+
| `AGENTPAYMENTS_PLATFORM_URL` | No | Override for a self-hosted platform API. |
|
|
87
|
+
|
|
88
|
+
## Hosted Platform Mode
|
|
89
|
+
|
|
90
|
+
Setting `AGENTPAYMENTS_API_KEY` switches agent-key issuance from local (`ag_...`) to platform-issued (`agp_...`), and — when the platform account has an on-chain fee configured — every 402 response's custom `payment` object gains a `platform_fee` field describing a second required USDC transfer, to be sent in the **same Solana transaction** as the vendor payment. Missing that second transfer is treated as an unpaid request, same as any other invalid payment. This is opt-in per vendor account and has no effect on self-hosted deployments (no `AGENTPAYMENTS_API_KEY`). The standards-compliant `accepts[]`/`X-PAYMENT-REQUIRED` x402 fields are untouched — they still describe only the vendor leg.
|
|
85
91
|
|
|
86
92
|
## Security Features
|
|
87
93
|
|
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const USDC_DECIMALS = 6;
|
|
|
27
27
|
const X402_VERSION = 1;
|
|
28
28
|
const SOLANA_CHAIN_ID_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
|
|
29
29
|
const SOLANA_CHAIN_ID_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
|
|
30
|
-
const PLATFORM_API_URL = 'https://api.agentpayments.
|
|
30
|
+
const PLATFORM_API_URL = 'https://api.agentpayments.cloud';
|
|
31
31
|
const HOSTED_KEY_PREFIX = 'agp_';
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
@@ -251,30 +251,50 @@ class EdgePlatformClient {
|
|
|
251
251
|
this.apiKey = apiKey;
|
|
252
252
|
this.platformUrl = platformUrl.replace(/\/$/, '');
|
|
253
253
|
this._verificationSecret = null;
|
|
254
|
-
this.
|
|
254
|
+
this._platformFeeInfo = undefined; // undefined = not fetched yet; null = fetched, no fee configured
|
|
255
|
+
this._accountFetch = null;
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
_authHeaders() {
|
|
258
259
|
return { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' };
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
if (this.
|
|
263
|
-
|
|
264
|
-
this._secretFetch = fetch(`${this.platformUrl}/v1/account`, { headers: this._authHeaders() })
|
|
262
|
+
_fetchAccount() {
|
|
263
|
+
if (this._accountFetch) return this._accountFetch;
|
|
264
|
+
this._accountFetch = fetch(`${this.platformUrl}/v1/account`, { headers: this._authHeaders() })
|
|
265
265
|
.then((r) => {
|
|
266
266
|
if (!r.ok) throw new Error(`Platform /v1/account returned ${r.status}`);
|
|
267
267
|
return r.json();
|
|
268
268
|
})
|
|
269
269
|
.then((data) => {
|
|
270
270
|
this._verificationSecret = data.verificationSecret;
|
|
271
|
-
|
|
271
|
+
this._platformFeeInfo = data.platformFeeWallet
|
|
272
|
+
? { wallet: data.platformFeeWallet, ratePct: data.platformFeeRatePct }
|
|
273
|
+
: null;
|
|
274
|
+
return data;
|
|
272
275
|
})
|
|
273
276
|
.catch((err) => {
|
|
274
|
-
this.
|
|
277
|
+
this._accountFetch = null;
|
|
275
278
|
throw err;
|
|
276
279
|
});
|
|
277
|
-
return this.
|
|
280
|
+
return this._accountFetch;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async getVerificationSecret() {
|
|
284
|
+
if (this._verificationSecret) return this._verificationSecret;
|
|
285
|
+
await this._fetchAccount();
|
|
286
|
+
return this._verificationSecret;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Lazily fetch + cache the on-chain platform fee config (same request as
|
|
291
|
+
* getVerificationSecret — no extra round trip if already fetched).
|
|
292
|
+
* Returns { wallet, ratePct } or null if no fee is configured.
|
|
293
|
+
*/
|
|
294
|
+
async getPlatformFeeInfo() {
|
|
295
|
+
if (this._platformFeeInfo !== undefined) return this._platformFeeInfo;
|
|
296
|
+
await this._fetchAccount();
|
|
297
|
+
return this._platformFeeInfo;
|
|
278
298
|
}
|
|
279
299
|
|
|
280
300
|
async issueKey() {
|
|
@@ -332,7 +352,15 @@ async function rpcCallWithFallback(rpcUrls, method, params, opts) {
|
|
|
332
352
|
throw lastError;
|
|
333
353
|
}
|
|
334
354
|
|
|
335
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Verify payment on-chain.
|
|
357
|
+
*
|
|
358
|
+
* feeInfo, when set (hosted-platform mode with an on-chain fee configured), is
|
|
359
|
+
* { wallet, ratePct }. When set, the SAME transaction that carries the vendor
|
|
360
|
+
* payment must also carry a USDC transfer to feeInfo.wallet of at least
|
|
361
|
+
* minPayment * ratePct / 100, or the payment is treated as unverified.
|
|
362
|
+
*/
|
|
363
|
+
export async function verifyPaymentOnChain(agentKey, walletAddress, rpcUrls, usdcMint, minPayment = MIN_PAYMENT, feeInfo = null) {
|
|
336
364
|
try {
|
|
337
365
|
// commitment: 'finalized' — confirmed blocks can be rolled back (rare but possible).
|
|
338
366
|
const ataData = await rpcCallWithFallback(rpcUrls, 'getTokenAccountsByOwner', [walletAddress, { mint: usdcMint }, { encoding: 'jsonParsed', commitment: 'finalized' }]);
|
|
@@ -343,6 +371,15 @@ async function verifyPaymentOnChain(agentKey, walletAddress, rpcUrls, usdcMint)
|
|
|
343
371
|
const vendorUsdcAccounts = new Set(tokenAccounts);
|
|
344
372
|
if (vendorUsdcAccounts.size === 0) return false; // vendor has no USDC account yet — no payment possible
|
|
345
373
|
|
|
374
|
+
let feeUsdcAccounts = null;
|
|
375
|
+
let feeAmountMicro = 0;
|
|
376
|
+
if (feeInfo) {
|
|
377
|
+
const feeAtaData = await rpcCallWithFallback(rpcUrls, 'getTokenAccountsByOwner', [feeInfo.wallet, { mint: usdcMint }, { encoding: 'jsonParsed', commitment: 'finalized' }]);
|
|
378
|
+
feeUsdcAccounts = new Set((feeAtaData.result?.value || []).map((entry) => entry.pubkey));
|
|
379
|
+
feeAmountMicro = Math.round(Math.round(minPayment * 1e6) * feeInfo.ratePct / 100);
|
|
380
|
+
if (feeUsdcAccounts.size === 0) return false; // fee wallet has no USDC account — fee can never be satisfied
|
|
381
|
+
}
|
|
382
|
+
|
|
346
383
|
const addressesToScan = [walletAddress, ...tokenAccounts];
|
|
347
384
|
const seen = new Set();
|
|
348
385
|
const allSignatures = [];
|
|
@@ -376,6 +413,7 @@ async function verifyPaymentOnChain(agentKey, walletAddress, rpcUrls, usdcMint)
|
|
|
376
413
|
|
|
377
414
|
let hasMemo = false;
|
|
378
415
|
let hasPayment = false;
|
|
416
|
+
let hasFeePayment = !feeInfo; // vacuously satisfied when no fee is required
|
|
379
417
|
|
|
380
418
|
for (const ix of allInstructions) {
|
|
381
419
|
if (ix.program === 'spl-memo' || ix.programId === MEMO_PROGRAM) {
|
|
@@ -387,19 +425,24 @@ async function verifyPaymentOnChain(agentKey, walletAddress, rpcUrls, usdcMint)
|
|
|
387
425
|
const parsed = ix.parsed || {};
|
|
388
426
|
if (parsed.type === 'transfer' || parsed.type === 'transferChecked') {
|
|
389
427
|
const info = parsed.info || {};
|
|
390
|
-
// Payment must be delivered to one of the vendor's
|
|
391
|
-
|
|
428
|
+
// Payment must be delivered to one of the vendor's or fee wallet's USDC
|
|
429
|
+
// token accounts — anything else is irrelevant.
|
|
430
|
+
const isVendorDest = vendorUsdcAccounts.has(info.destination);
|
|
431
|
+
const isFeeDest = feeUsdcAccounts !== null && feeUsdcAccounts.has(info.destination);
|
|
432
|
+
if (!isVendorDest && !isFeeDest) continue;
|
|
392
433
|
if (parsed.type === 'transferChecked' && info.mint !== usdcMint) continue;
|
|
393
434
|
// Integer base-unit comparison — avoids float precision issues at threshold.
|
|
394
435
|
const amountStr = info.tokenAmount?.amount ?? info.amount ?? '0';
|
|
395
436
|
const amountMicro = parseInt(amountStr, 10);
|
|
396
|
-
|
|
397
|
-
|
|
437
|
+
if (Number.isNaN(amountMicro)) continue;
|
|
438
|
+
const minPaymentMicro = Math.round(minPayment * 1e6);
|
|
439
|
+
if (isVendorDest && amountMicro >= minPaymentMicro) hasPayment = true;
|
|
440
|
+
else if (isFeeDest && amountMicro >= feeAmountMicro) hasFeePayment = true;
|
|
398
441
|
}
|
|
399
442
|
}
|
|
400
443
|
}
|
|
401
444
|
|
|
402
|
-
if (hasMemo && hasPayment) return true;
|
|
445
|
+
if (hasMemo && hasPayment && hasFeePayment) return true;
|
|
403
446
|
}
|
|
404
447
|
} catch (error) {
|
|
405
448
|
gateLog('error', 'Solana RPC error', { error: error.message });
|
|
@@ -473,6 +516,30 @@ function buildX402PaymentRequirements({ walletAddress, mint, minPayment, debug,
|
|
|
473
516
|
return req;
|
|
474
517
|
}
|
|
475
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Builds the custom `payment` object (NOT part of the x402 spec — that's
|
|
521
|
+
* buildX402PaymentRequirements above, which stays vendor-leg-only). When feeInfo
|
|
522
|
+
* is set (hosted-platform mode with an on-chain fee configured), adds a
|
|
523
|
+
* platform_fee field describing the second required transfer. Deliberately not
|
|
524
|
+
* added as a second x402 accepts[] entry — that would read to a spec-compliant
|
|
525
|
+
* client as an alternative payment method, not an additional requirement.
|
|
526
|
+
*/
|
|
527
|
+
function buildPaymentField({ network, minPayment, walletAddress, memo, feeInfo, instructions }) {
|
|
528
|
+
const payment = { chain: 'solana', network, token: 'USDC', amount: String(minPayment), wallet_address: walletAddress, memo };
|
|
529
|
+
if (feeInfo) {
|
|
530
|
+
const feeAmountMicro = Math.round(Math.round(minPayment * 1e6) * feeInfo.ratePct / 100);
|
|
531
|
+
payment.platform_fee = {
|
|
532
|
+
wallet_address: feeInfo.wallet,
|
|
533
|
+
amount: String(feeAmountMicro / 1e6),
|
|
534
|
+
token: 'USDC',
|
|
535
|
+
rate_pct: feeInfo.ratePct,
|
|
536
|
+
note: 'Must be a second USDC transfer inside the SAME Solana transaction as the payment above, or access will be denied.',
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (instructions) payment.instructions = instructions;
|
|
540
|
+
return payment;
|
|
541
|
+
}
|
|
542
|
+
|
|
476
543
|
/**
|
|
477
544
|
* Like jsonResponse(body, 402) but adds x402Version, accepts[], and
|
|
478
545
|
* the X-PAYMENT-REQUIRED header (base64-encoded PaymentRequirements).
|
|
@@ -633,6 +700,18 @@ export function createEdgeGate(options = {}) {
|
|
|
633
700
|
|
|
634
701
|
if (!isBrowser(request)) {
|
|
635
702
|
const agentKey = request.headers.get('X-Agent-Key');
|
|
703
|
+
const network = debug ? 'devnet' : 'mainnet-beta';
|
|
704
|
+
|
|
705
|
+
// Resolve the on-chain platform fee requirement once (hosted-platform mode
|
|
706
|
+
// only — always null for self-hosted vendors with no platformClient).
|
|
707
|
+
let feeInfo = null;
|
|
708
|
+
if (platformClient) {
|
|
709
|
+
try {
|
|
710
|
+
feeInfo = await platformClient.getPlatformFeeInfo();
|
|
711
|
+
} catch (err) {
|
|
712
|
+
gateLog('warn', 'Failed to fetch platform fee info, proceeding without fee enforcement', { error: err.message });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
636
715
|
|
|
637
716
|
if (!agentKey) {
|
|
638
717
|
// Hosted mode: issue a metered platform key (agp_...).
|
|
@@ -649,19 +728,14 @@ export function createEdgeGate(options = {}) {
|
|
|
649
728
|
} else {
|
|
650
729
|
newKey = await generateAgentKey(secret);
|
|
651
730
|
}
|
|
731
|
+
const noKeyInstructions = feeInfo
|
|
732
|
+
? `Send ${minPayment} USDC on Solana ${debug ? 'devnet' : 'mainnet'} to ${walletAddress} with memo "${newKey}", AND in the SAME transaction send the platform fee (see platform_fee below) to ${feeInfo.wallet}. Then include the header X-Agent-Key: ${newKey} on all subsequent requests.`
|
|
733
|
+
: `Send ${minPayment} USDC on Solana ${debug ? 'devnet' : 'mainnet'} to ${walletAddress} with memo "${newKey}". Then include the header X-Agent-Key: ${newKey} on all subsequent requests.`;
|
|
652
734
|
return paymentRequiredResponse({
|
|
653
735
|
error: 'payment_required',
|
|
654
736
|
message: 'Access requires a paid API key. A key has been generated for you below. Send a USDC payment on Solana with this key as the memo to activate it, then retry your request with the X-Agent-Key header.',
|
|
655
737
|
your_key: newKey,
|
|
656
|
-
payment: {
|
|
657
|
-
chain: 'solana',
|
|
658
|
-
network: debug ? 'devnet' : 'mainnet-beta',
|
|
659
|
-
token: 'USDC',
|
|
660
|
-
amount: String(minPayment),
|
|
661
|
-
wallet_address: walletAddress,
|
|
662
|
-
memo: newKey,
|
|
663
|
-
instructions: `Send ${minPayment} USDC on Solana ${debug ? 'devnet' : 'mainnet'} to ${walletAddress} with memo "${newKey}". Then include the header X-Agent-Key: ${newKey} on all subsequent requests.`,
|
|
664
|
-
},
|
|
738
|
+
payment: buildPaymentField({ network, minPayment, walletAddress, memo: newKey, feeInfo, instructions: noKeyInstructions }),
|
|
665
739
|
}, { walletAddress, mint: usdcMint, minPayment, debug, agentKey: newKey, resource: url.pathname });
|
|
666
740
|
}
|
|
667
741
|
|
|
@@ -708,24 +782,17 @@ export function createEdgeGate(options = {}) {
|
|
|
708
782
|
error: 'payment_required',
|
|
709
783
|
message: 'Key is valid but payment has not been verified on-chain yet. Please send the USDC payment and allow a few moments for confirmation.',
|
|
710
784
|
your_key: agentKey,
|
|
711
|
-
payment: {
|
|
785
|
+
payment: buildPaymentField({ network, minPayment, walletAddress, memo: agentKey, feeInfo }),
|
|
712
786
|
}, { walletAddress, mint: usdcMint, minPayment, debug, agentKey, resource: url.pathname });
|
|
713
787
|
}
|
|
714
|
-
const paid = await verifyPaymentOnChain(agentKey, walletAddress, rpcUrls, usdcMint);
|
|
788
|
+
const paid = await verifyPaymentOnChain(agentKey, walletAddress, rpcUrls, usdcMint, minPayment, feeInfo);
|
|
715
789
|
await store.setCachedPayment(agentKey, paid, paid ? PAYMENT_CACHE_TTL : NEGATIVE_CACHE_TTL_MS);
|
|
716
790
|
if (!paid) {
|
|
717
791
|
return paymentRequiredResponse({
|
|
718
792
|
error: 'payment_required',
|
|
719
793
|
message: 'Key is valid but payment has not been verified on-chain yet. Please send the USDC payment and allow a few moments for confirmation.',
|
|
720
794
|
your_key: agentKey,
|
|
721
|
-
payment: {
|
|
722
|
-
chain: 'solana',
|
|
723
|
-
network: debug ? 'devnet' : 'mainnet-beta',
|
|
724
|
-
token: 'USDC',
|
|
725
|
-
amount: String(minPayment),
|
|
726
|
-
wallet_address: walletAddress,
|
|
727
|
-
memo: agentKey,
|
|
728
|
-
},
|
|
795
|
+
payment: buildPaymentField({ network, minPayment, walletAddress, memo: agentKey, feeInfo }),
|
|
729
796
|
}, { walletAddress, mint: usdcMint, minPayment, debug, agentKey, resource: url.pathname });
|
|
730
797
|
}
|
|
731
798
|
|
package/package.json
CHANGED