xytara 3.1.2 → 3.1.4

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/.env.example CHANGED
@@ -10,6 +10,9 @@ XYTARA_X402_FACILITATOR_URL=https://x402.org/facilitator
10
10
  XYTARA_X402_FACILITATOR_HEADERS_JSON={}
11
11
  XYTARA_X402_FACILITATOR_TIMEOUT_MS=15000
12
12
  XYTARA_X402_SETTLE=true
13
+ # credit units a single self-serve /x402/credits/topup grants (default 1000);
14
+ # an accepts entry may override per-payment via its extra.credits
15
+ XYTARA_X402_TOPUP_UNITS=1000
13
16
  XYTARA_PAYMENT_MAX_AGE_SECONDS=300
14
17
  XYTARA_WALLET_REGISTRY_JSON=[]
15
18
  XYTARA_TREASURY_DESTINATION_ID=
@@ -45,6 +45,49 @@ is identical to the one in `services/mcp-execution-target/buyer-demo/` (an EIP-3
45
45
  authorization in a `PAYMENT-SIGNATURE` header); only the request body differs, since the main
46
46
  runtime takes a command-execute body rather than an MCP JSON-RPC call.
47
47
 
48
+ ### Self-serve credits top-up
49
+
50
+ Beyond paying per call, a buyer can convert a single x402 payment into spendable
51
+ runtime credits with no operator grant, via `POST /x402/credits/topup`:
52
+
53
+ - without a `PAYMENT-SIGNATURE`, it returns `402` advertising the same `accepts` plus
54
+ `credits_per_topup` (how many credit units one payment buys)
55
+ - with a valid payment, the facilitator settles it on-chain and the runtime mints the
56
+ credits to the account, returning the grant, the new balance, and a `PAYMENT-RESPONSE`
57
+ settlement receipt
58
+ - credits land on `account_id` from the body, falling back to the paying wallet address
59
+ - replays are idempotent: the payment nonce keys the grant, so re-sending the same settled
60
+ payment never mints twice
61
+
62
+ Each top-up grants `XYTARA_X402_TOPUP_UNITS` credit units (default `1000`), unless the matched
63
+ `accepts` entry sets its own `extra.credits`. The route is live only in `x402_facilitator`
64
+ mode with `XYTARA_X402_ACCEPTS_JSON` set, and credits persist only when runtime state
65
+ persistence (`XYTARA_SUPABASE_*` or `XYTARA_STATE_FILE`) is configured. A runnable buyer is in
66
+ `services/mcp-execution-target/buyer-demo/x402-credits-topup-demo.js`.
67
+
68
+ ### Going to Base mainnet (real USDC)
69
+
70
+ Base Sepolia uses test USDC. To accept real value, advertise a Base mainnet profile and
71
+ settle it through a mainnet-capable facilitator.
72
+
73
+ Important: the default `https://x402.org/facilitator` settles **Base Sepolia only** (its
74
+ `/supported` list has no `eip155:8453`). Pointing a mainnet `accepts` entry at it fails
75
+ closed and the route is unpayable. For mainnet you need a mainnet-capable facilitator, such
76
+ as Coinbase CDP (`https://api.cdp.coinbase.com/platform/v2/x402`), which requires a CDP API
77
+ key passed as auth:
78
+
79
+ ```env
80
+ # advertise mainnet (real USDC on Base); you can keep a Sepolia entry in the array too
81
+ XYTARA_X402_ACCEPTS_JSON=[{"scheme":"exact","network":"eip155:8453","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","amount":"10000","payTo":"<your-evm-address>","extra":{"name":"USDC","version":"2"},"maxTimeoutSeconds":60}]
82
+ # a mainnet-capable facilitator (CDP shown) and its auth header:
83
+ XYTARA_X402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402
84
+ XYTARA_X402_FACILITATOR_HEADERS_JSON={"Authorization":"Bearer <cdp-access-token>"}
85
+ ```
86
+
87
+ `amount` is in the asset's base units (USDC has 6 decimals, so `10000` = 0.01 USDC). Confirm
88
+ your facilitator lists `eip155:8453` in its `/supported` response before advertising it, and
89
+ remember that once mainnet is live, buyers spend real USDC.
90
+
48
91
  ## Minimum Production-Oriented Configuration
49
92
 
50
93
  Set these environment variables:
package/RELEASE_NOTES.md CHANGED
@@ -1,14 +1,9 @@
1
- `xytara` 3.1.2 adds trustless, on-chain, pay-per-call settlement to the main runtime.
1
+ `xytara` 3.1.4 makes the self-serve credits capability discoverable and documents the path to Base mainnet.
2
2
 
