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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ tsconfig.test.json
/test/
/e2e_tests/
/non-public/
/demo/cloudflare-demo/
/adapters/ethers/.upstream/
/adapters/viem/.upstream/viem/
/docs/migration-0.3-to-0.4.md
/demo/UTC--2026-08-03T13-05-08.008Z--7f8880845d4f215594c6fcf07f884f635216d958
/.codex/skills/architecture-boundary-review/SKILL.md
/demo/MyEtherWallet _ The Best Crypto Wallet For Web3.pdf
/docs/ml-llm-crash-course.md
/docs/job-application-tracker-extension-spec.md
/.codex/config.toml
/AGENTS.md
/.dockerignore
14 changes: 10 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ All notable public changes to d402 are documented here.
- Removed d402 core's runtime dependency on ethers and viem.
- Standardized client and executor composition around four neutral capabilities:
`rpcClient`, `codec`, `errorDecoder`, and `txSender`.
- Preserved direct custom integration by allowing applications to provide those
capabilities without constructing a provider-specific adapter object.
- Added the shared `D402Adapter` contract so applications can provide those
capabilities through one provider-neutral adapter object.
- Retained custom payment executors for relayers, custodial wallets, and other
integrations that do not use the standard dPayments execution path.

Expand All @@ -26,12 +26,18 @@ All notable public changes to d402 are documented here.

### API cleanup

- Split payment configuration into `adapter` and `payment` sections. Routes,
`paymentActions()`, verification, settlement, and refund helpers now share
the same nested `{ adapter, payment }` composition API.
- Added `PaymentOptions` for confirmations, settlement timing, caching,
identifiers, logging, events, and multicall settings.
- Removed client-side confirmation options. Transaction confirmation depth is
now configured on the adapter transaction sender; server verification
confirmations remain part of server payment configuration.
- Simplified `Once()` to accept only the payment actions it consumes.
- Removed unused adapter types and provider-specific dependencies from shared
protocol code.
- Removed unused provider-specific adapter type aliases and provider-specific
dependencies from shared protocol code.
- Updated the public documentation and runnable examples for the new API.

## 0.3.3 - 2026-08-13

Expand Down
4 changes: 2 additions & 2 deletions ELI5.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Use `payable()` when d402 can own the complete route:

```ts
const route = payable({
paymentConfig,
...paymentConfig,
terms,
handler,
});
Expand All @@ -32,7 +32,7 @@ Use `PaymentAuthorizer` when your controller owns the surrounding work:

```ts
const payment = new PaymentAuthorizer({
paymentConfig,
...paymentConfig,
terms,
});

Expand Down
50 changes: 17 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ the retried request from its proof.
## Install

```sh
npm install d402 ethers
npm install d402 @d402/ethers ethers
```

## 1. Protect a complete route
Expand All @@ -17,12 +17,14 @@ route:

