@routedock/routedock 0.1.0 → 0.1.2

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 CHANGED
@@ -1,35 +1,33 @@
1
- # @routedock/sdk
1
+ # @routedock/routedock
2
2
 
3
3
  Unified payment execution layer for autonomous agents on Stellar. One SDK for x402, MPP charge, and MPP session — mode selected automatically from the provider's `routedock.json` manifest.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @routedock/sdk
9
- # or
10
- pnpm add @routedock/sdk
8
+ npm install @routedock/routedock
11
9
  ```
12
10
 
13
11
  ## Agent Usage
14
12
 
15
13
  ```ts
16
- import { RouteDockClient } from '@routedock/sdk/client'
14
+ import { RouteDockClient } from '@routedock/routedock/client'
17
15
  import { Keypair } from '@stellar/stellar-sdk'
18
16
 
19
17
  const client = new RouteDockClient({
20
18
  wallet: Keypair.fromSecret(process.env.AGENT_SECRET),
21
19
  network: 'testnet',
22
- spendCap: { daily: '1.00', asset: 'USDC' }, // optional local guard
20
+ spendCap: { daily: '1.00', asset: 'USDC' },
23
21
  })
24
22
 
25
23
  // Single call — SDK reads manifest, picks mode automatically
26
- const result = await client.pay('https://provider.railway.app/price')
24
+ const result = await client.pay('https://provider.example.com/price')
27
25
  // result.data — response body
28
26
  // result.txHash — settlement hash (or null for session vouchers)
29
27
  // result.mode — 'x402' | 'mpp-charge' | 'mpp-session'
30
28
 
31
29
  // Sustained streaming access
