diff --git a/package.json b/package.json index cc50c92..7eb3cc8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwind-plus-icpay", - "version": "1.2.40", + "version": "1.2.41", "private": true, "packageManager": "pnpm@9.12.3", "scripts": { diff --git a/src/app/agentic-x402/page.mdx b/src/app/agentic-x402/page.mdx index b27b5d4..b34faa7 100644 --- a/src/app/agentic-x402/page.mdx +++ b/src/app/agentic-x402/page.mdx @@ -73,22 +73,48 @@ const x402Init = await icpay.createPaymentX402Usd({ x402Upto: true, }) -// x402Init.payment contains: -// { -// x402Version, -// paymentIntentId, -// accepts: [ { scheme: 'upto', maxAmountRequired, ... } ] -// } +// For up-to EVM flows, the SDK: +// - creates a payment intent with x402_upto = true +// - receives X402 accepts[] with scheme: 'upto' and maxAmountRequired +// - builds and signs an X402 v2 header (EIP‑712/EIP‑3009) for maxAmountRequired +// - returns a deferred object instead of auto-settling: +// { +// paymentIntentId, +// payment: { +// x402Version, +// paymentIntentId, +// accepts: [...], +// paymentHeader, // base64-encoded signed X402 header +// paymentRequirements, // the exact requirement used for signing +// }, +// status: 'pending', +// ... +// } ``` What happens: - ICPay creates a payment intent with `x402_upto = true`. - API responds with HTTP 402 + `accepts[]` (`scheme: 'upto'`). -- SDK builds and signs the X402 v2 authorization **for `maxAmountRequired`**. -- SDK verifies the authorization and returns when it is valid; **no final settlement is performed** for up-to. +- SDK builds and signs the X402 v2 authorization **for `maxAmountRequired`** and encodes it as `paymentHeader` (base64). +- SDK returns a deferred object that includes `paymentIntentId`, `payment.accepts`, `payment.paymentHeader`, and `payment.paymentRequirements` without calling the public settle endpoint. -You can pass the `paymentIntentId` to your backend (e.g., via metadata, params, or a direct API call). +You should pass **both** the `paymentIntentId` and the `paymentHeader` (and optionally `paymentRequirements`) to your backend. The backend will later send the header to ICPay when calling the secret-key settle endpoint. + +#### X402 up-to header structure (EVM) + +For EVM, `paymentHeader` is a base64-encoded JSON header that includes an EIP‑712 typed-data payload (EIP‑3009 style). Inside the decoded JSON: + +- `payload.authorization.maxAmount` — the **cap** in token smallest units. +- `payload.authorization.validBefore` — Unix timestamp (seconds) when the authorization expires. +- `payload.authorization.nonce` — unique nonce for replay protection. +- Other fields describe the token, spender, and chain. + +ICPay stores this header on the payment intent when you call the secret-key settle endpoint and uses it to: + +- Validate signature and constraints. +- Enforce `settledAmount <= maxAmountRequired`. +- Ensure the authorization is still valid (`validBefore` not passed) at settlement time. ### 2. Backend: run the job and settle later @@ -103,10 +129,15 @@ const icpayBackend = new Icpay({ }) export async function runJobAndSettle(paymentIntentId: string, usageUsd: number) { - // 1) Commit settlement (ICPay converts USD to token units and enforces cap) + // 1) Look up the signed X402 header your frontend sent you when the up-to intent was created + const paymentHeader = await loadPaymentHeaderForIntent(paymentIntentId) // app-specific storage + + // 2) Commit settlement (ICPay converts USD to token units and enforces cap, using the stored header) const result = await icpayBackend.protected.settleX402Upto({ paymentIntentId, settledAmountUsd: usageUsd, + // If you proxy the header through your backend, include it here so icpay-api can persist it + // paymentHeader, }) if (!result.ok) { @@ -147,13 +178,18 @@ export default function Page() { orderId: 'job-123', icpay: { icpay_context: 'agentic-x402' }, }, - onX402UptoIntent: async ({ paymentIntentId, amountUsd, accepts }) => { - console.log('X402 up-to intent created', { paymentIntentId, amountUsd, accepts }) - // Call your backend to start work and pass paymentIntentId + onX402UptoIntent: async ({ paymentIntentId, amountUsd, accepts, paymentHeader, paymentRequirements }) => { + console.log('X402 up-to intent created', { paymentIntentId, amountUsd, accepts, paymentHeader }) + // Call your backend to start work and pass paymentIntentId and the signed header await fetch('/api/start-job', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ paymentIntentId, maxUsd: amountUsd }), + body: JSON.stringify({ + paymentIntentId, + maxUsd: amountUsd, + paymentHeader, // base64 X402 header (optional but recommended) + paymentRequirements, // exact requirement used for signing (optional) + }), }) }, } @@ -205,7 +241,8 @@ Usage: - `x402Upto: true` → ICPay: - Marks the payment intent with `x402_upto = true`. - Emits X402 v2 acceptance with `scheme: 'upto'`. - - SDK **does not** auto-settle via `/sdk/public/payments/x402/settle`; settlement must be driven by your backend via `protected.settleX402Upto`. + - SDK builds and signs an X402 v2 header and exposes it as `payment.paymentHeader` and `payment.paymentRequirements` on the `createPaymentX402Usd` return value for up-to flows. + - SDK **does not** auto-settle via `/sdk/public/payments/x402/settle`; settlement must be driven by your backend via `protected.settleX402Upto`, optionally passing the header. ### SDK: protected API (backend) diff --git a/src/app/sdk/page.mdx b/src/app/sdk/page.mdx index f5c9278..ebf7e6f 100644 --- a/src/app/sdk/page.mdx +++ b/src/app/sdk/page.mdx @@ -115,12 +115,22 @@ const x402Init = await icpay.createPaymentX402Usd({ x402Upto: true, // mark this as an up-to scheme }) -// On up-to flows, the SDK creates an X402 v2 authorization and stops; -// settlement is performed later via secret-key SDK: +// On up-to flows, the SDK: +// - creates an X402 v2 authorization for maxAmountRequired +// - returns a deferred object instead of auto-settling +// - exposes header + requirement so you can pass them to your backend: // -// icpayBackend.protected.settleX402Upto({ paymentIntentId, settledAmount }) +// x402Init.payment.paymentHeader // base64 X402 header (signed) +// x402Init.payment.paymentRequirements // requirement used for signing // -// See /agentic-x402 for a complete example. +// Your backend later calls: +// icpayBackend.protected.settleX402Upto({ +// paymentIntentId: x402Init.paymentIntentId, +// settledAmountUsd: usageUsd, +// // optionally: paymentHeader: x402Init.payment.paymentHeader, +// }) +// +// See /agentic-x402 for a complete end-to-end example. ``` Events are emitted throughout the flow when `enableEvents` is true. See Events section below. diff --git a/src/app/widget/page.mdx b/src/app/widget/page.mdx index 08ee513..269eced 100644 --- a/src/app/widget/page.mdx +++ b/src/app/widget/page.mdx @@ -185,10 +185,15 @@ export default function Page() { const config = { publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK, amountUsd: 12, - // For X402 up-to agentic flows: + // For X402 up-to agentic flows (EVM): // x402Upto: true, - // onX402UptoIntent: ({ paymentIntentId, amountUsd, accepts }) => { - // // Start your long-running job and pass paymentIntentId to your backend + // onX402UptoIntent: ({ paymentIntentId, amountUsd, accepts, paymentHeader, paymentRequirements }) => { + // // Start your long-running job and pass paymentIntentId + signed header to your backend + // fetch('/api/start-job', { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ paymentIntentId, maxUsd: amountUsd, paymentHeader, paymentRequirements }), + // }) // }, } return (