```ts
import { JsonRpcProvider, Wallet } from "ethers";
import { createEthersAdapter } from "@d402/ethers";
import { payable } from "d402/server";

const provider = new JsonRpcProvider(process.env.RPC_URL);
// Any ethers Signer works here, including a KMS or custody-backed signer.
// Signer is only needed if the server performs an on-chain action during the request (for example, Once consumption or refunds).
const payee = new Wallet(process.env.PAYEE_PRIVATE_KEY, provider);
const adapter = createEthersAdapter({ provider, signer: payee });
const terms = {
chainId: 100,
payeeAddress: payee.address,
Expand All @@ -33,11 +35,8 @@ const terms = {
};

export const GET = payable({
paymentConfig: {
provider,
signer: payee,
settlementWindow: 3600,
},
adapter,
payment: { settlementWindow: 3600 },
terms,
handler: (_request, payment) =>
Response.json({
Expand All @@ -58,12 +57,8 @@ Use `PaymentAuthorizer` when your framework or application owns the controller:
import { PaymentAuthorizer } from "d402/server";

const reportPayment = new PaymentAuthorizer({
paymentConfig: {
provider,
// This may also be a KMS or custody-backed ethers Signer.
signer: payee,
settlementWindow: 3600,
},
adapter,
payment: { settlementWindow: 3600 },
terms,
});

Expand Down Expand Up @@ -97,17 +92,13 @@ most one protected operation:
import { Once, payable, paymentActions } from "d402/server";

const actions = paymentActions({
provider,
// Use the payee's Wallet, KMS, or custody-backed ethers Signer.
signer: payee,
adapter,
payment: {},
});

const download = payable({
paymentConfig: {
provider,
signer: payee,
settlementWindow: 3600,
},
adapter,
payment: { settlementWindow: 3600 },
terms,
consumer: Once(actions),
handler: async () =>
Expand All @@ -125,11 +116,8 @@ invoice. Put the order ID in the terms so retries reconstruct the same payment:

```ts
const orderRoute = payable({
paymentConfig: {
provider,
// This may also be a KMS or custody-backed ethers Signer.
signer: payee,
},
adapter,
payment: {},
terms: (request) => {
const orderId = new URL(request.url).pathname.split("/").at(-1);

Expand Down Expand Up @@ -159,17 +147,13 @@ payment instead of reusing an order identity:
import { Once, payable, paymentActions } from "d402/server";

const actions = paymentActions({
provider,
// Use the payee's Wallet, KMS, or custody-backed ethers Signer.
signer: payee,
adapter,
payment: {},
});

const independentPaymentRoute = payable({
paymentConfig: {
provider,
signer: payee,
identifier: "client",
},
adapter,
payment: { identifier: "client" },
terms,
consumer: Once(actions),
handler,
Expand Down
49 changes: 12 additions & 37 deletions adapters/ethers/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
import { ABI } from "@rakelabs/dpayments-sdk";
import type { AbiCodec } from "@rakelabs/dpayments-sdk";
import type {
AbstractProvider,
Signer,
} from "ethers";
import {ABI} from "@rakelabs/dpayments-sdk";
import type {AbstractProvider, Signer,} from "ethers";

import type {
D402ErrorDecoder,
D402RpcClient,
D402TxSender,
} from "d402/core";
import { decodeEthersError } from "@rakelabs/ethers-adapter";
import { createEthersAbiCodec } from "./codec.js";
import { createEthersRpcClient } from "./rpc-client.js";
import { createEthersTxSender } from "./tx-sender.js";
import type {D402Adapter, D402ErrorDecoder,} from "d402/core";
import {decodeEthersError} from "@rakelabs/ethers-adapter";
import {createEthersAbiCodec} from "./codec.js";
import {createEthersRpcClient} from "./rpc-client.js";
import {createEthersTxSender} from "./tx-sender.js";

export interface EthersAdapterOptions {
provider: AbstractProvider;
signer?: Signer;
confirmations?: number;
}

export interface EthersAdapter {
readonly rpcClient: D402RpcClient;
readonly codec: AbiCodec;
readonly errorDecoder: D402ErrorDecoder;
readonly txSender?: D402TxSender;
}

export function createEthersAdapter(
options: EthersAdapterOptions,
): EthersAdapter {
): D402Adapter & { readonly errorDecoder: D402ErrorDecoder } {
const rpcClient = createEthersRpcClient(options.provider);
const codec = createEthersAbiCodec(ABI);
const errorDecoder: D402ErrorDecoder = (error) =>
Expand All @@ -46,20 +31,10 @@ export function createEthersAdapter(
: { confirmations: options.confirmations }),
});

const components: {
rpcClient: D402RpcClient;
codec: AbiCodec;
errorDecoder: D402ErrorDecoder;
txSender?: D402TxSender;
} = {
return {
rpcClient,
codec,
errorDecoder,
};

if (txSender !== undefined) {
components.txSender = txSender;
}

return components;
}
...(txSender === undefined ? {} : {txSender}),
} satisfies D402Adapter;
}
4 changes: 2 additions & 2 deletions adapters/ethers/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ export interface EthersClientOptions extends Omit<
}

/**
* Ethers-backed compatibility constructor.
* Ethers-backed convenience constructor.
*
* The adapter accepts provider/signer construction inputs and supplies the
* neutral components consumed by d402 core.
*/
export function createClient(
export function createEthersClient(
options: EthersClientOptions,
): Promise<D402Client> {
const {
Expand Down
3 changes: 1 addition & 2 deletions adapters/ethers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
export {
createClient,
createEthersClient,
type EthersClientOptions,
} from "./client.js";

export {
createEthersAdapter,
type EthersAdapter,
type EthersAdapterOptions,
} from "./adapter.js";

Expand Down
5 changes: 5 additions & 0 deletions adapters/ethers/test/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AbstractProvider, Signer } from "ethers";
import type { PreparedTx } from "@rakelabs/dpayments-sdk";

import {
createEthersClient,
createEthersTxSender,
} from "../src/index.js";
import { createEthersAdapter } from "../src/adapter.js";
Expand All @@ -27,6 +28,10 @@ const preparedTx: PreparedTx = {
};

describe("@d402/ethers adapter", () => {
it("exports the named Ethers client constructor", () => {
expect(createEthersClient).toBeTypeOf("function");
});

it("creates a read-only adapter from a provider", () => {
const adapter = createEthersAdapter({ provider });

Expand Down
52 changes: 15 additions & 37 deletions adapters/viem/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import { ABI } from "@rakelabs/dpayments-sdk";
import type { AbiCodec } from "@rakelabs/dpayments-sdk";
import type { PublicClient, WalletClient } from "viem";
import {ABI} from "@rakelabs/dpayments-sdk";
import type {PublicClient, WalletClient} from "viem";

import type {
D402ErrorDecoder,
D402RpcClient,
D402TxSender,
} from "d402/core";
import { decodeViemError } from "@rakelabs/viem-adapter";
import { createViemAbiCodec } from "./codec.js";
import { createViemRpcClient } from "./rpc-client.js";
import { createViemTxSender } from "./tx-sender.js";
import type {D402Adapter, D402ErrorDecoder,} from "d402/core";
import {decodeViemError} from "@rakelabs/viem-adapter";
import {createViemAbiCodec} from "./codec.js";
import {createViemRpcClient} from "./rpc-client.js";
import {createViemTxSender} from "./tx-sender.js";

export interface ViemAdapterOptions {
publicClient: PublicClient;
walletClient?: WalletClient;
confirmations?: number;
}

export interface ViemAdapter {
readonly rpcClient: D402RpcClient;
readonly codec: AbiCodec;
readonly errorDecoder: D402ErrorDecoder;
readonly txSender?: D402TxSender;
}

export function createViemAdapter(
options: ViemAdapterOptions,
): ViemAdapter {
): D402Adapter {
const rpcClient = createViemRpcClient(options.publicClient);
const codec = createViemAbiCodec(ABI);
const errorDecoder: D402ErrorDecoder = (error) =>
Expand All @@ -42,20 +30,10 @@ export function createViemAdapter(
: { confirmations: options.confirmations }),
});

const components: {
rpcClient: D402RpcClient;
codec: AbiCodec;
errorDecoder: D402ErrorDecoder;
txSender?: D402TxSender;
} = {
rpcClient,
codec,
errorDecoder,
};

if (txSender !== undefined) {
components.txSender = txSender;
}

return components;
}
return {
rpcClient,
codec,
errorDecoder,
...(txSender === undefined ? {} : {txSender}),
} satisfies D402Adapter;
}
Loading
Loading