Skip to content

Commit 0a478b2

Browse files
feat: add HTTP support
1 parent 8ae2366 commit 0a478b2

File tree

7 files changed

+658
-0
lines changed

7 files changed

+658
-0
lines changed

pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript/packages/ampersend-sdk/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,16 @@
7171
"zod": "^3.24.2"
7272
},
7373
"peerDependencies": {
74+
"@x402/core": "^2.1.0",
7475
"fastmcp": "^3.17.0"
7576
},
77+
"peerDependenciesMeta": {
78+
"@x402/core": {
79+
"optional": true
80+
}
81+
},
7682
"devDependencies": {
83+
"@x402/core": "^2.1.0",
7784
"fastmcp": "github:edgeandnode/fastmcp#598d18f",
7885
"tsx": "^4.21.0"
7986
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# HTTP x402 Client Adapter
2+
3+
Wraps x402 v2 SDK clients to use Ampersend treasurers for payment decisions.
4+
5+
## Overview
6+
7+
Integrates ampersend-sdk treasurers with Coinbase's x402 v2 SDK (`@x402/fetch`), enabling sophisticated payment authorization logic (budgets, policies, approvals) with standard x402 HTTP clients.
8+
9+
**[Complete Documentation](../../../../README.md)**
10+
11+
## Quick Start
12+
13+
```typescript
14+
import { x402Client, wrapFetchWithPayment } from "@x402/fetch"
15+
import { AccountWallet, NaiveTreasurer } from "@ampersend_ai/ampersend-sdk"
16+
import { wrapWithAmpersend } from "@ampersend_ai/ampersend-sdk/x402"
17+
18+
const wallet = AccountWallet.fromPrivateKey("0x...")
19+
const treasurer = new NaiveTreasurer(wallet)
20+
21+
const client = new x402Client()
22+
wrapWithAmpersend(client, treasurer, ["base", "base-sepolia"])
23+
24+
const fetchWithPayment = wrapFetchWithPayment(fetch, client)
25+
const response = await fetchWithPayment("https://paid-api.example.com/resource")
26+
```
27+
28+
## API Reference
29+
30+
### wrapWithAmpersend
31+
32+
```typescript
33+
function wrapWithAmpersend(
34+
client: x402Client,
35+
treasurer: X402Treasurer,
36+
networks: Array<string>
37+
): x402Client
38+
```
39+
40+
Configures an x402Client to use an Ampersend treasurer for payment authorization.
41+
42+
**Parameters:**
43+
44+
- `client` - The x402Client instance to wrap
45+
- `treasurer` - The X402Treasurer that handles payment authorization decisions
46+
- `networks` - Array of v1 network names to register (e.g., `"base"`, `"base-sepolia"`)
47+
48+
**Returns:** The configured x402Client instance (same instance, mutated)
49+
50+
## Features
51+
52+
- **Transparent Integration**: Drop-in replacement for `registerExactEvmScheme`
53+
- **Treasurer Pattern**: Payment decisions via `X402Treasurer.onPaymentRequired()`
54+
- **Payment Lifecycle**: Tracks payment status (sending, success, error) via `onStatus()`
55+
- **v1 Protocol Support**: Works with EVM networks using v1 payment payloads
56+
57+
## How It Works
58+
59+
1. Wraps the x402Client with treasurer-based payment hooks
60+
2. On 402 response, calls `treasurer.onPaymentRequired()` for authorization
61+
3. If approved, creates payment using the treasurer's wallet
62+
4. Notifies treasurer of payment status via `onStatus()`
63+
64+
## Learn More
65+
66+
- [TypeScript SDK Guide](../../../../README.md)
67+
- [Treasurer Documentation](../../../../README.md#x402treasurer)
68+
- [x402-http-client Example](https://github.com/edgeandnode/ampersend-examples/tree/main/typescript/examples/x402-http-client)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import type {
2+
PaymentCreatedContext,
3+
PaymentCreationContext,
4+
PaymentCreationFailureContext,
5+
x402Client,
6+
} from "@x402/core/client"
7+
import type { PaymentRequirements } from "x402/types"
8+
9+
import type { Authorization, X402Treasurer } from "../treasurer.ts"
10+
11+
/**
12+
* Scheme client that retrieves payments from the treasurer via a shared WeakMap.
13+
* Compatible with @x402/core's SchemeNetworkClient interface for v1 protocol.
14+
*
15+
* Note: We don't implement SchemeNetworkClient directly because @x402/core
16+
* exports v2 types, but registerV1() passes v1 types at runtime.
17+
*/
18+
class TreasurerSchemeClient {
19+
readonly scheme = "exact"
20+
21+
constructor(private readonly paymentStore: WeakMap<PaymentRequirements, Authorization>) {}
22+
23+
async createPaymentPayload(
24+
x402Version: number,
25+
requirements: PaymentRequirements,
26+
): Promise<{ x402Version: number; payload: Record<string, unknown> }> {
27+
const authorization = this.paymentStore.get(requirements)
28+
if (!authorization) {
29+
throw new Error("No payment authorization found for requirements")
30+
}
31+
32+
// Clean up after retrieval
33+
this.paymentStore.delete(requirements)
34+
35+
return {
36+
x402Version,
37+
payload: authorization.payment.payload,
38+
}
39+
}
40+
}
41+
42+
/**
43+
* Wraps an x402Client to use an ampersend-sdk treasurer for payment decisions.
44+
*
45+
* This adapter integrates ampersend-sdk treasurers with Coinbase's x402 v2 SDK,
46+
* allowing you to use sophisticated payment authorization logic (budgets, policies,
47+
* approvals) with the standard x402 HTTP client ecosystem.
48+
*
49+
* Note: This adapter registers for v1 protocol because the underlying wallets
50+
* (AccountWallet, SmartAccountWallet) produce v1 payment payloads.
51+
*
52+
* @param client - The x402Client instance to wrap
53+
* @param treasurer - The X402Treasurer that handles payment authorization
54+
* @param networks - Array of v1 network names to register (e.g., 'base', 'base-sepolia')
55+
* @returns The configured x402Client instance (same instance, mutated)
56+
*
57+
* @example
58+
* ```typescript
59+
* import { x402Client } from '@x402/core/client'
60+
* import { wrapFetchWithPayment } from '@x402/fetch'
61+
* import { wrapWithAmpersend, NaiveTreasurer, AccountWallet } from '@ampersend_ai/ampersend-sdk'
62+
*
63+
* const wallet = AccountWallet.fromPrivateKey('0x...')
64+
* const treasurer = new NaiveTreasurer(wallet)
65+
*
66+
* const client = wrapWithAmpersend(
67+
* new x402Client(),
68+
* treasurer,
69+
* ['base', 'base-sepolia']
70+
* )
71+
*
72+
* const fetchWithPay = wrapFetchWithPayment(fetch, client)
73+
* const response = await fetchWithPay('https://paid-api.com/endpoint')
74+
* ```
75+
*/
76+
export function wrapWithAmpersend(client: x402Client, treasurer: X402Treasurer, networks: Array<string>): x402Client {
77+
// Shared store for correlating payments between hooks and scheme client
78+
const paymentStore = new WeakMap<PaymentRequirements, Authorization>()
79+
80+
// Register TreasurerSchemeClient for v1 protocol on each network
81+
// Using registerV1 because our wallets produce v1 payment payloads
82+
// Cast to any because @x402/core types are v2, but registerV1 accepts v1 at runtime
83+
const schemeClient = new TreasurerSchemeClient(paymentStore)
84+
for (const network of networks) {
85+
client.registerV1(network, schemeClient as any)
86+
}
87+
88+
// Track authorization for status updates
89+
const authorizationByRequirements = new WeakMap<PaymentRequirements, Authorization>()
90+
91+
// beforePaymentCreation: Consult treasurer for payment authorization
92+
client.onBeforePaymentCreation(async (context: PaymentCreationContext) => {
93+
// v1 requirements are passed directly to treasurer (no conversion needed)
94+
const requirements = context.selectedRequirements as unknown as PaymentRequirements
95+
96+
const authorization = await treasurer.onPaymentRequired([requirements], {
97+
method: "http",
98+
params: {
99+
resource: context.paymentRequired.resource,
100+
},
101+
})
102+
103+
if (!authorization) {
104+
return { abort: true, reason: "Payment declined by treasurer" }
105+
}
106+
107+
// Store for scheme client to retrieve
108+
paymentStore.set(requirements, authorization)
109+
// Store for status tracking
110+
authorizationByRequirements.set(requirements, authorization)
111+
112+
return
113+
})
114+
115+
// afterPaymentCreation: Notify treasurer payment is being sent
116+
client.onAfterPaymentCreation(async (context: PaymentCreatedContext) => {
117+
const requirements = context.selectedRequirements as unknown as PaymentRequirements
118+
const authorization = authorizationByRequirements.get(requirements)
119+
if (authorization) {
120+
await treasurer.onStatus("sending", authorization, {
121+
method: "http",
122+
params: {
123+
resource: context.paymentRequired.resource,
124+
},
125+
})
126+
}
127+
})
128+
129+
// onPaymentCreationFailure: Notify treasurer of error
130+
client.onPaymentCreationFailure(async (context: PaymentCreationFailureContext) => {
131+
const requirements = context.selectedRequirements as unknown as PaymentRequirements
132+
const authorization = authorizationByRequirements.get(requirements)
133+
if (authorization) {
134+
await treasurer.onStatus("error", authorization, {
135+
method: "http",
136+
params: {
137+
resource: context.paymentRequired.resource,
138+
error: context.error.message,
139+
},
140+
})
141+
}
142+
143+
// Don't recover - let the error propagate
144+
return
145+
})
146+
147+
return client
148+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { wrapWithAmpersend } from "./adapter.ts"

typescript/packages/ampersend-sdk/src/x402/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ export { NaiveTreasurer, createNaiveTreasurer } from "./treasurers/index.ts"
99
// X402Wallet implementations
1010
export { AccountWallet, SmartAccountWallet, createWalletFromConfig } from "./wallets/index.ts"
1111
export type { SmartAccountConfig, WalletConfig, EOAWalletConfig, SmartAccountWalletConfig } from "./wallets/index.ts"
12+
13+
// HTTP adapter for x402 v2 SDK
14+
export { wrapWithAmpersend } from "./http/index.ts"

0 commit comments

Comments
 (0)