3
- This release brings the facilitator verify-and-settle mechanism already proven on the public MCP executor to the runtime itself, so every x402 route can settle a real payment on-chain before any work runs, not just the standalone tool server.
3
+ This is a discovery and documentation release on top of the self-serve credits shipped earlier in the line. No existing behavior changes.
4
4
 
5
5
  Highlights:
6
6
 
7
- - adds a third payment-acceptance mode, `x402_facilitator`, enabled with `XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator`
8
- - when enabled, `/x402/commands/execute` and `/x402/mcp/tools/invoke` return a `402` that advertises the configured `accepts` (network, asset, amount, payTo) in the `PAYMENT-REQUIRED` header
9
- - a buyer signs a gasless USDC authorization (EIP-3009); on the paid retry a facilitator verifies and settles it on-chain before any work runs, and the response carries a `PAYMENT-RESPONSE` settlement receipt
10
- - fails closed: if the facilitator is unreachable or the payment does not settle, nothing runs and no result is returned
11
- - the mode governs only the x402 routes; native and instant lanes fall back to `local_signed`, so no existing paid path is disabled
12
- - keeps `local_signed` as the default and `presence` as the local-development opt-in
13
- - adds `npm run verify:x402-facilitator`, an end-to-end check against a mock facilitator covering the advertised challenge, malformed and mismatched payments, facilitator rejection, failed settlement, and a settled success with receipt
14
- - documents the mode and its environment in `REAL_PAYMENT_SETUP.md` and the architecture in `ARCHITECTURE.md`
7
+ - the x402 lane summary (`/v1/x402/summary`) and lane pack now advertise `credits_topup_ref` (`/x402/credits/topup`) and a `self_serve_credits` first-run flow, so agents discovering the runtime see the pay-once-get-credits path alongside direct command execution and paid MCP tooling
8
+ - documents going to Base mainnet in `REAL_PAYMENT_SETUP.md`: the mainnet `accepts` profile (real USDC on `eip155:8453`) and the requirement for a mainnet-capable facilitator, since the default `x402.org` facilitator settles Base Sepolia only
9
+ - no change to payment verification, settlement, the credits ledger, or any existing route
@@ -6,9 +6,9 @@
6
6
  "source_ref": "spine.job.commitment",
7
7
  "committed_ref": "transaction.job.commitment",
8
8
  "precommit_pack_ref": "precommit.job.commitment.readiness",
9
- "transaction_id": "txn_7f5d7cd15658",
10
- "receipt_id": "rcpt_2684569e5241",
11
- "proof_ref": "proof_rcpt_2684569e5241",
9
+ "transaction_id": "txn_01b29e65db7d",
10
+ "receipt_id": "rcpt_b40aa20be45e",
11
+ "proof_ref": "proof_rcpt_b40aa20be45e",
12
12
  "payment_protocol": "x402",
13
13
  "settlement_mode": "evm_payment",
14
14
  "treasury_destination_id": "treasury_acct_demo",
@@ -171,7 +171,10 @@ function loadX402FacilitatorConfig() {
171
171
  facilitator_url: String(process.env.XYTARA_X402_FACILITATOR_URL || "https://x402.org/facilitator").trim().replace(/\/$/, ""),
172
172
  facilitator_headers: headers,
173
173
  facilitator_timeout_ms: Math.max(1000, Number(process.env.XYTARA_X402_FACILITATOR_TIMEOUT_MS || 15000) || 15000),
174
- settle: String(process.env.XYTARA_X402_SETTLE || "true").trim().toLowerCase() !== "false"
174
+ settle: String(process.env.XYTARA_X402_SETTLE || "true").trim().toLowerCase() !== "false",
175
+ // Credit units a single self-serve x402 top-up grants, unless the matched accepts
176
+ // entry carries its own `extra.credits`.
177
+ topup_units_default: Math.max(0, Number(process.env.XYTARA_X402_TOPUP_UNITS || 1000) || 0)
175
178
  };
176
179
  }
177
180
 
@@ -36,6 +36,7 @@ function buildX402LanePack() {
36
36
  quote_detail_ref_template: "/v1/payment-quotes/:quote_id",
37
37
  command_execute_ref: "/x402/commands/execute",
38
38
  mcp_paid_invoke_ref: "/x402/mcp/tools/invoke",
39
+ credits_topup_ref: "/x402/credits/topup",
39
40
  payment_ledger_ref: "/v1/payment-ledger",
40
41
  native_settlement_ref: "/v1/settlement/bsv-teranode",
41
42
  native_settlement_readiness_ref: "/v1/settlement/bsv-teranode/readiness",
@@ -74,6 +75,21 @@ function buildX402LanePack() {
74
75
  paid_invoke_ref: "/x402/mcp/tools/invoke",
75
76
  settlement_ref: "/v1/settlement/bsv-teranode"
76
77
  }
78
+ },
79
+ {
80
+ flow_ref: "x402.self_serve_credits",
81
+ recommended_reason: "pay once with x402 and receive spendable runtime credits, with no operator grant",
82
+ sequence: [
83
+ "request_topup_without_payment",
84
+ "receive_x402_challenge_with_credits_per_topup",
85
+ "pay_and_mint_credits",
86
+ "spend_credits_on_subsequent_calls"
87
+ ],
88
+ example_surfaces: {
89
+ credits_topup_ref: "/x402/credits/topup",
90
+ command_execute_ref: "/x402/commands/execute",
91
+ payment_ledger_ref: "/v1/payment-ledger"
92
+ }
77
93
  }
