arc-builder-kit 0.2.0__py3-none-any.whl

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 (58) hide show
  1. arc_builder_kit/__init__.py +4 -0
  2. arc_builder_kit/__main__.py +6 -0
  3. arc_builder_kit/_paths.py +47 -0
  4. arc_builder_kit/cli.py +277 -0
  5. arc_builder_kit/config/arc_testnet.facts.json +31 -0
  6. arc_builder_kit/doctor.py +936 -0
  7. arc_builder_kit/examples/agent-commerce-components/components.js +200 -0
  8. arc_builder_kit/examples/agent-commerce-components/index.html +120 -0
  9. arc_builder_kit/examples/agent-commerce-flows/flows.js +271 -0
  10. arc_builder_kit/examples/agent-commerce-flows/index.html +114 -0
  11. arc_builder_kit/examples/agent-commerce-live/commerce-live.js +190 -0
  12. arc_builder_kit/examples/agent-commerce-live/index.html +105 -0
  13. arc_builder_kit/examples/agent-commerce-review-packet/index.html +96 -0
  14. arc_builder_kit/examples/agent-commerce-review-packet/packet.js +125 -0
  15. arc_builder_kit/examples/agent-identity-profile-preview/identity.js +126 -0
  16. arc_builder_kit/examples/agent-identity-profile-preview/index.html +104 -0
  17. arc_builder_kit/examples/arc-agent-treasury-lab/index.html +152 -0
  18. arc_builder_kit/examples/arc-agent-treasury-lab/treasury.js +532 -0
  19. arc_builder_kit/examples/arc-testnet-operator-evidence/evidence.example.json +47 -0
  20. arc_builder_kit/examples/arc-testnet-wallet-send-gate/index.html +233 -0
  21. arc_builder_kit/examples/arc-testnet-wallet-send-gate/live-infrastructure-policy.example.json +59 -0
  22. arc_builder_kit/examples/arc-testnet-wallet-send-gate/wallet-send-gate.js +472 -0
  23. arc_builder_kit/examples/circle-wallet-integration/index.html +155 -0
  24. arc_builder_kit/examples/circle-wallet-integration/wallet-lab.js +91 -0
  25. arc_builder_kit/examples/job-escrow-simulator/index.html +121 -0
  26. arc_builder_kit/examples/job-escrow-simulator/simulator.js +162 -0
  27. arc_builder_kit/examples/payment-intent-demo/index.html +132 -0
  28. arc_builder_kit/examples/payment-intent-playground/index.html +301 -0
  29. arc_builder_kit/examples/payment-intent-playground/playground.js +835 -0
  30. arc_builder_kit/examples/payment-intent-receipt-matcher/index.html +157 -0
  31. arc_builder_kit/examples/payment-intent-receipt-matcher/matcher.js +877 -0
  32. arc_builder_kit/examples/receipt-verifier-playground/index.html +120 -0
  33. arc_builder_kit/examples/receipt-verifier-playground/verifier.js +226 -0
  34. arc_builder_kit/examples/receipt-viewer/index.html +138 -0
  35. arc_builder_kit/examples/receipt-viewer/receipt-viewer.js +472 -0
  36. arc_builder_kit/examples/transaction-status-playground/index.html +135 -0
  37. arc_builder_kit/examples/transaction-status-playground/status.js +518 -0
  38. arc_builder_kit/examples/x402-local-challenge-server/.env.example +25 -0
  39. arc_builder_kit/examples/x402-local-challenge-server/README.md +111 -0
  40. arc_builder_kit/examples/x402-local-challenge-server/server.py +711 -0
  41. arc_builder_kit/mcp_server.py +463 -0
  42. arc_builder_kit/release_packet.py +469 -0
  43. arc_builder_kit/templates/README.md +25 -0
  44. arc_builder_kit/templates/job-escrow-starter/README.md +25 -0
  45. arc_builder_kit/templates/job-escrow-starter/index.html +41 -0
  46. arc_builder_kit/templates/job-escrow-starter/index.js +14 -0
  47. arc_builder_kit/templates/payment-intent-starter/README.md +25 -0
  48. arc_builder_kit/templates/payment-intent-starter/index.html +42 -0
  49. arc_builder_kit/templates/payment-intent-starter/index.js +7 -0
  50. arc_builder_kit/templates/x402-agent-starter/README.md +29 -0
  51. arc_builder_kit/templates/x402-agent-starter/server.py +201 -0
  52. arc_builder_kit/validate_repo.py +2212 -0
  53. arc_builder_kit-0.2.0.dist-info/METADATA +543 -0
  54. arc_builder_kit-0.2.0.dist-info/RECORD +58 -0
  55. arc_builder_kit-0.2.0.dist-info/WHEEL +5 -0
  56. arc_builder_kit-0.2.0.dist-info/entry_points.txt +3 -0
  57. arc_builder_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
  58. arc_builder_kit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,518 @@
