x402-engineer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/AGENT.md +102 -0
  2. package/README.md +43 -0
  3. package/dist/cli.cjs +137 -0
  4. package/package.json +51 -0
  5. package/skills/stellar-dev/SKILL.md +146 -0
  6. package/skills/stellar-dev/advanced-patterns.md +188 -0
  7. package/skills/stellar-dev/api-rpc-horizon.md +521 -0
  8. package/skills/stellar-dev/common-pitfalls.md +510 -0
  9. package/skills/stellar-dev/contracts-soroban.md +565 -0
  10. package/skills/stellar-dev/ecosystem.md +430 -0
  11. package/skills/stellar-dev/frontend-stellar-sdk.md +651 -0
  12. package/skills/stellar-dev/resources.md +306 -0
  13. package/skills/stellar-dev/security.md +491 -0
  14. package/skills/stellar-dev/standards-reference.md +94 -0
  15. package/skills/stellar-dev/stellar-assets.md +419 -0
  16. package/skills/stellar-dev/testing.md +786 -0
  17. package/skills/stellar-dev/zk-proofs.md +136 -0
  18. package/skills/x402-add-paywall/SKILL.md +208 -0
  19. package/skills/x402-add-paywall/references/patterns.md +132 -0
  20. package/skills/x402-debug/SKILL.md +92 -0
  21. package/skills/x402-debug/references/checklist.md +146 -0
  22. package/skills/x402-explain/SKILL.md +136 -0
  23. package/skills/x402-init/SKILL.md +129 -0
  24. package/skills/x402-init/templates/env-example.md +17 -0
  25. package/skills/x402-init/templates/express/config.ts.md +29 -0
  26. package/skills/x402-init/templates/express/server.ts.md +30 -0
  27. package/skills/x402-init/templates/fastify/adapter.ts.md +66 -0
  28. package/skills/x402-init/templates/fastify/config.ts.md +29 -0
  29. package/skills/x402-init/templates/fastify/server.ts.md +90 -0
  30. package/skills/x402-init/templates/hono/config.ts.md +29 -0
  31. package/skills/x402-init/templates/hono/server.ts.md +31 -0
  32. package/skills/x402-init/templates/next-app-router/config.ts.md +29 -0
  33. package/skills/x402-init/templates/next-app-router/server.ts.md +31 -0
  34. package/skills/x402-stellar/SKILL.md +139 -0
  35. package/skills/x402-stellar/references/api.md +237 -0
  36. package/skills/x402-stellar/references/patterns.md +276 -0
  37. package/skills/x402-stellar/references/setup.md +138 -0
  38. package/skills/x402-stellar/scripts/check-deps.js +218 -0