78
94
  ],
79
95
  settlement_posture: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xytara",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "Agent-commerce runtime for quote, pay, execute, deliver, meter, and integrate.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -158,6 +158,26 @@ async function main() {
158
158
  });
159
159
  assert.notStrictEqual(nativeAttempt.json && nativeAttempt.json.reason, "payment_verification_mode_unsupported", "native route must fall back to local_signed under x402_facilitator");
160
160
 
161
+ // 8. Self-serve credits top-up: unpaid -> 402 advertising accepts + credits_per_topup.
162
+ const topupUnpaid = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" });
163
+ assert.strictEqual(topupUnpaid.status, 402, "unpaid topup must 402");
164
+ assert.ok(Array.isArray(topupUnpaid.json.payment_required.accepts), "topup 402 must advertise accepts");
165
+ assert.strictEqual(typeof topupUnpaid.json.payment_required.credits_per_topup, "number", "topup 402 must state credits_per_topup");
166
+
167
+ // 9. Paid top-up -> 200, credits minted to the account, with a settlement receipt.
168
+ const topupPaid = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" }, { "payment-signature": paymentHeader("ok") });
169
+ assert.strictEqual(topupPaid.status, 200, "paid topup must 200");
170
+ assert.strictEqual(topupPaid.json.ok, true, "paid topup must be ok");
171
+ assert.strictEqual(typeof topupPaid.json.credit_grant, "object", "paid topup must return a credit grant");
172
+ const grantedUnits = Number(topupPaid.json.balance.available_units);
173
+ assert.ok(grantedUnits > 0, "paid topup must leave a positive credit balance");
174
+ assert.strictEqual(typeof topupPaid.headers.get("payment-response"), "string", "paid topup must carry a settlement receipt");
175
+
176
+ // 10. Replayed top-up (same payment nonce) is idempotent: no double-mint.
177
+ const topupReplay = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" }, { "payment-signature": paymentHeader("ok") });
178
+ assert.strictEqual(topupReplay.status, 200, "replayed topup must still 200");
179
+ assert.strictEqual(Number(topupReplay.json.balance.available_units), grantedUnits, "replayed topup must not mint credits twice");
180
+
161
181
  console.log("xytara verify:x402-facilitator passed");
162
182
  } finally {
163
183
  await new Promise((resolve) => server.close(resolve));
package/server.js CHANGED
@@ -50,6 +50,45 @@ function buildPaymentRequiredHeaders(paymentRequiredPayload) {
50
50
  };
51
51
  }
52
52
 
53
+ const {
54
+ createFacilitatorClient: createX402FacilitatorClient,
55
+ verifyX402Payment,
56
+ settleX402Payment,
57
+ encodePaymentResponse: encodeX402PaymentResponse
58
+ } = require("./lib/x402_facilitator");
59
+
60
+ // Verify and settle an x402 payment for a standalone route (not the command flow).
61
+ // Returns { ok:true, settlement, authorization, paymentResponse } or
62
+ // { ok:false, status, reason, detail, challenge } where challenge means a 402 is appropriate.
63
+ async function settleX402ForRoute(req, url) {
64
+ const config = loadPaymentConfig();
65
+ if (config.verification_mode !== "x402_facilitator") {
66
+ return { ok: false, status: 400, reason: "x402_facilitator_mode_not_enabled" };
67
+ }
68
+ if (!config.x402.accepts.length) {
69
+ return { ok: false, status: 400, reason: "x402_accepts_not_configured" };
70
+ }
71
+ const facilitator = createX402FacilitatorClient({
72
+ url: config.x402.facilitator_url,
73
+ headers: config.x402.facilitator_headers,
74
+ timeoutMs: config.x402.facilitator_timeout_ms
75
+ });
76
+ const auth = await verifyX402Payment({
77
+ paymentHeader: (req.headers || {})["payment-signature"],
78
+ accepts: config.x402.accepts,
79
+ resourceUrl: buildRequestResourceUrl(req, url),
80
+ facilitator
81
+ });
82
+ if (!auth.ok) {
83
+ return { ok: false, status: auth.status || 402, reason: auth.reason, detail: auth.detail || null, challenge: true };
84
+ }
85
+ const settled = await settleX402Payment({ authorization: auth.authorization, facilitator, settle: config.x402.settle });
86
+ if (!settled.ok) {
87
+ return { ok: false, status: settled.status || 402, reason: settled.reason, detail: settled.detail || null, challenge: true };
88
+ }
89
+ return { ok: true, settlement: settled.settlement, authorization: auth.authorization, paymentResponse: encodeX402PaymentResponse(settled.settlement) };
90
+ }
91
+
53
92
  // The public resource URL of the current request, used as the x402 payment resource.