32
- const session = await client.openSession('https://provider.railway.app/stream/orderbook')
30
+ const session = await client.openSession('https://provider.example.com/stream/orderbook')
33
31
  for await (const update of session.stream()) {
34
32
  console.log(update)
35
33
  if (done) break
@@ -41,47 +39,88 @@ await session.close() // triggers on-chain settlement
41
39
 
42
40
  ```ts
43
41
  import express from 'express'
44
- import { routedock, SupabaseSessionStore } from '@routedock/sdk/provider'
45
- import { createClient } from '@supabase/supabase-js'
42
+ import { routedock } from '@routedock/routedock/provider'
46
43
 
47
44
  const app = express()
48
45
 
49
- // Price endpoint — x402 + MPP charge
50
46
  app.use('/price', routedock({
51
47
  modes: ['x402', 'mpp-charge'],
52
48
  pricing: { x402: '0.001', 'mpp-charge': '0.0008' },
53
49
  asset: 'USDC',
50
+ assetContract: process.env.USDC_ASSET_CONTRACT,
54
51
  payee: process.env.STELLAR_PAYEE_ADDRESS,
55
- network: process.env.STELLAR_NETWORK as 'testnet' | 'mainnet',
56
- facilitatorApiKey: process.env.OPENZEPPELIN_API_KEY,
52
+ network: 'testnet',
53
+ payeeSecretKey: process.env.STELLAR_PAYEE_SECRET,
54
+ manifest,
55
+ onSettled: async (txHash, amount, mode) => {
56
+ console.log(`settled: ${mode} ${amount} USDC — ${txHash}`)
57
+ },
57
58
  }))
59
+ ```
60
+
61
+ ## Session Lifecycle Hooks
62
+
63
+ For `mpp-session` mode, the middleware exposes three hooks that fire at each stage of the payment channel lifecycle:
58
64
 
59
- // Stream endpoint — MPP session only
60
- const store = new SupabaseSessionStore(createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_KEY))
61
- app.use('/stream/orderbook', routedock({
65
+ ```ts
66
+ app.use('/stream', routedock({
62
67
  modes: ['mpp-session'],
63
- pricing: { 'mpp-session': { rate: '0.0001', channelContract: process.env.CHANNEL_CONTRACT_ID } },
68
+ pricing: { 'mpp-session': { rate: '0.0001', channelContract: CHANNEL_CONTRACT } },
64
69
  asset: 'USDC',
65
- payee: process.env.STELLAR_PAYEE_ADDRESS,
66
- network: process.env.STELLAR_NETWORK as 'testnet' | 'mainnet',
67
- store,
70
+ assetContract: USDC_ASSET_CONTRACT,
71
+ payee: PAYEE_ADDRESS,
72
+ network: 'testnet',
73
+ payeeSecretKey: PAYEE_SECRET,
74
+ manifest,
75
+ commitmentPublicKey: COMMITMENT_PUBLIC_KEY,
76
+
77
+ // Fires once when the first voucher is verified for a new session
78
+ onSessionOpen: async (channelId) => {
79
+ await db.from('sessions').insert({
80
+ channel_id: channelId,
81
+ status: 'open',
82
+ voucher_count: 0,
83
+ })
84
+ },
85
+
86
+ // Fires after each verified ed25519 commitment (off-chain, no tx fee)
87
+ onVoucher: async (voucherIndex, cumulativeAmount) => {
88
+ await db.from('sessions').update({
89
+ voucher_count: voucherIndex,
90
+ cumulative_amount: parseFloat(cumulativeAmount),
91
+ }).eq('channel_id', channelId)
92
+ },
93
+
94
+ // Fires after the on-chain channel close settles the cumulative amount
95
+ onSettled: async (txHash, totalPaid, mode) => {
96
+ await db.from('sessions').update({
97
+ status: 'closed',
98
+ settlement_tx_hash: txHash,
99
+ }).eq('channel_id', channelId)
100
+ },
68
101
  }))
69
102
  ```
70
103
 
104
+ | Hook | When it fires | On-chain? |
105
+ |---|---|---|
106
+ | `onSessionOpen(channelId)` | First verified voucher in a new session | No |
107
+ | `onVoucher(index, cumulativeAmount)` | Each verified ed25519 commitment | No |
108
+ | `onSettled(txHash, amount, mode)` | Channel close transaction confirmed | Yes |
109
+
71
110
  ## Mode Selection Logic
72
111
 
73
112
  The `ModeRouter` uses this decision tree (deterministic, no randomness):
74
113
 
75
114
  1. If `{ sustained: true }` passed AND manifest supports `mpp-session` → session
76
115
  2. Else if manifest supports `mpp-charge` AND network is Stellar → MPP charge (lower fees)
77
- 3. Else if manifest supports `x402` → x402 with OZ facilitator
116
+ 3. Else if manifest supports `x402` → x402 with facilitator
78
117
  4. Else throw `RouteDockNoSupportedModeError`
79
118
 
80
119
  Override with `{ forceMode: 'x402' }` to bypass auto-selection.
81
120
 
82
121
  ## Security
83
122
 
84
- The `one-way-channel` Soroban contract wrapped by `MppSessionClient` is **unaudited** (`stellar-experimental/one-way-channel`). Safe defaults are enforced: 17280 ledger refund window, durable `SupabaseSessionStore` with monotonic invariant. Production use should await a formal audit.
123
+ The `one-way-channel` Soroban contract wrapped by `MppSessionClient` is **unaudited** (`stellar-experimental/one-way-channel`). Safe defaults are enforced: 17280 ledger refund window, monotonic cumulative enforcement at application, database, and contract layers. Production mainnet use should await a formal audit.
85
124
 
86
125
  ## License
87
126
 
package/dist/index.cjs CHANGED
@@ -10,9 +10,11 @@ var client$3 = require('@stellar/mpp/charge/client');
10
10
  var client$2 = require('mppx/client');
11
11
  var client$4 = require('@stellar/mpp/channel/client');
12
12
  var facilitator = require('@x402/stellar/exact/facilitator');
13
- var server$1 = require('@stellar/mpp/charge/server');
14
- var server = require('mppx/server');
15
- var server$2 = require('@stellar/mpp/channel/server');
13
+ var server$1 = require('@x402/stellar/exact/server');
14
+ var server = require('@x402/core/server');
15
+ var server$3 = require('@stellar/mpp/charge/server');
16
+ var server$2 = require('mppx/server');
17
+ var server$4 = require('@stellar/mpp/channel/server');
16
18
 
17
19
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
20
 
@@ -611,13 +613,29 @@ var CAIP22 = {
611
613
  testnet: "stellar:testnet",
612
614
  mainnet: "stellar:pubnet"
613
615
  };
616
+ var OZ_FACILITATOR_URL = "https://channels.openzeppelin.com/x402";
614
617
  function createX402Handler(opts) {
615
618
  const caip2 = CAIP22[opts.network];
616
619
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
617
620
  const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
618
- const facilitatorScheme = new facilitator.ExactStellarScheme([signer], {
621
+ const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
622
+ const localFacilitator = new facilitator.ExactStellarScheme([signer], {
619
623
  areFeesSponsored: true
620
624
  });
625
+ let ozServer = null;
626
+ if (useOzFacilitator) {
627
+ const apiKey = opts.facilitatorApiKey;
628
+ const facilitator = new server.HTTPFacilitatorClient({
629
+ url: OZ_FACILITATOR_URL,
630
+ createAuthHeaders: async () => ({
631
+ verify: { Authorization: `Bearer ${apiKey}` },
632
+ settle: { Authorization: `Bearer ${apiKey}` },
633
+ supported: { Authorization: `Bearer ${apiKey}` }
634
+ })
635
+ });
636
+ ozServer = new server.x402ResourceServer(facilitator);
637
+ ozServer.register(caip2, new server$1.ExactStellarScheme());
638
+ }
621
639
  const amountInBaseUnits = String(Math.round(parseFloat(opts.amount) * 1e7));
622
640
  const requirements = {
623
641
  scheme: "exact",
@@ -628,52 +646,78 @@ function createX402Handler(opts) {
628
646
  maxTimeoutSeconds: 60,
629
647
  extra: {
630
648
  areFeesSponsored: true,
631
- facilitatorAddresses: [payeeKeypair.publicKey()]
649
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
632
650
  }
633
651
  };
634
652
  return async (req, res, next) => {
635
653
  try {
636
654
  const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
637
655
  if (!paymentHeader) {
638
- const x402Response = {
639
- x402Version: 2,
640
- resource: {
656
+ if (ozServer) {
657
+ const resourceInfo = {
641
658
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
642
659
  description: opts.manifest.name
643
- },
644
- accepts: [requirements]
645
- };
646
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
660
+ };
661
+ const paymentRequired = await ozServer.createPaymentRequiredResponse(
662
+ [requirements],
663
+ resourceInfo
664
+ );
665
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
666
+ } else {
667
+ const x402Response = {
668
+ x402Version: 2,
669
+ resource: {
670
+ url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
671
+ description: opts.manifest.name
672
+ },
673
+ accepts: [requirements]
674
+ };
675
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
676
+ }
647
677
  return;
648
678
  }
649
679
  const payload = http.decodePaymentSignatureHeader(paymentHeader);
650
- const verifyResult = await facilitatorScheme.verify(
651
- payload,
652
- requirements
653
- );
654
- if (!verifyResult.isValid) {
655
- res.status(401).json({
656
- error: "Payment verification failed",
657
- reason: verifyResult.invalidReason
658
- });
659
- return;
680
+ let txHash = null;
681
+ if (ozServer) {
682
+ const settleResult = await ozServer.settlePayment(payload, requirements);
683
+ txHash = settleResult.transaction ?? null;
684
+ if (settleResult) {
685
+ res.setHeader(
686
+ "X-Payment-Response",
687
+ http.encodePaymentResponseHeader(
688
+ settleResult
689
+ )
690
+ );
691
+ }
692
+ } else {
693
+ const verifyResult = await localFacilitator.verify(
694
+ payload,
695
+ requirements
696
+ );
697
+ if (!verifyResult.isValid) {
698
+ res.status(401).json({
699
+ error: "Payment verification failed",
700
+ reason: verifyResult.invalidReason
701
+ });
702
+ return;
703
+ }
704
+ const settleResult = await localFacilitator.settle(
705
+ payload,
706
+ requirements
707
+ );
708
+ txHash = settleResult.transaction ?? null;
709
+ if (settleResult) {
710
+ res.setHeader(
711
+ "X-Payment-Response",
712
+ http.encodePaymentResponseHeader(
713
+ settleResult
714
+ )
715
+ );
716
+ }
660
717
  }
661
- const settleResult = await facilitatorScheme.settle(
662
- payload,
663
- requirements
664
- );
665
- const txHash = settleResult.transaction ?? null;
666
718
  if (txHash && opts.onSettled) {
667
719
  await opts.onSettled(txHash, opts.amount, "x402");
668
720
  }
669
- if (settleResult) {
670
- res.setHeader(
671
- "X-Payment-Response",
672
- http.encodePaymentResponseHeader(
673
- settleResult
674
- )
675
- );
676
- }
677
721
  next();
678
722
  } catch (err) {
679
723
  console.error("[x402] Settlement error:", err);
@@ -688,10 +732,10 @@ var MPP_NETWORK = {
688
732
  function createMppChargeHandler(opts) {
689
733
  const networkId = MPP_NETWORK[opts.network];
690
734
  const amountHumanReadable = opts.amount;
691
- const mppx = server.Mppx.create({
735
+ const mppx = server$2.Mppx.create({
692
736
  secretKey: opts.payeeSecretKey,
693
737
  methods: [
694
- server$1.stellar.charge({
738
+ server$3.stellar.charge({
695
739
  recipient: opts.manifest.payee,
696
740
  currency: opts.assetContract,
697
741
  network: networkId,
@@ -701,7 +745,7 @@ function createMppChargeHandler(opts) {
701
745
  });
702
746
  return async (req, res, next) => {
703
747
  try {
704
- const fetchReq = server.Request.fromNodeListener(req, res);
748
+ const fetchReq = server$2.Request.fromNodeListener(req, res);
705
749
  const handler = mppx["stellar/charge"];
706
750
  const result = await handler({
707
751
  amount: amountHumanReadable,
@@ -745,8 +789,10 @@ function createMppSessionHandler(opts) {
745
789
  const rateHuman = opts.rate;
746
790
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
747
791
  const cumulativeKey = `stellar:channel:cumulative:${opts.channelContract}`;
748
- const innerStore = server$2.Store.memory();
792
+ const innerStore = server$4.Store.memory();
749
793
  let lastCumulativeAmount = 0n;
794
+ let voucherCount = 0;
795
+ let sessionOpened = false;
750
796
  const wrappedStore = {
751
797
  async get(key) {
752
798
  return innerStore.get(key);
@@ -755,16 +801,27 @@ function createMppSessionHandler(opts) {
755
801
  await innerStore.put(key, value);
756
802
  if (key === cumulativeKey && value && typeof value === "object" && "amount" in value) {
757
803
  lastCumulativeAmount = BigInt(value.amount);
804
+ voucherCount++;
805
+ if (!sessionOpened) {
806
+ sessionOpened = true;
807
+ if (opts.onSessionOpen) {
808
+ await opts.onSessionOpen(opts.channelContract);
809
+ }
810
+ }
811
+ if (opts.onVoucher) {
812
+ const humanAmount = (Number(lastCumulativeAmount) / 1e7).toFixed(7);
813
+ await opts.onVoucher(voucherCount, humanAmount);
814
+ }
758
815
  }
759
816
  },
760
817
  async delete(key) {
761
818
  return innerStore.delete(key);
762
819
  }
763
820
  };
764
- const mppx = server.Mppx.create({
821
+ const mppx = server$2.Mppx.create({
765
822
  secretKey: opts.payeeSecretKey,
766
823
  methods: [
767
- server$2.stellar.channel({
824
+ server$4.stellar.channel({
768
825
  channel: opts.channelContract,
769
826
  commitmentKey: opts.commitmentPublicKey,
770
827
  network: networkId,
@@ -782,7 +839,7 @@ function createMppSessionHandler(opts) {
782
839
  const closeAmount = body?.amount ? BigInt(body.amount) : lastCumulativeAmount;
783
840
  const closeSig = body?.signature ?? lastSignatureHex;
784
841
  if (closeAmount > 0n && closeSig) {
785
- const closeTxHash = await server$2.close({
842
+ const closeTxHash = await server$4.close({
786
843
  channel: opts.channelContract,
787
844
  amount: closeAmount,
788
845
  signature: Buffer.from(closeSig, "hex"),
@@ -797,6 +854,9 @@ function createMppSessionHandler(opts) {
797
854
  } else {
798
855
  res.json({ closeTxHash: null, message: "no vouchers received" });
799
856
  }
857
+ sessionOpened = false;
858
+ voucherCount = 0;
859
+ lastCumulativeAmount = 0n;
800
860
  return;
801
861
  }
802
862
  const authHeader = req.headers["authorization"];
@@ -813,7 +873,7 @@ function createMppSessionHandler(opts) {
813
873
  } catch {
814
874
  }
815
875
  }
816
- const fetchReq = server.Request.fromNodeListener(req, res);
876
+ const fetchReq = server$2.Request.fromNodeListener(req, res);
817
877
  const result = await mppx.channel({
818
878
  amount: rateHuman,
819
879
  description: opts.manifest.name
@@ -882,7 +942,9 @@ function routedock(opts) {
882
942
  assetContract: opts.assetContract,
883
943
  manifest: opts.manifest,
884
944
  commitmentPublicKey: opts.commitmentPublicKey,
885
- ...opts.onSettled ? { onSettled: opts.onSettled } : {}
945
+ ...opts.onSettled ? { onSettled: opts.onSettled } : {},
946
+ ...opts.onSessionOpen ? { onSessionOpen: opts.onSessionOpen } : {},
947
+ ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {}
886
948
  })
887
949
  );
888
950
  }
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { P as PaymentMode, a as PaymentResult, b as PricingConfig, R as RouteDockError, c as RouteDockManifest, d as RouteDockManifestError, e as RouteDockNoSupportedModeError, f as RouteDockPolicyRejectedError, g as RouteDockSessionError, S as SessionCloseResult, h as SessionHandle, i as SessionPricingConfig, j as SessionState, k as SessionStore, l as SupabaseSessionStore } from './SessionStore-CCVO0PHM.cjs';
2
2
  export { ModeSelectOptions, MppChargeClient, MppSessionClient, RouteDockClient, RouteDockClientConfig, SpendCap, X402Client, fetchManifest, selectMode } from './client.cjs';
3
- export { R as RouteDockMiddlewareOptions, r as routedock } from './routedockMiddleware-Beqan6QS.cjs';
3
+ export { R as RouteDockMiddlewareOptions, r as routedock } from './routedockMiddleware-B7WTB3LT.cjs';
4
4
  import '@supabase/supabase-js';
5
5
  import '@stellar/stellar-sdk';
6
6
  import 'express';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { P as PaymentMode, a as PaymentResult, b as PricingConfig, R as RouteDockError, c as RouteDockManifest, d as RouteDockManifestError, e as RouteDockNoSupportedModeError, f as RouteDockPolicyRejectedError, g as RouteDockSessionError, S as SessionCloseResult, h as SessionHandle, i as SessionPricingConfig, j as SessionState, k as SessionStore, l as SupabaseSessionStore } from './SessionStore-CCVO0PHM.js';
2
2
  export { ModeSelectOptions, MppChargeClient, MppSessionClient, RouteDockClient, RouteDockClientConfig, SpendCap, X402Client, fetchManifest, selectMode } from './client.js';
3
- export { R as RouteDockMiddlewareOptions, r as routedock } from './routedockMiddleware-CqnyHjhO.js';
3
+ export { R as RouteDockMiddlewareOptions, r as routedock } from './routedockMiddleware-DRgdrycG.js';
4
4
  import '@supabase/supabase-js';
5
5
  import '@stellar/stellar-sdk';
6
6
  import 'express';
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import { stellar } from '@stellar/mpp/charge/client';
8
8
  import { Mppx } from 'mppx/client';
9
9
  import { stellar as stellar$1 } from '@stellar/mpp/channel/client';
10
10
  import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/facilitator';
11
+ import { ExactStellarScheme as ExactStellarScheme$2 } from '@x402/stellar/exact/server';
12
+ import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
11
13
  import { stellar as stellar$2 } from '@stellar/mpp/charge/server';
12
14
  import { Mppx as Mppx$1, Request } from 'mppx/server';
13
15
  import { Store, stellar as stellar$3, close } from '@stellar/mpp/channel/server';
@@ -605,13 +607,29 @@ var CAIP22 = {
605
607
  testnet: "stellar:testnet",
606
608
  mainnet: "stellar:pubnet"
607
609
  };
610
+ var OZ_FACILITATOR_URL = "https://channels.openzeppelin.com/x402";
608
611
  function createX402Handler(opts) {
609
612
  const caip2 = CAIP22[opts.network];
610
613
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
611
614
  const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
612
- const facilitatorScheme = new ExactStellarScheme$1([signer], {
615
+ const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
616
+ const localFacilitator = new ExactStellarScheme$1([signer], {
613
617
  areFeesSponsored: true
614
618
  });
619
+ let ozServer = null;
620
+ if (useOzFacilitator) {
621
+ const apiKey = opts.facilitatorApiKey;
622
+ const facilitator = new HTTPFacilitatorClient({
623
+ url: OZ_FACILITATOR_URL,
624
+ createAuthHeaders: async () => ({
625
+ verify: { Authorization: `Bearer ${apiKey}` },
626
+ settle: { Authorization: `Bearer ${apiKey}` },
627
+ supported: { Authorization: `Bearer ${apiKey}` }
628
+ })
629
+ });
630
+ ozServer = new x402ResourceServer(facilitator);
631
+ ozServer.register(caip2, new ExactStellarScheme$2());
632
+ }
615
633
  const amountInBaseUnits = String(Math.round(parseFloat(opts.amount) * 1e7));
616
634
  const requirements = {
617
635
  scheme: "exact",
@@ -622,52 +640,78 @@ function createX402Handler(opts) {
622
640
  maxTimeoutSeconds: 60,
623
641
  extra: {
624
642
  areFeesSponsored: true,
625
- facilitatorAddresses: [payeeKeypair.publicKey()]
643
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
626
644
  }
627
645
  };
628
646
  return async (req, res, next) => {
629
647
  try {
630
648
  const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
631
649
  if (!paymentHeader) {
632
- const x402Response = {
633
- x402Version: 2,
634
- resource: {
650
+ if (ozServer) {
651
+ const resourceInfo = {
635
652
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
636
653
  description: opts.manifest.name
637
- },
638
- accepts: [requirements]
639
- };
640
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
654
+ };
655
+ const paymentRequired = await ozServer.createPaymentRequiredResponse(
656
+ [requirements],
657
+ resourceInfo
658
+ );
659
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
660
+ } else {
661
+ const x402Response = {
662
+ x402Version: 2,
663
+ resource: {
664
+ url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
665
+ description: opts.manifest.name
666
+ },
667
+ accepts: [requirements]
668
+ };
669
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
670
+ }
641
671
  return;
642
672
  }
643
673
  const payload = decodePaymentSignatureHeader(paymentHeader);
644
- const verifyResult = await facilitatorScheme.verify(
645
- payload,
646
- requirements
647
- );
648
- if (!verifyResult.isValid) {
649
- res.status(401).json({
650
- error: "Payment verification failed",
651
- reason: verifyResult.invalidReason
652
- });
653
- return;
674
+ let txHash = null;
675
+ if (ozServer) {
676
+ const settleResult = await ozServer.settlePayment(payload, requirements);
677
+ txHash = settleResult.transaction ?? null;
678
+ if (settleResult) {
679
+ res.setHeader(
680
+ "X-Payment-Response",
681
+ encodePaymentResponseHeader(
682
+ settleResult
683
+ )
684
+ );
685
+ }
686
+ } else {
687
+ const verifyResult = await localFacilitator.verify(
688
+ payload,
689
+ requirements
690
+ );
691
+ if (!verifyResult.isValid) {
692
+ res.status(401).json({
693
+ error: "Payment verification failed",
694
+ reason: verifyResult.invalidReason
695
+ });
696
+ return;
697
+ }
698
+ const settleResult = await localFacilitator.settle(
699
+ payload,
700
+ requirements
701
+ );
702
+ txHash = settleResult.transaction ?? null;
703
+ if (settleResult) {
704
+ res.setHeader(
705
+ "X-Payment-Response",
706
+ encodePaymentResponseHeader(
707
+ settleResult
708
+ )
709
+ );
710
+ }
654
711
  }
655
- const settleResult = await facilitatorScheme.settle(
656
- payload,
657
- requirements
658
- );
659
- const txHash = settleResult.transaction ?? null;
660
712
  if (txHash && opts.onSettled) {
661
713
  await opts.onSettled(txHash, opts.amount, "x402");
662
714
  }
663
- if (settleResult) {
664
- res.setHeader(
665
- "X-Payment-Response",
666
- encodePaymentResponseHeader(
667
- settleResult
668
- )
669
- );
670
- }
671
715
  next();
672
716
  } catch (err) {
673
717
  console.error("[x402] Settlement error:", err);
@@ -741,6 +785,8 @@ function createMppSessionHandler(opts) {
741
785
  const cumulativeKey = `stellar:channel:cumulative:${opts.channelContract}`;
742
786
  const innerStore = Store.memory();
743
787
  let lastCumulativeAmount = 0n;
788
+ let voucherCount = 0;
789
+ let sessionOpened = false;
744
790
  const wrappedStore = {
745
791
  async get(key) {
746
792
  return innerStore.get(key);
@@ -749,6 +795,17 @@ function createMppSessionHandler(opts) {
749
795
  await innerStore.put(key, value);
750
796
  if (key === cumulativeKey && value && typeof value === "object" && "amount" in value) {
751
797
  lastCumulativeAmount = BigInt(value.amount);
798
+ voucherCount++;
799
+ if (!sessionOpened) {
800
+ sessionOpened = true;
801
+ if (opts.onSessionOpen) {
802
+ await opts.onSessionOpen(opts.channelContract);
803
+ }
804
+ }
805
+ if (opts.onVoucher) {
806
+ const humanAmount = (Number(lastCumulativeAmount) / 1e7).toFixed(7);
807
+ await opts.onVoucher(voucherCount, humanAmount);
808
+ }
752
809
  }
753
810
  },
754
811
  async delete(key) {
@@ -791,6 +848,9 @@ function createMppSessionHandler(opts) {
791
848
  } else {
792
849
  res.json({ closeTxHash: null, message: "no vouchers received" });
793
850
  }
851
+ sessionOpened = false;
852
+ voucherCount = 0;
853
+ lastCumulativeAmount = 0n;
794
854
  return;
795
855
  }
796
856
  const authHeader = req.headers["authorization"];
@@ -876,7 +936,9 @@ function routedock(opts) {
876
936
  assetContract: opts.assetContract,
877
937
  manifest: opts.manifest,
878
938
  commitmentPublicKey: opts.commitmentPublicKey,
879
- ...opts.onSettled ? { onSettled: opts.onSettled } : {}
939
+ ...opts.onSettled ? { onSettled: opts.onSettled } : {},
940
+ ...opts.onSessionOpen ? { onSessionOpen: opts.onSessionOpen } : {},
941
+ ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {}
880
942
  })
881
943
  );
882
944
  }