@azeth/provider 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/agreement-cache.d.ts +20 -0
- package/dist/agreement-cache.d.ts.map +1 -0
- package/dist/agreement-cache.js +133 -0
- package/dist/agreement-cache.js.map +1 -0
- package/dist/agreement-keeper.d.ts +43 -0
- package/dist/agreement-keeper.d.ts.map +1 -0
- package/dist/agreement-keeper.js +102 -0
- package/dist/agreement-keeper.js.map +1 -0
- package/dist/examples/index.d.ts +4 -0
- package/dist/examples/index.d.ts.map +1 -0
- package/dist/examples/index.js +3 -0
- package/dist/examples/index.js.map +1 -0
- package/dist/examples/price-feed.d.ts +22 -0
- package/dist/examples/price-feed.d.ts.map +1 -0
- package/dist/examples/price-feed.js +148 -0
- package/dist/examples/price-feed.js.map +1 -0
- package/dist/examples/pricing-routes.d.ts +15 -0
- package/dist/examples/pricing-routes.d.ts.map +1 -0
- package/dist/examples/pricing-routes.js +77 -0
- package/dist/examples/pricing-routes.js.map +1 -0
- package/dist/extensions/payment-agreement.d.ts +37 -0
- package/dist/extensions/payment-agreement.d.ts.map +1 -0
- package/dist/extensions/payment-agreement.js +39 -0
- package/dist/extensions/payment-agreement.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/pre-settled.d.ts +29 -0
- package/dist/middleware/pre-settled.d.ts.map +1 -0
- package/dist/middleware/pre-settled.js +80 -0
- package/dist/middleware/pre-settled.js.map +1 -0
- package/dist/stack.d.ts +75 -0
- package/dist/stack.d.ts.map +1 -0
- package/dist/stack.js +169 -0
- package/dist/stack.js.map +1 -0
- package/dist/storage.d.ts +48 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +96 -0
- package/dist/storage.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Azeth.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @azeth/provider
|
|
2
|
+
|
|
3
|
+
x402 service provider tooling for [Azeth](https://azeth.ai). Gate your Hono endpoints behind x402 payments with built-in payment-agreement support, SIWx sessions, and automatic reputation feedback.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @azeth/provider
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @azeth/provider
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Hono } from 'hono';
|
|
17
|
+
import { createX402StackFromEnv } from '@azeth/provider';
|
|
18
|
+
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
|
|
21
|
+
const { middleware, facilitator } = await createX402StackFromEnv({
|
|
22
|
+
app,
|
|
23
|
+
routes: {
|
|
24
|
+
'/api/data': { price: '$0.01', resource: 'https://api.example.com/data' },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.use('/api/*', middleware);
|
|
29
|
+
|
|
30
|
+
app.get('/api/data', (c) => c.json({ answer: 42 }));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **x402 Payment Middleware** -- Returns 402 with payment requirements, validates on-chain USDC settlement
|
|
36
|
+
- **Payment Agreements** -- Recurring subscriptions via on-chain `PaymentAgreementModule`
|
|
37
|
+
- **SIWx Sessions** -- Agreement-aware session storage for repeat customers
|
|
38
|
+
- **Agreement Keeper** -- Periodic execution of due payment agreements
|
|
39
|
+
- **Pre-Settled Payments** -- Middleware for endpoints accepting pre-settled x402 proofs
|
|
40
|
+
- **Local Facilitator** -- On-chain payment verification without external facilitator dependency
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### Stack Setup
|
|
45
|
+
|
|
46
|
+
| Export | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `createX402Stack(config)` | Create x402 middleware + facilitator from explicit config |
|
|
49
|
+
| `createX402StackFromEnv(options)` | Create from environment variables |
|
|
50
|
+
| `LocalFacilitatorClient` | On-chain USDC settlement verification |
|
|
51
|
+
|
|
52
|
+
### Payment Agreements
|
|
53
|
+
|
|
54
|
+
| Export | Description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `createPaymentAgreementExtension()` | x402 extension for agreement-based payments |
|
|
57
|
+
| `AzethSIWxStorage` | Agreement-aware SIWx session storage |
|
|
58
|
+
| `AgreementKeeper` | Periodic execution of due agreements |
|
|
59
|
+
| `findActiveAgreementForPayee()` | LRU-cached agreement lookup |
|
|
60
|
+
|
|
61
|
+
### Middleware
|
|
62
|
+
|
|
63
|
+
| Export | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `preSettledPaymentMiddleware` | Accept pre-settled x402 payment proofs |
|
|
66
|
+
| `paymentMiddlewareFromHTTPServer` | Standard x402 HTTP resource server middleware |
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PublicClient, Chain, Transport } from 'viem';
|
|
2
|
+
import type { PaymentAgreement } from '@azeth/common';
|
|
3
|
+
/** Set the cache TTL (useful for tests) */
|
|
4
|
+
export declare function setAgreementCacheTtl(ttlMs: number): void;
|
|
5
|
+
/** Clear the agreement cache */
|
|
6
|
+
export declare function clearAgreementCache(): void;
|
|
7
|
+
/** Find an active, executable agreement from a given account to a specific payee.
|
|
8
|
+
* Uses getAgreementData for combined agreement + executability + count in 1 RPC call.
|
|
9
|
+
* Iterates from newest to oldest (newest more likely active).
|
|
10
|
+
*
|
|
11
|
+
* @param publicClient - viem public client for on-chain reads
|
|
12
|
+
* @param moduleAddress - PaymentAgreementModule contract address
|
|
13
|
+
* @param account - The payer's smart account address
|
|
14
|
+
* @param payee - The payee address to match
|
|
15
|
+
* @param token - Optional token address to match
|
|
16
|
+
* @param minAmount - Optional minimum amount per interval
|
|
17
|
+
* @returns The first matching active + executable agreement, or null
|
|
18
|
+
*/
|
|
19
|
+
export declare function findActiveAgreementForPayee(publicClient: PublicClient<Transport, Chain>, moduleAddress: `0x${string}`, account: `0x${string}`, payee: `0x${string}`, token?: `0x${string}`, minAmount?: bigint): Promise<PaymentAgreement | null>;
|
|
20
|
+
//# sourceMappingURL=agreement-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement-cache.d.ts","sourceRoot":"","sources":["../src/agreement-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAkBtD,2CAA2C;AAC3C,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,gCAAgC;AAChC,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAwHD;;;;;;;;;;;GAWG;AACH,wBAAsB,2BAA2B,CAC/C,YAAY,EAAE,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,EAC5C,aAAa,EAAE,KAAK,MAAM,EAAE,EAC5B,OAAO,EAAE,KAAK,MAAM,EAAE,EACtB,KAAK,EAAE,KAAK,MAAM,EAAE,EACpB,KAAK,CAAC,EAAE,KAAK,MAAM,EAAE,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAuBlC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { PaymentAgreementModuleAbi } from '@azeth/common/abis';
|
|
2
|
+
const _agreementCache = new Map();
|
|
3
|
+
let _cacheTtlMs = 60_000; // 60 seconds default
|
|
4
|
+
const MAX_CACHE_SIZE = 5_000;
|
|
5
|
+
/** Set the cache TTL (useful for tests) */
|
|
6
|
+
export function setAgreementCacheTtl(ttlMs) {
|
|
7
|
+
_cacheTtlMs = ttlMs;
|
|
8
|
+
}
|
|
9
|
+
/** Clear the agreement cache */
|
|
10
|
+
export function clearAgreementCache() {
|
|
11
|
+
_agreementCache.clear();
|
|
12
|
+
}
|
|
13
|
+
/** Get a cached agreement with executability, fetching from chain if stale or missing.
|
|
14
|
+
* Uses getAgreementData for a single RPC call instead of 3 separate calls.
|
|
15
|
+
* Returns stale cache on RPC error (stale-on-error pattern). */
|
|
16
|
+
async function getCachedAgreement(publicClient, moduleAddress, account, agreementId) {
|
|
17
|
+
const key = `${account.toLowerCase()}:${agreementId.toString()}`;
|
|
18
|
+
const cached = _agreementCache.get(key);
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (cached && (now - cached.fetchedAt) < _cacheTtlMs) {
|
|
21
|
+
cached.lastAccessedAt = now;
|
|
22
|
+
return cached;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = await publicClient.readContract({
|
|
26
|
+
address: moduleAddress,
|
|
27
|
+
abi: PaymentAgreementModuleAbi,
|
|
28
|
+
functionName: 'getAgreementData',
|
|
29
|
+
args: [account, agreementId],
|
|
30
|
+
});
|
|
31
|
+
const agreement = {
|
|
32
|
+
id: agreementId,
|
|
33
|
+
payee: result[0].payee,
|
|
34
|
+
token: result[0].token,
|
|
35
|
+
amount: result[0].amount,
|
|
36
|
+
interval: result[0].interval,
|
|
37
|
+
endTime: result[0].endTime,
|
|
38
|
+
lastExecuted: result[0].lastExecuted,
|
|
39
|
+
maxExecutions: result[0].maxExecutions,
|
|
40
|
+
executionCount: result[0].executionCount,
|
|
41
|
+
totalCap: result[0].totalCap,
|
|
42
|
+
totalPaid: result[0].totalPaid,
|
|
43
|
+
active: result[0].active,
|
|
44
|
+
};
|
|
45
|
+
const entry = {
|
|
46
|
+
agreement,
|
|
47
|
+
executable: result[1],
|
|
48
|
+
reason: result[2],
|
|
49
|
+
isDue: result[3],
|
|
50
|
+
nextExecutionTime: result[4],
|
|
51
|
+
count: result[5],
|
|
52
|
+
fetchedAt: now,
|
|
53
|
+
lastAccessedAt: now,
|
|
54
|
+
};
|
|
55
|
+
// LRU eviction if at capacity
|
|
56
|
+
if (_agreementCache.size >= MAX_CACHE_SIZE && !_agreementCache.has(key)) {
|
|
57
|
+
let lruKey;
|
|
58
|
+
let lruTime = Infinity;
|
|
59
|
+
for (const [k, v] of _agreementCache) {
|
|
60
|
+
if (v.lastAccessedAt < lruTime) {
|
|
61
|
+
lruTime = v.lastAccessedAt;
|
|
62
|
+
lruKey = k;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (lruKey !== undefined)
|
|
66
|
+
_agreementCache.delete(lruKey);
|
|
67
|
+
}
|
|
68
|
+
_agreementCache.set(key, entry);
|
|
69
|
+
return entry;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Stale-on-error: return stale cache if available (optimistic grant)
|
|
73
|
+
return cached ?? null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Check if an agreement is currently active and valid (metadata only — no balance check) */
|
|
77
|
+
function isAgreementValid(agreement, payee, token, minAmount) {
|
|
78
|
+
if (!agreement.active)
|
|
79
|
+
return false;
|
|
80
|
+
if (agreement.payee.toLowerCase() !== payee.toLowerCase())
|
|
81
|
+
return false;
|
|
82
|
+
if (token && agreement.token.toLowerCase() !== token.toLowerCase())
|
|
83
|
+
return false;
|
|
84
|
+
if (minAmount && agreement.amount < minAmount)
|
|
85
|
+
return false;
|
|
86
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
87
|
+
// Check expiry (0 = no expiry)
|
|
88
|
+
if (agreement.endTime !== 0n && agreement.endTime <= now)
|
|
89
|
+
return false;
|
|
90
|
+
// Check max executions (0 = unlimited)
|
|
91
|
+
if (agreement.maxExecutions !== 0n && agreement.executionCount >= agreement.maxExecutions)
|
|
92
|
+
return false;
|
|
93
|
+
// Check total cap (0 = unlimited) — defense-in-depth alongside contract's executable flag
|
|
94
|
+
if (agreement.totalCap !== 0n && agreement.totalPaid >= agreement.totalCap)
|
|
95
|
+
return false;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/** Find an active, executable agreement from a given account to a specific payee.
|
|
99
|
+
* Uses getAgreementData for combined agreement + executability + count in 1 RPC call.
|
|
100
|
+
* Iterates from newest to oldest (newest more likely active).
|
|
101
|
+
*
|
|
102
|
+
* @param publicClient - viem public client for on-chain reads
|
|
103
|
+
* @param moduleAddress - PaymentAgreementModule contract address
|
|
104
|
+
* @param account - The payer's smart account address
|
|
105
|
+
* @param payee - The payee address to match
|
|
106
|
+
* @param token - Optional token address to match
|
|
107
|
+
* @param minAmount - Optional minimum amount per interval
|
|
108
|
+
* @returns The first matching active + executable agreement, or null
|
|
109
|
+
*/
|
|
110
|
+
export async function findActiveAgreementForPayee(publicClient, moduleAddress, account, payee, token, minAmount) {
|
|
111
|
+
// Get count from first getAgreementData call (avoids separate getAgreementCount RPC)
|
|
112
|
+
// Start with agreementId=0 to get the count, then iterate from newest
|
|
113
|
+
const first = await getCachedAgreement(publicClient, moduleAddress, account, 0n);
|
|
114
|
+
if (!first)
|
|
115
|
+
return null;
|
|
116
|
+
const count = first.count;
|
|
117
|
+
if (count === 0n)
|
|
118
|
+
return null;
|
|
119
|
+
// Iterate from newest to oldest
|
|
120
|
+
for (let i = count - 1n; i >= 0n; i--) {
|
|
121
|
+
const cached = await getCachedAgreement(publicClient, moduleAddress, account, i);
|
|
122
|
+
if (!cached)
|
|
123
|
+
continue;
|
|
124
|
+
if (!isAgreementValid(cached.agreement, payee, token, minAmount))
|
|
125
|
+
continue;
|
|
126
|
+
// Check executability (balance + guardian checks) — already in cache, no extra RPC
|
|
127
|
+
if (!cached.executable)
|
|
128
|
+
continue;
|
|
129
|
+
return cached.agreement;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=agreement-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement-cache.js","sourceRoot":"","sources":["../src/agreement-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PublicClient, WalletClient, Chain, Transport, Account } from 'viem';
|
|
2
|
+
/** Configuration for the AgreementKeeper */
|
|
3
|
+
export interface AgreementKeeperConfig {
|
|
4
|
+
/** Public client for on-chain reads */
|
|
5
|
+
publicClient: PublicClient<Transport, Chain>;
|
|
6
|
+
/** Wallet client for gas settlement (facilitator wallet) */
|
|
7
|
+
walletClient: WalletClient<Transport, Chain, Account>;
|
|
8
|
+
/** PaymentAgreementModule contract address */
|
|
9
|
+
moduleAddress: `0x${string}`;
|
|
10
|
+
/** How often to scan for due agreements (ms). Default: 60_000 */
|
|
11
|
+
scanIntervalMs?: number;
|
|
12
|
+
/** Maximum executions per scan cycle. Default: 20 */
|
|
13
|
+
maxExecutionsPerScan?: number;
|
|
14
|
+
}
|
|
15
|
+
/** AgreementKeeper — executes due payment agreements so services receive recurring revenue.
|
|
16
|
+
*
|
|
17
|
+
* The keeper is notified when x402Storage.hasPaid() discovers an on-chain agreement.
|
|
18
|
+
* It periodically checks if tracked agreements are due for execution and calls
|
|
19
|
+
* executeAgreement() on the PaymentAgreementModule.
|
|
20
|
+
*
|
|
21
|
+
* executeAgreement() is permissionless — anyone can call it — so the facilitator
|
|
22
|
+
* wallet only pays gas, not the agreement amount.
|
|
23
|
+
*/
|
|
24
|
+
export declare class AgreementKeeper {
|
|
25
|
+
private timer;
|
|
26
|
+
private readonly trackedAgreements;
|
|
27
|
+
private readonly config;
|
|
28
|
+
private readonly scanIntervalMs;
|
|
29
|
+
private readonly maxExecutionsPerScan;
|
|
30
|
+
constructor(config: AgreementKeeperConfig);
|
|
31
|
+
/** Register an agreement for periodic execution.
|
|
32
|
+
* Called by x402Storage.hasPaid() when an agreement is discovered. */
|
|
33
|
+
trackAgreement(account: `0x${string}`, agreementId: bigint): void;
|
|
34
|
+
/** Number of tracked agreements */
|
|
35
|
+
get trackedCount(): number;
|
|
36
|
+
/** Start the periodic scan loop */
|
|
37
|
+
start(): void;
|
|
38
|
+
/** Stop the keeper (for graceful shutdown) */
|
|
39
|
+
stop(): void;
|
|
40
|
+
/** Run a single scan: check + execute due agreements */
|
|
41
|
+
scan(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=agreement-keeper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement-keeper.d.ts","sourceRoot":"","sources":["../src/agreement-keeper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAUlF,4CAA4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,uCAAuC;IACvC,YAAY,EAAE,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC7C,4DAA4D;IAC5D,YAAY,EAAE,YAAY,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACtD,8CAA8C;IAC9C,aAAa,EAAE,KAAK,MAAM,EAAE,CAAC;IAC7B,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAuC;IACzE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;gBAElC,MAAM,EAAE,qBAAqB;IAMzC;2EACuE;IACvE,cAAc,CAAC,OAAO,EAAE,KAAK,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAUjE,mCAAmC;IACnC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,mCAAmC;IACnC,KAAK,IAAI,IAAI;IAab,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAOZ,wDAAwD;IAClD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAqD5B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { PaymentAgreementModuleAbi } from '@azeth/common/abis';
|
|
2
|
+
/** AgreementKeeper — executes due payment agreements so services receive recurring revenue.
|
|
3
|
+
*
|
|
4
|
+
* The keeper is notified when x402Storage.hasPaid() discovers an on-chain agreement.
|
|
5
|
+
* It periodically checks if tracked agreements are due for execution and calls
|
|
6
|
+
* executeAgreement() on the PaymentAgreementModule.
|
|
7
|
+
*
|
|
8
|
+
* executeAgreement() is permissionless — anyone can call it — so the facilitator
|
|
9
|
+
* wallet only pays gas, not the agreement amount.
|
|
10
|
+
*/
|
|
11
|
+
export class AgreementKeeper {
|
|
12
|
+
timer = null;
|
|
13
|
+
trackedAgreements = new Map();
|
|
14
|
+
config;
|
|
15
|
+
scanIntervalMs;
|
|
16
|
+
maxExecutionsPerScan;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.scanIntervalMs = config.scanIntervalMs ?? 60_000;
|
|
20
|
+
this.maxExecutionsPerScan = config.maxExecutionsPerScan ?? 20;
|
|
21
|
+
}
|
|
22
|
+
/** Register an agreement for periodic execution.
|
|
23
|
+
* Called by x402Storage.hasPaid() when an agreement is discovered. */
|
|
24
|
+
trackAgreement(account, agreementId) {
|
|
25
|
+
const key = `${account.toLowerCase()}:${agreementId}`;
|
|
26
|
+
if (this.trackedAgreements.has(key))
|
|
27
|
+
return;
|
|
28
|
+
this.trackedAgreements.set(key, {
|
|
29
|
+
account,
|
|
30
|
+
agreementId,
|
|
31
|
+
lastChecked: 0,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Number of tracked agreements */
|
|
35
|
+
get trackedCount() {
|
|
36
|
+
return this.trackedAgreements.size;
|
|
37
|
+
}
|
|
38
|
+
/** Start the periodic scan loop */
|
|
39
|
+
start() {
|
|
40
|
+
if (this.timer)
|
|
41
|
+
return;
|
|
42
|
+
this.timer = setInterval(() => {
|
|
43
|
+
void this.scan().catch((err) => {
|
|
44
|
+
console.error('[AgreementKeeper] Scan error:', err instanceof Error ? err.message : err);
|
|
45
|
+
});
|
|
46
|
+
}, this.scanIntervalMs);
|
|
47
|
+
this.timer.unref();
|
|
48
|
+
}
|
|
49
|
+
/** Stop the keeper (for graceful shutdown) */
|
|
50
|
+
stop() {
|
|
51
|
+
if (this.timer) {
|
|
52
|
+
clearInterval(this.timer);
|
|
53
|
+
this.timer = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Run a single scan: check + execute due agreements */
|
|
57
|
+
async scan() {
|
|
58
|
+
let executed = 0;
|
|
59
|
+
const keysToRemove = [];
|
|
60
|
+
for (const [key, entry] of this.trackedAgreements) {
|
|
61
|
+
if (executed >= this.maxExecutionsPerScan)
|
|
62
|
+
break;
|
|
63
|
+
try {
|
|
64
|
+
// Check if the agreement is due for execution (view call, no gas)
|
|
65
|
+
const result = await this.config.publicClient.readContract({
|
|
66
|
+
address: this.config.moduleAddress,
|
|
67
|
+
abi: PaymentAgreementModuleAbi,
|
|
68
|
+
functionName: 'canExecutePayment',
|
|
69
|
+
args: [entry.account, entry.agreementId],
|
|
70
|
+
});
|
|
71
|
+
const [executable, reason] = result;
|
|
72
|
+
if (!executable) {
|
|
73
|
+
entry.lastChecked = Date.now();
|
|
74
|
+
// Remove completed/cancelled agreements from tracking to avoid unnecessary scans
|
|
75
|
+
if (reason.includes('not active') || reason.includes('max executions') || reason.includes('total cap')) {
|
|
76
|
+
keysToRemove.push(key);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Execute the agreement (state-changing, requires gas)
|
|
81
|
+
await this.config.walletClient.writeContract({
|
|
82
|
+
address: this.config.moduleAddress,
|
|
83
|
+
abi: PaymentAgreementModuleAbi,
|
|
84
|
+
functionName: 'executeAgreement',
|
|
85
|
+
args: [entry.account, entry.agreementId],
|
|
86
|
+
});
|
|
87
|
+
executed++;
|
|
88
|
+
entry.lastChecked = Date.now();
|
|
89
|
+
console.log(`[AgreementKeeper] Executed agreement ${entry.agreementId} for ${entry.account}`);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
// Individual failure is non-fatal — continue with other agreements
|
|
93
|
+
console.warn(`[AgreementKeeper] Failed to execute agreement ${entry.agreementId} for ${entry.account}:`, err instanceof Error ? err.message : err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Clean up completed/cancelled agreements after iteration
|
|
97
|
+
for (const key of keysToRemove) {
|
|
98
|
+
this.trackedAgreements.delete(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=agreement-keeper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement-keeper.js","sourceRoot":"","sources":["../src/agreement-keeper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/examples/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC5F,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/examples/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Supported CoinGecko coin identifiers */
|
|
2
|
+
declare const SUPPORTED_COINS: readonly ["bitcoin", "ethereum", "solana", "usd-coin", "chainlink", "aave", "uniswap", "maker", "compound-governance-token"];
|
|
3
|
+
export type SupportedCoinId = (typeof SUPPORTED_COINS)[number];
|
|
4
|
+
/** Price data returned by the service */
|
|
5
|
+
export interface PriceData {
|
|
6
|
+
coinId: string;
|
|
7
|
+
price: number;
|
|
8
|
+
currency: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
source: 'coingecko';
|
|
11
|
+
}
|
|
12
|
+
/** Check if a coin ID is in the supported whitelist */
|
|
13
|
+
export declare function isSupportedCoin(coinId: string): coinId is SupportedCoinId;
|
|
14
|
+
/** Get a price with caching. Returns cached data if within TTL, otherwise fetches fresh.
|
|
15
|
+
* On fetch failure, returns stale cache if available (stale-on-error). */
|
|
16
|
+
export declare function getPrice(coinId: SupportedCoinId, currency?: string): Promise<PriceData>;
|
|
17
|
+
/** Bypass cache and fetch fresh data. Rate-limited to 1 per coin per 10 seconds. */
|
|
18
|
+
export declare function getFreshPrice(coinId: SupportedCoinId, currency?: string): Promise<PriceData>;
|
|
19
|
+
/** Clear all cached data (for tests) */
|
|
20
|
+
export declare function clearPriceCache(): void;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=price-feed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price-feed.d.ts","sourceRoot":"","sources":["../../src/examples/price-feed.ts"],"names":[],"mappings":"AAEA,2CAA2C;AAC3C,QAAA,MAAM,eAAe,8HAUX,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;CACrB;AAeD,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,eAAe,CAEzE;AAuFD;2EAC2E;AAC3E,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,GAAE,MAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAuBpG;AAED,oFAAoF;AACpF,wBAAsB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,GAAE,MAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAiBzG;AAED,wCAAwC;AACxC,wBAAgB,eAAe,IAAI,IAAI,CAGtC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { AzethError } from '@azeth/common';
|
|
2
|
+
/** Supported CoinGecko coin identifiers */
|
|
3
|
+
const SUPPORTED_COINS = [
|
|
4
|
+
'bitcoin',
|
|
5
|
+
'ethereum',
|
|
6
|
+
'solana',
|
|
7
|
+
'usd-coin',
|
|
8
|
+
'chainlink',
|
|
9
|
+
'aave',
|
|
10
|
+
'uniswap',
|
|
11
|
+
'maker',
|
|
12
|
+
'compound-governance-token',
|
|
13
|
+
];
|
|
14
|
+
const CACHE_TTL_MS = 60_000; // 60 seconds
|
|
15
|
+
const MAX_CACHE_ENTRIES = 100;
|
|
16
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
17
|
+
const FORCE_REFRESH_COOLDOWN_MS = 10_000; // 1 per coin per 10s
|
|
18
|
+
const priceCache = new Map();
|
|
19
|
+
const lastForceRefresh = new Map();
|
|
20
|
+
/** Check if a coin ID is in the supported whitelist */
|
|
21
|
+
export function isSupportedCoin(coinId) {
|
|
22
|
+
return SUPPORTED_COINS.includes(coinId);
|
|
23
|
+
}
|
|
24
|
+
/** Build a cache key from coin ID and currency */
|
|
25
|
+
function cacheKey(coinId, currency) {
|
|
26
|
+
return `${coinId}:${currency}`;
|
|
27
|
+
}
|
|
28
|
+
/** Fetch a price from CoinGecko */
|
|
29
|
+
async function fetchFromCoinGecko(coinId, currency) {
|
|
30
|
+
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(coinId)}&vs_currencies=${encodeURIComponent(currency)}`;
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(url, {
|
|
34
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
39
|
+
throw new AzethError('CoinGecko request timed out', 'NETWORK_ERROR', {
|
|
40
|
+
coinId,
|
|
41
|
+
timeoutMs: FETCH_TIMEOUT_MS,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// TimeoutError from AbortSignal.timeout
|
|
45
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
46
|
+
throw new AzethError('CoinGecko request timed out', 'NETWORK_ERROR', {
|
|
47
|
+
coinId,
|
|
48
|
+
timeoutMs: FETCH_TIMEOUT_MS,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
throw new AzethError('CoinGecko request failed', 'NETWORK_ERROR', {
|
|
52
|
+
coinId,
|
|
53
|
+
detail: err instanceof Error ? err.message : 'Unknown error',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (response.status === 429) {
|
|
57
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
58
|
+
throw new AzethError('CoinGecko rate limit exceeded', 'NETWORK_ERROR', {
|
|
59
|
+
coinId,
|
|
60
|
+
retryAfterSeconds: retryAfter ? parseInt(retryAfter, 10) : 60,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new AzethError('CoinGecko returned error', 'NETWORK_ERROR', {
|
|
65
|
+
coinId,
|
|
66
|
+
status: response.status,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const json = (await response.json());
|
|
70
|
+
const coinData = json[coinId];
|
|
71
|
+
const price = coinData?.[currency];
|
|
72
|
+
if (typeof price !== 'number') {
|
|
73
|
+
throw new AzethError('Malformed CoinGecko response', 'NETWORK_ERROR', {
|
|
74
|
+
coinId,
|
|
75
|
+
detail: `Missing price for ${coinId} in ${currency}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
coinId,
|
|
80
|
+
price,
|
|
81
|
+
currency,
|
|
82
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
83
|
+
source: 'coingecko',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Evict the oldest cache entry if at capacity */
|
|
87
|
+
function evictIfNeeded() {
|
|
88
|
+
if (priceCache.size >= MAX_CACHE_ENTRIES) {
|
|
89
|
+
// Evict oldest entry
|
|
90
|
+
let oldestKey;
|
|
91
|
+
let oldestTime = Infinity;
|
|
92
|
+
for (const [k, v] of priceCache) {
|
|
93
|
+
if (v.fetchedAt < oldestTime) {
|
|
94
|
+
oldestTime = v.fetchedAt;
|
|
95
|
+
oldestKey = k;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (oldestKey !== undefined)
|
|
99
|
+
priceCache.delete(oldestKey);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Get a price with caching. Returns cached data if within TTL, otherwise fetches fresh.
|
|
103
|
+
* On fetch failure, returns stale cache if available (stale-on-error). */
|
|
104
|
+
export async function getPrice(coinId, currency = 'usd') {
|
|
105
|
+
const key = cacheKey(coinId, currency);
|
|
106
|
+
const cached = priceCache.get(key);
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
// Cache hit — within TTL
|
|
109
|
+
if (cached && (now - cached.fetchedAt) < CACHE_TTL_MS) {
|
|
110
|
+
return cached.data;
|
|
111
|
+
}
|
|
112
|
+
// Cache miss or stale — fetch fresh
|
|
113
|
+
try {
|
|
114
|
+
const data = await fetchFromCoinGecko(coinId, currency);
|
|
115
|
+
evictIfNeeded();
|
|
116
|
+
priceCache.set(key, { data, fetchedAt: now });
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// Stale-on-error: return stale cache if available
|
|
121
|
+
if (cached) {
|
|
122
|
+
return cached.data;
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Bypass cache and fetch fresh data. Rate-limited to 1 per coin per 10 seconds. */
|
|
128
|
+
export async function getFreshPrice(coinId, currency = 'usd') {
|
|
129
|
+
const key = cacheKey(coinId, currency);
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
// Rate limit: 1 fresh fetch per coin per 10s
|
|
132
|
+
const lastRefresh = lastForceRefresh.get(key);
|
|
133
|
+
if (lastRefresh && (now - lastRefresh) < FORCE_REFRESH_COOLDOWN_MS) {
|
|
134
|
+
// Fall back to cached or regular fetch
|
|
135
|
+
return getPrice(coinId, currency);
|
|
136
|
+
}
|
|
137
|
+
lastForceRefresh.set(key, now);
|
|
138
|
+
const data = await fetchFromCoinGecko(coinId, currency);
|
|
139
|
+
evictIfNeeded();
|
|
140
|
+
priceCache.set(key, { data, fetchedAt: now });
|
|
141
|
+
return data;
|
|
142
|
+
}
|
|
143
|
+
/** Clear all cached data (for tests) */
|
|
144
|
+
export function clearPriceCache() {
|
|
145
|
+
priceCache.clear();
|
|
146
|
+
lastForceRefresh.clear();
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=price-feed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price-feed.js","sourceRoot":"","sources":["../../src/examples/price-feed.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { x402HTTPResourceServer } from '@x402/core/server';
|
|
3
|
+
import type { ProviderEnv } from '../middleware/pre-settled.js';
|
|
4
|
+
/** Create the x402-gated pricing routes.
|
|
5
|
+
*
|
|
6
|
+
* Uses @x402/hono middleware for V2 protocol support including:
|
|
7
|
+
* - PAYMENT-SIGNATURE / PAYMENT-RESPONSE headers (v2)
|
|
8
|
+
* - SIWx wallet-based sessions (pay once, then access via wallet sig)
|
|
9
|
+
* - Payment agreement extension (subscription terms in 402 response)
|
|
10
|
+
* - Pre-settled smart account payments (X-Payment-Tx header bypass)
|
|
11
|
+
*
|
|
12
|
+
* When httpServer is null, all routes return 503 (graceful degradation).
|
|
13
|
+
*/
|
|
14
|
+
export declare function createPricingRoutes(httpServer: x402HTTPResourceServer | null): Hono<ProviderEnv>;
|
|
15
|
+
//# sourceMappingURL=pricing-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing-routes.d.ts","sourceRoot":"","sources":["../../src/examples/pricing-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAIhE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,sBAAsB,GAAG,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAwEhG"}
|