54
93
  function buildRequestResourceUrl(req, url) {
55
94
  const headers = (req && req.headers) || {};
@@ -8938,6 +8977,76 @@ async function routeRequest(req, res) {
8938
8977
  return;
8939
8978
  }
8940
8979
 
8980
+ if (req.method === "POST" && url.pathname === "/x402/credits/topup") {
8981
+ // Self-serve credits: pay once with x402 and credits are minted to the account, with no
8982
+ // operator grant. Only available when the runtime is in x402_facilitator mode with accepts set.
8983
+ const body = await readJsonBody(req);
8984
+ const config = loadPaymentConfig();
8985
+ if (config.verification_mode !== "x402_facilitator") {
8986
+ sendJson(res, 400, { ok: false, reason: "x402_facilitator_mode_not_enabled" });
8987
+ return;
8988
+ }
8989
+ if (!config.x402.accepts.length) {
8990
+ sendJson(res, 400, { ok: false, reason: "x402_accepts_not_configured" });
8991
+ return;
8992
+ }
8993
+ const topupUnitsFor = (accept) => {
8994
+ const perAccept = accept && accept.extra && Number(accept.extra.credits);
8995
+ return Number.isFinite(perAccept) && perAccept > 0 ? perAccept : config.x402.topup_units_default;
8996
+ };
8997
+ const paymentSignatureHeader = ((req.headers || {})["payment-signature"] || "").trim();
8998
+ if (!paymentSignatureHeader) {
8999
+ const paymentRequired = {
9000
+ x402Version: 2,
9001
+ scheme: "exact",
9002
+ accepts: config.x402.accepts,
9003
+ credits_per_topup: topupUnitsFor(config.x402.accepts[0])
9004
+ };
9005
+ sendJson(res, 402, { ok: false, payment_required: paymentRequired }, buildPaymentRequiredHeaders(paymentRequired));
9006
+ return;
9007
+ }
9008
+ const settled = await settleX402ForRoute(req, url);
9009
+ if (!settled.ok) {
9010
+ const payload = { ok: false, reason: settled.reason, ...(settled.detail ? { detail: settled.detail } : {}) };
9011
+ let extraHeaders;
9012
+ if (settled.challenge) {
9013
+ payload.payment_required = { x402Version: 2, scheme: "exact", accepts: config.x402.accepts };
9014
+ extraHeaders = buildPaymentRequiredHeaders(payload.payment_required);
9015
+ }
9016
+ sendJson(res, settled.status, payload, extraHeaders);
9017
+ return;
9018
+ }
9019
+ const matched = settled.authorization.paymentRequirements;
9020
+ const bodyAccountId = body && typeof body.account_id === "string" ? body.account_id.trim() : "";
9021
+ const accountId = bodyAccountId || settled.authorization.payer || "acct_demo";
9022
+ const nonce = settled.authorization.paymentPayload
9023
+ && settled.authorization.paymentPayload.payload
9024
+ && settled.authorization.paymentPayload.payload.authorization
9025
+ ? settled.authorization.paymentPayload.payload.authorization.nonce
9026
+ : null;
9027
+ const grant = createExternalCreditGrant(state, {
9028
+ account_id: accountId,
9029
+ units: topupUnitsFor(matched),
9030
+ source: "x402_settlement",
9031
+ provider_reference: settled.settlement.transaction || null,
9032
+ idempotency_key: nonce ? `x402:${nonce}` : null,
9033
+ metadata: {
9034
+ x402_payer: settled.authorization.payer,
9035
+ x402_network: matched.network,
9036
+ x402_asset: matched.asset,
9037
+ x402_amount: matched.amount
9038
+ }
9039
+ });
9040
+ sendJson(res, 200, {
9041
+ ok: true,
9042
+ account_id: accountId,
9043
+ credit_grant: grant,
9044
+ balance: buildCreditBalanceSummary(state, accountId),
9045
+ settlement: settled.settlement
9046
+ }, { "PAYMENT-RESPONSE": settled.paymentResponse, "Cache-Control": "no-store" });
9047
+ return;
9048
+ }
9049
+
8941
9050
  if (req.method === "POST" && url.pathname === "/l402/commands/execute") {
8942
9051
  const body = await readJsonBody(req);
8943
9052
  const payload = {