Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tailwind-plus-icpay",
"version": "1.2.40",
"version": "1.2.41",
"private": true,
"packageManager": "pnpm@9.12.3",
"scripts": {
Expand Down
67 changes: 52 additions & 15 deletions src/app/agentic-x402/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}),
})
},
}
Expand Down Expand Up @@ -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)

Expand Down
18 changes: 14 additions & 4 deletions src/app/sdk/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 8 additions & 3 deletions src/app/widget/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading