diff --git a/package.json b/package.json index 15518e5..c3bdb8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwind-plus-icpay", - "version": "1.2.37", + "version": "1.2.38", "private": true, "packageManager": "pnpm@9.12.3", "scripts": { diff --git a/src/app/agentic-x402/page.mdx b/src/app/agentic-x402/page.mdx new file mode 100644 index 0000000..e7c5de4 --- /dev/null +++ b/src/app/agentic-x402/page.mdx @@ -0,0 +1,258 @@ +export const metadata = { + title: 'Agentic X402 up-to payments', + description: + 'Usage-based and agentic payments with X402 up-to scheme in ICPay: create capped authorizations, run work, and settle later using icpay-sdk or icpay-widget.', +} + +export const sections = [ + { title: 'Overview', id: 'overview' }, + { title: 'Flow overview', id: 'flow' }, + { title: 'Using icpay-sdk only', id: 'sdk-only' }, + { title: 'Using icpay-widget', id: 'widget' }, + { title: 'Config reference (SDK & widget)', id: 'config-reference' }, +] + +# Agentic X402 up-to payments + +X402 v2 supports **up-to** schemes: the wallet signs an authorization for a **maximum** amount, and your service decides the **final cost** after the work is done. ICPay exposes this via **`x402Upto`** and the `upto` scheme so you can build agentic, usage-based, or long-running flows. {{ className: 'lead' }} + +## Overview + +Typical use cases: + +- AI / LLM calls where token usage is unknown up front (only a maximum budget is known). +- Long-running jobs where you want to cap the user’s exposure but bill only what they actually used. +- Agentic workflows where an agent is authorized to spend “up to X” for a task. + +You get: + +- **Cap for the user**: wallet signs a maximum amount. +- **Flexibility for the service**: backend chooses the final settled amount. +- **Separation of concerns**: frontend handles X402 authorization; backend handles settlement with secret key. + +## Flow overview + +End-to-end flow: + +1. **User requests a service**. Your app decides a **maximum price** (cap). +2. **Create X402 up-to intent** via SDK or widget: + - Intent is stored with `x402_upto = true`. + - ICPay returns an X402 acceptance with `scheme: 'upto'` and `maxAmountRequired` (cap). +3. **User signs X402 v2 authorization**: + - EVM: EIP-712 typed data (EIP‑3009) for `maxAmountRequired`. + - ICPay’s backend verifies the authorization. +4. **Your service starts work** once the authorization is valid. +5. **Service finishes and computes `settledAmount`** (in smallest unit), satisfying: + - `0 < settledAmount <= maxAmountRequired`. +6. **Backend settles**: + - Uses `secretKey` + `protected.settleX402Upto` to finalize the payment with `settledAmount`. +7. **ICPay settles on-chain** using the signed authorization, enforces the cap, and records `settledAmount` on the intent. +8. **You notify the user and/or client** via webhooks, polling, or your own messaging. + +## Using icpay-sdk only + +### 1. Frontend: create X402 up-to intent and authorization + +On the **frontend** (publishable key), call `createPaymentX402Usd` with `x402Upto: true`: + +```ts {{ title: 'Create X402 up-to intent (frontend)' }} +import { Icpay } from '@ic-pay/icpay-sdk' + +const icpay = new Icpay({ + publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK!, + enableEvents: true, + debug: true, +}) + +// User approves up to $25 for this job +const maxUsd = 25 +const x402Init = await icpay.createPaymentX402Usd({ + usdAmount: maxUsd, + tokenShortcode: 'base_usdc', + metadata: { orderId: 'job-123', context: 'agentic-x402' }, + x402Upto: true, +}) + +// x402Init.payment contains: +// { +// x402Version, +// paymentIntentId, +// accepts: [ { scheme: 'upto', maxAmountRequired, ... } ] +// } +``` + +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. + +You can pass the `paymentIntentId` to your backend (e.g., via metadata, params, or a direct API call). + +### 2. Backend: run the job and settle later + +On the **backend** (Node, server-side only), configure `secretKey`: + +```ts {{ title: 'Backend: settle X402 up-to intent' }} +import { Icpay } from '@ic-pay/icpay-sdk' + +const icpayBackend = new Icpay({ + secretKey: process.env.ICPAY_SECRET_KEY!, + apiUrl: process.env.ICPAY_API_URL, +}) + +export async function runJobAndSettle(paymentIntentId: string, usageUsd: number) { + // 1) Convert usageUsd to token smallest unit (you can use ICPay helpers or your own pricing) + const usageAmountAtomic = await computeAtomicAmountFromUsd(usageUsd, /* token info */) + + // 2) Commit settlement (must satisfy 0 < settledAmount <= maxAmountRequired) + const result = await icpayBackend.protected.settleX402Upto({ + paymentIntentId, + settledAmount: usageAmountAtomic.toString(), + }) + + if (!result.ok) { + throw new Error(`X402 upto settlement failed: ${result.error || 'unknown error'}`) + } + + // 3) Optionally look up the payment and notify your app + const paymentAgg = await icpayBackend.protected.getPaymentById(paymentIntentId) + // paymentAgg.payment / paymentAgg.intent contain final values +} +``` + +The `protected.settleX402Upto` method: + +- Is available only on the **secret key** SDK (`icpayBackend.protected`). +- Calls `POST /sdk/payments/x402/upto/settle` on icpay-api. +- Enforces: + - Intent belongs to your account. + - Intent has `x402_upto = true`. + - `0 < settledAmount <= maxAmountRequired`. + +## Using icpay-widget + +When you want a drop-in UI but still need agentic/usage-based billing, use `icpay-pay-button` with `x402Upto` and `onX402UptoIntent`. + +### 1. Configure the pay button + +```tsx {{ title: 'React: icpay-pay-button with X402 up-to' }} +'use client' +import { IcpayPayButton, IcpaySuccess } from '@ic-pay/icpay-widget/react' + +export default function Page() { + const config = { + publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK, + amountUsd: 25, // max budget (cap) + x402Upto: true, + metadata: { + 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 + await fetch('/api/start-job', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paymentIntentId, maxUsd: amountUsd }), + }) + }, + } + + return ( + console.log('Job paid', detail)} + /> + ) +} +``` + +Under the hood: + +- Widget calls `createPaymentX402Usd({ x402Upto: true, ... })`. +- SDK performs X402 authorization but **does not** call the public settle endpoint for up-to. +- Widget calls `onX402UptoIntent` with: + - `paymentIntentId` + - `amountUsd` (cap) + - `metadata` + - `accepts[]` (X402 requirements) +- Widget then polls the intent via `GET /sdk/public/payments/intents/:id` until it is terminal: + - When your backend calls `protected.settleX402Upto` and ICPay marks the intent `completed`, the widget: + - Updates its UI. + - Calls `onSuccess` for the host. + +### 2. Backend: same `settleX402Upto` call + +Your backend implementation is identical to the SDK-only case: it receives `paymentIntentId` from `onX402UptoIntent` and later calls `protected.settleX402Upto` with the computed `settledAmount`. + +## Config reference (SDK & widget) + +### SDK: createPaymentX402Usd (frontend) + +New field on `CreatePaymentUsdRequest`: + +```ts +export interface CreatePaymentUsdRequest { + usdAmount: string | number; + ... + /** When true, create an x402 intent that uses the 'upto' scheme instead of 'exact'. */ + x402Upto?: boolean; +} +``` + +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: protected API (backend) + +New protected method: + +```ts +icpayBackend.protected.settleX402Upto({ + paymentIntentId: string; + settledAmount: string | number; // smallest unit, <= maxAmountRequired +}) +``` + +Requires: + +- `secretKey` configured in `IcpayConfig`. +- Intent must belong to the authenticated account and have `x402_upto = true`. + +### Widget: pay button config + +New fields on `PayButtonConfig`: + +```ts +export type PayButtonConfig = CommonConfig & { + amountUsd?: number; + buttonLabel?: string; + onSuccess?: (tx: { id: number; status: string }) => void; + + // X402 up-to support + x402Upto?: boolean; + onX402UptoIntent?: (info: { + paymentIntentId: string; + amountUsd: number; + metadata?: Record; + accepts: any[]; + }) => void | Promise; +}; +``` + +Behavior: + +- When `x402Upto` is `true` and the selected token supports X402: + - Widget initiates X402 up-to intent and authorization via SDK. + - Calls `onX402UptoIntent` when the intent + acceptances are ready. + - Polls the intent until status becomes terminal, then fires `onSuccess` (and emits global SDK events). + +This combination—`x402Upto` + `onX402UptoIntent` + `protected.settleX402Upto`—is the recommended pattern for **agentic, usage-based X402 payments** in ICPay. + diff --git a/src/app/page.mdx b/src/app/page.mdx index bbedb96..fe0a2a2 100644 --- a/src/app/page.mdx +++ b/src/app/page.mdx @@ -57,6 +57,7 @@ The ICPay SDK ships as a single package that supports two usage modes. Use the p - Solana chain and tokens are now supported across SDK and Widget. - Relay payments: accept and forward funds directly to your per‑chain recipient addresses. See [Relay payments](/relay-payments). - X402 v2: ICPay includes its own facilitator for X402 flows (IC and EVM). See [X402 payments](/x402). + - Agentic X402 up-to payments: usage-based and long-running jobs with capped authorizations and deferred settlement. See [Agentic X402 up-to payments](/agentic-x402). diff --git a/src/app/sdk/page.mdx b/src/app/sdk/page.mdx index 1368c65..f5c9278 100644 --- a/src/app/sdk/page.mdx +++ b/src/app/sdk/page.mdx @@ -106,6 +106,23 @@ const out = await icpay.createPaymentX402Usd({ // and settlement, then wait for terminal status, emitting events along the way. ``` +```ts {{ title: 'X402 up-to payments (agentic / usage-based)' }} +// Authorize up to $25; backend decides final cost (<= $25) and settles later +const x402Init = await icpay.createPaymentX402Usd({ + usdAmount: 25, + tokenShortcode: 'base_usdc', + metadata: { orderId: 'job-123', context: 'agentic-x402' }, + 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: +// +// icpayBackend.protected.settleX402Upto({ paymentIntentId, settledAmount }) +// +// See /agentic-x402 for a complete example. +``` + Events are emitted throughout the flow when `enableEvents` is true. See Events section below. ### Solana support diff --git a/src/app/widget/page.mdx b/src/app/widget/page.mdx index 5d9ed78..08ee513 100644 --- a/src/app/widget/page.mdx +++ b/src/app/widget/page.mdx @@ -185,6 +185,11 @@ export default function Page() { const config = { publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK, amountUsd: 12, + // For X402 up-to agentic flows: + // x402Upto: true, + // onX402UptoIntent: ({ paymentIntentId, amountUsd, accepts }) => { + // // Start your long-running job and pass paymentIntentId to your backend + // }, } return ( + ``` + + - The widget: + - Calls `createPaymentX402Usd` with `x402Upto: true`. + - Gets an X402 v2 `accepts[]` response with `scheme: 'upto'`, `maxAmountRequired` corresponding to `$25`. + - Guides the wallet to sign the X402 authorization. + +2. **SDK (client-side X402 flow)** + + If you use the SDK directly instead of the widget: + + ```ts + import { Icpay } from '@ic-pay/icpay-sdk'; + + const icpay = new Icpay({ + publishableKey: process.env.NEXT_PUBLIC_ICPAY_PK!, + enableEvents: true, + debug: true, + }); + + // User approves up-to $25 for this job + const x402Init = await icpay.createPaymentX402Usd({ + usdAmount: 25, + tokenShortcode: 'base_usdc', + metadata: { orderId: 'job-123' }, + x402Upto: true, + }); + ``` + + - ICPay creates a payment intent flagged as `x402_upto = true` and returns an HTTP 402 with `accepts[]` (`scheme: 'upto'`). + - The SDK: + - Builds the EIP‑712 payload (`value = maxAmountRequired`). + - Requests the wallet signature. + - Verifies/settles X402 authorization server-side. + - The job is now **authorized up to the cap**, but not yet finally billed. + +3. **Backend service work** + + - Your backend sees the X402 authorization is valid (e.g., via webhook or polling) and starts the long-running job. + - When the job finishes, your backend calculates the actual cost (for example, `$9.75` → `900000` units in smallest token unit). + +4. **Backend settlement with secret key** + + On your **server**, using secret key + account id, you call the new SDK protected method: + + ```ts + import { Icpay } from '@ic-pay/icpay-sdk'; + + const icpayBackend = new Icpay({ + secretKey: process.env.ICPAY_SECRET_KEY!, + apiUrl: process.env.ICPAY_API_URL, // e.g. https://api.icpay.org + enableEvents: false, + }); + + // Example: usage cost = 900000 token units (<= maxAmountRequired) + await icpayBackend.protected.settleX402Upto({ + paymentIntentId: 'pi_123', + settledAmount: '900000', + }); + ``` + + - This hits `POST /sdk/payments/x402/upto/settle`: + - Confirms the intent is `x402_upto` and belongs to your account. + - Enforces business rule `settledAmount <= maxAmountRequired`. + - icpay-api then calls icpay-services `/evm/settlement/x402-upto` to perform on-chain settlement. + +5. **Notifications** + + - Once settlement is done, ICPay updates the payment/intent status. + - You can: + - Use webhooks (existing ICPay webhooks) to be notified when the payment is completed. + - Or use `icpayBackend.protected.getPaymentById` / `getPaymentHistory` to reconcile. + +This pattern gives you: + +- **Guardrails for the user:** wallet signs an authorization capped at `maxAmountRequired`. +- **Flexibility for the service:** backend chooses the final `settledAmount` based on real usage. +- **Security:** final settlement for up-to flows is restricted to **secret-key server calls**, not client-side publishable-key flows. + ## SDK usage example ```ts