@@ -0,0 +1,276 @@
1
+ # x402-stellar Code Patterns
2
+
3
+ These are **working, production-tested** patterns extracted from a deployed x402 POC. Use these exact patterns — do not improvise the import paths or class usage.
4
+
5
+ ## Server Pattern (Express + x402 Middleware)
6
+
7
+ ```typescript
8
+ import express from "express";
9
+ import { paymentMiddleware, x402ResourceServer } from "@x402/express";
10
+ import { ExactStellarScheme } from "@x402/stellar/exact/server";
11
+ import { HTTPFacilitatorClient } from "@x402/core/server";
12
+ import "dotenv/config";
13
+
14
+ // 1. Create the facilitator client with auth headers
15
+ const facilitatorClient = new HTTPFacilitatorClient({
16
+ url: process.env.FACILITATOR_URL || "https://channels.openzeppelin.com/x402/testnet",
17
+ createAuthHeaders: async () => {
18
+ const headers = { Authorization: `Bearer ${process.env.FACILITATOR_API_KEY}` };
19
+ return { verify: headers, settle: headers, supported: headers };
20
+ },
21
+ });
22
+
23
+ // 2. Create the resource server and register Stellar testnet
24
+ const resourceServer = new x402ResourceServer(facilitatorClient).register(
25
+ "stellar:testnet",
26
+ new ExactStellarScheme()
27
+ );
28
+
29
+ // 3. Create Express app
30
+ const app = express();
31
+ app.use(express.json());
32
+
33
+ // 4. Add payment middleware with route pricing
34
+ app.use(
35
+ paymentMiddleware(
36
+ {
37
+ "POST /api/weather": {
38
+ accepts: [
39
+ {
40
+ scheme: "exact",
41
+ price: "$0.001",
42
+ network: "stellar:testnet",
43
+ payTo: process.env.SERVER_STELLAR_ADDRESS!,
44
+ },
45
+ ],
46
+ description: "Get weather data for a city",
47
+ mimeType: "application/json",
48
+ },
49
+ },
50
+ resourceServer
51
+ )
52
+ );
53
+
54
+ // 5. Define the actual route handler (runs only after payment verified)
55
+ app.post("/api/weather", (req, res) => {
56
+ const { city } = req.body;
57
+ res.json({
58
+ city,
59
+ temperature: "22°C",
60
+ condition: "Sunny",
61
+ paid: true,
62
+ });
63
+ });
64
+
65
+ // 6. Start server
66
+ const PORT = process.env.PORT || 3000;
67
+ app.listen(PORT, () => {
68
+ console.log(`x402 server running on port ${PORT}`);
69
+ });
70
+ ```
71
+
72
+ ### Key Server Details
73
+
74
+ - `ExactStellarScheme` on the server takes **no arguments** — it only verifies payments.
75
+ - The `facilitatorClient` must return auth headers for `verify`, `settle`, and `supported`.
76
+ - Route keys in the middleware config must match the HTTP method and path exactly: `"POST /api/weather"`.
77
+ - The `price` field uses dollar-prefixed strings: `"$0.001"`, `"$0.01"`, `"$1.00"`.
78
+ - The `payTo` address must be a valid Stellar public key (G...) with a USDC trustline.
79
+
80
+ ## Client Pattern (x402 Payment Client)
81
+
82
+ ```typescript
83
+ import { createEd25519Signer } from "@x402/stellar";
84
+ import { ExactStellarScheme } from "@x402/stellar/exact/client";
85
+ import { x402Client, x402HTTPClient } from "@x402/core/client";
86
+ import "dotenv/config";
87
+
88
+ const SERVER_URL = process.env.SERVER_URL || "http://localhost:3000";
89
+
90
+ // 1. Create the signer from the client's secret key
91
+ const signer = createEd25519Signer(process.env.CLIENT_STELLAR_SECRET!);
92
+
93
+ // 2. Create the scheme and register it with the x402 client
94
+ const scheme = new ExactStellarScheme(signer);
95
+ const client = new x402Client().register("stellar:testnet", scheme);
96
+ const httpClient = new x402HTTPClient(client);
97
+
98
+ async function callPaidEndpoint() {
99
+ // 3. Make the initial request (will get 402)
100
+ const initialResponse = await fetch(`${SERVER_URL}/api/weather`, {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({ city: "Buenos Aires" }),
104
+ });
105
+
106
+ if (initialResponse.status !== 402) {
107
+ // Either succeeded without payment or got an error
108
+ console.log("Status:", initialResponse.status);
109
+ console.log("Body:", await initialResponse.text());
110
+ return;
111
+ }
112
+
113
+ // 4. Parse the 402 response to get payment requirements
114
+ const paymentRequired = httpClient.getPaymentRequiredResponse(
115
+ (name: string) => initialResponse.headers.get(name)
116
+ );
117
+
118
+ console.log("Payment required:", JSON.stringify(paymentRequired, null, 2));
119
+
120
+ // 5. Create the payment payload (signs the transaction)
121
+ const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
122
+
123
+ // 6. Encode the payment into headers
124
+ const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
125
+
126
+ // 7. Resend the request with payment headers
127
+ const paidResponse = await fetch(`${SERVER_URL}/api/weather`, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ ...paymentHeaders,
132
+ },
133
+ body: JSON.stringify({ city: "Buenos Aires" }),
134
+ });
135
+
136
+ console.log("Paid response status:", paidResponse.status);
137
+ console.log("Paid response body:", await paidResponse.json());
138
+ }
139
+
140
+ callPaidEndpoint().catch(console.error);
141
+ ```
142
+
143
+ ### Key Client Details
144
+
145
+ - `ExactStellarScheme` on the client takes the **signer** as an argument.
146
+ - Import from `@x402/stellar/exact/client` — NOT from `@x402/stellar/exact/server`.
147
+ - The `getPaymentRequiredResponse` method takes a header getter function.
148
+ - The `encodePaymentSignatureHeader` returns an object that can be spread into fetch headers.
149
+ - The client account must have USDC balance and a USDC trustline.
150
+
151
+ ## Multi-Endpoint Pricing Pattern
152
+
153
+ ```typescript
154
+ app.use(
155
+ paymentMiddleware(
156
+ {
157
+ "POST /api/weather": {
158
+ accepts: [
159
+ {
160
+ scheme: "exact",
161
+ price: "$0.001",
162
+ network: "stellar:testnet",
163
+ payTo: process.env.SERVER_STELLAR_ADDRESS!,
164
+ },
165
+ ],
166
+ description: "Get weather data",
167
+ mimeType: "application/json",
168
+ },
169
+ "GET /api/premium-data": {
170
+ accepts: [
171
+ {
172
+ scheme: "exact",
173
+ price: "$0.01",
174
+ network: "stellar:testnet",
175
+ payTo: process.env.SERVER_STELLAR_ADDRESS!,
176
+ },
177
+ ],
178
+ description: "Access premium dataset",
179
+ mimeType: "application/json",
180
+ },
181
+ "POST /api/ai-summary": {
182
+ accepts: [
183
+ {
184
+ scheme: "exact",
185
+ price: "$0.05",
186
+ network: "stellar:testnet",
187
+ payTo: process.env.SERVER_STELLAR_ADDRESS!,
188
+ },
189
+ ],
190
+ description: "AI-generated summary",
191
+ mimeType: "application/json",
192
+ },
193
+ },
194
+ resourceServer
195
+ )
196
+ );
197
+ ```
198
+
199
+ Different endpoints can have different prices. Unprotected routes are not listed in the middleware config and work normally without payment.
200
+
201
+ ## Error Handling Patterns
202
+
203
+ ### Server-Side Error Handling
204
+
205
+ ```typescript
206
+ // Wrap route handlers to catch errors after payment verification
207
+ app.post("/api/weather", async (req, res) => {
208
+ try {
209
+ const { city } = req.body;
210
+ if (!city) {
211
+ return res.status(400).json({ error: "City parameter required" });
212
+ }
213
+ // Process and respond
214
+ res.json({ city, temperature: "22°C" });
215
+ } catch (error) {
216
+ console.error("Handler error:", error);
217
+ res.status(500).json({ error: "Internal server error" });
218
+ }
219
+ });
220
+ ```
221
+
222
+ ### Client-Side Error Handling
223
+
224
+ ```typescript
225
+ async function callWithRetry(url: string, options: RequestInit, maxRetries = 2) {
226
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
227
+ try {
228
+ const response = await fetch(url, options);
229
+
230
+ if (response.status === 402) {
231
+ // Parse payment requirements and pay
232
+ const paymentRequired = httpClient.getPaymentRequiredResponse(
233
+ (name: string) => response.headers.get(name)
234
+ );
235
+ const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
236
+ const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
237
+
238
+ // Resend with payment
239
+ const paidResponse = await fetch(url, {
240
+ ...options,
241
+ headers: { ...options.headers, ...paymentHeaders },
242
+ });
243
+
244
+ if (!paidResponse.ok) {
245
+ throw new Error(`Payment accepted but request failed: ${paidResponse.status}`);
246
+ }
247
+ return paidResponse;
248
+ }
249
+
250
+ if (!response.ok) {
251
+ throw new Error(`Request failed: ${response.status}`);
252
+ }
253
+
254
+ return response;
255
+ } catch (error) {
256
+ if (attempt === maxRetries) throw error;
257
+ console.warn(`Attempt ${attempt + 1} failed, retrying...`);
258
+ }
259
+ }
260
+ }
261
+ ```
262
+
263
+ ### Checking Payment Status
264
+
265
+ ```typescript
266
+ // The payment middleware adds payment info to the request
267
+ // Access it in your route handler if needed
268
+ app.post("/api/weather", (req, res) => {
269
+ // The request only reaches here if payment was verified
270
+ // You can log payment details from the x-payment header
271
+ const paymentHeader = req.headers["x-payment"];
272
+ console.log("Payment received:", paymentHeader ? "yes" : "no");
273
+
274
+ res.json({ city: req.body.city, temperature: "22°C", paid: true });
275
+ });
276
+ ```
@@ -0,0 +1,138 @@
1
+ # x402-stellar Setup Reference
2
+
3
+ ## 1. Create a Stellar Testnet Account
4
+
5
+ Use the Stellar Laboratory to create and fund a testnet account.
6
+
7
+ ### Via Stellar Laboratory (recommended)
8
+
9
+ 1. Go to https://laboratory.stellar.org/#account-creator?network=test
10
+ 2. Click "Generate Keypair" — save both the **Public Key** (G...) and **Secret Key** (S...)
11
+ 3. Click "Fund account on testnet" to receive 10,000 test XLM from Friendbot
12
+
13
+ ### Via CLI
14
+
15
+ ```bash
16
+ curl -s "https://friendbot.stellar.org?addr=YOUR_PUBLIC_KEY" | jq .
17
+ ```
18
+
19
+ ### Via Stellar SDK
20
+
21
+ ```typescript
22
+ import { Keypair } from "@stellar/stellar-sdk";
23
+
24
+ const keypair = Keypair.random();
25
+ console.log("Public:", keypair.publicKey()); // G...
26
+ console.log("Secret:", keypair.secret()); // S...
27
+
28
+ // Fund on testnet
29
+ await fetch(`https://friendbot.stellar.org?addr=${keypair.publicKey()}`);
30
+ ```
31
+
32
+ You need **two accounts**:
33
+ - **Server account** — receives USDC payments (use the Public Key as `SERVER_STELLAR_ADDRESS`)
34
+ - **Client account** — signs and sends payments (use the Secret Key as `CLIENT_STELLAR_SECRET`)
35
+
36
+ ## 2. Get OpenZeppelin Facilitator API Key
37
+
38
+ The facilitator verifies and settles x402 payments on behalf of your server.
39
+
40
+ 1. Go to https://channels.openzeppelin.com/testnet/gen
41
+ 2. Generate a new API key
42
+ 3. Save the key as `FACILITATOR_API_KEY` in your `.env`
43
+
44
+ The facilitator URL for testnet is:
45
+ ```
46
+ https://channels.openzeppelin.com/x402/testnet
47
+ ```
48
+
49
+ For mainnet, use:
50
+ ```
51
+ https://channels.openzeppelin.com/x402/mainnet
52
+ ```
53
+
54
+ ## 3. Environment Variable Reference
55
+
56
+ | Variable | Format | Description | Where to Get It |
57
+ |----------|--------|-------------|-----------------|
58
+ | `SERVER_STELLAR_ADDRESS` | `G...` (56 chars) | Public key of the account receiving payments | Stellar Laboratory or SDK keypair generation |
59
+ | `FACILITATOR_URL` | URL | OpenZeppelin facilitator endpoint | `https://channels.openzeppelin.com/x402/testnet` |
60
+ | `FACILITATOR_API_KEY` | String | API key for the facilitator service | `channels.openzeppelin.com/testnet/gen` |
61
+ | `CLIENT_STELLAR_SECRET` | `S...` (56 chars) | Secret key of the account making payments | Stellar Laboratory or SDK keypair generation |
62
+
63
+ ### Example `.env` file
64
+
65
+ ```env
66
+ # Server configuration
67
+ SERVER_STELLAR_ADDRESS=GBZXN7PIRZGNMHGA7MUUUF4GWZPWTQVL6NKRTZSKU74AQKLLP5LCBUS
68
+ FACILITATOR_URL=https://channels.openzeppelin.com/x402/testnet
69
+ FACILITATOR_API_KEY=oz_fac_abc123def456
70
+
71
+ # Client configuration
72
+ CLIENT_STELLAR_SECRET=SCZANGBA5YHTNYVVV3C7CAZMCLP4JMQ3HZRQMIV6ILWLHYZPXTLCWBI
73
+ ```
74
+
75
+ ## 4. Fund Testnet Account with USDC
76
+
77
+ Testnet USDC is needed for the client account to make payments.
78
+
79
+ ### Step 1: Add USDC Trustline
80
+
81
+ Before an account can hold USDC, it must establish a trustline to the USDC issuer.
82
+
83
+ **Testnet USDC issuer:** `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`
84
+
85
+ ```typescript
86
+ import {
87
+ Keypair,
88
+ Server,
89
+ TransactionBuilder,
90
+ Networks,
91
+ Operation,
92
+ Asset,
93
+ } from "@stellar/stellar-sdk";
94
+
95
+ const server = new Server("https://horizon-testnet.stellar.org");
96
+ const keypair = Keypair.fromSecret(process.env.CLIENT_STELLAR_SECRET!);
97
+ const account = await server.loadAccount(keypair.publicKey());
98
+
99
+ const usdcAsset = new Asset(
100
+ "USDC",
101
+ "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
102
+ );
103
+
104
+ const tx = new TransactionBuilder(account, {
105
+ fee: "100",
106
+ networkPassphrase: Networks.TESTNET,
107
+ })
108
+ .addOperation(Operation.changeTrust({ asset: usdcAsset }))
109
+ .setTimeout(30)
110
+ .build();
111
+
112
+ tx.sign(keypair);
113
+ await server.submitTransaction(tx);
114
+ console.log("USDC trustline added");
115
+ ```
116
+
117
+ ### Step 2: Get Testnet USDC
118
+
119
+ Use the testnet USDC faucet or transfer from a funded testnet account. Check the Stellar Laboratory for testnet USDC distribution tools.
120
+
121
+ You can also use the Stellar Laboratory to manually add a trustline:
122
+ 1. Go to https://laboratory.stellar.org/#txbuilder?network=test
123
+ 2. Set source account to your client public key
124
+ 3. Add a "Change Trust" operation with USDC asset and the testnet issuer
125
+ 4. Sign and submit
126
+
127
+ ## 5. Verify Setup
128
+
129
+ Run the dependency checker script to validate everything is configured:
130
+
131
+ ```bash
132
+ node scripts/check-deps.js
133
+ ```
134
+
135
+ This checks:
136
+ - All @x402 packages are installed
137
+ - All required environment variables are set
138
+ - Variable formats are correct (G... for public keys, S... for secrets)
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * x402-stellar dependency and environment checker
5
+ *
6
+ * Checks that all required @x402 packages are installed and
7
+ * that environment variables are properly configured.
8
+ *
9
+ * Usage: node scripts/check-deps.js
10
+ * Exit codes: 0 = all checks passed, 1 = one or more checks failed
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const REQUIRED_PACKAGES = [
17
+ "@x402/express",
18
+ "@x402/stellar",
19
+ "@x402/core",
20
+ "@stellar/stellar-sdk",
21
+ ];
22
+
23
+ const REQUIRED_ENV_VARS = [
24
+ {
25
+ name: "SERVER_STELLAR_ADDRESS",
26
+ description: "Stellar public key for receiving payments",
27
+ validate: (val) => /^G[A-Z0-9]{55}$/.test(val),
28
+ hint: "Must be a Stellar public key starting with G (56 characters)",
29
+ },
30
+ {
31
+ name: "FACILITATOR_URL",
32
+ description: "OpenZeppelin facilitator endpoint URL",
33
+ validate: (val) => val.startsWith("https://"),
34
+ hint: "Use https://channels.openzeppelin.com/x402/testnet for testnet",
35
+ },
36
+ {
37
+ name: "FACILITATOR_API_KEY",
38
+ description: "API key for the facilitator service",
39
+ validate: (val) => val.length > 0,
40
+ hint: "Generate at channels.openzeppelin.com/testnet/gen",
41
+ },
42
+ {
43
+ name: "CLIENT_STELLAR_SECRET",
44
+ description: "Stellar secret key for signing payments",
45
+ validate: (val) => /^S[A-Z0-9]{55}$/.test(val),
46
+ hint: "Must be a Stellar secret key starting with S (56 characters)",
47
+ },
48
+ ];
49
+
50
+ function checkPackages() {
51
+ const checks = [];
52
+ let packageJson;
53
+
54
+ // Find package.json by walking up from cwd
55
+ let dir = process.cwd();
56
+ let packageJsonPath = null;
57
+
58
+ while (dir !== path.dirname(dir)) {
59
+ const candidate = path.join(dir, "package.json");
60
+ if (fs.existsSync(candidate)) {
61
+ packageJsonPath = candidate;
62
+ break;
63
+ }
64
+ dir = path.dirname(dir);
65
+ }
66
+
67
+ if (!packageJsonPath) {
68
+ checks.push({
69
+ type: "package",
70
+ name: "package.json",
71
+ status: "error",
72
+ message: "No package.json found in current directory or parent directories",
73
+ });
74
+ return checks;
75
+ }
76
+
77
+ try {
78
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
79
+ } catch (err) {
80
+ checks.push({
81
+ type: "package",
82
+ name: "package.json",
83
+ status: "error",
84
+ message: `Failed to parse package.json: ${err.message}`,
85
+ });
86
+ return checks;
87
+ }
88
+
89
+ const allDeps = {
90
+ ...packageJson.dependencies,
91
+ ...packageJson.devDependencies,
92
+ };
93
+
94
+ for (const pkg of REQUIRED_PACKAGES) {
95
+ if (allDeps && allDeps[pkg]) {
96
+ checks.push({
97
+ type: "package",
98
+ name: pkg,
99
+ status: "ok",
100
+ message: `Installed (${allDeps[pkg]})`,
101
+ });
102
+ } else {
103
+ checks.push({
104
+ type: "package",
105
+ name: pkg,
106
+ status: "error",
107
+ message: `Not found in package.json. Run: npm install ${pkg}`,
108
+ });
109
+ }
110
+ }
111
+
112
+ return checks;
113
+ }
114
+
115
+ function checkEnvVars() {
116
+ const checks = [];
117
+
118
+ // Try to load .env file
119
+ const envPath = path.join(process.cwd(), ".env");
120
+ const envVars = { ...process.env };
121
+
122
+ if (fs.existsSync(envPath)) {
123
+ try {
124
+ const envContent = fs.readFileSync(envPath, "utf8");
125
+ const lines = envContent.split("\n");
126
+ for (const line of lines) {
127
+ const trimmed = line.trim();
128
+ if (!trimmed || trimmed.startsWith("#")) continue;
129
+ const eqIndex = trimmed.indexOf("=");
130
+ if (eqIndex === -1) continue;
131
+ const key = trimmed.substring(0, eqIndex).trim();
132
+ let value = trimmed.substring(eqIndex + 1).trim();
133
+ // Remove surrounding quotes
134
+ if (
135
+ (value.startsWith('"') && value.endsWith('"')) ||
136
+ (value.startsWith("'") && value.endsWith("'"))
137
+ ) {
138
+ value = value.slice(1, -1);
139
+ }
140
+ envVars[key] = value;
141
+ }
142
+ } catch (err) {
143
+ checks.push({
144
+ type: "env",
145
+ name: ".env",
146
+ status: "error",
147
+ message: `Failed to read .env file: ${err.message}`,
148
+ });
149
+ }
150
+ } else {
151
+ checks.push({
152
+ type: "env",
153
+ name: ".env",
154
+ status: "warning",
155
+ message: "No .env file found. Environment variables must be set in the shell.",
156
+ });
157
+ }
158
+
159
+ for (const envVar of REQUIRED_ENV_VARS) {
160
+ const value = envVars[envVar.name];
161
+ if (!value) {
162
+ checks.push({
163
+ type: "env",
164
+ name: envVar.name,
165
+ status: "error",
166
+ message: `Not set. ${envVar.description}. ${envVar.hint}`,
167
+ });
168
+ } else if (!envVar.validate(value)) {
169
+ checks.push({
170
+ type: "env",
171
+ name: envVar.name,
172
+ status: "error",
173
+ message: `Invalid format. ${envVar.hint}`,
174
+ });
175
+ } else {
176
+ const masked = value.substring(0, 4) + "..." + value.substring(value.length - 4);
177
+ checks.push({
178
+ type: "env",
179
+ name: envVar.name,
180
+ status: "ok",
181
+ message: `Set (${masked})`,
182
+ });
183
+ }
184
+ }
185
+
186
+ return checks;
187
+ }
188
+
189
+ function main() {
190
+ const packageChecks = checkPackages();
191
+ const envChecks = checkEnvVars();
192
+ const allChecks = [...packageChecks, ...envChecks];
193
+
194
+ const hasErrors = allChecks.some((c) => c.status === "error");
195
+ const result = {
196
+ status: hasErrors ? "error" : "ok",
197
+ checks: allChecks,
198
+ };
199
+
200
+ // Pretty print to stderr for humans
201
+ console.error("\nx402-stellar Dependency Check");
202
+ console.error("=".repeat(40));
203
+
204
+ for (const check of allChecks) {
205
+ const icon = check.status === "ok" ? "[OK]" : check.status === "warning" ? "[WARN]" : "[ERR]";
206
+ console.error(`${icon} ${check.name}: ${check.message}`);
207
+ }
208
+
209
+ console.error("=".repeat(40));
210
+ console.error(hasErrors ? "Some checks failed.\n" : "All checks passed.\n");
211
+
212
+ // JSON to stdout for programmatic use
213
+ console.log(JSON.stringify(result, null, 2));
214
+
215
+ process.exit(hasErrors ? 1 : 0);
216
+ }
217
+
218
+ main();