diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/RIGHTS.md b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/RIGHTS.md new file mode 100644 index 0000000..569b26c --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/RIGHTS.md @@ -0,0 +1,19 @@ +# Submission rights declaration + +Project: SafeGate Commerce Outcome +Submission slug: nurexen-safegate-commerce-outcome +Submitter: Rahmi Özal +Team / builder: Nurexen Labs +Date: 2026-08-29 + +The submitter confirms that they own, or have sufficient authorization for, the source code, service, data, branding, and other materials submitted in this pull request. + +Subject to the official program terms, the submitter authorizes X-Agent to retain, reproduce, audit, test, archive, and publish the submitted program artifact for judging, fraud prevention, dispute handling, ecosystem submission, and post-award accountability. Closing the pull request, deleting a fork, or deleting an external repository does not revoke the official archive rights attached to an accepted and rewarded entry. + +Third-party components and their licenses: No third-party npm packages are bundled by the submitted verifier. It uses Node.js built-in APIs and a read-only Base Mainnet JSON-RPC service. + +Exceptions or restrictions: This declaration applies only to the public-safe SafeGate Commerce Outcome verifier artifact submitted in this pull request. The private SafeGate core, private signing key, secrets, credentials, private commerce data, and undisclosed infrastructure are not submitted and are outside this declaration. + +Contact: Rahmi Özal - LinkedIn / X @NurexenLabs + +This template is an operational declaration, not a substitute for event terms reviewed by qualified counsel. diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/SUBMISSION.md b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/SUBMISSION.md new file mode 100644 index 0000000..19598a8 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/SUBMISSION.md @@ -0,0 +1,52 @@ +# SafeGate Commerce Outcome + +## Capability + +- **One-line description:** Verifies whether a Base Mainnet USDC payment is bound to a valid SafeGate-signed completed-commerce attestation and returns a deterministic commerce outcome. +- **Who it helps:** AI agents, merchant systems, and commerce infrastructure that need post-payment verification and evidence. +- **Capability boundary:** Read-only verification. It does not custody funds, initiate payments, sign transactions, approve tokens, perform fulfillment, or expose the private SafeGate core. + +## Live API + +- **API base URL:** https://safegate-xagent-commerce-outcome.vercel.app/v1 +- **SafeGate website:** https://safegatelabs.xyz +- **Commerce demo surface:** https://www.safeshelf.store +- **Health-check URL:** https://safegate-xagent-commerce-outcome.vercel.app/health +- **Authentication:** none +- **Rate limits / known limits:** No application-level custom rate limit. Hosting-platform and Base Mainnet RPC limits may apply. Base RPC timeout is 12 seconds. +- **API contract:** POST /v1/commerce-outcome with a SafeGate commerce attestation. GET on the same endpoint returns capability metadata. + +## Source and reproducibility + +- **Source repository:** https://github.com/Nurexen-Labs/safegate-xagent-commerce-outcome +- **Review commit:** b460b88ce800839e69839a5d31aac949dfe2543c +- **Source submitted in this PR:** source/ +- **Run syntax checks:** cd source && npm run check +- **Run tests:** cd source && npm test +- **Run locally:** From source/, set XAGENT_REVIEW_COMMIT=b460b88ce800839e69839a5d31aac949dfe2543c and run vercel dev. +- **Deploy:** Deploy source/ to Vercel with XAGENT_REVIEW_COMMIT set to the exact review commit. +- **Version binding:** /health and /.well-known/xagent-verification.json both expose the exact review commit. + +## Verification + +- **Health-check result:** HTTP 200, status=ok, commit=b460b88ce800839e69839a5d31aac949dfe2543c. +- **Capability call:** POST /v1/commerce-outcome with a SafeGate-signed review attestation. +- **Expected success:** COMMERCE_VERIFIED with PAYMENT_VERIFIED, VALID attestation, FULFILLMENT_COMPLETED, and EVIDENCE_CREATED. +- **Expected error behavior:** Invalid or tampered attestations return HTTP 400 verification errors. Base RPC timeout or upstream RPC failures return HTTP 502. +- Repeatable evidence is documented in verification/README.md. + +## Security and data handling + +- **Data collected:** Submitted commerce-attestation fields and transaction identifiers required for verification. +- **Purpose and retention:** Request data is processed for verification. The submitted application source implements no persistent request-payload storage. +- **Third parties / outbound network calls:** Base Mainnet JSON-RPC via https://mainnet.base.org by default, or operator-supplied BASE_RPC_URL. +- **Secrets:** No secrets or private signing keys are committed. The Ed25519 public verification key is intentionally included. +- **Known risks / restrictions:** Payment is independently checked on Base Mainnet. Fulfillment and evidence status are trusted only when carried by a valid SafeGate Ed25519 attestation. Availability partly depends on the Base RPC endpoint. + +## Support + +- **Team / builder:** Nurexen Labs +- **Submitter:** Rahmi Özal +- **X:** @NurexenLabs +- **LinkedIn:** Rahmi Özal +- **License / rights:** See RIGHTS.md. No additional open-source license is granted by this submission. diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/.gitignore b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/.gitignore new file mode 100644 index 0000000..6f1b383 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +.vercel/ +.env +.env.* +!.env.example +verification/sample-real-commerce-attestation.json +*.key +*.p8 +*.p12 +*.pfx +*private* \ No newline at end of file diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/PUBLIC-SOURCE-SHA256.txt b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/PUBLIC-SOURCE-SHA256.txt new file mode 100644 index 0000000..b2ddaf9 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/PUBLIC-SOURCE-SHA256.txt @@ -0,0 +1,18 @@ +DC6DCFAC9767AF8178E1F995E34F8216900621C42ABE96F24DF17DFDEC9D5793 .gitignore +D12059E67BF51681B8E5596F4AB4B7EDFFFD40E62129FA72BFA1B3140684777E api\commerce-outcome.js +1E28E94C18BE063D705E597DD42580D3C90223B568C7606E2F21B89E82EE00E9 api\health.js +0C44FA19700C2551BC84676EED3AB6EFE5DE4C41C6534C1091E18458C2CC71B0 api\xagent-verification.js +F5199E875CD1A0269F8DAFA36B132ED23719D25DAFE40B382011EAD33D59F8CF lib\attestation-verifier.js +665063C396BFE6AA1751B1328770BBC0A17A8064F5751CFF8ACEC2B060656185 lib\base-usdc.js +6546E70FBA21C030126F5365418CCB300FCABAFC910BA3BDB0B2A796FE854C86 lib\canonical-json.js +2080E830A95BC9E2AC88A88A8A2569EF4C6A346BE95A56B6783127AD76AB4717 lib\commerce-outcome.js +7D41EA92D8DAD2016840554EAE5333A470A83A6FB9329FC7D373F2544F87EAA1 lib\commerce-proof-verifier.js +95DFB27A8ED0FFB39A115B2C76A530D16E72B5A200AEDDED60791BF6B9B0B569 lib\meta.js +F85DD92AE31997CDF1212B271F61C90E801BFC4503F2353D5A068D90046C6937 package.json +134B887BCDD86507330F2F28E6166EDA137A7A9B98208E13B4C8C756EEC8A955 SCOPE.txt +4FAED715D39FABFF6A062EE76EB24F748E2AD364E8669D9AF3C38B3F86DBD6F4 test\attestation-verifier.test.js +058E79864CC11EE7540E3E65AC927A725BD90C7F1BB6B4BD0E2F49D1569FD4E3 test\base-usdc.test.js +6D78DFC3D53BB90680C5C82514EDC62A69E97D824571BDF47CCAE95704B28B8C test\commerce-outcome.test.js +2FE98F63A0C9C1E0954D2CB5C8B4CC2766D243EBAD87DBA07633672B6B7D62F8 vercel.json +478D67701AD4BC141BFEEA7CEF1B18892C21DC8B7188F5FE3DB9F79601891902 verification\public-key.txt +B0E345BBDEA33F88631D2897997C3ED9F1E1E5E3CEC933B49E45C6CE4E495E7F verification\sample-synthetic-attestation.json diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/SCOPE.txt b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/SCOPE.txt new file mode 100644 index 0000000..c0f6e26 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/SCOPE.txt @@ -0,0 +1,29 @@ +SAFEGATE X-AGENT MCP HACKATHON + +Capability: SafeGate Commerce Outcome API + +PURPOSE +Agent-callable post-payment commerce outcome verification. + +CORE OUTPUT +- payment_status +- request_binding +- fulfillment_status +- evidence_status +- evidence_reference +- proof_reference +- outcome + +BOUNDARY +- Does not custody funds +- Does not move funds +- Does not score wallet risk +- Does not audit smart contracts +- Does not detect scams +- Does not provide escrow + +TARGET FLOW +Agent -> Commerce Outcome API -> deterministic commerce state -> evidence/proof + +FUTURE DISTRIBUTION +X-Agent MCP -> OKX Agent ecosystem -> optional x402 paid calls diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/commerce-outcome.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/commerce-outcome.js new file mode 100644 index 0000000..43fd748 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/commerce-outcome.js @@ -0,0 +1,45 @@ +"use strict"; + +const {verifyCommerceProof}=require("../lib/commerce-proof-verifier"); + +module.exports=async function handler(req,res){ + if(req.method==="GET"){ + return res.status(200).json({ + ok:true, + capability:"safegate_commerce_outcome", + version:"0.3.0", + method:"POST", + side_effects:"none", + verifies:[ + "SafeGate Ed25519 commerce attestation", + "Base Mainnet USDC payment" + ] + }); + } + + if(req.method!=="POST"){ + res.setHeader("Allow","GET, POST"); + return res.status(405).json({ok:false,error:{code:"METHOD_NOT_ALLOWED"}}); + } + + try{ + const attestation=req.body?.attestation||req.body; + const result=await verifyCommerceProof(attestation); + return res.status(200).json(result); + }catch(error){ + const code=String(error.code||"VERIFICATION_FAILED"); + const upstream=[ + "BASE_RPC_TIMEOUT", + "BASE_RPC_HTTP_ERROR", + "BASE_RPC_ERROR" + ].includes(code); + + return res.status(upstream?502:400).json({ + ok:false, + error:{ + code, + message:String(error.message||"Commerce verification failed.") + } + }); + } +}; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/health.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/health.js new file mode 100644 index 0000000..dcf9e45 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/health.js @@ -0,0 +1,10 @@ +"use strict"; +const { COMMIT_RE, reviewedCommit } = require("../lib/meta"); + +module.exports = function handler(req, res) { + const commit = reviewedCommit(); + if (!COMMIT_RE.test(commit)) { + return res.status(503).json({ status: "misconfigured", code: "REVIEW_COMMIT_NOT_SET" }); + } + return res.status(200).json({ status: "ok", commit }); +}; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/xagent-verification.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/xagent-verification.js new file mode 100644 index 0000000..1de19bc --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/api/xagent-verification.js @@ -0,0 +1,10 @@ +"use strict"; +const { PROJECT_SLUG, COMMIT_RE, reviewedCommit } = require("../lib/meta"); + +module.exports = function handler(req, res) { + const commit = reviewedCommit(); + if (!COMMIT_RE.test(commit)) { + return res.status(503).json({ schemaVersion: 1, slug: PROJECT_SLUG, code: "REVIEW_COMMIT_NOT_SET" }); + } + return res.status(200).json({ schemaVersion: 1, slug: PROJECT_SLUG, commit }); +}; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/attestation-verifier.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/attestation-verifier.js new file mode 100644 index 0000000..dffe77e --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/attestation-verifier.js @@ -0,0 +1,79 @@ +"use strict"; + +const crypto=require("crypto"); +const fs=require("fs"); +const path=require("path"); +const {canonicalJson}=require("./canonical-json"); + +const PUBLIC_KEY_PATH=path.join(__dirname,"..","verification","public-key.txt"); + +function fail(code,message){ + const error=new Error(message||code); + error.code=code; + throw error; +} + +function requirePattern(value,pattern,code){ + const text=String(value||"").trim(); + if(!pattern.test(text))fail(code,"Invalid attestation field."); + return text; +} + +function validatePayload(payload){ + if(!payload||typeof payload!=="object"||Array.isArray(payload))fail("INVALID_ATTESTATION_PAYLOAD"); + + if(payload.schema!=="SAFEGATE_COMMERCE_ATTESTATION_V1")fail("UNSUPPORTED_ATTESTATION_SCHEMA"); + + requirePattern(payload.requestId,/^SG-EVM-REQ-[A-Z0-9-]{12,80}$/,"INVALID_REQUEST_ID"); + requirePattern(payload.orderReference,/^SG-ORDER-[A-F0-9]{20}$/,"INVALID_ORDER_REFERENCE"); + const txRef=requirePattern(payload.transactionReference,/^SG-TX-[A-F0-9]{24}$/,"INVALID_TRANSACTION_REFERENCE"); + requirePattern(payload.transactionHash,/^0x[a-fA-F0-9]{64}$/,"INVALID_TRANSACTION_HASH"); + requirePattern(payload.paymentSender,/^0x[a-fA-F0-9]{40}$/,"INVALID_PAYMENT_SENDER"); + requirePattern(payload.merchantReceiver,/^0x[a-fA-F0-9]{40}$/,"INVALID_MERCHANT_RECEIVER"); + requirePattern(payload.amountBaseUnits,/^[1-9][0-9]*$/,"INVALID_AMOUNT"); + + if(Number(payload.chainId)!==8453)fail("UNSUPPORTED_CHAIN"); + if(String(payload.asset||"").toUpperCase()!=="USDC")fail("UNSUPPORTED_ASSET"); + if(String(payload.fulfillmentStatus||"").toUpperCase()!=="FULFILLMENT_COMPLETED")fail("FULFILLMENT_NOT_COMPLETED"); + if(String(payload.evidenceStatus||"").toUpperCase()!=="EVIDENCE_CREATED")fail("EVIDENCE_NOT_CREATED"); + + if(String(payload.receiptReference||"")!==txRef+"-RCPT")fail("RECEIPT_BINDING_MISMATCH"); + if(String(payload.evidenceReference||"")!==txRef+"-EVID")fail("EVIDENCE_BINDING_MISMATCH"); + if(String(payload.proofReference||"")!==txRef+"-PROOF")fail("PROOF_BINDING_MISMATCH"); + + if(String(payload.paymentSender).toLowerCase()===String(payload.merchantReceiver).toLowerCase()){ + fail("BUYER_AND_MERCHANT_WALLET_MUST_DIFFER"); + } + + return payload; +} + +function verifyCommerceAttestation(attestation){ + if(!attestation||typeof attestation!=="object")fail("INVALID_ATTESTATION"); + + const payload=validatePayload(attestation.payload); + const signature=String(attestation.signature||"").trim(); + + if(!/^[A-Za-z0-9_-]{80,120}$/.test(signature))fail("INVALID_ATTESTATION_SIGNATURE_FORMAT"); + + const publicKey=crypto.createPublicKey(fs.readFileSync(PUBLIC_KEY_PATH)); + const valid=crypto.verify( + null, + Buffer.from(canonicalJson(payload),"utf8"), + publicKey, + Buffer.from(signature,"base64url") + ); + + if(!valid)fail("ATTESTATION_SIGNATURE_INVALID"); + + return { + ok:true, + attestation_status:"VALID", + issuer:"SAFEGATE", + schema:payload.schema, + transaction_reference:payload.transactionReference, + proof_reference:payload.proofReference + }; +} + +module.exports={verifyCommerceAttestation,validatePayload}; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/base-usdc.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/base-usdc.js new file mode 100644 index 0000000..ac69d94 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/base-usdc.js @@ -0,0 +1,116 @@ +"use strict"; + +const BASE_CHAIN_ID = 8453; +const BASE_CHAIN_HEX = "0x2105"; +const BASE_USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; +const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; +const DEFAULT_RPC = "https://mainnet.base.org"; + +function normalizedAddress(value) { + const address = String(value || "").trim().toLowerCase(); + if (!/^0x[a-f0-9]{40}$/.test(address)) throw Object.assign(new Error("Invalid EVM address."), { code: "INVALID_EVM_ADDRESS" }); + return address; +} + +function topicAddress(topic) { + const value = String(topic || "").toLowerCase(); + if (!/^0x[a-f0-9]{64}$/.test(value)) return null; + return "0x" + value.slice(-40); +} + +function rpcError(code, message) { + const error = new Error(message); + error.code = code; + return error; +} + +async function rpcCall(method, params, options = {}) { + const rpcUrl = String(options.rpcUrl || process.env.BASE_RPC_URL || DEFAULT_RPC); + const timeoutMs = Number(options.timeoutMs || 12000); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(rpcUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), + signal: controller.signal + }); + + if (!response.ok) throw rpcError("BASE_RPC_HTTP_ERROR", "Base RPC returned HTTP " + response.status); + + const payload = await response.json(); + if (payload && payload.error) throw rpcError("BASE_RPC_ERROR", String(payload.error.message || "Base RPC error")); + return payload ? payload.result : null; + } catch (error) { + if (error && error.name === "AbortError") throw rpcError("BASE_RPC_TIMEOUT", "Base RPC request timed out."); + throw error; + } finally { + clearTimeout(timer); + } +} + +function verifyReceiptTransfer(receipt, expected) { + if (!receipt) throw rpcError("TRANSACTION_NOT_FOUND", "Transaction receipt was not found."); + if (String(receipt.status || "").toLowerCase() !== "0x1") throw rpcError("TRANSACTION_FAILED", "Transaction did not succeed on-chain."); + + const sender = normalizedAddress(expected.paymentSender); + const receiver = normalizedAddress(expected.merchantReceiver); + const amount = BigInt(String(expected.amountBaseUnits || "0")); + + if (amount <= 0n) throw rpcError("INVALID_AMOUNT", "Expected USDC amount must be positive."); + + const logs = Array.isArray(receipt.logs) ? receipt.logs : []; + + const matchingLog = logs.find(log => { + if (String(log.address || "").toLowerCase() !== BASE_USDC) return false; + const topics = Array.isArray(log.topics) ? log.topics : []; + if (String(topics[0] || "").toLowerCase() !== TRANSFER_TOPIC) return false; + if (topicAddress(topics[1]) !== sender) return false; + if (topicAddress(topics[2]) !== receiver) return false; + + try { + return BigInt(String(log.data || "0x0")) === amount; + } catch (_) { + return false; + } + }); + + if (!matchingLog) { + throw rpcError("USDC_TRANSFER_MISMATCH", "No matching Base USDC transfer was found for sender, receiver and exact amount."); + } + + return { + payment_status: "PAYMENT_VERIFIED", + chain_id: BASE_CHAIN_ID, + asset: "USDC", + token_contract: BASE_USDC, + transaction_hash: String(receipt.transactionHash || expected.transactionHash || "").toLowerCase(), + block_number: parseInt(String(receipt.blockNumber || "0x0"), 16), + payment_sender: sender, + merchant_receiver: receiver, + amount_base_units: amount.toString() + }; +} + +async function verifyBaseUsdcTransfer(expected, options = {}) { + const txHash = String(expected.transactionHash || "").trim().toLowerCase(); + if (!/^0x[a-f0-9]{64}$/.test(txHash)) throw rpcError("INVALID_TRANSACTION_HASH", "A valid transaction hash is required."); + + const chainId = await rpcCall("eth_chainId", [], options); + if (String(chainId || "").toLowerCase() !== BASE_CHAIN_HEX) { + throw rpcError("UNEXPECTED_RPC_CHAIN", "Configured RPC is not Base Mainnet."); + } + + const receipt = await rpcCall("eth_getTransactionReceipt", [txHash], options); + return verifyReceiptTransfer(receipt, { ...expected, transactionHash: txHash }); +} + +module.exports = { + BASE_CHAIN_ID, + BASE_USDC, + TRANSFER_TOPIC, + verifyReceiptTransfer, + verifyBaseUsdcTransfer +}; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/canonical-json.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/canonical-json.js new file mode 100644 index 0000000..915fcb3 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/canonical-json.js @@ -0,0 +1,15 @@ +"use strict"; + +function canonicalJson(value){ + if(value===null)return "null"; + if(Array.isArray(value)){ + return "["+value.map(canonicalJson).join(",")+"]"; + } + if(typeof value==="object"){ + const keys=Object.keys(value).sort(); + return "{"+keys.map(k=>JSON.stringify(k)+":"+canonicalJson(value[k])).join(",")+"}"; + } + return JSON.stringify(value); +} + +module.exports={canonicalJson}; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/commerce-outcome.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/commerce-outcome.js new file mode 100644 index 0000000..ca5a974 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/commerce-outcome.js @@ -0,0 +1,88 @@ +"use strict"; + +function fail(code, message) { + const error = new Error(message); + error.code = code; + throw error; +} + +function requireString(value, code, pattern) { + const normalized = String(value || "").trim(); + if (!normalized || (pattern && !pattern.test(normalized))) { + fail(code, "Invalid or missing field."); + } + return normalized; +} + +function evaluateCommerceOutcome(input) { + if (!input || typeof input !== "object" || Array.isArray(input)) { + fail("INVALID_INPUT", "A JSON object is required."); + } + + const requestId = requireString(input.requestId, "INVALID_REQUEST_ID", /^SG-EVM-REQ-[A-Z0-9-]{12,80}$/); + const orderReference = requireString(input.orderReference, "INVALID_ORDER_REFERENCE", /^SG-ORDER-[A-Z0-9]{16,40}$/); + const transactionHash = requireString(input.transactionHash, "INVALID_TRANSACTION_HASH", /^0x[a-fA-F0-9]{64}$/).toLowerCase(); + const safeGateTransaction = requireString(input.safeGateTransaction, "INVALID_SAFEGATE_TRANSACTION", /^SG-TX-[A-Z0-9]{16,40}$/); + const evidenceReference = requireString(input.evidenceReference, "INVALID_EVIDENCE_REFERENCE", /^SG-TX-[A-Z0-9]{16,40}-EVID$/); + const proofReference = requireString(input.proofReference, "INVALID_PROOF_REFERENCE", /^SG-TX-[A-Z0-9]{16,40}-PROOF$/); + const paymentSender = requireString(input.paymentSender, "INVALID_PAYMENT_SENDER", /^0x[a-fA-F0-9]{40}$/).toLowerCase(); + const merchantReceiver = requireString(input.merchantReceiver, "INVALID_MERCHANT_RECEIVER", /^0x[a-fA-F0-9]{40}$/).toLowerCase(); + const amountBaseUnits = requireString(input.amountBaseUnits, "INVALID_AMOUNT", /^[1-9][0-9]*$/); + + if (Number(input.chainId) !== 8453) fail("UNSUPPORTED_CHAIN", "Base Mainnet chain ID 8453 is required."); + if (String(input.asset || "").toUpperCase() !== "USDC") fail("UNSUPPORTED_ASSET", "USDC is required."); + if (paymentSender === merchantReceiver) fail("BUYER_AND_MERCHANT_WALLET_MUST_DIFFER", "Buyer and merchant wallets must differ."); + + if (evidenceReference !== safeGateTransaction + "-EVID") { + fail("EVIDENCE_BINDING_MISMATCH", "Evidence reference is not bound to the SafeGate transaction."); + } + + if (proofReference !== safeGateTransaction + "-PROOF") { + fail("PROOF_BINDING_MISMATCH", "Proof reference is not bound to the SafeGate transaction."); + } + + const paymentStatus = String(input.paymentStatus || "").toUpperCase(); + const fulfillmentStatus = String(input.fulfillmentStatus || "").toUpperCase(); + const evidenceStatus = String(input.evidenceStatus || "").toUpperCase(); + + if (paymentStatus !== "PAYMENT_VERIFIED") { + fail("PAYMENT_NOT_VERIFIED", "Payment must be verified before a commerce outcome can be confirmed."); + } + + let outcome = "PAYMENT_VERIFIED_FULFILLMENT_PENDING"; + + if (fulfillmentStatus === "FULFILLMENT_COMPLETED") { + if (evidenceStatus !== "EVIDENCE_CREATED") { + fail("EVIDENCE_REQUIRED", "Completed fulfillment requires created evidence."); + } + outcome = "COMMERCE_VERIFIED"; + } else if (fulfillmentStatus !== "PENDING_MERCHANT_ACTION") { + fail("INVALID_FULFILLMENT_STATUS", "Unsupported fulfillment status."); + } + + return { + ok: true, + capability: "safegate_commerce_outcome", + version: "0.1.0", + side_effects: "none", + verification_scope: "REFERENCE_AND_COMMERCE_STATE_CONSISTENCY", + outcome, + payment_status: paymentStatus, + request_binding: "VALID", + fulfillment_status: fulfillmentStatus, + evidence_status: evidenceStatus, + evidence_reference: evidenceReference, + proof_reference: proofReference, + request_id: requestId, + order_reference: orderReference, + safegate_transaction: safeGateTransaction, + transaction_hash: transactionHash, + chain_id: 8453, + asset: "USDC", + amount_base_units: amountBaseUnits, + payment_sender: paymentSender, + merchant_receiver: merchantReceiver + }; +} + +module.exports = { evaluateCommerceOutcome }; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/commerce-proof-verifier.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/commerce-proof-verifier.js new file mode 100644 index 0000000..f00376a --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/commerce-proof-verifier.js @@ -0,0 +1,82 @@ +"use strict"; + +const {verifyCommerceAttestation}=require("./attestation-verifier"); +const {verifyBaseUsdcTransfer}=require("./base-usdc"); + +function fail(code,message){ + const error=new Error(message||code); + error.code=code; + throw error; +} + +async function verifyCommerceProof(attestation,options={}){ + const attestationResult=verifyCommerceAttestation(attestation); + const payload=attestation.payload; + + const payment=await verifyBaseUsdcTransfer({ + transactionHash:payload.transactionHash, + paymentSender:payload.paymentSender, + merchantReceiver:payload.merchantReceiver, + amountBaseUnits:payload.amountBaseUnits + },options); + + if(payment.transaction_hash!==String(payload.transactionHash).toLowerCase()) + fail("TRANSACTION_BINDING_MISMATCH"); + + if(payment.payment_sender!==String(payload.paymentSender).toLowerCase()) + fail("PAYMENT_SENDER_BINDING_MISMATCH"); + + if(payment.merchant_receiver!==String(payload.merchantReceiver).toLowerCase()) + fail("MERCHANT_RECEIVER_BINDING_MISMATCH"); + + if(payment.amount_base_units!==String(payload.amountBaseUnits)) + fail("PAYMENT_AMOUNT_BINDING_MISMATCH"); + + if(payment.chain_id!==Number(payload.chainId)) + fail("CHAIN_BINDING_MISMATCH"); + + if(payment.asset!==String(payload.asset).toUpperCase()) + fail("ASSET_BINDING_MISMATCH"); + + return { + ok:true, + capability:"safegate_commerce_outcome", + version:"0.3.0", + outcome:"COMMERCE_VERIFIED", + side_effects:"none", + + payment_status:payment.payment_status, + attestation_status:attestationResult.attestation_status, + fulfillment_status:String(payload.fulfillmentStatus).toUpperCase(), + evidence_status:String(payload.evidenceStatus).toUpperCase(), + + request_id:payload.requestId, + order_reference:payload.orderReference, + safegate_transaction:payload.transactionReference, + transaction_hash:payment.transaction_hash, + + chain_id:payment.chain_id, + asset:payment.asset, + amount_base_units:payment.amount_base_units, + payment_sender:payment.payment_sender, + merchant_receiver:payment.merchant_receiver, + + receipt_reference:payload.receiptReference, + evidence_reference:payload.evidenceReference, + proof_reference:payload.proofReference, + + payment_verification:{ + source:"BASE_MAINNET_RPC", + token_contract:payment.token_contract, + block_number:payment.block_number + }, + + attestation_verification:{ + issuer:"SAFEGATE", + scheme:"Ed25519", + schema:payload.schema + } + }; +} + +module.exports={verifyCommerceProof}; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/meta.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/meta.js new file mode 100644 index 0000000..d61e560 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/lib/meta.js @@ -0,0 +1,10 @@ +"use strict"; + +const PROJECT_SLUG = "nurexen-safegate-commerce-outcome"; +const COMMIT_RE = /^[a-f0-9]{40}$/i; + +function reviewedCommit() { + return String(process.env.XAGENT_REVIEW_COMMIT || "").trim().toLowerCase(); +} + +module.exports = { PROJECT_SLUG, COMMIT_RE, reviewedCommit }; diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/package.json b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/package.json new file mode 100644 index 0000000..f25f30e --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/package.json @@ -0,0 +1,13 @@ +{ + "name": "safegate-xagent-commerce-outcome", + "version": "0.3.0", + "private": true, + "type": "commonjs", + "engines": { + "node": "\u003e=18.17.0" + }, + "scripts": { + "check": "node --check api/health.js \u0026\u0026 node --check api/xagent-verification.js \u0026\u0026 node --check api/commerce-outcome.js \u0026\u0026 node --check lib/canonical-json.js \u0026\u0026 node --check lib/attestation-verifier.js \u0026\u0026 node --check lib/base-usdc.js \u0026\u0026 node --check lib/commerce-proof-verifier.js", + "test": "node test/commerce-outcome.test.js \u0026\u0026 node test/base-usdc.test.js \u0026\u0026 node test/attestation-verifier.test.js" + } +} \ No newline at end of file diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/attestation-verifier.test.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/attestation-verifier.test.js new file mode 100644 index 0000000..3c2eb6f --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/attestation-verifier.test.js @@ -0,0 +1,27 @@ +"use strict"; +const assert=require("assert"); +const fs=require("fs"); +const path=require("path"); +const {verifyCommerceAttestation}=require("../lib/attestation-verifier"); + +const sample=JSON.parse( + fs.readFileSync( + path.join(__dirname,"..","verification","sample-synthetic-attestation.json"), + "utf8" + ) +); + +const valid=verifyCommerceAttestation(sample); +assert.strictEqual(valid.ok,true); +assert.strictEqual(valid.attestation_status,"VALID"); + +const tampered=JSON.parse(JSON.stringify(sample)); +tampered.payload.amountBaseUnits="100001"; + +assert.throws( + ()=>verifyCommerceAttestation(tampered), + error=>error&&error.code==="ATTESTATION_SIGNATURE_INVALID" +); + +console.log("ATTESTATION_VERIFIER_TEST=PASS"); +console.log("TAMPER_DETECTION_TEST=PASS"); diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/base-usdc.test.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/base-usdc.test.js new file mode 100644 index 0000000..b640a51 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/base-usdc.test.js @@ -0,0 +1,45 @@ +"use strict"; +const assert = require("assert"); +const { BASE_USDC, TRANSFER_TOPIC, verifyReceiptTransfer } = require("../lib/base-usdc"); + +function topic(address) { + return "0x" + "0".repeat(24) + address.toLowerCase().replace(/^0x/, ""); +} + +const sender = "0x" + "1".repeat(40); +const receiver = "0x" + "2".repeat(40); +const txHash = "0x" + "a".repeat(64); + +const receipt = { + status: "0x1", + transactionHash: txHash, + blockNumber: "0x1234", + logs: [{ + address: BASE_USDC, + topics: [TRANSFER_TOPIC, topic(sender), topic(receiver)], + data: "0x186a0" + }] +}; + +const verified = verifyReceiptTransfer(receipt, { + transactionHash: txHash, + paymentSender: sender, + merchantReceiver: receiver, + amountBaseUnits: "100000" +}); + +assert.strictEqual(verified.payment_status, "PAYMENT_VERIFIED"); +assert.strictEqual(verified.amount_base_units, "100000"); +assert.strictEqual(verified.chain_id, 8453); + +assert.throws( + () => verifyReceiptTransfer(receipt, { + transactionHash: txHash, + paymentSender: sender, + merchantReceiver: receiver, + amountBaseUnits: "100001" + }), + error => error && error.code === "USDC_TRANSFER_MISMATCH" +); + +console.log("BASE_USDC_RECEIPT_TEST=PASS"); diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/commerce-outcome.test.js b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/commerce-outcome.test.js new file mode 100644 index 0000000..bb67699 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/test/commerce-outcome.test.js @@ -0,0 +1,38 @@ +"use strict"; +const assert = require("assert"); +const { evaluateCommerceOutcome } = require("../lib/commerce-outcome"); + +const base = { + requestId: "SG-EVM-REQ-TEST123456789ABC", + orderReference: "SG-ORDER-1234567890ABCDEF", + transactionHash: "0x" + "a".repeat(64), + safeGateTransaction: "SG-TX-1234567890ABCDEF", + evidenceReference: "SG-TX-1234567890ABCDEF-EVID", + proofReference: "SG-TX-1234567890ABCDEF-PROOF", + paymentSender: "0x" + "1".repeat(40), + merchantReceiver: "0x" + "2".repeat(40), + amountBaseUnits: "100000", + chainId: 8453, + asset: "USDC", + paymentStatus: "PAYMENT_VERIFIED", + fulfillmentStatus: "FULFILLMENT_COMPLETED", + evidenceStatus: "EVIDENCE_CREATED" +}; + +const complete = evaluateCommerceOutcome(base); +assert.strictEqual(complete.outcome, "COMMERCE_VERIFIED"); +assert.strictEqual(complete.request_binding, "VALID"); + +const pending = evaluateCommerceOutcome({ + ...base, + fulfillmentStatus: "PENDING_MERCHANT_ACTION", + evidenceStatus: "EVIDENCE_CREATED" +}); +assert.strictEqual(pending.outcome, "PAYMENT_VERIFIED_FULFILLMENT_PENDING"); + +assert.throws( + () => evaluateCommerceOutcome({ ...base, proofReference: "SG-TX-1234567890ABCDEF-EVID" }), + error => error && error.code === "INVALID_PROOF_REFERENCE" +); + +console.log("COMMERCE_OUTCOME_CONTRACT_TEST=PASS"); diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/vercel.json b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/vercel.json new file mode 100644 index 0000000..0cebea6 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/vercel.json @@ -0,0 +1,8 @@ +{ + "version": 2, + "rewrites": [ + { "source": "/health", "destination": "/api/health" }, + { "source": "/.well-known/xagent-verification.json", "destination": "/api/xagent-verification" }, + { "source": "/v1/commerce-outcome", "destination": "/api/commerce-outcome" } + ] +} diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/verification/public-key.txt b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/verification/public-key.txt new file mode 100644 index 0000000..b72a252 --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/verification/public-key.txt @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAMBjw8klRaesSK+Nd6zl3WNkbYf3cpU8AJxMjtNp6fFk= +-----END PUBLIC KEY----- diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/verification/sample-synthetic-attestation.json b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/verification/sample-synthetic-attestation.json new file mode 100644 index 0000000..43111ef --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/source/verification/sample-synthetic-attestation.json @@ -0,0 +1,20 @@ +{ + "payload": { + "schema": "SAFEGATE_COMMERCE_ATTESTATION_V1", + "requestId": "SG-EVM-REQ-DEMO123456789ABC", + "orderReference": "SG-ORDER-AAAAAAAAAAAAAAAAAAAA", + "transactionReference": "SG-TX-BBBBBBBBBBBBBBBBBBBBBBBB", + "transactionHash": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "paymentSender": "0x1111111111111111111111111111111111111111", + "merchantReceiver": "0x2222222222222222222222222222222222222222", + "chainId": 8453, + "asset": "USDC", + "amountBaseUnits": "100000", + "fulfillmentStatus": "FULFILLMENT_COMPLETED", + "evidenceStatus": "EVIDENCE_CREATED", + "receiptReference": "SG-TX-BBBBBBBBBBBBBBBBBBBBBBBB-RCPT", + "evidenceReference": "SG-TX-BBBBBBBBBBBBBBBBBBBBBBBB-EVID", + "proofReference": "SG-TX-BBBBBBBBBBBBBBBBBBBBBBBB-PROOF" + }, + "signature": "NcaHRsZuXs96oGzf2NzjlliV3KltZ3tO4QVp407wwxk1AbsdacyklQYBbvDr29Q_2YhiJu4i8AFbgwi06eSJAg" +} \ No newline at end of file diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/submission.json b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/submission.json new file mode 100644 index 0000000..9840bca --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/submission.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 1, + "name": "SafeGate Commerce Outcome", + "slug": "nurexen-safegate-commerce-outcome", + "sourceRepository": "https://github.com/Nurexen-Labs/safegate-xagent-commerce-outcome", + "reviewCommit": "b460b88ce800839e69839a5d31aac949dfe2543c", + "apiBaseUrl": "https://safegate-xagent-commerce-outcome.vercel.app/v1", + "healthCheckUrl": "https://safegate-xagent-commerce-outcome.vercel.app/health", + "deploymentProofUrl": "https://safegate-xagent-commerce-outcome.vercel.app/.well-known/xagent-verification.json" +} \ No newline at end of file diff --git a/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/verification/README.md b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/verification/README.md new file mode 100644 index 0000000..a69837f --- /dev/null +++ b/submissions/mcp-hackathon/nurexen-safegate-commerce-outcome/verification/README.md @@ -0,0 +1,91 @@ +# Verification evidence + +## Prerequisites + +- Review commit: b460b88ce800839e69839a5d31aac949dfe2543c +- API base URL: https://safegate-xagent-commerce-outcome.vercel.app/v1 +- Authentication: none for the public API, health, and deployment-proof endpoints. +- The successful real-commerce review attestation is intentionally not committed because it contains production commerce identifiers. It can be supplied through an approved private review channel if required. + +## 1. Health check + +```bash +curl --fail --silent --show-error https://safegate-xagent-commerce-outcome.vercel.app/health +``` + +Expected response: + +```json +{"status":"ok","commit":"b460b88ce800839e69839a5d31aac949dfe2543c"} +``` + +Verified against production: PASS. + +## 2. Deployment proof + +```bash +curl --fail --silent --show-error https://safegate-xagent-commerce-outcome.vercel.app/.well-known/xagent-verification.json +``` + +Expected response: + +```json +{"schemaVersion":1,"slug":"nurexen-safegate-commerce-outcome","commit":"b460b88ce800839e69839a5d31aac949dfe2543c"} +``` + +Verified against production: PASS. + +## 3. Real capability call + +With the SafeGate-signed review attestation stored locally as review-attestation.json: + +```bash +curl --fail --silent --show-error --request POST https://safegate-xagent-commerce-outcome.vercel.app/v1/commerce-outcome --header "content-type: application/json" --data-binary @review-attestation.json +``` + +The production verification call has been executed successfully against a completed Base Mainnet USDC commerce event. + +Expected redacted success fields: + +```text +ok=true +outcome=COMMERCE_VERIFIED +payment_status=PAYMENT_VERIFIED +attestation_status=VALID +fulfillment_status=FULFILLMENT_COMPLETED +evidence_status=EVIDENCE_CREATED +side_effects=none +``` + +Verified against production: PASS. + +The real production attestation fixture and its commerce identifiers are intentionally excluded from the public submission package. + +## 4. Safe failure behavior + +```bash +curl --silent --show-error --request POST https://safegate-xagent-commerce-outcome.vercel.app/v1/commerce-outcome --header "content-type: application/json" --data '{"foo":"bar"}' +``` + +Expected behavior: + +- HTTP 400. +- ok=false with a deterministic verification error. +- No payment, wallet, fulfillment, evidence, or other state-changing side effect. + +## 5. Offline verification + +```bash +cd source +npm run check +npm test +``` + +Validated tests: + +- COMMERCE_OUTCOME_CONTRACT_TEST=PASS +- BASE_USDC_RECEIPT_TEST=PASS +- ATTESTATION_VERIFIER_TEST=PASS +- TAMPER_DETECTION_TEST=PASS + +The public synthetic signed fixture used by offline verification is included at source/verification/sample-synthetic-attestation.json.