diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx
index 333b400..773c9d4 100644
--- a/sdk/arbiter/ai-integration.mdx
+++ b/sdk/arbiter/ai-integration.mdx
@@ -23,9 +23,6 @@ interface CaseEvaluationContext {
/** The payment information struct */
paymentInfo: PaymentInfo;
- /** The record index (nonce) identifying which charge */
- nonce: bigint;
-
/** Current payment state */
paymentState: PaymentState;
@@ -89,15 +86,15 @@ const handler = createWebhookHandler({
```
-Setting `autoSubmitDecision: true` calls `approveRefundRequest` or `denyRefundRequest` on-chain automatically. This submits the decision only -- executing the actual refund transfer via `executeRefundInEscrow` is a separate step you handle after approval.
+Setting `autoSubmitDecision: true` calls `refundInEscrow` (for approvals) or `denyRefundRequest` on-chain automatically. In v3, `refundInEscrow()` both executes the refund and auto-approves the pending request in one step.
## Webhook Handler Configuration
```typescript
interface WebhookHandlerConfig {
- /** X402rArbiter instance */
- arbiter: X402rArbiter;
+ /** Arbiter client instance */
+ arbiter: ReturnType;
/** Your evaluation function */
evaluationHook: ArbiterHook;
@@ -127,8 +124,7 @@ interface WebhookResult extends DecisionResult {
The most common pattern combines `watchNewCases` with the webhook handler to automatically evaluate incoming refund requests:
```typescript
-import { X402rArbiter, createWebhookHandler } from '@x402r/arbiter';
-import type { CaseEvaluationContext, DecisionResult, RefundRequestEventLog } from '@x402r/arbiter';
+import { createArbiterClient } from '@x402r/sdk';
import { PaymentState, RequestStatus } from '@x402r/core';
import type { PaymentInfo } from '@x402r/core';
@@ -143,23 +139,21 @@ const handler = createWebhookHandler({
});
// Step 2: Watch for new cases and feed them to the handler
-const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLog) => {
- const paymentInfoHash = event.args.paymentInfoHash!;
- const nonce = event.args.nonce ?? 0n;
+const unsubscribe = arbiter.watch.onRefundRequest(async (event: any) => {
+ const paymentInfoHash = event.args?.paymentInfoHash;
+ if (!paymentInfoHash) return;
- console.log(`[NEW CASE] ${paymentInfoHash} (nonce: ${nonce})`);
+ console.log(`[NEW CASE] ${paymentInfoHash}`);
- // Build the evaluation context
- // NOTE: You need to reconstruct the full PaymentInfo from your database or event logs
- const paymentInfo = await lookupPaymentInfo(paymentInfoHash);
+ // Retrieve stored PaymentInfo from RefundRequest contract
+ const paymentInfo = await arbiter.refund.getStoredPaymentInfo(paymentInfoHash);
const context: CaseEvaluationContext = {
paymentInfo,
- nonce,
paymentState: PaymentState.InEscrow,
refundStatus: RequestStatus.Pending,
paymentInfoHash,
- refundAmount: event.args.amount,
+ refundAmount: event.args?.amount,
};
// Evaluate and optionally auto-submit
@@ -170,15 +164,7 @@ const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLo
if (result.executed) {
console.log(`[ON-CHAIN] Decision submitted: ${result.txHash}`);
-
- // If approved, execute the refund transfer
- if (result.decision === 'approve') {
- const { txHash } = await arbiter.executeRefundInEscrow(
- paymentInfo,
- result.refundAmount // partial refund if specified
- );
- console.log(`[REFUND] Executed: ${txHash}`);
- }
+ // In v3, refundInEscrow already executes the refund and auto-approves
} else {
console.log('[SKIPPED] Confidence below threshold, requires manual review');
}
diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx
index 7f32132..10dbce7 100644
--- a/sdk/arbiter/batch-operations.mdx
+++ b/sdk/arbiter/batch-operations.mdx
@@ -1,122 +1,92 @@
---
-title: "Batch Operations"
+title: "Batch operations"
description: "Process multiple refund decisions efficiently with the Arbiter SDK"
icon: "layer-group"
---
-The Arbiter SDK provides batch operations for processing multiple refund requests in a single call. Both `batchApprove` and `batchDeny` accept an array of `{ paymentInfo, nonce }` objects.
+You can process multiple refund decisions by iterating over requests and calling individual methods. In v3, there is no separate approve step — calling `payment.refundInEscrow()` executes the refund and auto-approves the pending request in one transaction.
-Batch items are processed **sequentially**, not atomically. If one item fails mid-batch, all previously processed items will **not** be rolled back. Design your error handling accordingly.
+Each decision is a separate on-chain transaction. If one fails mid-batch, previously processed items will **not** be rolled back. Design your error handling accordingly.
-## Batch Approve
+## Batch refund
-Approve multiple refund requests in one call:
+Execute refunds for multiple pending requests:
```typescript
-const items = [
- { paymentInfo: paymentInfo1, nonce: 0n },
- { paymentInfo: paymentInfo2, nonce: 0n },
- { paymentInfo: paymentInfo3, nonce: 1n },
-];
+const items = [paymentInfo1, paymentInfo2, paymentInfo3];
-const results = await arbiter.batchApprove(items);
-
-for (const { txHash } of results) {
- console.log('Approved:', txHash);
+for (const paymentInfo of items) {
+ const request = await arbiter.refund.get(paymentInfo);
+ const txHash = await arbiter.payment.refundInEscrow(paymentInfo, request.amount);
+ console.log('Refund executed:', txHash);
}
```
-## Batch Deny
+## Batch deny
-Deny multiple refund requests in one call:
+Deny multiple refund requests:
```typescript
-const items = [
- { paymentInfo: paymentInfo4, nonce: 0n },
- { paymentInfo: paymentInfo5, nonce: 0n },
-];
-
-const results = await arbiter.batchDeny(items);
+const items = [paymentInfo4, paymentInfo5];
-for (const { txHash } of results) {
+for (const paymentInfo of items) {
+ const txHash = await arbiter.refund.deny(paymentInfo);
console.log('Denied:', txHash);
}
```
-## Empty Batch Handling
-
-Both batch methods safely handle empty arrays and return an empty results array:
-
-```typescript
-const results = await arbiter.batchApprove([]);
-console.log(results.length); // 0
-```
-
-## Item Format
-
-Each item in the batch array must include both the `paymentInfo` struct and the `nonce`:
-
-```typescript
-interface BatchItem {
- /** The full PaymentInfo struct identifying the payment */
- paymentInfo: PaymentInfo;
- /** The record index (nonce) from PaymentIndexRecorder */
- nonce: bigint;
-}
-```
-
-
-The `nonce` identifies which specific charge record the refund request targets. For most single-charge payments, this is `0n`.
-
-
-## Example: Triage and Batch Process Pending Cases
+## Example: triage and batch process pending cases
-Fetch all pending cases, evaluate each one, then batch approve and deny:
+Fetch all pending cases, evaluate each one, then process:
```typescript
-import { X402rArbiter } from '@x402r/arbiter';
+import { createArbiterClient } from '@x402r/sdk';
import { RequestStatus } from '@x402r/core';
import type { PaymentInfo, RefundRequestData } from '@x402r/core';
async function triageAndProcess(
- arbiter: X402rArbiter,
- receiverAddress: `0x${string}`,
- lookupPaymentInfo: (hash: `0x${string}`) => Promise
+ arbiter: ReturnType,
+ operatorAddress: `0x${string}`
) {
- // Step 1: Fetch pending cases
- const { keys, total } = await arbiter.getPendingRefundRequests(0n, 100n, receiverAddress);
- console.log(`Processing ${total} pending cases`);
+ // Step 1: Fetch cases for this operator
+ const { keys, total } = await arbiter.refund.getOperatorRequests(
+ operatorAddress, 0n, 100n
+ );
+ console.log(`Processing ${total} cases`);
- const toApprove: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
- const toDeny: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = [];
+ const toRefund: PaymentInfo[] = [];
+ const toDeny: PaymentInfo[] = [];
// Step 2: Evaluate each case
for (const key of keys) {
- const request = await arbiter.getRefundRequestByKey(key);
+ const request = await arbiter.refund.getByKey(key);
if (request.status !== RequestStatus.Pending) {
continue;
}
- const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash);
- const item = { paymentInfo, nonce: request.nonce };
+ const paymentInfo = await arbiter.refund.getStoredPaymentInfo(request.paymentInfoHash);
if (shouldApprove(request)) {
- toApprove.push(item);
+ toRefund.push(paymentInfo);
} else {
- toDeny.push(item);
+ toDeny.push(paymentInfo);
}
}
- // Step 3: Batch process decisions
- const approveResults = await arbiter.batchApprove(toApprove);
- const denyResults = await arbiter.batchDeny(toDeny);
+ // Step 3: Process decisions
+ for (const paymentInfo of toRefund) {
+ const request = await arbiter.refund.get(paymentInfo);
+ await arbiter.payment.refundInEscrow(paymentInfo, request.amount);
+ }
- console.log(`Approved: ${approveResults.length}, Denied: ${denyResults.length}`);
+ for (const paymentInfo of toDeny) {
+ await arbiter.refund.deny(paymentInfo);
+ }
- return { approved: approveResults, denied: denyResults };
+ console.log(`Refunded: ${toRefund.length}, Denied: ${toDeny.length}`);
}
function shouldApprove(request: RefundRequestData): boolean {
@@ -125,56 +95,20 @@ function shouldApprove(request: RefundRequestData): boolean {
}
```
-## Example: Batch Approve with Refund Execution
-
-After batch approving, execute refunds individually for each approved payment:
-
-```typescript
-import { X402rArbiter } from '@x402r/arbiter';
-import type { PaymentInfo } from '@x402r/core';
-
-async function batchApproveAndExecute(
- arbiter: X402rArbiter,
- items: Array<{ paymentInfo: PaymentInfo; nonce: bigint }>
-) {
- // Step 1: Batch approve all items
- const approveResults = await arbiter.batchApprove(items);
- console.log(`Approved ${approveResults.length} refund requests`);
-
- // Step 2: Execute refunds individually
- const executeResults: Array<{ txHash: `0x${string}` }> = [];
-
- for (const { paymentInfo } of items) {
- try {
- const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo);
- executeResults.push({ txHash });
- console.log('Refund executed:', txHash);
- } catch (error) {
- console.error(`Failed to execute refund for ${paymentInfo.payer}:`, error);
- }
- }
-
- return {
- approved: approveResults,
- executed: executeResults,
- };
-}
-```
-
-## Performance Considerations
+## Performance considerations
-Each item in a batch results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits.
+Each item results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits.
| Factor | Detail |
|--------|--------|
| **Transaction ordering** | Items are processed sequentially to ensure correct nonce ordering. |
-| **Gas costs** | Each item is a separate transaction. Batch methods save on SDK overhead, not gas. |
+| **Gas costs** | Each item is a separate transaction. |
| **Partial failures** | If one transaction fails, previous ones remain on-chain. Handle partial failures in your logic. |
| **Rate limiting** | Large batches may hit RPC rate limits. Consider adding delays for 50+ item batches. |
-## Next Steps
+## Next steps
@@ -184,7 +118,7 @@ Each item in a batch results in a separate on-chain transaction. Gas costs scale
Watch for new cases in real-time.
- Individual approve/deny methods and executeRefundInEscrow.
+ Individual deny methods and refundInEscrow.
Review the complete arbiter setup guide.
diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx
index 696479e..b75ee3f 100644
--- a/sdk/arbiter/decision-submission.mdx
+++ b/sdk/arbiter/decision-submission.mdx
@@ -1,58 +1,53 @@
---
-title: "Decision Submission"
+title: "Decision submission"
description: "Submit decisions on refund requests and execute refunds with the Arbiter SDK"
icon: "gavel"
---
-The Arbiter SDK provides methods for reviewing refund requests, making decisions, and executing refunds for disputed payments.
+The arbiter client provides methods for reviewing refund requests, making decisions, and executing refunds for disputed payments. In v3, there is no separate approve step — calling `payment.refundInEscrow()` both executes the refund and auto-approves the pending request.
-## Approve a Refund Request
+## Execute refund in escrow
-Approve a pending refund request. This updates the on-chain status but does not transfer funds.
+Execute a refund to transfer funds back to the payer. This also auto-approves any pending refund request via the IRecorder plugin.
```typescript
-const { txHash } = await arbiter.approveRefundRequest(paymentInfo, 0n);
-console.log('Refund approved:', txHash);
-```
+// Full refund
+const txHash = await arbiter.payment.refundInEscrow(
+ paymentInfo,
+ paymentInfo.maxAmount
+);
+console.log('Full refund executed:', txHash);
-
-Approving a refund request updates the request status to `Approved` but does not transfer funds. You must call `executeRefundInEscrow()` separately to move funds back to the payer.
-
+// Partial refund
+const partialAmount = BigInt('500000'); // 0.5 USDC
+const partialTx = await arbiter.payment.refundInEscrow(paymentInfo, partialAmount);
+console.log('Partial refund executed:', partialTx);
+```
-## Deny a Refund Request
+## Deny a refund request
Deny a pending refund request:
```typescript
-const { txHash } = await arbiter.denyRefundRequest(paymentInfo, 0n);
+const txHash = await arbiter.refund.deny(paymentInfo);
console.log('Refund denied:', txHash);
```
-## Execute Refund in Escrow
+## Refuse a refund request
-After approving a refund request, execute the actual fund transfer back to the payer:
+Refuse a pending refund request (similar to deny, but signals a different intent):
```typescript
-// Full refund (defaults to paymentInfo.maxAmount)
-const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo);
-console.log('Full refund executed:', txHash);
-
-// Partial refund
-const partialAmount = BigInt('500000'); // 0.5 USDC
-const { txHash: partialTx } = await arbiter.executeRefundInEscrow(paymentInfo, partialAmount);
-console.log('Partial refund executed:', partialTx);
+const txHash = await arbiter.refund.refuse(paymentInfo);
+console.log('Refund refused:', txHash);
```
-
-When no `amount` is provided, `executeRefundInEscrow` defaults to `paymentInfo.maxAmount`, issuing a full refund.
-
-
-## Check If a Refund Request Exists
+## Check if a refund request exists
-Verify whether a refund request has been submitted for a given payment and nonce:
+Verify whether a refund request has been submitted for a given payment:
```typescript
-const hasRequest = await arbiter.hasRefundRequest(paymentInfo, 0n);
+const hasRequest = await arbiter.refund.has(paymentInfo);
if (!hasRequest) {
console.log('No refund request found for this payment');
@@ -60,47 +55,35 @@ if (!hasRequest) {
}
```
-## Get Refund Request Data
+## Get refund request data
Retrieve the full refund request data, including amount and status:
```typescript
import { RequestStatus } from '@x402r/core';
-const request = await arbiter.getRefundRequest(paymentInfo, 0n);
+const request = await arbiter.refund.get(paymentInfo);
console.log('Payment hash:', request.paymentInfoHash);
-console.log('Nonce:', request.nonce);
console.log('Refund amount:', request.amount);
console.log('Status:', RequestStatus[request.status]);
```
-The `RefundRequestData` type contains:
-
-```typescript
-interface RefundRequestData {
- paymentInfoHash: `0x${string}`;
- nonce: bigint;
- amount: bigint;
- status: RequestStatus;
-}
-```
-
-## Get Refund Request Status
+## Get refund request status
Query the current status of a specific refund request:
```typescript
import { RequestStatus } from '@x402r/core';
-const status = await arbiter.getRefundStatus(paymentInfo, 0n);
+const status = await arbiter.refund.getStatus(paymentInfo);
switch (status) {
case RequestStatus.Pending:
console.log('Awaiting decision');
break;
case RequestStatus.Approved:
- console.log('Already approved');
+ console.log('Already approved (via refundInEscrow)');
break;
case RequestStatus.Denied:
console.log('Already denied');
@@ -111,58 +94,41 @@ switch (status) {
}
```
-## Get Pending Refund Requests (Paginated)
+## Get refund requests by operator (paginated)
-Retrieve a paginated list of refund request keys for a receiver. You can optionally filter by receiver address:
+Retrieve a paginated list of refund request keys for an operator:
```typescript
-// Get the first 10 pending requests for a specific receiver
-const { keys, total } = await arbiter.getPendingRefundRequests(
+const { keys, total } = await arbiter.refund.getOperatorRequests(
+ operatorAddress,
0n, // offset
- 10n, // count
- '0xReceiverAddress...' // optional: defaults to the arbiter's wallet address
+ 10n // count
);
console.log(`${total} total cases, showing first ${keys.length}`);
-// Look up each request by its composite key
for (const key of keys) {
- const request = await arbiter.getRefundRequestByKey(key);
+ const request = await arbiter.refund.getByKey(key);
console.log(`Amount: ${request.amount}, Status: ${RequestStatus[request.status]}`);
}
```
-## Get Refund Request Count
+## Get refund request by key
-Get the total number of refund requests for a receiver:
+Look up a specific refund request using its `paymentInfoHash`:
```typescript
-const count = await arbiter.getRefundRequestCount('0xReceiverAddress...');
-console.log(`Total refund requests: ${count}`);
-
-// Defaults to the arbiter's wallet address if not specified
-const myCount = await arbiter.getRefundRequestCount();
-console.log(`My refund requests: ${myCount}`);
-```
-
-## Get Refund Request by Composite Key
-
-Look up a specific refund request using its `keccak256(paymentInfoHash, nonce)` composite key:
-
-```typescript
-const request = await arbiter.getRefundRequestByKey(compositeKey);
+const request = await arbiter.refund.getByKey(paymentInfoHash);
console.log('Payment hash:', request.paymentInfoHash);
console.log('Amount:', request.amount);
console.log('Status:', RequestStatus[request.status]);
```
-## Check If a Payment Is Frozen
-
-Verify whether a payment is currently frozen by a Freeze condition contract:
+## Check if a payment is frozen
```typescript
-const frozen = await arbiter.isFrozen(paymentInfo, freezeAddress);
+const frozen = await arbiter.freeze.isFrozen(paymentInfo);
if (frozen) {
console.log('Payment is frozen - dispute in progress');
@@ -171,23 +137,28 @@ if (frozen) {
}
```
-## Complete Decision Workflow
+## Complete decision workflow
This example shows the full arbiter workflow: fetching pending cases, reviewing them, making a decision, and executing the refund.
```typescript
-import { X402rArbiter } from '@x402r/arbiter';
+import { createArbiterClient } from '@x402r/sdk';
import { RequestStatus } from '@x402r/core';
import type { PaymentInfo } from '@x402r/core';
-async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0x${string}`) {
- // Step 1: Get all pending refund requests
- const { keys, total } = await arbiter.getPendingRefundRequests(0n, 50n, receiverAddress);
- console.log(`Found ${total} pending cases`);
+async function processAllPendingCases(
+ arbiter: ReturnType,
+ operatorAddress: `0x${string}`
+) {
+ // Step 1: Get all refund requests for this operator
+ const { keys, total } = await arbiter.refund.getOperatorRequests(
+ operatorAddress, 0n, 50n
+ );
+ console.log(`Found ${total} cases`);
for (const key of keys) {
// Step 2: Retrieve request details
- const request = await arbiter.getRefundRequestByKey(key);
+ const request = await arbiter.refund.getByKey(key);
// Step 3: Skip if already decided
if (request.status !== RequestStatus.Pending) {
@@ -195,48 +166,41 @@ async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0
continue;
}
- // Step 4: Apply your decision logic
- const shouldApprove = await evaluateCase(request);
-
- if (shouldApprove) {
- // Step 5a: Approve and execute the refund
- // NOTE: You need the full PaymentInfo struct to call these methods.
- // Retrieve it from your application's database or event logs.
- const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash);
+ // Step 4: Retrieve the stored PaymentInfo
+ const paymentInfo = await arbiter.refund.getStoredPaymentInfo(request.paymentInfoHash);
- const { txHash: approveTx } = await arbiter.approveRefundRequest(paymentInfo, request.nonce);
- console.log(`Approved: ${approveTx}`);
+ // Step 5: Apply your decision logic
+ const shouldRefund = await evaluateCase(request);
- const { txHash: executeTx } = await arbiter.executeRefundInEscrow(paymentInfo);
- console.log(`Refund executed: ${executeTx}`);
+ if (shouldRefund) {
+ // Execute refund (auto-approves the request)
+ const txHash = await arbiter.payment.refundInEscrow(paymentInfo, request.amount);
+ console.log(`Refund executed: ${txHash}`);
} else {
- // Step 5b: Deny the refund
- const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash);
-
- const { txHash } = await arbiter.denyRefundRequest(paymentInfo, request.nonce);
+ // Deny the refund
+ const txHash = await arbiter.refund.deny(paymentInfo);
console.log(`Denied: ${txHash}`);
}
}
}
```
-## Decision Flow Diagram
+## Decision flow diagram
```mermaid
flowchart TD
- A[Get Pending Requests] --> B[getRefundRequestByKey]
+ A[Get Operator Requests] --> B[refund.getByKey]
B --> C{Status Pending?}
C -->|No| D[Skip - Already Decided]
C -->|Yes| E[Evaluate Case]
E --> F{Decision}
- F -->|Approve| G[approveRefundRequest]
- F -->|Deny| H[denyRefundRequest]
- G --> I[executeRefundInEscrow]
- I --> J[Funds Returned to Payer]
+ F -->|Refund| G[payment.refundInEscrow]
+ F -->|Deny| H[refund.deny]
+ G --> I[Funds Returned + Request Auto-Approved]
H --> K[Merchant Keeps Funds]
```
-## Next Steps
+## Next steps
diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx
index dc24b32..1024d7e 100644
--- a/sdk/arbiter/quickstart.mdx
+++ b/sdk/arbiter/quickstart.mdx
@@ -8,49 +8,53 @@ icon: "rocket"
The Arbiter SDK is experimental. The dispute resolution system design is actively evolving.
-The `@x402r/arbiter` package provides methods for arbiters to resolve disputes: reviewing refund requests, approving or denying them, and executing refunds.
+The `@x402r/sdk` package provides methods for arbiters to resolve disputes: reviewing refund requests, denying them, and executing refunds via `refundInEscrow()`.
## Setup
```typescript
-import { X402rArbiter } from '@x402r/arbiter';
-import { getNetworkConfig } from '@x402r/core';
+import { createArbiterClient } from '@x402r/sdk';
-const config = getNetworkConfig('eip155:84532')!;
-
-const arbiter = new X402rArbiter({
+const arbiter = createArbiterClient({
publicClient,
walletClient,
operatorAddress: '0x...',
- refundRequestAddress: config.refundRequest,
- arbiterRegistryAddress: config.arbiterRegistry,
+ // Optional: enable escrow and freeze features
+ escrowPeriodAddress: '0x...',
+ freezeAddress: '0x...',
});
```
-## Available Methods
+Refund request and evidence addresses are auto-resolved from the chain config.
+
+## Available methods
-The Arbiter SDK currently supports:
+The arbiter client currently supports:
-- **`approveRefundRequest()`** — Approve a pending refund request
-- **`denyRefundRequest()`** — Deny a pending refund request
-- **`executeRefundInEscrow()`** — Execute an approved refund to transfer funds back
-- **`getPendingRefundRequests()`** — List refund requests awaiting decision
-- **`getRefundRequestByKey()`** — Get details of a specific refund request
-- **`registerArbiter()`** — Register in the on-chain ArbiterRegistry
-- **`isArbiterRegistered()`** — Check registration status
-- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request
-- **`getEvidence()`** — Retrieve a single evidence entry by index
-- **`getAllEvidence()`** — Retrieve all evidence for a refund request
+- **`payment.refundInEscrow()`** — Execute a refund (auto-approves pending request)
+- **`refund.deny()`** — Deny a pending refund request
+- **`refund.refuse()`** — Refuse a pending refund request
+- **`refund.get()`** — Get full refund request data
+- **`refund.getByKey()`** — Look up by paymentInfoHash
+- **`refund.getStatus()`** — Check request status
+- **`refund.has()`** — Check if a request exists
+- **`refund.getOperatorRequests()`** — List requests by operator
+- **`freeze.isFrozen()`** — Check if a payment is frozen
+- **`freeze.unfreeze()`** — Unfreeze a frozen payment
+- **`evidence.submit()`** — Attach evidence (IPFS CID) to a refund request
+- **`evidence.get()`** — Retrieve a single evidence entry by index
+- **`evidence.getBatch()`** — Retrieve multiple evidence entries
+- **`operator.distributeFees()`** — Distribute accumulated fees
-## Try It Now
+## Try it now
-The easiest way to try arbiter features is with the **arbiter-cli** example, which provides a command-line interface for all arbiter operations:
+The easiest way to try arbiter features is with the **arbiter-cli** example:
CLI tool for arbiters to review cases, make decisions, and manage registry.
-## Next Steps
+## Next steps
diff --git a/sdk/arbiter/registry.mdx b/sdk/arbiter/registry.mdx
index 1855c8d..1f88866 100644
--- a/sdk/arbiter/registry.mdx
+++ b/sdk/arbiter/registry.mdx
@@ -11,20 +11,22 @@ The ArbiterRegistry is an on-chain contract that allows arbiters to register the
To use registry methods, provide the `arbiterRegistryAddress` when creating the arbiter instance:
```typescript
-import { X402rArbiter } from '@x402r/arbiter';
-import { getNetworkConfig } from '@x402r/core';
+import { createArbiterClient } from '@x402r/sdk';
+import { getChainConfig } from '@x402r/core';
-const config = getNetworkConfig('eip155:84532')!;
+const config = getChainConfig(84532);
-const arbiter = new X402rArbiter({
+const arbiter = createArbiterClient({
publicClient,
walletClient,
operatorAddress: '0x...',
- refundRequestAddress: config.refundRequest,
- arbiterRegistryAddress: config.arbiterRegistry,
});
```
+
+The arbiter registry address is resolved automatically from the chain config. Registry methods are not currently exposed on the preset client — use `createX402r()` for direct access if needed.
+
+
## Register as an Arbiter
Register your address in the on-chain registry with a URI pointing to your metadata or API endpoint:
@@ -120,17 +122,16 @@ interface ArbiterList {
## Complete Example
```typescript
-import { X402rArbiter } from '@x402r/arbiter';
-import { getNetworkConfig } from '@x402r/core';
+import { createArbiterClient } from '@x402r/sdk';
+import { getChainConfig } from '@x402r/core';
async function main() {
- const config = getNetworkConfig('eip155:84532')!;
+ const config = getChainConfig(84532);
- const arbiter = new X402rArbiter({
+ const arbiter = createArbiterClient({
publicClient,
walletClient,
operatorAddress: '0x...',
- arbiterRegistryAddress: config.arbiterRegistry,
});
// Register
diff --git a/sdk/arbiter/subscriptions.mdx b/sdk/arbiter/subscriptions.mdx
index e2436ae..062a152 100644
--- a/sdk/arbiter/subscriptions.mdx
+++ b/sdk/arbiter/subscriptions.mdx
@@ -1,120 +1,74 @@
---
-title: "Arbiter Events"
+title: "Arbiter events"
description: "Subscribe to dispute events and build real-time arbiter dashboards"
icon: "bell"
---
-The Arbiter SDK provides three subscription methods for monitoring dispute activity in real-time. Each returns an object with an `unsubscribe` function you call to stop watching.
+The arbiter client provides watch methods for monitoring dispute activity in real-time. Each returns an unsubscribe function you call to stop watching.
-## Watch New Cases
+## Watch refund requests
-Subscribe to `RefundRequested` events -- these are new refund requests that need your attention:
+Subscribe to RefundRequest contract events — these include new requests, status updates, and cancellations:
```typescript
-import type { RefundRequestEventLog } from '@x402r/core';
-
-const { unsubscribe } = arbiter.watchNewCases((event: RefundRequestEventLog) => {
- console.log('New refund request!');
- console.log('Payment hash:', event.args.paymentInfoHash);
- console.log('Payer:', event.args.payer);
- console.log('Receiver:', event.args.receiver);
- console.log('Amount:', event.args.amount);
- console.log('Nonce:', event.args.nonce);
- console.log('Block:', event.blockNumber);
+const unsubscribe = arbiter.watch.onRefundRequest((event) => {
+ console.log('Refund request event:', event);
});
// Later: stop watching
unsubscribe();
```
-## Watch Decisions
+## Watch refund execution
-Subscribe to `RefundRequestStatusUpdated` events -- these fire when a refund request is approved or denied:
+Subscribe to `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` events on the operator:
```typescript
-import type { RefundRequestEventLog } from '@x402r/core';
-
-const { unsubscribe } = arbiter.watchDecisions((event: RefundRequestEventLog) => {
- console.log('Decision made!');
- console.log('Payment hash:', event.args.paymentInfoHash);
- console.log('New status:', event.args.status);
- console.log('Transaction:', event.transactionHash);
+const unsubscribe = arbiter.watch.onRefundExecuted((event) => {
+ console.log('Refund executed:', event);
});
+
+unsubscribe();
```
-## Watch Freeze Events
+## Watch payment events
-Subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a Freeze condition contract:
+Subscribe to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events:
```typescript
-import type { FreezeEventLog } from '@x402r/core';
-
-const freezeAddress = '0xFreezeContractAddress...' as `0x${string}`;
-
-const { unsubscribe } = arbiter.watchFreezeEvents(
- freezeAddress,
- (event: FreezeEventLog) => {
- if (event.eventName === 'PaymentFrozen') {
- console.log('Payment frozen:', event.args.paymentInfoHash);
- console.log('Frozen by:', event.args.caller);
- } else if (event.eventName === 'PaymentUnfrozen') {
- console.log('Payment unfrozen:', event.args.paymentInfoHash);
- }
- }
-);
-```
-
-## Event Type Reference
+const unsubscribe = arbiter.watch.onPayment((event) => {
+ console.log('Payment event:', event);
+});
-| Method | Contract Event | Fires When |
-|--------|---------------|------------|
-| `watchNewCases` | `RefundRequested` | A payer submits a new refund request |
-| `watchDecisions` | `RefundRequestStatusUpdated` | An arbiter or receiver approves/denies a request |
-| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | A payment is frozen or unfrozen |
+unsubscribe();
+```
-## Event Log Types
+## Watch fee distribution
-Both `watchNewCases` and `watchDecisions` emit `RefundRequestEventLog` events:
+Subscribe to `FeesDistributed` events:
```typescript
-interface RefundRequestEventLog {
- eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled';
- args: {
- paymentInfoHash?: `0x${string}`;
- payer?: `0x${string}`;
- receiver?: `0x${string}`;
- amount?: bigint;
- nonce?: bigint;
- status?: number;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
+const unsubscribe = arbiter.watch.onFeeDistribution((event) => {
+ console.log('Fees distributed:', event);
+});
+
+unsubscribe();
```
-The `watchFreezeEvents` method emits `FreezeEventLog` events:
+## Event type reference
-```typescript
-interface FreezeEventLog {
- eventName: 'PaymentFrozen' | 'PaymentUnfrozen';
- args: {
- paymentInfoHash?: `0x${string}`;
- caller?: `0x${string}`;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
-```
+| Method | Events Watched | Contract | Fires When |
+|--------|---------------|----------|------------|
+| `watch.onRefundRequest` | All RefundRequest events | RefundRequest | Request created, status updated, or cancelled |
+| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | A refund is executed |
+| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Payment lifecycle events |
+| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | Fees are distributed |
-All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket).
+For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket).
-## Next Steps
+## Next steps
diff --git a/sdk/client/escrow-management.mdx b/sdk/client/escrow-management.mdx
index 5f86fd3..40e7ec3 100644
--- a/sdk/client/escrow-management.mdx
+++ b/sdk/client/escrow-management.mdx
@@ -1,33 +1,28 @@
---
-title: "Escrow Management"
+title: "Escrow management"
description: "Manage escrow periods and freeze payments with the Client SDK"
icon: "lock"
---
-The Client SDK provides methods to interact with the escrow system, including freezing payments during disputes and querying escrow period timing. All methods documented on this page are **fully functional**.
+The payer client provides methods to interact with the escrow system, including freezing payments during disputes and querying escrow period timing.
-## Freeze Operations
+## Freeze operations
-Freezing a payment pauses the escrow timer, preventing the merchant from releasing funds while a dispute is being resolved. Freeze operations interact with the `Freeze` contract.
+Freezing a payment pauses the escrow timer, preventing the merchant from releasing funds while a dispute is being resolved.
-### freezePayment
+### freeze
Freeze a payment to pause the escrow timer. Only the payer can freeze a payment.
```typescript
-const freezeAddress = '0x...'; // Freeze contract address
-
-const { txHash } = await client.freezePayment(paymentInfo, freezeAddress);
+const txHash = await client.freeze.freeze(paymentInfo);
console.log(`Payment frozen: ${txHash}`);
```
#### Signature
```typescript
-freezePayment(
- paymentInfo: PaymentInfo,
- freezeAddress: `0x${string}`
-): Promise<{ txHash: `0x${string}` }>
+freeze(paymentInfo: PaymentInfo, data?: Hex): Promise
```
@@ -37,22 +32,19 @@ Freezing is useful when:
- The merchant is unresponsive to your refund request
-### unfreezePayment
+### unfreeze
Unfreeze a previously frozen payment. The receiver (merchant) or arbiter can unfreeze a payment once the dispute is resolved.
```typescript
-const { txHash } = await client.unfreezePayment(paymentInfo, freezeAddress);
+const txHash = await client.freeze.unfreeze(paymentInfo);
console.log(`Payment unfrozen: ${txHash}`);
```
#### Signature
```typescript
-unfreezePayment(
- paymentInfo: PaymentInfo,
- freezeAddress: `0x${string}`
-): Promise<{ txHash: `0x${string}` }>
+unfreeze(paymentInfo: PaymentInfo, data?: Hex): Promise
```
### isFrozen
@@ -60,7 +52,7 @@ unfreezePayment(
Check whether a payment is currently frozen.
```typescript
-const frozen = await client.isFrozen(paymentInfo, freezeAddress);
+const frozen = await client.freeze.isFrozen(paymentInfo);
if (frozen) {
console.log('Payment is frozen - escrow timer paused');
@@ -72,24 +64,19 @@ if (frozen) {
#### Signature
```typescript
-isFrozen(
- paymentInfo: PaymentInfo,
- freezeAddress: `0x${string}`
-): Promise
+isFrozen(paymentInfo: PaymentInfo): Promise
```
-## Escrow Period Operations
+## Escrow period operations
-These methods interact with the `EscrowPeriod` contract to query timing information about a payment's escrow window.
+These methods interact with the `EscrowPeriod` contract to query timing information about a payment's escrow window. You must provide an `escrowPeriodAddress` in your client config to use these methods.
### getAuthorizationTime
Get the timestamp (in seconds) when a payment was authorized on-chain. This is the starting point of the escrow period.
```typescript
-const escrowPeriodAddress = '0x...'; // EscrowPeriod contract address
-
-const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress);
+const authTime = await client.escrow.getAuthorizationTime(paymentInfo);
const authDate = new Date(Number(authTime) * 1000);
console.log(`Payment authorized at: ${authDate.toISOString()}`);
@@ -98,21 +85,15 @@ console.log(`Payment authorized at: ${authDate.toISOString()}`);
#### Signature
```typescript
-getAuthorizationTime(
- paymentInfo: PaymentInfo,
- escrowPeriodAddress: `0x${string}`
-): Promise
+getAuthorizationTime(paymentInfo: PaymentInfo): Promise
```
-### isDuringEscrowPeriod
+### isDuringEscrow
Check whether a payment is still within its escrow period. Returns `true` if the escrow period has not yet passed, meaning the payment can still be refunded.
```typescript
-const duringEscrow = await client.isDuringEscrowPeriod(
- paymentInfo,
- escrowPeriodAddress
-);
+const duringEscrow = await client.escrow.isDuringEscrow(paymentInfo);
if (duringEscrow) {
console.log('Still in escrow period - refund is possible');
@@ -124,17 +105,25 @@ if (duringEscrow) {
#### Signature
```typescript
-isDuringEscrowPeriod(
- paymentInfo: PaymentInfo,
- escrowPeriodAddress: `0x${string}`
-): Promise
+isDuringEscrow(paymentInfo: PaymentInfo): Promise
+```
+
+### getDuration
+
+Get the configured escrow period duration in seconds.
+
+```typescript
+const duration = await client.escrow.getDuration();
+console.log(`Escrow period: ${Number(duration) / 86400} days`);
```
-
-The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It returns `true` when the escrow period is still **active** (refund window is open), and `false` when it has passed.
-
+#### Signature
+
+```typescript
+getDuration(): Promise
+```
-## Understanding Escrow Timing
+## Understanding escrow timing
| Condition | Escrow Timer | Can Request Refund | Can Release |
|-----------|--------------|-------------------|-------------|
@@ -143,63 +132,47 @@ The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It re
| Escrow period passed (unfrozen) | Stopped | No | Full amount |
-The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can calculate the remaining time from `getAuthorizationTime` and the configured period.
+The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can use `getDuration()` to retrieve the configured period.
-## Example: Freeze and Request Refund
+## Example: freeze and request refund
A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot release funds while the request is pending.
```typescript
-import { X402rClient } from '@x402r/client';
+import { createPayerClient } from '@x402r/sdk';
import type { PaymentInfo } from '@x402r/core';
async function freezeAndRequestRefund(
- client: X402rClient,
+ client: ReturnType,
paymentInfo: PaymentInfo,
- freezeAddress: `0x${string}`,
refundAmount: bigint
) {
// Step 1: Check if already frozen
- const alreadyFrozen = await client.isFrozen(paymentInfo, freezeAddress);
+ const alreadyFrozen = await client.freeze.isFrozen(paymentInfo);
if (!alreadyFrozen) {
- // Freeze the payment first
- const { txHash: freezeTx } = await client.freezePayment(
- paymentInfo,
- freezeAddress
- );
+ const freezeTx = await client.freeze.freeze(paymentInfo);
console.log(`Payment frozen: ${freezeTx}`);
}
// Step 2: Check if refund request already exists
- const nonce = 0n;
- const hasRequest = await client.hasRefundRequest(paymentInfo, nonce);
+ const hasRequest = await client.refund.has(paymentInfo);
if (!hasRequest) {
- // Submit the refund request
- const { txHash: refundTx } = await client.requestRefund(
- paymentInfo,
- refundAmount,
- nonce
- );
+ const refundTx = await client.refund.request(paymentInfo, refundAmount);
console.log(`Refund requested: ${refundTx}`);
}
// Step 3: Watch for resolution
- const { unsubscribe } = client.watchFreezeEvents(
- freezeAddress,
- (event) => {
- if (event.eventName === 'PaymentUnfrozen') {
- console.log('Payment was unfrozen - dispute may be resolved');
- unsubscribe();
- }
- }
- );
+ const unsubscribe = client.watch.onPayment((event) => {
+ console.log('Payment event received');
+ unsubscribe();
+ });
}
```
-## Freeze / Unfreeze Flow
+## Freeze / unfreeze flow
```mermaid
sequenceDiagram
@@ -208,19 +181,19 @@ sequenceDiagram
participant M as Merchant / Arbiter
Note over F: Escrow timer running
- P->>F: freezePayment()
+ P->>F: freeze()
Note over F: Timer paused
alt Dispute resolved favorably
- M->>F: unfreezePayment()
+ M->>F: unfreeze()
Note over F: Timer resumes
else Payer cancels dispute
- P->>F: unfreezePayment()
+ P->>F: unfreeze()
Note over F: Timer resumes
end
```
-## Next Steps
+## Next steps
@@ -230,7 +203,7 @@ sequenceDiagram
Request refunds while payment is in escrow.
- Planned query methods and current workarounds.
+ Query payment state and details.
Full setup guide for the Client SDK.
diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx
index 38d3103..ac84d15 100644
--- a/sdk/client/payment-queries.mdx
+++ b/sdk/client/payment-queries.mdx
@@ -1,108 +1,68 @@
---
-title: "Payment Queries"
+title: "Payment queries"
description: "Query payment states and details with the Client SDK"
icon: "magnifying-glass"
---
-The Client SDK provides five methods for querying payment state and history. These read directly from the escrow contract and on-chain event logs.
+The payer client provides methods for querying payment state and retrieving payment information.
-## getPaymentState
+## getState
Derive the lifecycle state of a payment from the escrow contract (amounts and expiry).
```typescript
-import { PaymentState } from '@x402r/core';
+const [hasCollected, capturableAmount, refundableAmount] = await client.payment.getState(paymentInfo);
-const state = await client.getPaymentState(paymentInfo);
-
-// PaymentState enum:
-// 0 = NonExistent - Payment has never been authorized
-// 1 = InEscrow - Funds locked, capturableAmount > 0
-// 2 = Released - Funds released to receiver, may still be refundable
-// 3 = Settled - No funds in escrow or refundable
-// 4 = Expired - Authorization expired, payer can reclaim
-```
-
-```typescript
-getPaymentState(paymentInfo: PaymentInfo): Promise
-```
-
-## paymentExists
-
-Check whether a payment has been collected by reading the escrow's `hasCollectedPayment` flag.
-
-```typescript
-const exists = await client.paymentExists(paymentInfoHash);
-if (exists) {
- console.log('Payment found');
-}
-```
-
-```typescript
-paymentExists(paymentInfoHash: `0x${string}`): Promise
-```
-
-## isInEscrow
-
-Check if a payment currently has capturable funds in escrow.
-
-```typescript
-const inEscrow = await client.isInEscrow(paymentInfoHash);
-if (inEscrow) {
+if (capturableAmount > 0n) {
console.log('Payment has funds in escrow');
}
```
```typescript
-isInEscrow(paymentInfoHash: `0x${string}`): Promise
+getState(paymentInfo: PaymentInfo): Promise
```
-## getPaymentDetails
+## getAmounts
-Retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for the given hash.
+Get the capturable and refundable amounts for a payment.
```typescript
-const details = await client.getPaymentDetails(paymentInfoHash);
+const amounts = await client.payment.getAmounts(paymentInfo);
-console.log('Payer:', details.payer);
-console.log('Receiver:', details.receiver);
-console.log('Amount:', details.maxAmount);
+console.log('Capturable:', amounts.capturableAmount);
+console.log('Refundable:', amounts.refundableAmount);
```
```typescript
-getPaymentDetails(
- paymentInfoHash: `0x${string}`,
- fromBlock?: bigint
-): Promise
+getAmounts(paymentInfo: PaymentInfo): Promise
```
-
-This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks).
-
-
-## getPayerPayments
+## Query payments
-List all payments where the connected wallet is the payer, by scanning `AuthorizationCreated` events.
+If you configured a `paymentIndexRecorderAddress` or `paymentStore`, you can use the query action group to look up payments.
```typescript
-const payments = await client.getPayerPayments();
+import { createPayerClient, queryActions } from '@x402r/sdk';
-for (const { hash, paymentInfo } of payments) {
- console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`);
-}
-```
+const client = createPayerClient({
+ publicClient,
+ walletClient,
+ operatorAddress: '0x...',
+ paymentIndexRecorderAddress: '0x...',
+}).extend(queryActions('0xRecorderAddress...'));
-```typescript
-getPayerPayments(
- fromBlock?: bigint
-): Promise>
+// Get all payments where you are the payer
+const payments = await client.query.getPayerPayments(payerAddress);
+
+// Get a specific payment by hash
+const payment = await client.query.getPayment(paymentInfoHash);
```
-Like `getPaymentDetails`, this scans event logs. Pass `fromBlock` to limit the range for large histories.
+The query plugin uses a tiered provider stack: in-memory store, on-chain recorder, then event logs (if `eventFromBlock` is configured). The first provider returning results wins.
-## Next Steps
+## Next steps
diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx
index f451a5f..69d3955 100644
--- a/sdk/client/quickstart.mdx
+++ b/sdk/client/quickstart.mdx
@@ -8,43 +8,47 @@ icon: "rocket"
The Client SDK is experimental. APIs will change as the refund and dispute system design evolves.
-The `@x402r/client` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state.
+The `@x402r/sdk` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state.
## Setup
```typescript
-import { X402rClient } from '@x402r/client';
-import { getNetworkConfig } from '@x402r/core';
+import { createPayerClient } from '@x402r/sdk';
-const networkConfig = getNetworkConfig('eip155:84532')!;
-
-const client = new X402rClient({
+const client = createPayerClient({
publicClient,
walletClient,
operatorAddress: '0x...', // Your PaymentOperator address
- refundRequestAddress: networkConfig.refundRequest,
- escrowAddress: networkConfig.authCaptureEscrow,
+ // Optional: enable escrow, freeze, and refund features
+ escrowPeriodAddress: '0x...',
+ freezeAddress: '0x...',
});
```
-## Available Methods
+Refund and evidence addresses are resolved automatically from the chain config.
+
+## Available methods
-The Client SDK currently supports:
+The payer client exposes action groups:
-- **`requestRefund()`** — Submit a refund request for a payment in escrow
-- **`getRefundStatus()`** — Check the status of a refund request
-- **`freezePayment()`** — Freeze a payment to prevent release during a dispute
-- **`isFrozen()`** — Check if a payment is frozen
-- **`isDuringEscrowPeriod()`** — Check if a payment is still in its escrow window
-- **`getAuthorizationTime()`** — Get when a payment was authorized
-- **`getPaymentState()`** — Derive the lifecycle state of a payment
-- **`paymentExists()`** — Check if a payment has been authorized
-- **`isInEscrow()`** — Check if a payment has capturable funds
-- **`getPaymentDetails()`** — Retrieve full PaymentInfo from event logs
-- **`getPayerPayments()`** — List all payments for the connected wallet
-- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request
-- **`getEvidence()`** — Retrieve a single evidence entry by index
-- **`getAllEvidence()`** — Retrieve all evidence for a refund request
+- **`client.payment.getState()`** — Derive the lifecycle state of a payment
+- **`client.payment.getAmounts()`** — Get capturable and refundable amounts
+- **`client.refund.request()`** — Submit a refund request for a payment
+- **`client.refund.cancel()`** — Cancel a pending refund request
+- **`client.refund.getStatus()`** — Check the status of a refund request
+- **`client.refund.has()`** — Check if a refund request exists
+- **`client.refund.get()`** — Get full refund request data
+- **`client.refund.getByKey()`** — Look up by paymentInfoHash
+- **`client.refund.getStoredPaymentInfo()`** — Retrieve stored PaymentInfo
+- **`client.refund.getPayerRequests()`** — List refund requests by payer
+- **`client.freeze.freeze()`** — Freeze a payment to prevent release
+- **`client.freeze.isFrozen()`** — Check if a payment is frozen
+- **`client.escrow.isDuringEscrow()`** — Check if still in escrow window
+- **`client.escrow.getAuthorizationTime()`** — Get when payment was authorized
+- **`client.escrow.getDuration()`** — Get the escrow period duration
+- **`client.evidence.submit()`** — Attach evidence (IPFS CID) to a refund
+- **`client.evidence.get()`** — Retrieve a single evidence entry by index
+- **`client.evidence.getBatch()`** — Retrieve multiple evidence entries
## Try It Now
diff --git a/sdk/client/refund-operations.mdx b/sdk/client/refund-operations.mdx
index 7acd7f3..0fe2936 100644
--- a/sdk/client/refund-operations.mdx
+++ b/sdk/client/refund-operations.mdx
@@ -1,106 +1,115 @@
---
-title: "Refund Operations"
+title: "Refund operations"
description: "Request and manage refunds with the Client SDK - submit, cancel, and track refund requests"
icon: "rotate-left"
---
-The Client SDK provides complete refund management capabilities for payers. All refund methods interact directly with the `RefundRequest` contract on-chain.
+The payer client provides complete refund management capabilities. All refund methods interact with the `RefundRequest` contract on-chain. Each payment supports one refund request, keyed by its `paymentInfoHash`.
-**About the `nonce` parameter:** Every refund method requires a `nonce: bigint` parameter. This is the record index from the `PaymentIndexRecorder` and identifies which charge within a payment you are requesting a refund for. For the first (and most common) charge, use `0n`.
+In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant calls `refundInEscrow()` — there is no separate approve step.
-## requestRefund
+## request
Submit a refund request for a payment that is in escrow. The request goes on-chain and is visible to the merchant and any assigned arbiter.
```typescript
-const { txHash } = await client.requestRefund(
+const txHash = await client.refund.request(
paymentInfo,
- BigInt('1000000'), // amount to refund (e.g., 1 USDC with 6 decimals)
- 0n // nonce: first charge
+ BigInt('1000000') // amount to refund (e.g., 1 USDC with 6 decimals)
);
console.log(`Refund requested: ${txHash}`);
```
-## cancelRefundRequest
+## cancel
Cancel a pending refund request that you submitted. Only the original requester (payer) can cancel, and only while the request status is `Pending`.
```typescript
-const { txHash } = await client.cancelRefundRequest(paymentInfo, 0n);
+const txHash = await client.refund.cancel(paymentInfo);
console.log(`Refund request cancelled: ${txHash}`);
```
-## Query Refund State
+## Query refund state
-These methods read on-chain state for refund requests. None require a wallet client.
+These methods read on-chain state for refund requests.
### Check existence and status
```typescript
// Check if a refund request exists
-const hasRequest = await client.hasRefundRequest(paymentInfo, 0n);
+const hasRequest = await client.refund.has(paymentInfo);
// Get just the status
-const status = await client.getRefundStatus(paymentInfo, 0n);
+const status = await client.refund.getStatus(paymentInfo);
// Returns: RequestStatus.Pending | Approved | Denied | Cancelled
```
### Get full refund request data
```typescript
-// By paymentInfo + nonce
-const request = await client.getRefundRequest(paymentInfo, 0n);
+// By paymentInfo
+const request = await client.refund.get(paymentInfo);
console.log(request.amount, request.status);
-// By composite key (from getMyRefundRequests)
-const request2 = await client.getRefundRequestByKey(compositeKey);
+// By paymentInfoHash
+const request2 = await client.refund.getByKey(paymentInfoHash);
```
### List your refund requests
```typescript
-// Get total count
-const count = await client.getMyRefundRequestCount();
-
-// Get paginated keys, then fetch details
-const { keys, total } = await client.getMyRefundRequests(0n, 10n);
+// Get paginated refund request keys for a payer address
+const { keys, total } = await client.refund.getPayerRequests(
+ payerAddress,
+ 0n, // offset
+ 10n // count
+);
for (const key of keys) {
- const request = await client.getRefundRequestByKey(key);
+ const request = await client.refund.getByKey(key);
console.log(`Amount: ${request.amount}, Status: ${request.status}`);
}
```
-## Refund Request Lifecycle
+### Retrieve stored payment info
+
+```typescript
+// Get PaymentInfo stored by RefundRequest for a given hash
+const storedInfo = await client.refund.getStoredPaymentInfo(paymentInfoHash);
+```
+
+## Refund request lifecycle
```mermaid
stateDiagram-v2
- [*] --> Pending: requestRefund()
- Pending --> Approved: Merchant/Arbiter approves
+ [*] --> Pending: refund.request()
+ Pending --> Approved: Merchant calls refundInEscrow()
Pending --> Denied: Merchant/Arbiter denies
- Pending --> Cancelled: cancelRefundRequest()
+ Pending --> Cancelled: refund.cancel()
Approved --> [*]: Funds returned
Denied --> [*]
Cancelled --> [*]
```
-## Method Reference
+## Method reference
| Method | Parameters | Returns |
|--------|-----------|---------|
-| `requestRefund` | `paymentInfo, amount, nonce` | `{ txHash }` |
-| `cancelRefundRequest` | `paymentInfo, nonce` | `{ txHash }` |
-| `hasRefundRequest` | `paymentInfo, nonce` | `boolean` |
-| `getRefundStatus` | `paymentInfo, nonce` | `RequestStatus` |
-| `getRefundRequest` | `paymentInfo, nonce` | `RefundRequestData` |
-| `getRefundRequestByKey` | `compositeKey` | `RefundRequestData` |
-| `getMyRefundRequests` | `offset, count` | `{ keys, total }` |
-| `getMyRefundRequestCount` | none | `bigint` |
-
-## Next Steps
+| `refund.request` | `paymentInfo, amount` | `Hash` |
+| `refund.cancel` | `paymentInfo` | `Hash` |
+| `refund.has` | `paymentInfo` | `boolean` |
+| `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` |
+| `refund.get` | `paymentInfo` | `RefundRequestData` |
+| `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` |
+| `refund.getStoredPaymentInfo` | `paymentInfoHash` | `PaymentInfo` |
+| `refund.getPayerRequests` | `payer, offset, count` | `{ keys, total }` |
+| `refund.getCancelCount` | `paymentInfo` | `bigint` |
+| `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` |
+
+## Next steps
diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx
index cac10ce..52dff2f 100644
--- a/sdk/client/subscriptions.mdx
+++ b/sdk/client/subscriptions.mdx
@@ -1,35 +1,19 @@
---
-title: "Client Events"
+title: "Client events"
description: "Subscribe to real-time payment, refund, and freeze events as a payer"
icon: "bell"
---
-The Client SDK provides methods to subscribe to blockchain events in real-time using viem's `watchContractEvent` under the hood. All subscription methods return an object with an `unsubscribe` function that you should call when you no longer need the watcher.
+The SDK provides methods to subscribe to blockchain events in real-time using viem's `watchContractEvent` under the hood. All watch methods return an unsubscribe function.
-## watchPaymentState
+## onPayment
-Watch for state changes on a specific payment. This subscribes to `ReleaseExecuted`, `RefundInEscrowExecuted`, and `RefundPostEscrowExecuted` events on the `PaymentOperator` contract.
+Watch for payment lifecycle events on the operator contract. This subscribes to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events.
```typescript
-const { unsubscribe } = client.watchPaymentState(
- paymentInfoHash,
- (event) => {
- console.log(`Payment event: ${event.eventName}`);
-
- switch (event.eventName) {
- case 'ReleaseExecuted':
- console.log('Funds released to merchant');
- console.log('Amount:', event.args.amount);
- break;
- case 'RefundInEscrowExecuted':
- console.log('Funds refunded from escrow');
- break;
- case 'RefundPostEscrowExecuted':
- console.log('Funds refunded after escrow period');
- break;
- }
- }
-);
+const unsubscribe = client.watch.onPayment((event) => {
+ console.log('Payment event received:', event);
+});
// Stop watching when done
unsubscribe();
@@ -38,55 +22,16 @@ unsubscribe();
### Signature
```typescript
-watchPaymentState(
- paymentInfoHash: `0x${string}`,
- callback: (event: PaymentOperatorEventLog) => void
-): { unsubscribe: () => void }
-```
-
-### PaymentOperatorEventLog Type
-
-```typescript
-interface PaymentOperatorEventLog {
- eventName:
- | 'ReleaseExecuted'
- | 'RefundInEscrowExecuted'
- | 'RefundPostEscrowExecuted'
- | 'AuthorizationCreated'
- | 'ChargeExecuted';
- args: {
- paymentInfoHash?: `0x${string}`;
- payer?: `0x${string}`;
- receiver?: `0x${string}`;
- amount?: bigint;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
+onPayment(callback: (log: unknown) => void): () => void
```
-## watchRefundRequests
+## onRefundRequest
-Watch for refund request lifecycle events. This subscribes to `RefundRequested`, `RefundRequestStatusUpdated`, and `RefundRequestCancelled` events on the `RefundRequest` contract.
+Watch for refund request lifecycle events on the RefundRequest contract.
```typescript
-const { unsubscribe } = client.watchRefundRequests((event) => {
- switch (event.eventName) {
- case 'RefundRequested':
- console.log('New refund request submitted');
- console.log('Payment:', event.args.paymentInfoHash);
- console.log('Amount:', event.args.amount);
- break;
- case 'RefundRequestStatusUpdated':
- console.log('Refund status changed:', event.args.status);
- // 1 = Approved, 2 = Denied
- break;
- case 'RefundRequestCancelled':
- console.log('Refund request cancelled');
- break;
- }
+const unsubscribe = client.watch.onRefundRequest((event) => {
+ console.log('Refund request event:', event);
});
// Stop watching when done
@@ -96,130 +41,63 @@ unsubscribe();
### Signature
```typescript
-watchRefundRequests(
- callback: (event: RefundRequestEventLog) => void
-): { unsubscribe: () => void }
+onRefundRequest(callback: (log: unknown) => void): () => void
```
-Requires `refundRequestAddress` to be configured on the client. Throws an error if not set.
+This is a no-op if no `refundRequestAddress` was resolved. The address is auto-resolved from the chain config.
-### RefundRequestEventLog Type
+## onRefundExecuted
-```typescript
-interface RefundRequestEventLog {
- eventName:
- | 'RefundRequested'
- | 'RefundRequestStatusUpdated'
- | 'RefundRequestCancelled';
- args: {
- paymentInfoHash?: `0x${string}`;
- payer?: `0x${string}`;
- receiver?: `0x${string}`;
- amount?: bigint;
- nonce?: bigint;
- status?: number;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
-```
-
-## watchMyPayments
-
-Watch for new payment authorizations where the connected wallet is the payer. This subscribes to `AuthorizationCreated` events on the `PaymentOperator` contract, filtered by the wallet's address.
+Watch for refund execution events (`RefundInEscrowExecuted`, `RefundPostEscrowExecuted`) on the operator contract.
```typescript
-const { unsubscribe } = client.watchMyPayments((event) => {
- console.log('New payment authorized!');
- console.log('Event:', event.eventName); // 'AuthorizationCreated'
- console.log('Hash:', event.args.paymentInfoHash);
- console.log('Receiver:', event.args.receiver);
- console.log('Amount:', event.args.amount);
+const unsubscribe = client.watch.onRefundExecuted((event) => {
+ console.log('Refund executed:', event);
});
-// Stop watching when done
unsubscribe();
```
### Signature
```typescript
-watchMyPayments(
- callback: (event: PaymentOperatorEventLog) => void
-): { unsubscribe: () => void }
+onRefundExecuted(callback: (log: unknown) => void): () => void
```
-
-Requires a `walletClient` with an account to be configured, since the events are filtered by the payer address.
-
-
-## watchFreezeEvents
+## onFeeDistribution
-Watch for freeze and unfreeze events on a specific `Freeze` contract. This subscribes to `PaymentFrozen` and `PaymentUnfrozen` events.
+Watch for `FeesDistributed` events on the operator contract.
```typescript
-const freezeAddress = '0x...'; // Freeze contract address
-
-const { unsubscribe } = client.watchFreezeEvents(
- freezeAddress,
- (event) => {
- if (event.eventName === 'PaymentFrozen') {
- console.log('Payment frozen:', event.args.paymentInfoHash);
- console.log('Frozen by:', event.args.caller);
- } else if (event.eventName === 'PaymentUnfrozen') {
- console.log('Payment unfrozen:', event.args.paymentInfoHash);
- console.log('Unfrozen by:', event.args.caller);
- }
- }
-);
+const unsubscribe = client.watch.onFeeDistribution((event) => {
+ console.log('Fees distributed:', event);
+});
-// Stop watching when done
unsubscribe();
```
### Signature
```typescript
-watchFreezeEvents(
- freezeAddress: `0x${string}`,
- callback: (event: FreezeEventLog) => void
-): { unsubscribe: () => void }
-```
-
-### FreezeEventLog Type
-
-```typescript
-interface FreezeEventLog {
- eventName: 'PaymentFrozen' | 'PaymentUnfrozen';
- args: {
- paymentInfoHash?: `0x${string}`;
- caller?: `0x${string}`;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
+onFeeDistribution(callback: (log: unknown) => void): () => void
```
-## Event Types Reference
+## Event types reference
| Method | Events Watched | Contract | Use Case |
|--------|---------------|----------|----------|
-| `watchPaymentState` | `ReleaseExecuted`, `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track a single payment's lifecycle |
-| `watchRefundRequests` | `RefundRequested`, `RefundRequestStatusUpdated`, `RefundRequestCancelled` | RefundRequest | Monitor refund request workflow |
-| `watchMyPayments` | `AuthorizationCreated` (filtered by payer) | PaymentOperator | Track new payments for your wallet |
-| `watchFreezeEvents` | `PaymentFrozen`, `PaymentUnfrozen` | Freeze | Monitor dispute freeze activity |
+| `onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Track payment lifecycle |
+| `onRefundRequest` | All RefundRequest events | RefundRequest | Monitor refund request workflow |
+| `onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track refund execution |
+| `onFeeDistribution` | `FeesDistributed` | PaymentOperator | Monitor fee distribution |
-All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket).
+For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket).
-## Next Steps
+## Next steps
diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx
index 3b66c61..2a49212 100644
--- a/sdk/concepts.mdx
+++ b/sdk/concepts.mdx
@@ -64,65 +64,66 @@ The **EscrowPeriod** contract tracks when a payment was authorized and enforces
- **After the period** — merchants can release funds to themselves
```typescript
-import { X402rClient } from '@x402r/client';
+import { createPayerClient } from '@x402r/sdk';
-// Check when payment was authorized
-const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress);
+// Check when payment was authorized (requires escrowPeriodAddress in config)
+const authTime = await client.escrow.getAuthorizationTime(paymentInfo);
// Check if still within escrow period
-const inEscrow = await client.isDuringEscrowPeriod(paymentInfo, escrowPeriodAddress);
+const inEscrow = await client.escrow.isDuringEscrow(paymentInfo);
if (!inEscrow) {
console.log('Escrow period has passed - funds can be released');
}
```
-## Refund Requests
+## Refund requests
-When a payer wants a refund, they create a refund request that goes through approval:
+When a payer wants a refund, they create a refund request. Each payment supports one refund request, keyed by its `paymentInfoHash`.
```typescript
import { RequestStatus } from '@x402r/core';
// RequestStatus values:
RequestStatus.Pending // 0 - Awaiting decision
-RequestStatus.Approved // 1 - Approved by merchant/arbiter
+RequestStatus.Approved // 1 - Auto-approved via refundInEscrow
RequestStatus.Denied // 2 - Denied by merchant/arbiter
RequestStatus.Cancelled // 3 - Cancelled by payer
```
-### Refund Flow
+### Refund flow
+
+In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant or arbiter calls `refundInEscrow()` on the operator — no separate approve step is needed.
```mermaid
sequenceDiagram
participant P as Payer
- participant R as RefundRequest Contract
+ participant R as RefundRequest (IRecorder)
participant M as Merchant
+ participant O as PaymentOperator
participant A as Arbiter
- P->>R: requestRefund(paymentInfo, amount, nonce)
+ P->>R: requestRefund(paymentInfo, amount)
R-->>M: RefundRequested event
- alt Merchant approves
- M->>R: approveRefundRequest(paymentInfo, nonce)
- M->>R: refundInEscrow(paymentInfo, amount)
+ alt Merchant refunds
+ M->>O: refundInEscrow(paymentInfo, amount)
+ O->>R: record() auto-approves pending request
+ O->>P: Funds returned
else Merchant denies
- M->>R: denyRefundRequest(paymentInfo, nonce)
+ M->>R: denyRefundRequest(paymentInfo)
else Escalate to arbiter
- A->>R: approveRefundRequest(paymentInfo, nonce)
- A->>R: executeRefundInEscrow(paymentInfo, amount)
+ A->>O: refundInEscrow(paymentInfo, amount)
+ O->>R: record() auto-approves pending request
+ O->>P: Funds returned
end
```
-### The Nonce Parameter
-
-All refund methods require a `nonce` parameter. This is the record index from the PaymentIndexRecorder that identifies which charge the refund request applies to. For the first charge, use `0n`.
-
```typescript
-// Request refund for the first charge
-await client.requestRefund(paymentInfo, amount, 0n);
+// Request a refund (one per payment)
+await client.refund.request(paymentInfo, amount);
-// Check status for the first charge
-const status = await client.getRefundStatus(paymentInfo, 0n);
+// Check status
+const status = await client.refund.getStatus(paymentInfo);
```
## Freeze / Unfreeze
@@ -130,14 +131,14 @@ const status = await client.getRefundStatus(paymentInfo, 0n);
The **Freeze** contract allows payers to freeze a payment during the escrow period, preventing release until the freeze expires or is lifted:
```typescript
-// Payer freezes payment (requires payer authorization)
-await client.freezePayment(paymentInfo, freezeAddress);
+// Payer freezes payment (requires freezeAddress in config)
+await client.freeze.freeze(paymentInfo);
-// Merchant unfreezes payment (requires receiver authorization)
-await merchant.unfreezePayment(paymentInfo, freezeAddress);
+// Merchant unfreezes payment
+await merchant.freeze.unfreeze(paymentInfo);
// Check frozen status
-const frozen = await client.isFrozen(paymentInfo, freezeAddress);
+const frozen = await client.freeze.isFrozen(paymentInfo);
```
@@ -149,8 +150,8 @@ Freezing a payment does not automatically escalate to an arbiter. It pauses the
| Role | Can Do |
|------|--------|
| **Payer** | Request refunds, freeze payments, cancel requests, query escrow state |
-| **Merchant** | Release payments, charge, approve/deny refunds, unfreeze payments |
-| **Arbiter** | Approve/deny disputed refunds, execute refunds, batch operations, registry |
+| **Merchant** | Release payments, charge, refund in escrow (auto-approves requests), deny refunds |
+| **Arbiter** | Deny/refuse disputed refunds, refund in escrow, review evidence |
## Contract Architecture
@@ -167,9 +168,10 @@ flowchart TB
subgraph Components[Supporting Contracts]
direction LR
- subgraph RR[RefundRequest]
+ subgraph RR[RefundRequest IRecorder]
rr1[Request/Cancel]
- rr2[Approve/Deny]
+ rr2[Deny/Refuse]
+ rr3[Auto-approve on refund]
end
subgraph EP[EscrowPeriod]
ep1[Track auth time]
diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx
index a97990b..4280541 100644
--- a/sdk/deploy-operator.mdx
+++ b/sdk/deploy-operator.mdx
@@ -16,12 +16,12 @@ A complete marketplace operator deployment includes:
1. **EscrowPeriod** — Records authorization time, enforces waiting period before release
2. **Freeze** — Allows payer to freeze payment during escrow, receiver to unfreeze
-3. **StaticAddressCondition** — Restricts refund approval to the designated arbiter
-4. **OrCondition** — Allows either the receiver OR the arbiter to approve in-escrow refunds
+3. **ReceiverCondition** — Gates in-escrow refunds to the merchant (receiver)
+4. **RefundRequest (IRecorder)** — Wired as `refundInEscrowRecorder`, auto-approves pending refund requests during `refundInEscrow()`
5. **StaticFeeCalculator** — Optional operator fee (basis points)
6. **PaymentOperator** — The main contract tying everything together
-All contracts are deployed via factories using CREATE2, so identical configurations produce identical addresses across deployments.
+All contracts are deployed via factories using CREATE3, so identical configurations produce identical addresses across all supported chains.
## Deploy Your Operator
@@ -143,8 +143,8 @@ interface MarketplaceOperatorDeployment {
operatorAddress: Address; // The PaymentOperator
escrowPeriodAddress: Address; // EscrowPeriod recorder/condition
freezeAddress: Address; // Freeze condition
- arbiterConditionAddress: Address; // StaticAddressCondition for arbiter
- refundInEscrowCondition: Address; // OR(Receiver, Arbiter)
+ refundInEscrowCondition: Address; // ReceiverCondition (merchant-gated)
+ refundInEscrowRecorder: Address; // RefundRequest (auto-approve on refund)
feeCalculatorAddress: Address | null; // null if no fee
txHashes: Hash[]; // All deployment tx hashes
summary: {
@@ -155,7 +155,7 @@ interface MarketplaceOperatorDeployment {
```
-Because all contracts use CREATE2, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused.
+Because all contracts use CREATE3, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused.
## Preview Addresses (No Deploy)
@@ -189,7 +189,8 @@ The deployed operator has the following slot configuration:
| `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp |
| `CHARGE_CONDITION` | (none) | No restrictions on charge |
| `RELEASE_CONDITION` | EscrowPeriod | Blocks release during escrow period |
-| `REFUND_IN_ESCROW_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve |
+| `REFUND_IN_ESCROW_CONDITION` | ReceiverCondition | Only the receiver (merchant) can call |
+| `REFUND_IN_ESCROW_RECORDER` | RefundRequest | Auto-approves pending requests during refund |
| `REFUND_POST_ESCROW_CONDITION` | Receiver | Only receiver after escrow |
| `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee |
| `FEE_RECIPIENT` | Your address | Receives fees |
@@ -201,16 +202,18 @@ Deployment is supported on all configured networks:
| Network | Chain ID | EIP-155 ID |
|---------|----------|------------|
| Base Sepolia | 84532 | `eip155:84532` |
-| Base Mainnet | 8453 | `eip155:8453` |
+| Base | 8453 | `eip155:8453` |
| Ethereum | 1 | `eip155:1` |
| Ethereum Sepolia | 11155111 | `eip155:11155111` |
-| Arbitrum Sepolia | 421614 | `eip155:421614` |
-| Polygon | 137 | `eip155:137` |
| Arbitrum | 42161 | `eip155:42161` |
+| Arbitrum Sepolia | 421614 | `eip155:421614` |
| Optimism | 10 | `eip155:10` |
-| Avalanche | 43114 | `eip155:43114` |
+| Polygon | 137 | `eip155:137` |
| Celo | 42220 | `eip155:42220` |
+| Avalanche | 43114 | `eip155:43114` |
+| Linea | 59144 | `eip155:59144` |
| Monad | 143 | `eip155:143` |
+| SKALE Base | 1187947933 | `eip155:1187947933` |
Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can get testnet ETH from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets).
diff --git a/sdk/helpers/refundable.mdx b/sdk/helpers/refundable.mdx
index 1d5fbfe..b2d8d77 100644
--- a/sdk/helpers/refundable.mdx
+++ b/sdk/helpers/refundable.mdx
@@ -121,7 +121,7 @@ const option = refundable({
## Supported Networks
-The function resolves addresses from the network config for all supported networks. See `getNetworkConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Arbitrum Sepolia, Polygon, Arbitrum, Optimism, Avalanche, Celo, Monad).
+The function resolves addresses from the chain config for all supported networks. See `getChainConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Arbitrum, Arbitrum Sepolia, Optimism, Polygon, Celo, Avalanche, Linea, Monad, SKALE Base).
```typescript
refundable({ network: 'eip155:84532', ... }, '0x...'); // Base Sepolia
diff --git a/sdk/installation.mdx b/sdk/installation.mdx
index 7485080..a54373b 100644
--- a/sdk/installation.mdx
+++ b/sdk/installation.mdx
@@ -9,19 +9,9 @@ icon: "download"
Install only the packages you need for your use case:
-
+
```bash
- npm install @x402r/client @x402r/core viem
- ```
-
-
- ```bash
- npm install @x402r/merchant @x402r/helpers @x402r/core viem
- ```
-
-
- ```bash
- npm install @x402r/arbiter @x402r/core viem
+ npm install @x402r/sdk @x402r/core viem
```
@@ -31,27 +21,29 @@ Install only the packages you need for your use case:
-## Setup viem Clients
+The `@x402r/sdk` package includes factory functions for all roles (payer, merchant, arbiter).
+
+## Setup viem clients
-Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clients/public). All SDK classes require these as constructor arguments.
+Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clients/public). All SDK factory functions require these as config arguments.
-## Contract Addresses
+## Contract addresses
-Get the deployed contract addresses from the network config:
+Get the deployed contract addresses from the chain config:
```typescript
-import { getNetworkConfig } from '@x402r/core';
+import { getChainConfig } from '@x402r/core';
-const config = getNetworkConfig('eip155:84532'); // Base Sepolia
+const config = getChainConfig(84532); // Base Sepolia
console.log(config.authCaptureEscrow); // Escrow contract
-console.log(config.refundRequest); // RefundRequest contract
console.log(config.arbiterRegistry); // ArbiterRegistry contract
-console.log(config.usdc); // USDC token address
+console.log(config.usdc); // USDC token address
+console.log(config.factories); // Factory addresses
```
-Network identifiers use the [EIP-155](https://eips.ethereum.org/EIPS/eip-155) format: `eip155:`. For Base Sepolia, use `'eip155:84532'`. For Base Mainnet, use `'eip155:8453'`.
+`getChainConfig()` accepts a numeric chain ID (e.g., `84532` for Base Sepolia). All v3 protocol addresses are identical across every supported chain thanks to CREATE3 deployment.
diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx
index 7e3a23c..ae1904b 100644
--- a/sdk/limitations.mdx
+++ b/sdk/limitations.mdx
@@ -8,38 +8,43 @@ The SDK provides full coverage of core payment flows including authorization, re
## API Constraints
-### EIP-155 Network Identifiers
+### Chain ID lookups
-Network configuration requires EIP-155 format strings, not chain ID numbers:
+Network configuration uses numeric chain IDs:
```typescript
// Correct
-const config = getNetworkConfig('eip155:84532');
+const config = getChainConfig(84532);
-// Incorrect - will return undefined
-const config = getNetworkConfig(84532);
+// Incorrect - EIP-155 strings are no longer used for chain config
+// const config = getNetworkConfig('eip155:84532');
```
-### PaymentInfo Must Be Complete
+Use `toNetworkId(chainId)` and `fromNetworkId(networkId)` to convert between numeric chain IDs and EIP-155 format strings when needed.
-All SDK methods require a complete `PaymentInfo` object. You cannot query by hash alone:
+### PaymentInfo must be complete
+
+Most SDK methods require a complete `PaymentInfo` object. You can use `refund.getStoredPaymentInfo(paymentInfoHash)` to retrieve stored PaymentInfo from the RefundRequest contract, or use the query plugin to look up payments by hash:
```typescript
-// Works - full PaymentInfo
-const status = await client.getRefundStatus(paymentInfo, 0n);
+// Retrieve stored PaymentInfo from RefundRequest contract
+const paymentInfo = await client.refund.getStoredPaymentInfo(paymentInfoHash);
-// Not supported - hash-only queries require the full struct
-// const state = await client.getPaymentStateByHash(hash);
+// Or use the query plugin
+const paymentInfo = await client.query.getPayment(paymentInfoHash);
```
### Event Log Scanning Limits
-`getPayerPayments()`, `getReceiverPayments()`, and `getPaymentDetails()` scan `AuthorizationCreated` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Pass a `fromBlock` parameter for large ranges:
+The event-based query provider scans `AuthorizationCreated` and `ChargeExecuted` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Configure `eventFromBlock` in your client config to set the scan start:
```typescript
-// Scan only recent blocks to avoid RPC limits
-const payments = await client.getPayerPayments(recentBlockNumber);
-const details = await client.getPaymentDetails(hash, recentBlockNumber);
+const client = createPayerClient({
+ publicClient,
+ walletClient,
+ operatorAddress: '0x...',
+ eventFromBlock: recentBlockNumber, // Required to enable event fallback provider
+});
```
### No Express/Hono Middleware
diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx
index e5bdd20..b6aa703 100644
--- a/sdk/merchant/payment-operations.mdx
+++ b/sdk/merchant/payment-operations.mdx
@@ -1,66 +1,64 @@
---
-title: "Payment Operations"
+title: "Payment operations"
description: "Release funds, charge payments, process refunds, and query escrow state with the Merchant SDK"
icon: "coins"
---
-The `X402rMerchant` class provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration.
+The merchant client provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration.
-## Payment Operations
+## Payment operations
-### Release Funds from Escrow
+### Release funds from escrow
-Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units.
+Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required**.
```typescript
-import { X402rMerchant } from '@x402r/merchant';
-
// Release 10 USDC (6 decimals) from escrow
-const { txHash } = await merchant.release(paymentInfo, BigInt('10000000'));
+const txHash = await merchant.payment.release(paymentInfo, BigInt('10000000'));
console.log('Released:', txHash);
```
-For partial releases, specify a smaller amount. The remaining funds stay in escrow and can be released or refunded later.
+For partial releases, specify a smaller amount. The remaining funds stay in escrow.
```typescript
// Release 3 USDC of a 10 USDC escrow
-const { txHash } = await merchant.release(paymentInfo, BigInt('3000000'));
+const txHash = await merchant.payment.release(paymentInfo, BigInt('3000000'));
console.log('Partial release:', txHash);
// Check what remains
-const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo);
-console.log('Remaining in escrow:', capturableAmount); // 7000000n
+const amounts = await merchant.payment.getAmounts(paymentInfo);
+console.log('Remaining in escrow:', amounts.capturableAmount); // 7000000n
```
-The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount.
+The `amount` parameter is always required. There is no default "release all" behavior. Always query `payment.getAmounts()` first to determine the available capturable amount.
-### Refund While in Escrow
+### Refund while in escrow
-Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**.
+Use `payment.refundInEscrow()` to return escrowed funds to the payer. This also auto-approves any pending RefundRequest.
```typescript
// Full refund of 10 USDC
-const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000'));
+const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('10000000'));
console.log('Refunded from escrow:', txHash);
```
```typescript
// Partial refund: return 2 USDC, keep 8 USDC in escrow
-const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000'));
+const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('2000000'));
console.log('Partial refund:', txHash);
```
-### Charge Directly
+### Charge directly
-Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`).
+Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments.
```typescript
const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...';
const collectorData: `0x${string}` = '0xSignatureOrCalldata...';
-const { txHash } = await merchant.charge(
+const txHash = await merchant.payment.charge(
paymentInfo,
BigInt('5000000'), // 5 USDC
tokenCollectorAddress, // token collector contract
@@ -73,15 +71,15 @@ console.log('Charged:', txHash);
The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer.
-### Refund After Release (Post-Escrow)
+### Refund after release (post-escrow)
-Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance.
+Use `payment.refundPostEscrow()` to refund funds that have already been released to the receiver.
```typescript
const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...';
const collectorData: `0x${string}` = '0xSignatureOrCalldata...';
-const { txHash } = await merchant.refundPostEscrow(
+const txHash = await merchant.payment.refundPostEscrow(
paymentInfo,
BigInt('5000000'), // 5 USDC to refund
tokenCollectorAddress, // token collector that sources the refund
@@ -94,167 +92,80 @@ console.log('Post-escrow refund:', txHash);
Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer.
-## Query Methods
-
-### Get Payment Amounts
+## Query methods
-Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment. This method reads directly from the escrow contract.
+### Get payment amounts
```typescript
-const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo);
+const amounts = await merchant.payment.getAmounts(paymentInfo);
-console.log('Capturable:', capturableAmount); // Funds available to release
-console.log('Refundable:', refundableAmount); // Funds available to refund
+console.log('Capturable:', amounts.capturableAmount); // Funds available to release
+console.log('Refundable:', amounts.refundableAmount); // Funds available to refund
-if (capturableAmount > 0n) {
- // Release available funds
- const { txHash } = await merchant.release(paymentInfo, capturableAmount);
+if (amounts.capturableAmount > 0n) {
+ const txHash = await merchant.payment.release(paymentInfo, amounts.capturableAmount);
console.log('Released all capturable funds:', txHash);
}
```
-
-`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance.
-
-
-### Get Operator Configuration
+### Get operator configuration
-Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract. This includes the escrow address, fee configuration, all 5 condition slots, and all 5 recorder slots.
+Use `operator.getConfig()` to retrieve all immutable slot addresses from the PaymentOperator contract, including conditions and recorders.
```typescript
-const config = await merchant.getOperatorConfig();
+const config = await merchant.operator.getConfig();
// Core state
-console.log('Escrow:', config.escrow);
console.log('Fee recipient:', config.feeRecipient);
console.log('Fee calculator:', config.feeCalculator);
-console.log('Protocol fee config:', config.protocolFeeConfig);
// Condition slots (address(0) = always allow)
-console.log('Authorize condition:', config.authorizeCondition);
-console.log('Charge condition:', config.chargeCondition);
console.log('Release condition:', config.releaseCondition);
console.log('Refund in-escrow condition:', config.refundInEscrowCondition);
-console.log('Refund post-escrow condition:', config.refundPostEscrowCondition);
// Recorder slots (address(0) = no-op)
-console.log('Authorize recorder:', config.authorizeRecorder);
-console.log('Charge recorder:', config.chargeRecorder);
-console.log('Release recorder:', config.releaseRecorder);
console.log('Refund in-escrow recorder:', config.refundInEscrowRecorder);
-console.log('Refund post-escrow recorder:', config.refundPostEscrowRecorder);
```
-### Get Fee Structure
+### Get fee addresses
-Use `getFeeStructure()` to retrieve the fee-related addresses for the operator. This is a lighter alternative to `getOperatorConfig()` when you only need fee information.
+Use `operator.getFeeAddresses()` for the fee-related addresses. This is a lighter alternative to `operator.getConfig()`.
```typescript
-const fees = await merchant.getFeeStructure();
+const fees = await merchant.operator.getFeeAddresses();
console.log('Fee calculator:', fees.feeCalculator);
console.log('Protocol fee config:', fees.protocolFeeConfig);
console.log('Fee recipient:', fees.feeRecipient);
```
-The returned `FeeStructure` contains three fields:
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `feeCalculator` | `0x${string}` | Contract that computes fee amounts |
-| `protocolFeeConfig` | `0x${string}` | Protocol-level fee configuration |
-| `feeRecipient` | `0x${string}` | Address that receives the operator's fee share |
-
-### Get Release Conditions
-
-Use `getReleaseConditions()` to check which condition contract governs release operations. A zero address means releases are always allowed.
-
-```typescript
-const releaseCondition = await merchant.getReleaseConditions();
-
-const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
-if (releaseCondition === ZERO_ADDRESS) {
- console.log('No release conditions configured - releases always allowed');
-} else {
- console.log('Release condition contract:', releaseCondition);
-}
-```
-
-### Get Payment State
-
-Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract.
-
-```typescript
-import { PaymentState } from '@x402r/core';
-
-const state = await merchant.getPaymentState(paymentInfo);
-// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired
-```
-
-```typescript
-getPaymentState(paymentInfo: PaymentInfo): Promise
-```
-
-### Get Receiver Payments
-
-Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver, by scanning `AuthorizationCreated` events.
-
-```typescript
-const payments = await merchant.getReceiverPayments();
-
-for (const { hash, paymentInfo } of payments) {
- console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`);
-}
-```
-
-```typescript
-getReceiverPayments(
- fromBlock?: bigint
-): Promise>
-```
-
-
-This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks).
-
-
-### Get Payment Details
-
-Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash.
-
-```typescript
-const details = await merchant.getPaymentDetails(paymentInfoHash);
-console.log('Payer:', details.payer);
-console.log('Amount:', details.maxAmount);
-```
+### Get payment state
```typescript
-getPaymentDetails(
- paymentInfoHash: `0x${string}`,
- fromBlock?: bigint
-): Promise
+const [hasCollected, capturableAmount, refundableAmount] = await merchant.payment.getState(paymentInfo);
```
-## Release vs Refund Decision Flow
+## Release vs refund decision flow
```mermaid
flowchart TD
- A[Payment in Escrow] --> B{Check getPaymentAmounts}
+ A[Payment in Escrow] --> B{Check payment.getAmounts}
B --> C{capturableAmount > 0?}
C -->|Yes| D{Has refund request?}
C -->|No| E[Nothing to release]
D -->|No| F[Safe to release]
D -->|Yes| G{Approve refund?}
- F --> H["release(paymentInfo, amount)"]
- G -->|Yes| I["refundInEscrow(paymentInfo, amount)"]
- G -->|No| J[Deny request, then release]
+ F --> H["payment.release(paymentInfo, amount)"]
+ G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"]
+ G -->|No| J["refund.deny(paymentInfo), then release"]
J --> H
```
-## Next Steps
+## Next steps
- Process incoming refund requests with approve/deny workflows.
+ Process incoming refund requests with deny workflows.
Watch for real-time payment and refund events.
diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx
index 4929d3f..dae3664 100644
--- a/sdk/merchant/quickstart.mdx
+++ b/sdk/merchant/quickstart.mdx
@@ -1,19 +1,19 @@
---
title: "Merchant SDK"
-description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/merchant"
+description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/sdk"
icon: "rocket"
---
-The `@x402r/merchant` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state.
+The `@x402r/sdk` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state.
-**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `X402rMerchant` class for managing payments after they arrive.
+**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the merchant client for managing payments after they arrive.
## Installation
```bash
-npm install @x402r/merchant @x402r/helpers @x402r/core viem
+npm install @x402r/sdk @x402r/helpers @x402r/core viem
```
## Setup
@@ -21,27 +21,27 @@ npm install @x402r/merchant @x402r/helpers @x402r/core viem
Create viem clients as described in [Installation](/sdk/installation), then:
```typescript
-import { X402rMerchant } from '@x402r/merchant';
-import { getNetworkConfig } from '@x402r/core';
+import { createMerchantClient } from '@x402r/sdk';
-const config = getNetworkConfig('eip155:84532')!;
-
-const merchant = new X402rMerchant({
+const merchant = createMerchantClient({
publicClient,
walletClient,
operatorAddress: '0x...', // Your PaymentOperator address
- escrowAddress: config.authCaptureEscrow,
- refundRequestAddress: config.refundRequest,
+ // Optional: enable escrow and freeze features
+ escrowPeriodAddress: '0x...',
+ freezeAddress: '0x...',
});
```
-## Release Funds from Escrow
+Refund request and evidence addresses are auto-resolved from the chain config.
+
+## Release funds from escrow
-Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units.
+Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units.
```typescript
// Release 10 USDC (6 decimals) from escrow
-const { txHash } = await merchant.release(paymentInfo, BigInt('10000000'));
+const txHash = await merchant.payment.release(paymentInfo, BigInt('10000000'));
console.log('Released:', txHash);
```
@@ -49,43 +49,47 @@ For partial releases, specify a smaller amount. The remaining funds stay in escr
```typescript
// Release 3 USDC of a 10 USDC escrow
-const { txHash } = await merchant.release(paymentInfo, BigInt('3000000'));
+const txHash = await merchant.payment.release(paymentInfo, BigInt('3000000'));
console.log('Partial release:', txHash);
// Check what remains
-const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo);
-console.log('Remaining in escrow:', capturableAmount); // 7000000n
+const amounts = await merchant.payment.getAmounts(paymentInfo);
+console.log('Remaining in escrow:', amounts.capturableAmount); // 7000000n
```
-The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount.
+The `amount` parameter is always required. There is no default "release all" behavior. Always query `payment.getAmounts()` first to determine the available capturable amount.
-## Refund While in Escrow
+## Refund while in escrow
-Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**.
+Use `payment.refundInEscrow()` to return escrowed funds to the payer before release. This also auto-approves any pending RefundRequest for this payment.
```typescript
// Full refund of 10 USDC
-const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000'));
+const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('10000000'));
console.log('Refunded from escrow:', txHash);
```
```typescript
// Partial refund: return 2 USDC, keep 8 USDC in escrow
-const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000'));
+const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('2000000'));
console.log('Partial refund:', txHash);
```
-## Charge Directly
+
+In v3, calling `refundInEscrow()` automatically approves any pending RefundRequest for this payment via the IRecorder plugin. You do not need a separate approve step.
+
+
+## Charge directly
-Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`).
+Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`).
```typescript
const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...';
const collectorData: `0x${string}` = '0xSignatureOrCalldata...';
-const { txHash } = await merchant.charge(
+const txHash = await merchant.payment.charge(
paymentInfo,
BigInt('5000000'), // 5 USDC
tokenCollectorAddress, // token collector contract
@@ -98,15 +102,15 @@ console.log('Charged:', txHash);
The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer.
-## Refund After Release (Post-Escrow)
+## Refund after release (post-escrow)
-Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance.
+Use `payment.refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance.
```typescript
const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...';
const collectorData: `0x${string}` = '0xSignatureOrCalldata...';
-const { txHash } = await merchant.refundPostEscrow(
+const txHash = await merchant.payment.refundPostEscrow(
paymentInfo,
BigInt('5000000'), // 5 USDC to refund
tokenCollectorAddress, // token collector that sources the refund
@@ -119,118 +123,73 @@ console.log('Post-escrow refund:', txHash);
Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer.
-## Query Methods
-
-### Get Payment Amounts
+## Query methods
-Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment.
+### Get payment amounts
```typescript
-const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo);
+const amounts = await merchant.payment.getAmounts(paymentInfo);
-console.log('Capturable:', capturableAmount); // Funds available to release
-console.log('Refundable:', refundableAmount); // Funds available to refund
+console.log('Capturable:', amounts.capturableAmount); // Funds available to release
+console.log('Refundable:', amounts.refundableAmount); // Funds available to refund
-if (capturableAmount > 0n) {
- const { txHash } = await merchant.release(paymentInfo, capturableAmount);
+if (amounts.capturableAmount > 0n) {
+ const txHash = await merchant.payment.release(paymentInfo, amounts.capturableAmount);
console.log('Released all capturable funds:', txHash);
}
```
-
-`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance.
-
-
-### Get Payment State
-
-Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract.
-
-```typescript
-import { PaymentState } from '@x402r/core';
-
-const state = await merchant.getPaymentState(paymentInfo);
-// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired
-```
-
-### Get Receiver Payments
-
-Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver.
-
-```typescript
-const payments = await merchant.getReceiverPayments();
-
-for (const { hash, paymentInfo } of payments) {
- console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`);
-}
-```
-
-
-This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks).
-
-
-### Get Payment Details
-
-Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash.
+### Get payment state
```typescript
-const details = await merchant.getPaymentDetails(paymentInfoHash);
-console.log('Payer:', details.payer);
-console.log('Amount:', details.maxAmount);
+const [hasCollected, capturableAmount, refundableAmount] = await merchant.payment.getState(paymentInfo);
```
-### Get Operator Configuration
+### Get operator configuration
-Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract.
+Use `operator.getConfig()` to retrieve all immutable slot addresses from the PaymentOperator contract.
```typescript
-const config = await merchant.getOperatorConfig();
+const config = await merchant.operator.getConfig();
-console.log('Escrow:', config.escrow);
console.log('Fee recipient:', config.feeRecipient);
console.log('Fee calculator:', config.feeCalculator);
console.log('Release condition:', config.releaseCondition);
```
-### Get Fee Structure
+### Get fee structure
-Use `getFeeStructure()` for just the fee-related addresses — a lighter alternative to `getOperatorConfig()`.
+Use `operator.getFeeAddresses()` for just the fee-related addresses.
```typescript
-const fees = await merchant.getFeeStructure();
+const fees = await merchant.operator.getFeeAddresses();
console.log('Fee calculator:', fees.feeCalculator);
console.log('Protocol fee config:', fees.protocolFeeConfig);
console.log('Fee recipient:', fees.feeRecipient);
```
-### Get Release Conditions
-
-```typescript
-const releaseCondition = await merchant.getReleaseConditions();
-// address(0) means releases are always allowed
-```
-
-## Release vs Refund Decision Flow
+## Release vs refund decision flow
```mermaid
flowchart TD
- A[Payment in Escrow] --> B{Check getPaymentAmounts}
+ A[Payment in Escrow] --> B{Check payment.getAmounts}
B --> C{capturableAmount > 0?}
C -->|Yes| D{Has refund request?}
C -->|No| E[Nothing to release]
D -->|No| F[Safe to release]
D -->|Yes| G{Approve refund?}
- F --> H["release(paymentInfo, amount)"]
- G -->|Yes| I["refundInEscrow(paymentInfo, amount)"]
- G -->|No| J[Deny request, then release]
+ F --> H["payment.release(paymentInfo, amount)"]
+ G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"]
+ G -->|No| J["refund.deny(paymentInfo), then release"]
J --> H
```
-## Next Steps
+## Next steps
- Process incoming refund requests with approve/deny workflows.
+ Process incoming refund requests with deny workflows.
Mark payment options as refundable with your operator.
diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx
index 02e8b31..bc298c6 100644
--- a/sdk/merchant/refund-handling.mdx
+++ b/sdk/merchant/refund-handling.mdx
@@ -1,23 +1,21 @@
---
-title: "Refund Handling"
-description: "Process, approve, deny, and manage refund requests with the Merchant SDK"
+title: "Refund handling"
+description: "Process, deny, and manage refund requests with the Merchant SDK"
icon: "rotate-left"
---
-The `X402rMerchant` class provides a complete set of methods for handling refund requests from payers. Every refund-related method requires a `nonce: bigint` parameter that identifies which specific charge the refund targets.
+The merchant client provides methods for handling refund requests from payers. In v3, refund approval happens automatically when you call `payment.refundInEscrow()` — the RefundRequest IRecorder plugin auto-approves any pending request during execution.
-The `nonce` parameter corresponds to the record index from the `PaymentIndexRecorder`. For the first charge against a payment, the nonce is `0n`. Each subsequent charge increments the nonce.
+Each payment supports one refund request, keyed by `paymentInfoHash`. There is no nonce parameter — one request per payment.
-## Refund Request Queries
+## Refund request queries
-### Check If a Refund Request Exists
-
-Use `hasRefundRequest()` to check whether a payer has submitted a refund request for a specific payment and nonce.
+### Check if a refund request exists
```typescript
-const hasRequest = await merchant.hasRefundRequest(paymentInfo, 0n);
+const hasRequest = await merchant.refund.has(paymentInfo);
if (hasRequest) {
console.log('Refund request exists for this payment');
@@ -26,21 +24,19 @@ if (hasRequest) {
}
```
-### Get Refund Request Status
-
-Use `getRefundStatus()` to retrieve the current status of a refund request. Returns a `RequestStatus` enum value.
+### Get refund request status
```typescript
import { RequestStatus } from '@x402r/core';
-const status = await merchant.getRefundStatus(paymentInfo, 0n);
+const status = await merchant.refund.getStatus(paymentInfo);
switch (status) {
case RequestStatus.Pending:
console.log('Awaiting your decision');
break;
case RequestStatus.Approved:
- console.log('You approved this refund');
+ console.log('Auto-approved via refundInEscrow');
break;
case RequestStatus.Denied:
console.log('You denied this refund');
@@ -51,118 +47,59 @@ switch (status) {
}
```
-### Get Full Refund Request Data
-
-Use `getRefundRequest()` to retrieve the complete refund request data, including the amount and status.
+### Get full refund request data
```typescript
-import type { RefundRequestData } from '@x402r/core';
-
-const request: RefundRequestData = await merchant.getRefundRequest(paymentInfo, 0n);
+const request = await merchant.refund.get(paymentInfo);
console.log('Payment hash:', request.paymentInfoHash);
-console.log('Nonce:', request.nonce);
console.log('Requested amount:', request.amount);
console.log('Status:', request.status);
```
-The `RefundRequestData` type contains:
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `paymentInfoHash` | `0x${string}` | Hash of the PaymentInfo struct |
-| `nonce` | `bigint` | Record index this refund targets |
-| `amount` | `bigint` | Amount requested for refund (uint120) |
-| `status` | `RequestStatus` | Current status (Pending, Approved, Denied, Cancelled) |
-
-### Get Refund Request by Composite Key
+### Get refund request by key
-Use `getRefundRequestByKey()` to look up a refund request directly by its composite key (the `keccak256(paymentInfoHash, nonce)` value returned from paginated queries).
+Use `getByKey()` to look up a refund request directly by its `paymentInfoHash` (returned from paginated queries).
```typescript
-const request = await merchant.getRefundRequestByKey(compositeKey);
+const request = await merchant.refund.getByKey(paymentInfoHash);
console.log('Amount:', request.amount);
console.log('Status:', request.status);
```
-## Paginated Refund Request Listing
+## Paginated refund request listing
-### Get Pending Refund Requests
+### Get refund requests for a receiver
-Use `getPendingRefundRequests()` to retrieve paginated refund request keys for the current receiver address. This method uses the wallet address associated with your `X402rMerchant` instance.
+Use `getReceiverRequests()` to retrieve paginated refund request keys for a receiver address.
```typescript
+const receiverAddress = walletClient.account.address;
+
// Get the first 10 refund request keys
-const { keys, total } = await merchant.getPendingRefundRequests(0n, 10n);
+const { keys, total } = await merchant.refund.getReceiverRequests(
+ receiverAddress,
+ 0n, // offset
+ 10n // count
+);
console.log(`Showing ${keys.length} of ${total} total refund requests`);
-// Look up each request by its composite key
for (const key of keys) {
- const request = await merchant.getRefundRequestByKey(key);
- console.log(`Key: ${key}`);
- console.log(` Amount: ${request.amount}`);
- console.log(` Status: ${request.status}`);
-}
-```
-
-For pagination, adjust the `offset` and `count` parameters:
-
-```typescript
-// Page through all refund requests, 20 at a time
-const pageSize = 20n;
-let offset = 0n;
-let hasMore = true;
-
-while (hasMore) {
- const { keys, total } = await merchant.getPendingRefundRequests(offset, pageSize);
-
- for (const key of keys) {
- const request = await merchant.getRefundRequestByKey(key);
- // Process each request...
- }
-
- offset += pageSize;
- hasMore = offset < total;
-}
-```
-
-### Get Refund Request Count
-
-Use `getRefundRequestCount()` to get the total number of refund requests targeting the current receiver.
-
-```typescript
-const count = await merchant.getRefundRequestCount();
-console.log(`Total refund requests: ${count}`);
-
-if (count > 0n) {
- const { keys } = await merchant.getPendingRefundRequests(0n, count);
- console.log(`Retrieved all ${keys.length} request keys`);
+ const request = await merchant.refund.getByKey(key);
+ console.log(`Key: ${key}, Amount: ${request.amount}, Status: ${request.status}`);
}
```
-## Refund Request Actions
+## Refund request actions
-### Approve a Refund Request
+### Deny a refund request
-Use `approveRefundRequest()` to approve a pending refund request. This changes the request status to `Approved`.
+Use `refund.deny()` to deny a pending refund request. This changes the request status to `Denied`.
```typescript
-const { txHash } = await merchant.approveRefundRequest(paymentInfo, 0n);
-console.log('Refund approved:', txHash);
-```
-
-
-Approving a refund request changes its status but does **not** transfer funds. You must also call `refundInEscrow()` or `refundPostEscrow()` to execute the actual token transfer.
-
-
-### Deny a Refund Request
-
-Use `denyRefundRequest()` to deny a pending refund request. This changes the request status to `Denied`.
-
-```typescript
-const { txHash } = await merchant.denyRefundRequest(paymentInfo, 0n);
+const txHash = await merchant.refund.deny(paymentInfo);
console.log('Refund denied:', txHash);
```
@@ -170,60 +107,37 @@ console.log('Refund denied:', txHash);
If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk.
-## Freeze Management
-
-### Check If a Payment Is Frozen
-
-Use `isFrozen()` to check whether a payment has been frozen by the payer or an arbiter. Frozen payments cannot be released until unfrozen.
-
-```typescript
-const freezeAddress: `0x${string}` = '0xFreezeContract...';
-
-const frozen = await merchant.isFrozen(paymentInfo, freezeAddress);
+### Refund in escrow (auto-approves)
-if (frozen) {
- console.log('Payment is frozen - cannot release until unfrozen');
-} else {
- console.log('Payment is not frozen');
-}
-```
-
-### Unfreeze a Payment
-
-Use `unfreezePayment()` to remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze.
+Instead of a separate approve step, you call `payment.refundInEscrow()` directly. The RefundRequest recorder plugin auto-approves any pending request during execution.
```typescript
-const freezeAddress: `0x${string}` = '0xFreezeContract...';
-
-const { txHash } = await merchant.unfreezePayment(paymentInfo, freezeAddress);
-console.log('Payment unfrozen:', txHash);
+const txHash = await merchant.payment.refundInEscrow(paymentInfo, request.amount);
+console.log('Refund executed (request auto-approved):', txHash);
```
-## Complete Refund Workflow
+## Complete refund workflow
Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved.
```typescript
-import { createPublicClient, createWalletClient, http } from 'viem';
-import { baseSepolia } from 'viem/chains';
-import { privateKeyToAccount } from 'viem/accounts';
-import { X402rMerchant } from '@x402r/merchant';
-import { getNetworkConfig, RequestStatus } from '@x402r/core';
+import { createMerchantClient } from '@x402r/sdk';
+import { RequestStatus } from '@x402r/core';
+import type { PaymentInfo } from '@x402r/core';
async function handleRefundWorkflow(
- merchant: X402rMerchant,
- paymentInfo: PaymentInfo,
- nonce: bigint
+ merchant: ReturnType,
+ paymentInfo: PaymentInfo
) {
// Step 1: Check if a refund request exists
- const hasRequest = await merchant.hasRefundRequest(paymentInfo, nonce);
+ const hasRequest = await merchant.refund.has(paymentInfo);
if (!hasRequest) {
- console.log('No refund request for this payment/nonce');
+ console.log('No refund request for this payment');
return;
}
// Step 2: Get the full request data
- const request = await merchant.getRefundRequest(paymentInfo, nonce);
+ const request = await merchant.refund.get(paymentInfo);
console.log(`Refund request: ${request.amount} tokens, status: ${request.status}`);
// Step 3: Only process pending requests
@@ -232,82 +146,68 @@ async function handleRefundWorkflow(
return;
}
- // Step 4: Check if the payment is frozen
- const freezeAddress: `0x${string}` = '0xFreezeContract...';
- const frozen = await merchant.isFrozen(paymentInfo, freezeAddress);
- if (frozen) {
- console.log('Payment is frozen - resolve dispute before processing refund');
- return;
- }
-
- // Step 5: Check available amounts
- const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo);
- console.log(`Available to refund: ${refundableAmount}`);
-
- // Step 6: Make a decision
- const shouldApprove = request.amount <= refundableAmount;
+ // Step 4: Check available amounts
+ const amounts = await merchant.payment.getAmounts(paymentInfo);
+ console.log(`Available to refund: ${amounts.refundableAmount}`);
- if (shouldApprove) {
- // Approve the request
- const { txHash: approveTx } = await merchant.approveRefundRequest(paymentInfo, nonce);
- console.log('Approved:', approveTx);
+ // Step 5: Make a decision
+ const shouldRefund = request.amount <= amounts.refundableAmount;
- // Execute the refund from escrow
- const { txHash: refundTx } = await merchant.refundInEscrow(paymentInfo, request.amount);
- console.log('Refund executed:', refundTx);
+ if (shouldRefund) {
+ // Execute the refund (auto-approves the request)
+ const txHash = await merchant.payment.refundInEscrow(paymentInfo, request.amount);
+ console.log('Refund executed:', txHash);
} else {
// Deny the request
- const { txHash: denyTx } = await merchant.denyRefundRequest(paymentInfo, nonce);
- console.log('Denied:', denyTx);
+ const txHash = await merchant.refund.deny(paymentInfo);
+ console.log('Denied:', txHash);
}
}
```
-## Refund Request Lifecycle
+## Refund request lifecycle
```mermaid
sequenceDiagram
- participant P as Payer (Client SDK)
- participant R as RefundRequest Contract
- participant M as Merchant (Merchant SDK)
+ participant P as Payer
+ participant R as RefundRequest (IRecorder)
+ participant M as Merchant
participant O as PaymentOperator
- P->>R: requestRefund(paymentInfo, amount, nonce)
+ P->>R: refund.request(paymentInfo, amount)
R-->>M: RefundRequested event
- M->>R: hasRefundRequest(paymentInfo, nonce)
+ M->>R: refund.has(paymentInfo)
R-->>M: true
- M->>R: getRefundRequest(paymentInfo, nonce)
+ M->>R: refund.get(paymentInfo)
R-->>M: RefundRequestData
M->>M: Review request (policy check)
- alt Approve
- M->>R: approveRefundRequest(paymentInfo, nonce)
- M->>O: refundInEscrow(paymentInfo, amount)
+ alt Refund
+ M->>O: payment.refundInEscrow(paymentInfo, amount)
+ O->>R: record() auto-approves pending request
O->>P: Funds returned to payer
else Deny
- M->>R: denyRefundRequest(paymentInfo, nonce)
+ M->>R: refund.deny(paymentInfo)
Note over P: Payer may escalate to arbiter
end
```
-## Method Reference
+## Method reference
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
-| `hasRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Check if refund request exists |
-| `getRefundStatus` | `paymentInfo, nonce: bigint` | `Promise` | Get request status |
-| `getRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Get full request data |
-| `approveRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Approve a pending request |
-| `denyRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Deny a pending request |
-| `getPendingRefundRequests` | `offset: bigint, count: bigint` | `Promise<{ keys, total }>` | Paginated request keys |
-| `getRefundRequestCount` | _(none)_ | `Promise` | Total requests for receiver |
-| `getRefundRequestByKey` | `compositeKey: hex` | `Promise` | Look up by composite key |
-| `unfreezePayment` | `paymentInfo, freezeAddress: hex` | `Promise<{ txHash }>` | Remove payment freeze |
-| `isFrozen` | `paymentInfo, freezeAddress: hex` | `Promise` | Check if payment is frozen |
-
-## Next Steps
+| `refund.has` | `paymentInfo` | `Promise` | Check if refund request exists |
+| `refund.getStatus` | `paymentInfo` | `Promise` | Get request status |
+| `refund.get` | `paymentInfo` | `Promise` | Get full request data |
+| `refund.deny` | `paymentInfo` | `Promise` | Deny a pending request |
+| `refund.getReceiverRequests` | `receiver, offset, count` | `Promise<{ keys, total }>` | Paginated request keys |
+| `refund.getByKey` | `paymentInfoHash` | `Promise` | Look up by key |
+| `payment.refundInEscrow` | `paymentInfo, amount, data?` | `Promise` | Refund and auto-approve |
+| `freeze.isFrozen` | `paymentInfo` | `Promise` | Check if payment is frozen |
+
+## Next steps
diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx
index 813b31d..0d1dba5 100644
--- a/sdk/merchant/subscriptions.mdx
+++ b/sdk/merchant/subscriptions.mdx
@@ -1,190 +1,115 @@
---
-title: "Merchant Events"
+title: "Merchant events"
description: "Subscribe to real-time refund, release, and freeze events with the Merchant SDK"
icon: "bell"
---
-The `X402rMerchant` class provides three subscription methods for watching blockchain events in real-time. Each returns an object with an `unsubscribe` function for cleanup.
+The merchant client provides watch methods for subscribing to blockchain events in real-time. Each returns an unsubscribe function for cleanup.
-## Watch Refund Requests
+## Watch refund requests
-Use `watchRefundRequests()` to subscribe to `RefundRequested` events emitted by the RefundRequest contract. The callback receives a `RefundRequestEventLog` object for each event.
+Use `watch.onRefundRequest()` to subscribe to events emitted by the RefundRequest contract.
```typescript
-const { unsubscribe } = merchant.watchRefundRequests((event) => {
- console.log('Event:', event.eventName);
- console.log('Payment hash:', event.args.paymentInfoHash);
- console.log('Payer:', event.args.payer);
- console.log('Receiver:', event.args.receiver);
- console.log('Amount:', event.args.amount);
- console.log('Nonce:', event.args.nonce);
- console.log('Block:', event.blockNumber);
- console.log('Tx hash:', event.transactionHash);
+const unsubscribe = merchant.watch.onRefundRequest((event) => {
+ console.log('Refund request event:', event);
});
// Later: stop watching
unsubscribe();
```
-The `RefundRequestEventLog` type has the following shape:
-
-```typescript
-interface RefundRequestEventLog {
- eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled';
- args: {
- paymentInfoHash?: `0x${string}`;
- payer?: `0x${string}`;
- receiver?: `0x${string}`;
- amount?: bigint;
- nonce?: bigint;
- status?: number;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
-```
-
-`watchRefundRequests()` requires the `refundRequestAddress` to be configured when creating the `X402rMerchant` instance.
+This is a no-op if no `refundRequestAddress` was resolved. The address is auto-resolved from the chain config.
-### Example: Auto-respond to Small Refund Requests
+### Example: auto-respond to small refund requests
```typescript
-import { X402rMerchant } from '@x402r/merchant';
-import { RequestStatus } from '@x402r/core';
+import { createMerchantClient } from '@x402r/sdk';
const AUTO_APPROVE_THRESHOLD = BigInt('5000000'); // 5 USDC
-const { unsubscribe } = merchant.watchRefundRequests(async (event) => {
- const amount = event.args.amount;
- const paymentHash = event.args.paymentInfoHash;
+const unsubscribe = merchant.watch.onRefundRequest(async (event: any) => {
+ const amount = event.args?.amount;
- console.log(`New refund request: ${paymentHash}, amount: ${amount}`);
+ console.log('New refund request event');
if (amount && amount < AUTO_APPROVE_THRESHOLD) {
- console.log('Auto-approving small refund request');
- // You would look up the paymentInfo from your database
- // and call merchant.approveRefundRequest(paymentInfo, nonce)
+ console.log('Auto-refunding small request');
+ // Look up paymentInfo from your database, then:
+ // await merchant.payment.refundInEscrow(paymentInfo, amount)
} else {
console.log('Queuing for manual review');
}
});
```
-## Watch Releases
+## Watch payment events
-Use `watchReleases()` to subscribe to `ReleaseExecuted` events emitted by the PaymentOperator contract. The callback receives a `PaymentOperatorEventLog` object for each event.
+Use `watch.onPayment()` to subscribe to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events on the PaymentOperator contract.
```typescript
-const { unsubscribe } = merchant.watchReleases((event) => {
- console.log('Release executed!');
- console.log('Payment hash:', event.args.paymentInfoHash);
- console.log('Amount:', event.args.amount);
- console.log('Payer:', event.args.payer);
- console.log('Receiver:', event.args.receiver);
- console.log('Block:', event.blockNumber);
- console.log('Tx hash:', event.transactionHash);
+const unsubscribe = merchant.watch.onPayment((event) => {
+ console.log('Payment event:', event);
});
// Later: stop watching
unsubscribe();
```
-The `PaymentOperatorEventLog` type has the following shape:
-
-```typescript
-interface PaymentOperatorEventLog {
- eventName: 'ReleaseExecuted' | 'RefundInEscrowExecuted' | 'RefundPostEscrowExecuted'
- | 'AuthorizationCreated' | 'ChargeExecuted';
- args: {
- paymentInfoHash?: `0x${string}`;
- payer?: `0x${string}`;
- receiver?: `0x${string}`;
- amount?: bigint;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
-```
-
-### Example: Revenue Tracking
+### Example: revenue tracking
```typescript
let totalReleased = 0n;
-const { unsubscribe } = merchant.watchReleases((event) => {
- const amount = event.args.amount ?? 0n;
- totalReleased += amount;
-
- console.log(`Release: +${amount} tokens`);
- console.log(`Total released: ${totalReleased}`);
+const unsubscribe = merchant.watch.onPayment((event: any) => {
+ if (event.eventName === 'ReleaseExecuted') {
+ const amount = event.args?.amount ?? 0n;
+ totalReleased += amount;
+ console.log(`Release: +${amount} tokens, Total: ${totalReleased}`);
+ }
});
```
-## Watch Freeze Events
+## Watch refund execution
-Use `watchFreezeEvents()` to subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a specific Freeze contract. You must provide the Freeze contract address as the first argument.
+Use `watch.onRefundExecuted()` to subscribe to `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` events.
```typescript
-const freezeAddress: `0x${string}` = '0xFreezeContract...';
-
-const { unsubscribe } = merchant.watchFreezeEvents(
- freezeAddress,
- (event) => {
- if (event.eventName === 'PaymentFrozen') {
- console.log('Payment FROZEN:', event.args.paymentInfoHash);
- console.log('Frozen by:', event.args.caller);
- // Alert: a dispute may be in progress
- } else if (event.eventName === 'PaymentUnfrozen') {
- console.log('Payment UNFROZEN:', event.args.paymentInfoHash);
- console.log('Unfrozen by:', event.args.caller);
- // The payment can now be released
- }
- }
-);
+const unsubscribe = merchant.watch.onRefundExecuted((event) => {
+ console.log('Refund executed:', event);
+});
-// Later: stop watching
unsubscribe();
```
-The `FreezeEventLog` type has the following shape:
+## Watch fee distribution
+
+Use `watch.onFeeDistribution()` to subscribe to `FeesDistributed` events.
```typescript
-interface FreezeEventLog {
- eventName: 'PaymentFrozen' | 'PaymentUnfrozen';
- args: {
- paymentInfoHash?: `0x${string}`;
- caller?: `0x${string}`;
- };
- address: `0x${string}`;
- blockNumber: bigint;
- transactionHash: `0x${string}`;
- logIndex: number;
-}
-```
+const unsubscribe = merchant.watch.onFeeDistribution((event) => {
+ console.log('Fees distributed:', event);
+});
-
-The `freezeAddress` parameter is the address of the Freeze condition contract, not the PaymentOperator. You can retrieve it from your operator config via `merchant.getOperatorConfig()`.
-
+unsubscribe();
+```
-## Event Types Reference
+## Event types reference
-| Method | Event Name | Callback Type | Use Case |
-|--------|-----------|---------------|----------|
-| `watchRefundRequests` | `RefundRequested` | `RefundRequestEventLog` | Detect incoming refund requests |
-| `watchReleases` | `ReleaseExecuted` | `PaymentOperatorEventLog` | Track revenue and release confirmations |
-| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | `FreezeEventLog` | Monitor dispute-related freezes |
+| Method | Events Watched | Contract | Use Case |
+|--------|---------------|----------|----------|
+| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Track payment lifecycle |
+| `watch.onRefundRequest` | All RefundRequest events | RefundRequest | Detect incoming refund requests |
+| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track refund execution |
+| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | Monitor fee distribution |
-All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket).
+For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket).
-## Next Steps
+## Next steps
@@ -194,7 +119,7 @@ All subscription methods use viem's `watchContractEvent` under the hood. For rel
Learn about dispute resolution from the arbiter perspective.
- Process refund requests with approve/deny workflows.
+ Process refund requests with deny workflows.
See how clients subscribe to the same events.
diff --git a/sdk/overview.mdx b/sdk/overview.mdx
index c0e9ceb..ffe2c55 100644
--- a/sdk/overview.mdx
+++ b/sdk/overview.mdx
@@ -5,7 +5,7 @@ icon: "cube"
---
-The X402r SDK is in active development (v0.0.2). APIs may change between releases. Always test on Base Sepolia before using real funds on mainnet.
+The X402r SDK is in active development. APIs may change between releases. Always test on Base Sepolia before using real funds on mainnet.
The X402r SDK provides a complete TypeScript implementation for integrating with the X402r refundable payments protocol. It enables clients, merchants, and arbiters to interact with smart contracts for payment authorization, escrow management, and dispute resolution.
@@ -32,18 +32,22 @@ The SDK is organized into packages designed for specific roles in the payment ec
-## Network Support
+## Network support
+
+All v3 contracts are deployed to the same address on every chain via CREATE3.
| Network | Chain ID | Status |
|---------|----------|--------|
| Base Sepolia | 84532 | Tested |
-| Base Mainnet | 8453 | Deployed, not yet tested |
-| Ethereum | 1 | Deployed, not yet tested |
-| Ethereum Sepolia | 11155111 | Deployed, not yet tested |
-| Arbitrum Sepolia | 421614 | Deployed, not yet tested |
-| Polygon | 137 | Deployed, not yet tested |
-| Arbitrum | 42161 | Deployed, not yet tested |
-| Optimism | 10 | Deployed, not yet tested |
-| Avalanche | 43114 | Deployed, not yet tested |
-| Celo | 42220 | Deployed, not yet tested |
-| Monad | 143 | Deployed, not yet tested |
+| Base | 8453 | Deployed |
+| Ethereum | 1 | Deployed |
+| Ethereum Sepolia | 11155111 | Deployed |
+| Arbitrum | 42161 | Deployed |
+| Arbitrum Sepolia | 421614 | Deployed |
+| Optimism | 10 | Deployed |
+| Polygon | 137 | Deployed |
+| Celo | 42220 | Deployed |
+| Avalanche | 43114 | Deployed |
+| Linea | 59144 | Deployed |
+| Monad | 143 | Deployed |
+| SKALE Base | 1187947933 | Deployed (Shanghai EVM) |