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 +3 -0
- package/REAL_PAYMENT_SETUP.md +43 -0
- package/RELEASE_NOTES.md +5 -10
- package/examples/generated.carried-handoff.json +3 -3
- package/lib/payment_config.js +4 -1
- package/lib/x402_lane_contract.js +16 -0
- package/package.json +1 -1
- package/scripts/verify_x402_facilitator.js +20 -0
- package/server.js +109 -0
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=
|
package/REAL_PAYMENT_SETUP.md
CHANGED
|
@@ -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.
|
|
1
|
+
`xytara` 3.1.4 makes the self-serve credits capability discoverable and documents the path to Base mainnet.
|
|
2
2
|
|
|
3
|
-
This
|
|
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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": "
|
|
10
|
-
"receipt_id": "
|
|
11
|
-
"proof_ref": "
|
|
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",
|
package/lib/payment_config.js
CHANGED
|
@@ -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
|
@@ -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 = {
|