local402-facilitator 1.0.0 → 1.0.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 +44 -44
- package/dist/config.d.ts +5 -0
- package/dist/config.js +4 -0
- package/dist/facilitator.js +39 -6
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
# local402-facilitator
|
|
2
|
-
|
|
3
|
-
An [x402](https://x402.org) facilitator for Stellar that you run yourself. It verifies and settles
|
|
4
|
-
`exact` payments (USDC and any Stellar token) and `exact-fx` payments (the payer spends XLM or EURC, the
|
|
5
|
-
seller receives exact USDC through the FxPay contract), and pays the network fees from its own accounts.
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npx local402-facilitator keys # two fresh fee-paying testnet accounts, funded, written to .env
|
|
9
|
-
npx local402-facilitator check # config, RPC, balances and FxPay: exits 0 when it can settle
|
|
10
|
-
npx local402-facilitator # listens on :4022
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Then point a seller at it: `local402Server("http://localhost:4022")` in `local402-server`, or any x402
|
|
14
|
-
seller that takes a facilitator URL.
|
|
15
|
-
|
|
16
|
-
Node 20.12 or later. A `Dockerfile` that installs this package from npm is in the
|
|
17
|
-
[repository](https://github.com/DiegoPoveda01/local402/blob/main/packages/facilitator/Dockerfile).
|
|
18
|
-
|
|
19
|
-
## What a public instance needs
|
|
20
|
-
|
|
21
|
-
- **A seller allowlist and a minimum amount.** Every settlement spends this instance's XLM.
|
|
22
|
-
`PAY_TO_ALLOWLIST` limits it to the sellers you name; on `stellar:pubnet` it refuses to start without one.
|
|
23
|
-
- **`TRUST_PROXY` set to the proxies really in front of it** (0 by default), so the limits count real clients.
|
|
24
|
-
- **Separate limits for verify and settle**, per client per minute: verify only simulates, settle spends fees.
|
|
25
|
-
- **Its own fee-paying keys.** Two instances sharing an account collide on its sequence number.
|
|
26
|
-
- **`/health` for liveness and `/ready` for readiness.** `/ready` answers 503, saying which check failed,
|
|
27
|
-
when the RPC is down, a fee payer is below `MIN_FEE_BALANCE_XLM`, or FxPay is gone from the ledger.
|
|
28
|
-
|
|
29
|
-
Every variable, the endpoints and the mainnet configuration:
|
|
30
|
-
[docs/facilitator.md](https://github.com/DiegoPoveda01/local402/blob/main/docs/facilitator.md).
|
|
31
|
-
Keys, funding, top-ups and rotation:
|
|
32
|
-
[docs/facilitator-runbook.md](https://github.com/DiegoPoveda01/local402/blob/main/docs/facilitator-runbook.md).
|
|
33
|
-
|
|
34
|
-
## As a library
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import { createFacilitator, facilitatorConfig } from "local402-facilitator";
|
|
38
|
-
|
|
39
|
-
const config = facilitatorConfig(process.env); // throws INVALID_CONFIG naming the variable, never a key
|
|
40
|
-
const { app } = createFacilitator(config); // an Express app
|
|
41
|
-
app.listen(config.port);
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
MIT
|
|
1
|
+
# local402-facilitator
|
|
2
|
+
|
|
3
|
+
An [x402](https://x402.org) facilitator for Stellar that you run yourself. It verifies and settles
|
|
4
|
+
`exact` payments (USDC and any Stellar token) and `exact-fx` payments (the payer spends XLM or EURC, the
|
|
5
|
+
seller receives exact USDC through the FxPay contract), and pays the network fees from its own accounts.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx local402-facilitator keys # two fresh fee-paying testnet accounts, funded, written to .env
|
|
9
|
+
npx local402-facilitator check # config, RPC, balances and FxPay: exits 0 when it can settle
|
|
10
|
+
npx local402-facilitator # listens on :4022
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then point a seller at it: `local402Server("http://localhost:4022")` in `local402-server`, or any x402
|
|
14
|
+
seller that takes a facilitator URL.
|
|
15
|
+
|
|
16
|
+
Node 20.12 or later. A `Dockerfile` that installs this package from npm is in the
|
|
17
|
+
[repository](https://github.com/DiegoPoveda01/local402/blob/main/packages/facilitator/Dockerfile).
|
|
18
|
+
|
|
19
|
+
## What a public instance needs
|
|
20
|
+
|
|
21
|
+
- **A seller allowlist and a minimum amount.** Every settlement spends this instance's XLM.
|
|
22
|
+
`PAY_TO_ALLOWLIST` limits it to the sellers you name; on `stellar:pubnet` it refuses to start without one.
|
|
23
|
+
- **`TRUST_PROXY` set to the proxies really in front of it** (0 by default), so the limits count real clients.
|
|
24
|
+
- **Separate limits for verify and settle**, per client per minute: verify only simulates, settle spends fees.
|
|
25
|
+
- **Its own fee-paying keys.** Two instances sharing an account collide on its sequence number.
|
|
26
|
+
- **`/health` for liveness and `/ready` for readiness.** `/ready` answers 503, saying which check failed,
|
|
27
|
+
when the RPC is down, a fee payer is below `MIN_FEE_BALANCE_XLM`, or FxPay is gone from the ledger.
|
|
28
|
+
|
|
29
|
+
Every variable, the endpoints and the mainnet configuration:
|
|
30
|
+
[docs/facilitator.md](https://github.com/DiegoPoveda01/local402/blob/main/docs/facilitator.md).
|
|
31
|
+
Keys, funding, top-ups and rotation:
|
|
32
|
+
[docs/facilitator-runbook.md](https://github.com/DiegoPoveda01/local402/blob/main/docs/facilitator-runbook.md).
|
|
33
|
+
|
|
34
|
+
## As a library
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { createFacilitator, facilitatorConfig } from "local402-facilitator";
|
|
38
|
+
|
|
39
|
+
const config = facilitatorConfig(process.env); // throws INVALID_CONFIG naming the variable, never a key
|
|
40
|
+
const { app } = createFacilitator(config); // an Express app
|
|
41
|
+
app.listen(config.port);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
MIT
|
package/dist/config.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface FacilitatorConfig {
|
|
|
23
23
|
minFeeBalanceXlm: number;
|
|
24
24
|
/** Proxies in front of the instance whose `X-Forwarded-For` it believes, for per-client limits. */
|
|
25
25
|
trustProxy: number;
|
|
26
|
+
/** Upstash Redis over REST, where the Bazaar catalog survives restarts. Without it the catalog lives in memory. */
|
|
27
|
+
catalogRedis?: {
|
|
28
|
+
url: string;
|
|
29
|
+
token: string;
|
|
30
|
+
};
|
|
26
31
|
}
|
|
27
32
|
type Env = Record<string, string | undefined>;
|
|
28
33
|
/**
|
package/dist/config.js
CHANGED
|
@@ -84,6 +84,9 @@ export function facilitatorConfig(env = process.env) {
|
|
|
84
84
|
const minFeeBalanceXlm = Number(env.MIN_FEE_BALANCE_XLM?.trim() || "2");
|
|
85
85
|
if (!Number.isFinite(minFeeBalanceXlm) || minFeeBalanceXlm < 0)
|
|
86
86
|
invalid(`MIN_FEE_BALANCE_XLM must be a number of XLM, got ${shown(env.MIN_FEE_BALANCE_XLM ?? "")}.`);
|
|
87
|
+
// Vercel's Upstash integration names them KV_REST_API_*; Upstash itself, UPSTASH_REDIS_REST_*.
|
|
88
|
+
const redisUrl = env.KV_REST_API_URL?.trim() || env.UPSTASH_REDIS_REST_URL?.trim();
|
|
89
|
+
const redisToken = env.KV_REST_API_TOKEN?.trim() || env.UPSTASH_REDIS_REST_TOKEN?.trim();
|
|
87
90
|
return {
|
|
88
91
|
network,
|
|
89
92
|
secrets,
|
|
@@ -97,5 +100,6 @@ export function facilitatorConfig(env = process.env) {
|
|
|
97
100
|
limits: { verify: whole(env, "VERIFY_RATE_LIMIT", 60, 1), settle: whole(env, "SETTLE_RATE_LIMIT", 20, 1) },
|
|
98
101
|
minFeeBalanceXlm,
|
|
99
102
|
trustProxy: whole(env, "TRUST_PROXY", 0, 0, 10),
|
|
103
|
+
catalogRedis: redisUrl && redisToken ? { url: redisUrl, token: redisToken } : undefined,
|
|
100
104
|
};
|
|
101
105
|
}
|
package/dist/facilitator.js
CHANGED
|
@@ -69,8 +69,22 @@ export function refusal(body, config) {
|
|
|
69
69
|
export function createFacilitator(config, options = {}) {
|
|
70
70
|
const { network } = config;
|
|
71
71
|
const rpcConfig = config.rpcUrl ? { url: config.rpcUrl } : undefined;
|
|
72
|
-
// Bazaar catalog: every resource that settles here and declares discovery info, keyed by URL.
|
|
72
|
+
// Bazaar catalog: every resource that settles here and declares discovery info, keyed by URL. Serverless instances
|
|
73
|
+
// start empty, so with Redis configured the catalog lives there; memory is the fallback.
|
|
73
74
|
const catalog = new Map();
|
|
75
|
+
const catalogKey = `local402:bazaar:${network}`;
|
|
76
|
+
const redisConfig = config.catalogRedis;
|
|
77
|
+
/** One command over Upstash's REST API. */
|
|
78
|
+
const redis = async (...command) => {
|
|
79
|
+
const response = await fetch(redisConfig.url, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { Authorization: `Bearer ${redisConfig.token}` },
|
|
82
|
+
body: JSON.stringify(command),
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
throw new Error(`Redis ${response.status}`);
|
|
86
|
+
return (await response.json()).result;
|
|
87
|
+
};
|
|
74
88
|
// Several fee-paying accounts ("channels") let settlements run in parallel without sequence number clashes.
|
|
75
89
|
const signers = config.secrets.map((secret) => createEd25519Signer(secret, network));
|
|
76
90
|
const feePayers = signers.map((signer) => signer.address);
|
|
@@ -88,15 +102,25 @@ export function createFacilitator(config, options = {}) {
|
|
|
88
102
|
if (!discovered)
|
|
89
103
|
return;
|
|
90
104
|
const { resourceUrl, x402Version, discoveryInfo, ...metadata } = discovered;
|
|
91
|
-
|
|
92
|
-
catalog.
|
|
105
|
+
// Another instance may have stored the route's other scheme; keep it.
|
|
106
|
+
let previous = catalog.get(resourceUrl);
|
|
107
|
+
if (!previous && redisConfig) {
|
|
108
|
+
previous = await redis("HGET", catalogKey, resourceUrl).then((value) => (typeof value === "string" ? JSON.parse(value) : undefined), () => undefined);
|
|
109
|
+
}
|
|
110
|
+
const accepts = (previous?.accepts ?? []).filter((a) => a.scheme !== requirements.scheme);
|
|
111
|
+
const entry = {
|
|
93
112
|
...metadata,
|
|
94
113
|
resource: resourceUrl,
|
|
95
114
|
type: discoveryInfo.input.type,
|
|
96
115
|
x402Version,
|
|
97
116
|
accepts: [...accepts, requirements],
|
|
98
117
|
lastUpdated: new Date().toISOString(),
|
|
99
|
-
}
|
|
118
|
+
};
|
|
119
|
+
catalog.set(resourceUrl, entry);
|
|
120
|
+
// The payment has settled by now: a Redis failure only costs the catalog entry, never the result.
|
|
121
|
+
if (redisConfig) {
|
|
122
|
+
await redis("HSET", catalogKey, resourceUrl, JSON.stringify(entry)).catch(() => console.warn("bazaar: could not save the catalog entry to Redis"));
|
|
123
|
+
}
|
|
100
124
|
});
|
|
101
125
|
// Verification simulates on RPC and settlement spends fees, so both are bounded, each with its own cap.
|
|
102
126
|
const limiter = new RateLimiter(config.limits);
|
|
@@ -123,8 +147,17 @@ export function createFacilitator(config, options = {}) {
|
|
|
123
147
|
app.get("/supported", (_req, res) => {
|
|
124
148
|
res.json(facilitator.getSupported());
|
|
125
149
|
});
|
|
126
|
-
app.get("/discovery/resources", (req, res) => {
|
|
127
|
-
|
|
150
|
+
app.get("/discovery/resources", async (req, res) => {
|
|
151
|
+
let all = [...catalog.values()];
|
|
152
|
+
if (redisConfig) {
|
|
153
|
+
try {
|
|
154
|
+
all = (await redis("HVALS", catalogKey)).map((value) => JSON.parse(value));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
console.warn("bazaar: could not read the catalog from Redis, serving this instance's copy");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const items = all.filter((item) => !req.query.type || item.type === req.query.type);
|
|
128
161
|
const limit = Number(req.query.limit ?? 100);
|
|
129
162
|
const offset = Number(req.query.offset ?? 0);
|
|
130
163
|
res.json({ x402Version: 2, items: items.slice(offset, offset + limit), pagination: { limit, offset, total: items.length } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local402-facilitator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Deployable x402 facilitator for Stellar: settles exact and exact-fx, with a seller allowlist, separate verify and settle limits, health and readiness",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"x402",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"node": ">=20"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"local402-fx": "^1.0.
|
|
39
|
-
"@stellar/stellar-sdk": "16.
|
|
38
|
+
"local402-fx": "^1.0.1",
|
|
39
|
+
"@stellar/stellar-sdk": "16.4.1",
|
|
40
40
|
"@x402/core": "2.25.0",
|
|
41
41
|
"@x402/extensions": "2.25.0",
|
|
42
42
|
"@x402/stellar": "2.25.0",
|