1
+ const transactionHashInput = document.querySelector('#transaction-hash');
2
+ const expectedRecipientInput = document.querySelector('#expected-recipient');
3
+ const expectedAmountInput = document.querySelector('#expected-amount');
4
+ const checkButton = document.querySelector('#check-transaction');
5
+ const resetButton = document.querySelector('#reset-transaction');
6
+ const statusPill = document.querySelector('#status-pill');
7
+ const statusCheckList = document.querySelector('#status-check-list');
8
+ const transactionStatusJson = document.querySelector('#transaction-status-json');
9
+
10
+ const ARC_TRANSACTION_STATUS = Object.freeze({
11
+ network: 'Arc Testnet',
12
+ expectedChainId: 5042002,
13
+ expectedChainIdHex: '0x4cef52',
14
+ rpcUrl: 'https://rpc.testnet.arc.network',
15
+ explorerUrl: 'https://testnet.arcscan.app',
16
+ usdcAddress: '0x3600000000000000000000000000000000000000',
17
+ usdcDecimals: 6,
18
+ walletConnected: false,
19
+ backendCalls: false,
20
+ readOnlyRpcCheckOnly: true,
21
+ transactionBroadcast: false,
22
+ autonomousSpending: false,
23
+ humanApprovalRequired: true,
24
+ signingRequiresWalletChainGateAndHumanApproval: true,
25
+ });
26
+
27
+ const SAMPLE_TRANSACTION_HASH = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
28
+ const RPC_TIMEOUT_MS = 10_000;
29
+ const MAX_RPC_RESPONSE_BYTES = 1_000_000;
30
+ const RPC_REQUEST_ID = 'arc-transaction-status-read-only';
31
+ const TRANSFER_SELECTOR = 'a9059cbb';
32
+ const READ_ONLY_RPC_METHODS = Object.freeze([
33
+ { method: 'eth_chainId', params: [] },
34
+ { method: 'eth_getTransactionByHash', params: ['transactionHash'] },
35
+ { method: 'eth_getTransactionReceipt', params: ['transactionHash'] },
36
+ ]);
37
+
38
+ function isValidHash(value) {
39
+ return /^0x[a-fA-F0-9]{64}$/.test(String(value || '').trim());
40
+ }
41
+
42
+ function hashMatchesExpected(value, expectedHash) {
43
+ return isValidHash(value) && String(value).toLowerCase() === expectedHash.toLowerCase();
44
+ }
45
+
46
+ function parseHexQuantity(value) {
47
+ if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value)) return null;
48
+ return Number.parseInt(value, 16);
49
+ }
50
+
51
+ function isNonZeroAddress(value) {
52
+ return /^0x[a-fA-F0-9]{40}$/.test(String(value || '').trim())
53
+ && !/^0x0{40}$/i.test(String(value || '').trim());
54
+ }
55
+
56
+ function parseUsdcAmount(rawValue) {
57
+ const value = String(rawValue || '').trim();
58
+ if (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/.test(value)) {
59
+ throw new Error('Expected USDC amount must be a plain positive decimal with at most 6 fractional digits.');
60
+ }
61
+ const [whole, fraction = ''] = value.split('.');
62
+ const baseUnits = (BigInt(whole) * 1000000n) + BigInt(fraction.padEnd(6, '0'));
63
+ if (baseUnits <= 0n) throw new Error('Expected USDC amount must be greater than zero.');
64
+ return { decimal: value, baseUnits: baseUnits.toString() };
65
+ }
66
+
67
+ function decodeTransferCalldata(data) {
68
+ const normalized = String(data || '').toLowerCase();
69
+ if (!new RegExp(`^0x${TRANSFER_SELECTOR}[0-9a-f]{128}$`).test(normalized)) {
70
+ throw new Error('Transaction input is not ERC-20 transfer(address,uint256) calldata.');
71
+ }
72
+ return {
73
+ method: 'transfer(address,uint256)',
74
+ recipient: `0x${normalized.slice(34, 74)}`,
75
+ amountBaseUnits: BigInt(`0x${normalized.slice(74, 138)}`).toString(),
76
+ };
77
+ }
78
+
79
+ function buildExpectedTransfer() {
80
+ const recipient = expectedRecipientInput.value.trim().toLowerCase();
81
+ if (!isNonZeroAddress(recipient)) {
82
+ throw new Error('Expected recipient must be a non-zero 0x-prefixed 20-byte address.');
83
+ }
84
+ if (recipient === ARC_TRANSACTION_STATUS.usdcAddress.toLowerCase()) {
85
+ throw new Error('Expected recipient cannot be the pinned USDC token contract.');
86
+ }
87
+ const amount = parseUsdcAmount(expectedAmountInput.value);
88
+ return {
89
+ token: ARC_TRANSACTION_STATUS.usdcAddress,
90
+ tokenDecimals: ARC_TRANSACTION_STATUS.usdcDecimals,
91
+ recipient,
92
+ amountDecimal: amount.decimal,
93
+ amountBaseUnits: amount.baseUnits,
94
+ };
95
+ }
96
+
97
+ function reviewExpectedTransfer(transaction, expectedTransfer) {
98
+ if (!transaction || typeof transaction !== 'object' || Array.isArray(transaction)) {
99
+ return {
100
+ state: 'unknown',
101
+ allMatched: false,
102
+ reason: 'Transaction was not found, so expected transfer fields cannot be compared.',
103
+ checks: [],
104
+ observed: null,
105
+ };
106
+ }
107
+ let decoded = null;
108
+ let decodeError = '';
109
+ try {
110
+ decoded = decodeTransferCalldata(transaction.input);
111
+ } catch (error) {
112
+ decodeError = error.message;
113
+ }
114
+ const checks = [
115
+ {
116
+ id: 'token-target',
117
+ passed: String(transaction.to || '').toLowerCase() === expectedTransfer.token.toLowerCase(),
118
+ detail: 'Transaction target must be the pinned Arc Testnet USDC interface.',
119
+ },
120
+ {
121
+ id: 'zero-native-value',
122
+ passed: parseHexQuantity(transaction.value) === 0,
123
+ detail: 'ERC-20 transfer must send zero native value.',
124
+ },
125
+ {
126
+ id: 'transfer-calldata',
127
+ passed: Boolean(decoded),
128
+ detail: decoded ? 'Calldata decodes as transfer(address,uint256).' : decodeError,
129
+ },
130
+ {
131
+ id: 'recipient',
132
+ passed: Boolean(decoded && decoded.recipient === expectedTransfer.recipient),
133
+ detail: 'Decoded recipient must match the expected recipient.',
134
+ },
135
+ {
136
+ id: 'amount',
137
+ passed: Boolean(decoded && decoded.amountBaseUnits === expectedTransfer.amountBaseUnits),
138
+ detail: 'Decoded 6-decimal USDC base units must match the expected amount.',
139
+ },
140
+ ];
141
+ const allMatched = checks.every((check) => check.passed);
142
+ return {
143
+ state: allMatched ? 'match' : 'mismatch',
144
+ allMatched,
145
+ reason: allMatched
146
+ ? 'Observed transaction shape matches the expected Arc Testnet USDC transfer.'
147
+ : 'Observed transaction shape does not match every expected transfer field.',
148
+ checks,
149
+ observed: {
150
+ from: transaction.from || null,
151
+ tokenTarget: transaction.to || null,
152
+ nativeValue: transaction.value || null,
153
+ decoded,
154
+ },
155
+ };
156
+ }
157
+
158
+ function withTransferEvidence(result, transaction, expectedTransfer) {
159
+ const transferReview = reviewExpectedTransfer(transaction, expectedTransfer);
160
+ const evidenceVerdict = !result.rpcChainIdMatchesArcTestnet
161
+ ? 'unknown_wrong_chain'
162
+ : result.rpcObjectHashesMatch === false
163
+ ? 'unknown_hash_mismatch'
164
+ : transferReview.state === 'mismatch'
165
+ ? 'mismatch_expected_transfer'
166
+ : transferReview.state === 'unknown'
167
+ ? 'unknown_expected_transfer'
168
+ : `${result.state}_expected_transfer_shape`;
169
+ return {
170
+ ...result,
171
+ expectedTransfer,
172
+ transferReview,
173
+ evidenceVerdict,
174
+ settlementProven: false,
175
+ businessAcceptanceProven: false,
176
+ };
177
+ }
178
+
179
+ async function rpcCall(method, params = []) {
180
+ const controller = new AbortController();
181
+ const timeout = window.setTimeout(() => controller.abort(), RPC_TIMEOUT_MS);
182
+ try {
183
+ const response = await fetch(ARC_TRANSACTION_STATUS.rpcUrl, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ jsonrpc: '2.0', id: RPC_REQUEST_ID, method, params }),
187
+ signal: controller.signal,
188
+ });
189
+ if (!response.ok) {
190
+ throw new Error(`RPC HTTP ${response.status}`);
191
+ }
192
+ const responseText = await response.text();
193
+ if (new TextEncoder().encode(responseText).byteLength > MAX_RPC_RESPONSE_BYTES) {
194
+ throw new Error('RPC response exceeded the 1 MB safety limit');
195
+ }
196
+ const payload = JSON.parse(responseText);
197
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
198
+ throw new Error('RPC response must be a JSON object');
199
+ }
200
+ if (payload.jsonrpc !== '2.0' || payload.id !== RPC_REQUEST_ID) {
201
+ throw new Error('RPC response envelope did not match the request');
202
+ }
203
+ const hasResult = Object.prototype.hasOwnProperty.call(payload, 'result');
204
+ const hasError = Object.prototype.hasOwnProperty.call(payload, 'error');
205
+ if (hasResult === hasError) {
206
+ throw new Error('RPC response must contain exactly one result or error field');
207
+ }
208
+ if (hasError) {
209
+ const message = payload.error && typeof payload.error.message === 'string'
210
+ ? payload.error.message.slice(0, 200)
211
+ : 'unknown RPC error';
212
+ throw new Error(`${method} failed: ${message}`);
213
+ }
214
+ return payload.result;
215
+ } finally {
216
+ window.clearTimeout(timeout);
217
+ }
218
+ }
219
+
220
+ async function readOnlyLookup(transactionHash) {
221
+ const chainIdHex = await rpcCall('eth_chainId');
222
+ if (parseHexQuantity(chainIdHex) !== ARC_TRANSACTION_STATUS.expectedChainId) {
223
+ return { chainIdHex, transaction: null, receipt: null };
224
+ }
225
+ const transaction = await rpcCall('eth_getTransactionByHash', [transactionHash]);
226
+ const receipt = await rpcCall('eth_getTransactionReceipt', [transactionHash]);
227
+ return { chainIdHex, transaction, receipt };
228
+ }
229
+
230
+ function buildCheck(id, label, level, detail) {
231
+ return { id, label, level, detail };
232
+ }
233
+
234
+ function classifyTransactionStatus(chainIdHex, transaction, receipt, expectedTransfer, expectedTransactionHash) {
235
+ const chainIdDecimal = parseHexQuantity(chainIdHex);
236
+ const chainMatches = chainIdDecimal === ARC_TRANSACTION_STATUS.expectedChainId;
237
+ const transactionHashMatches = !transaction || hashMatchesExpected(transaction.hash, expectedTransactionHash);
238
+ const receiptHashMatches = !receipt || hashMatchesExpected(receipt.transactionHash, expectedTransactionHash);
239
+ const rpcObjectHashesMatch = transactionHashMatches && receiptHashMatches;
240
+ const base = {
241
+ kind: 'arc_testnet_transaction_status',
242
+ network: ARC_TRANSACTION_STATUS.network,
243
+ rpcUrl: ARC_TRANSACTION_STATUS.rpcUrl,
244
+ explorerUrl: ARC_TRANSACTION_STATUS.explorerUrl,
245
+ expectedChainId: ARC_TRANSACTION_STATUS.expectedChainId,
246
+ expectedChainIdHex: ARC_TRANSACTION_STATUS.expectedChainIdHex,
247
+ chainIdHex,
248
+ chainIdDecimal,
249
+ rpcChainIdMatchesArcTestnet: chainMatches,
250
+ expectedTransactionHash,
251
+ transactionHashMatches,
252
+ receiptHashMatches,
253
+ rpcObjectHashesMatch,
254
+ checkedAt: new Date().toISOString(),
255
+ safety: {
256
+ walletConnected: false,
257
+ backendCalls: false,
258
+ readOnlyRpcCheckOnly: true,
259
+ transactionBroadcast: false,
260
+ autonomousSpending: false,
261
+ humanApprovalRequired: true,
262
+ signingRequiresWalletChainGateAndHumanApproval: true,
263
+ },
264
+ };
265
+
266
+ if (!chainMatches) {
267
+ return withTransferEvidence({
268
+ ...base,
269
+ state: 'unknown',
270
+ reason: 'RPC chain ID did not match Arc Testnet.',
271
+ transactionFound: Boolean(transaction),
272
+ receiptFound: Boolean(receipt),
273
+ transaction,
274
+ receipt,
275
+ }, transaction, expectedTransfer);
276
+ }
277
+
278
+ if (!rpcObjectHashesMatch) {
279
+ return withTransferEvidence({
280
+ ...base,
281
+ state: 'unknown',
282
+ reason: 'RPC returned a transaction or receipt for a different hash.',
283
+ transactionFound: Boolean(transaction),
284
+ receiptFound: Boolean(receipt),
285
+ observedTransactionHash: transaction && transaction.hash ? transaction.hash : null,
286
+ observedReceiptTransactionHash: receipt && receipt.transactionHash ? receipt.transactionHash : null,
287
+ }, null, expectedTransfer);
288
+ }
289
+
290
+ if (receipt && receipt.status === '0x1') {
291
+ return withTransferEvidence({
292
+ ...base,
293
+ state: 'confirmed',
294
+ reason: 'Transaction receipt exists and status is 0x1.',
295
+ transactionHash: receipt.transactionHash,
296
+ blockNumberHex: receipt.blockNumber,
297
+ blockNumberDecimal: parseHexQuantity(receipt.blockNumber),
298
+ transactionFound: Boolean(transaction),
299
+ receiptFound: true,
300
+ transaction,
301
+ receipt,
302
+ }, transaction, expectedTransfer);
303
+ }
304
+
305
+ if (receipt && receipt.status === '0x0') {
306
+ return withTransferEvidence({
307
+ ...base,
308
+ state: 'failed',
309
+ reason: 'Transaction receipt exists and status is 0x0.',
310
+ transactionHash: receipt.transactionHash,
311
+ blockNumberHex: receipt.blockNumber,
312
+ blockNumberDecimal: parseHexQuantity(receipt.blockNumber),
313
+ transactionFound: Boolean(transaction),
314
+ receiptFound: true,
315
+ transaction,
316
+ receipt,
317
+ }, transaction, expectedTransfer);
318
+ }
319
+
320
+ if (transaction && !receipt) {
321
+ return withTransferEvidence({
322
+ ...base,
323
+ state: 'pending',
324
+ reason: 'Transaction was found but no receipt is available yet.',
325
+ transactionHash: transaction.hash,
326
+ transactionFound: true,
327
+ receiptFound: false,
328
+ transaction,
329
+ receipt: null,
330
+ }, transaction, expectedTransfer);
331
+ }
332
+
333
+ return withTransferEvidence({
334
+ ...base,
335
+ state: 'unknown',
336
+ reason: 'Transaction hash was not found or receipt status was ambiguous.',
337
+ transactionFound: Boolean(transaction),
338
+ receiptFound: Boolean(receipt),
339
+ transaction,
340
+ receipt,
341
+ }, transaction, expectedTransfer);
342
+ }
343
+
344
+ function checksForResult(result, hash) {
345
+ return [
346
+ buildCheck(
347
+ 'hash',
348
+ 'Transaction hash shape',
349
+ isValidHash(hash) ? 'pass' : 'fail',
350
+ 'Expected a 32-byte 0x-prefixed transaction hash.'
351
+ ),
352
+ buildCheck(
353
+ 'chain',
354
+ 'Arc Testnet chain ID',
355
+ result.rpcChainIdMatchesArcTestnet ? 'pass' : 'fail',
356
+ `Expected ${ARC_TRANSACTION_STATUS.expectedChainIdHex}; got ${result.chainIdHex || 'unavailable'}.`
357
+ ),
358
+ buildCheck(
359
+ 'method-scope',
360
+ 'RPC method scope',
361
+ 'pass',
362
+ 'Used only eth_chainId, eth_getTransactionByHash, and eth_getTransactionReceipt.'
363
+ ),
364
+ buildCheck(
365
+ 'wallet',
366
+ 'Wallet/signing boundary',
367
+ 'pass',
368
+ 'No wallet was connected and no transaction was submitted.'
369
+ ),
370
+ buildCheck(
371
+ 'state',
372
+ 'Status state',
373
+ result.state === 'confirmed' ? 'pass' : result.state === 'failed' ? 'fail' : 'warn',
374
+ result.reason
375
+ ),
376
+ buildCheck(
377
+ 'expected-transfer',
378
+ 'Expected USDC transfer shape',
379
+ result.transferReview && result.transferReview.state === 'match'
380
+ ? 'pass'
381
+ : result.transferReview && result.transferReview.state === 'mismatch'
382
+ ? 'fail'
383
+ : 'warn',
384
+ result.transferReview ? result.transferReview.reason : 'Expected transfer was not reviewed.'
385
+ ),
386
+ buildCheck(
387
+ 'settlement-boundary',
388
+ 'Settlement boundary',
389
+ 'warn',
390
+ 'A receipt and matching calldata do not prove settlement, finality, or business acceptance.'
391
+ ),
392
+ ];
393
+ }
394
+
395
+ function renderChecks(checks) {
396
+ statusCheckList.replaceChildren(
397
+ ...checks.map((item) => {
398
+ const listItem = document.createElement('li');
399
+ listItem.className = item.level;
400
+ const strong = document.createElement('strong');
401
+ strong.textContent = item.level === 'pass' ? 'PASS' : item.level === 'fail' ? 'REVIEW' : 'INFO';
402
+ listItem.append(strong, ` — ${item.label}: ${item.detail}`);
403
+ return listItem;
404
+ })
405
+ );
406
+ }
407
+
408
+ function renderStatus(result, checks) {
409
+ statusPill.textContent = result.state;
410
+ statusPill.className = `status ${result.state}`;
411
+ renderChecks(checks);
412
+ transactionStatusJson.textContent = JSON.stringify(result, null, 2);
413
+ }
414
+
415
+ function renderInitialState() {
416
+ const result = {
417
+ kind: 'arc_testnet_transaction_status',
418
+ state: 'not_checked',
419
+ reason: 'No read-only lookup has run yet.',
420
+ network: ARC_TRANSACTION_STATUS.network,
421
+ rpcUrl: ARC_TRANSACTION_STATUS.rpcUrl,
422
+ expectedChainId: ARC_TRANSACTION_STATUS.expectedChainId,
423
+ expectedChainIdHex: ARC_TRANSACTION_STATUS.expectedChainIdHex,
424
+ evidenceVerdict: 'not_checked',
425
+ settlementProven: false,
426
+ businessAcceptanceProven: false,
427
+ safety: {
428
+ walletConnected: false,
429
+ backendCalls: false,
430
+ readOnlyRpcCheckOnly: true,
431
+ transactionBroadcast: false,
432
+ autonomousSpending: false,
433
+ humanApprovalRequired: true,
434
+ signingRequiresWalletChainGateAndHumanApproval: true,
435
+ },
436
+ };
437
+ renderStatus(result, [
438
+ buildCheck('state', 'Status state', 'warn', 'Paste a transaction hash and run the read-only lookup.'),
439
+ buildCheck('wallet', 'Wallet/signing boundary', 'pass', 'No wallet was connected and no transaction was submitted.'),
440
+ ]);
441
+ }
442
+
443
+ function renderValidationError(hash, reason = 'Transaction hash is not a 32-byte 0x-prefixed value.') {
444
+ const result = {
445
+ kind: 'arc_testnet_transaction_status',
446
+ state: 'unknown',
447
+ reason,
448
+ checkedAt: new Date().toISOString(),
449
+ transactionHash: hash,
450
+ safety: {
451
+ walletConnected: false,
452
+ backendCalls: false,
453
+ readOnlyRpcCheckOnly: true,
454
+ transactionBroadcast: false,
455
+ autonomousSpending: false,
456
+ humanApprovalRequired: true,
457
+ signingRequiresWalletChainGateAndHumanApproval: true,
458
+ },
459
+ };
460
+ renderStatus(result, checksForResult({ ...result, rpcChainIdMatchesArcTestnet: false, chainIdHex: 'not_checked' }, hash));
461
+ }
462
+
463
+ async function checkTransactionStatus() {
464
+ const hash = transactionHashInput.value.trim();
465
+ if (!isValidHash(hash)) {
466
+ renderValidationError(hash);
467
+ return;
468
+ }
469
+ let expectedTransfer;
470
+ try {
471
+ expectedTransfer = buildExpectedTransfer();
472
+ } catch (error) {
473
+ renderValidationError(hash, error.message);
474
+ return;
475
+ }
476
+ statusPill.textContent = 'checking';
477
+ statusPill.className = 'status pending';
478
+ transactionStatusJson.textContent = JSON.stringify({ state: 'checking_arc_rpc', transactionHash: hash }, null, 2);
479
+ try {
480
+ const lookup = await readOnlyLookup(hash);
481
+ const result = classifyTransactionStatus(lookup.chainIdHex, lookup.transaction, lookup.receipt, expectedTransfer, hash);
482
+ renderStatus({ ...result, transactionHash: hash }, checksForResult(result, hash));
483
+ } catch (error) {
484
+ const result = {
485
+ kind: 'arc_testnet_transaction_status',
486
+ state: 'unknown',
487
+ reason: `RPC lookup unavailable: ${error.message}`,
488
+ checkedAt: new Date().toISOString(),
489
+ transactionHash: hash,
490
+ rpcUrl: ARC_TRANSACTION_STATUS.rpcUrl,
491
+ expectedTransfer,
492
+ evidenceVerdict: 'unknown_rpc_unavailable',
493
+ settlementProven: false,
494
+ businessAcceptanceProven: false,
495
+ safety: {
496
+ walletConnected: false,
497
+ backendCalls: false,
498
+ readOnlyRpcCheckOnly: true,
499
+ transactionBroadcast: false,
500
+ autonomousSpending: false,
501
+ humanApprovalRequired: true,
502
+ signingRequiresWalletChainGateAndHumanApproval: true,
503
+ },
504
+ };
505
+ renderStatus(result, checksForResult({ ...result, rpcChainIdMatchesArcTestnet: false, chainIdHex: 'unavailable' }, hash));
506
+ }
507
+ }
508
+
509
+ function resetSample() {
510
+ transactionHashInput.value = SAMPLE_TRANSACTION_HASH;
511
+ expectedRecipientInput.value = '0x1111111111111111111111111111111111111111';
512
+ expectedAmountInput.value = '0.01';
513
+ renderInitialState();
514
+ }
515
+
516
+ checkButton.addEventListener('click', checkTransactionStatus);
517
+ resetButton.addEventListener('click', resetSample);
518
+ renderInitialState();
@@ -0,0 +1,25 @@
1
+ # Placeholder only. Do not put real secrets in this file.
2
+ # Copy to .env locally, then fill values in your deployment platform's secret store.
3
+
4
+ # Local demo defaults only. These keep the bundled x402 challenge server safe.
5
+ X402_DEMO_NETWORK=arc-testnet
6
+ X402_DEMO_ASSET=USDC
7
+ X402_DEMO_AMOUNT=0.01
8
+ X402_DEMO_PAY_TO=0xA11CE00000000000000000000000000000000000
9
+ X402_DEMO_MAINNET_ENABLED=false
10
+
11
+ # Production-shaped paid-agent endpoint smoke checks.
12
+ # ARC_PAID_AGENT_URL should point at the deployed protected endpoint, for example
13
+ # https://your-service.example/protected. The live smoke script first checks an
14
+ # unpaid 402 challenge and never creates payments by itself.
15
+ ARC_PAID_AGENT_URL=
16
+ EXPECT_402_ONLY=true
17
+
18
+ # Optional: paste an externally created x402/Circle Gateway proof only in your
19
+ # private shell/CI secret store when you want to test the paid retry path.
20
+ ARC_LIVE_X_PAYMENT=
21
+
22
+ # Future verifier handoff placeholders. Keep these empty in the public repo.
23
+ # Store real values only in a deployment secret manager.
24
+ CIRCLE_GATEWAY_API_KEY=
25
+ X402_GATEWAY_VERIFIER_URL=
@@ -0,0 +1,111 @@
1
+ # Local x402 challenge server demo
2
+
3
+ This example is a production-shaped boundary for a future Arc/Circle/x402 payment verifier. It is intentionally local-only:
4
+
5
+ - returns an HTTP `402 Payment Required` challenge for `/protected`;
6
+ - accepts only an explicit local demo proof shape;
7
+ - never opens a wallet, broadcasts transactions, or claims settlement;
8
+ - keeps human approval and mainnet disabled by default.
9
+ - refuses non-loopback HTTP bind hosts and invalid ports.
10
+
11
+ ## Run locally
12
+
13
+ ```bash
14
+ python3 examples/x402-local-challenge-server/server.py --port 8087
15
+ ```
16
+
17
+ HTTP mode accepts only `127.0.0.1` or `localhost`. Use a separately reviewed
18
+ deployment implementation for remote or staging access; do not expose the
19
+ deterministic local proof verifier on a network interface.
20
+
21
+ The server reads safe local overrides from environment variables documented in
22
+ [`../../.env.example`](../../.env.example). Defaults stay Arc Testnet only:
23
+
24
+ ```bash
25
+ X402_DEMO_AMOUNT=0.05 \
26
+ python3 examples/x402-local-challenge-server/server.py --print-challenge
27
+ ```
28
+
29
+ Unsafe overrides are rejected with clear errors: non-Arc network, any asset
30
+ other than the pinned `USDC` economics, `X402_DEMO_MAINNET_ENABLED=true`,
31
+ non-positive or over-precision amounts, and malformed or zero
32
+ `X402_DEMO_PAY_TO` addresses. The deterministic local proof is bound to the
33
+ reviewed pay-to address through the challenge id.
34
+
35
+ The same safety contract is enforced when Python helpers are called directly:
36
+ the reviewed resource, `local-simulation` verifier mode, mandatory human
37
+ approval, Arc Testnet, USDC, and disabled mainnet settings cannot be bypassed
38
+ with a manually constructed `PaymentConfig`.
39
+
40
+ In another terminal:
41
+
42
+ ```bash
43
+ curl -i http://127.0.0.1:8087/protected
44
+ ```
45
+
46
+ The response includes a challenge id plus an `mcpManifest` object with safe paid-agent tool metadata. To simulate approval, send:
47
+
48
+ ```bash
49
+ curl -i \
50
+ -H 'X-Payment: local-demo:<challenge-id>:0.01' \
51
+ http://127.0.0.1:8087/protected
52
+ ```
53
+
54
+ ## MCP-style manifest
55
+
56
+ The unpaid `402` response includes `mcpManifest`, a machine-readable discovery contract for future agents. It lists the local tool surface, Arc Testnet constants, integer microUSD unit economics, safety flags, and the Circle Gateway/x402 production replacement boundary.
57
+
58
+ The same local tool surface is available through a dependency-free JSON-RPC/MCP-style stdio mode:
59
+
60
+ ```bash
61
+ printf '{"jsonrpc":"2.0","id":"tools","method":"tools/list"}\n' \
62
+ | python3 examples/x402-local-challenge-server/server.py --mcp-stdio
63
+ ```
64
+
65
+ The stdio parser rejects requests over 1 MB, invalid UTF-8, duplicate JSON
66
+ keys, non-`2.0` JSON-RPC envelopes, invalid request IDs, and non-string
67
+ methods before dispatching a tool. Runtime dispatch also enforces each
68
+ manifest `additionalProperties: false` contract for request, params, and tool
69
+ arguments.
70
+
71
+ HTTP and helper proof inputs accept exactly one `X-Payment` value, reject
72
+ control characters, and enforce a 4 KB maximum before verifier dispatch.
73
+ Verifier exceptions, malformed verifier results, and any successful verifier
74
+ receipt that does not keep both `settled` and `transactionBroadcast`
75
+ explicitly `false` are converted into a safe `402` response without reflecting
76
+ internal verifier failure details.
77
+
78
+ Quick JSON helpers:
79
+
80
+ ```bash
81
+ python3 examples/x402-local-challenge-server/server.py --print-manifest
82
+ python3 examples/x402-local-challenge-server/server.py --print-challenge
83
+ python3 examples/x402-local-challenge-server/server.py --verify-payment 'local-demo:<challenge-id>:0.01'
84
+ ```
85
+
86
+ These helpers never open wallets, accept private keys, broadcast transactions, or claim settlement.
87
+
88
+ See [x402 MCP manifest](../../docs/x402-mcp-manifest.md) for the field-by-field walkthrough.
89
+
90
+ ## Boundary contract
91
+
92
+ The swappable production boundary is the `PaymentVerifier` protocol in `server.py`:
93
+
94
+ ```python
95
+ class PaymentVerifier(Protocol):
96
+ def verify(self, proof: str, challenge: Mapping[str, object], config: PaymentConfig) -> VerificationResult:
97
+ ...
98
+ ```
99
+
100
+ A real verifier should replace `LocalDemoVerifier` only after these rules are explicit:
101
+
102
+ - exact supported network and asset, e.g. Arc Testnet USDC;
103
+ - recipient address ownership and rotation policy;
104
+ - proof format, expiry, replay protection, and nonce storage;
105
+ - settlement finality requirements;
106
+ - logging and privacy policy for payment proofs;
107
+ - failure behavior when the verifier is unavailable.
108
+
109
+ ## Safety posture
110
+
111
+ This demo is not a mainnet payment processor. `mainnetEnabled` and `transactionBroadcast` remain `false` in both the challenge and receipt. The local proof is only a deterministic UI/API switch for demonstrating the 402 -> proof -> 200 flow.