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
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions typescript/packages/ampersend-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,16 @@
"zod": "^3.24.2"
},
"peerDependencies": {
"@x402/core": "^2.1.0",
"fastmcp": "^3.17.0"
},
"peerDependenciesMeta": {
"@x402/core": {
"optional": true
}
},
"devDependencies": {
"@x402/core": "^2.1.0",
"fastmcp": "github:edgeandnode/fastmcp#598d18f",
"tsx": "^4.21.0"
}
Expand Down
64 changes: 64 additions & 0 deletions typescript/packages/ampersend-sdk/src/x402/http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# HTTP x402 Client Adapter

Wraps x402 v2 SDK clients to use Ampersend treasurers for payment decisions.

## Overview

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.

**→ [Complete Documentation](../../../../README.md)**

## Quick Start

```typescript
import { AccountWallet, NaiveTreasurer } from "@ampersend_ai/ampersend-sdk"
import { wrapWithAmpersend } from "@ampersend_ai/ampersend-sdk/x402"
import { wrapFetchWithPayment, x402Client } from "@x402/fetch"

const wallet = AccountWallet.fromPrivateKey("0x...")
const treasurer = new NaiveTreasurer(wallet)

const client = new x402Client()
wrapWithAmpersend(client, treasurer, ["base", "base-sepolia"])

const fetchWithPayment = wrapFetchWithPayment(fetch, client)
const response = await fetchWithPayment("https://paid-api.example.com/resource")
```

## API Reference

### wrapWithAmpersend

```typescript
function wrapWithAmpersend(client: x402Client, treasurer: X402Treasurer, networks: Array<string>): x402Client
```

Configures an x402Client to use an Ampersend treasurer for payment authorization.

**Parameters:**

- `client` - The x402Client instance to wrap
- `treasurer` - The X402Treasurer that handles payment authorization decisions
- `networks` - Array of v1 network names to register (e.g., `"base"`, `"base-sepolia"`)

**Returns:** The configured x402Client instance (same instance, mutated)

## Features

- **Transparent Integration**: Drop-in replacement for `registerExactEvmScheme`
- **Treasurer Pattern**: Payment decisions via `X402Treasurer.onPaymentRequired()`
- **Payment Lifecycle**: Tracks payment status (sending, success, error) via `onStatus()`
- **v1 Protocol Support**: Works with EVM networks using v1 payment payloads

## How It Works

1. Wraps the x402Client with treasurer-based payment hooks
2. On 402 response, calls `treasurer.onPaymentRequired()` for authorization
3. If approved, creates payment using the treasurer's wallet
4. Notifies treasurer of payment status via `onStatus()`

## Learn More

- [TypeScript SDK Guide](../../../../README.md)
- [Treasurer Documentation](../../../../README.md#x402treasurer)
- [x402-http-client Example](https://github.com/edgeandnode/ampersend-examples/tree/main/typescript/examples/x402-http-client)
148 changes: 148 additions & 0 deletions typescript/packages/ampersend-sdk/src/x402/http/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type {
PaymentCreatedContext,
PaymentCreationContext,
PaymentCreationFailureContext,
x402Client,
} from "@x402/core/client"
import type { PaymentRequirements } from "x402/types"

import type { Authorization, X402Treasurer } from "../treasurer.ts"

/**
* Scheme client that retrieves payments from the treasurer via a shared WeakMap.
* Compatible with @x402/core's SchemeNetworkClient interface for v1 protocol.
*
* Note: We don't implement SchemeNetworkClient directly because @x402/core
* exports v2 types, but registerV1() passes v1 types at runtime.
*/
class TreasurerSchemeClient {
readonly scheme = "exact"

constructor(private readonly paymentStore: WeakMap<PaymentRequirements, Authorization>) {}

async createPaymentPayload(
x402Version: number,
requirements: PaymentRequirements,
): Promise<{ x402Version: number; payload: Record<string, unknown> }> {
const authorization = this.paymentStore.get(requirements)
if (!authorization) {
throw new Error("No payment authorization found for requirements")
}

// Clean up after retrieval
this.paymentStore.delete(requirements)

return {
x402Version,
payload: authorization.payment.payload,
}
}
}

/**
* Wraps an x402Client to use an ampersend-sdk treasurer for payment decisions.
*
* This adapter integrates ampersend-sdk treasurers with Coinbase's x402 v2 SDK,
* allowing you to use sophisticated payment authorization logic (budgets, policies,
* approvals) with the standard x402 HTTP client ecosystem.
*
* Note: This adapter registers for v1 protocol because the underlying wallets
* (AccountWallet, SmartAccountWallet) produce v1 payment payloads.
*
* @param client - The x402Client instance to wrap
* @param treasurer - The X402Treasurer that handles payment authorization
* @param networks - Array of v1 network names to register (e.g., 'base', 'base-sepolia')
* @returns The configured x402Client instance (same instance, mutated)
*
* @example
* ```typescript
* import { x402Client } from '@x402/core/client'
* import { wrapFetchWithPayment } from '@x402/fetch'
* import { wrapWithAmpersend, NaiveTreasurer, AccountWallet } from '@ampersend_ai/ampersend-sdk'
*
* const wallet = AccountWallet.fromPrivateKey('0x...')
* const treasurer = new NaiveTreasurer(wallet)
*
* const client = wrapWithAmpersend(
* new x402Client(),
* treasurer,
* ['base', 'base-sepolia']
* )
*
* const fetchWithPay = wrapFetchWithPayment(fetch, client)
* const response = await fetchWithPay('https://paid-api.com/endpoint')
* ```
*/
export function wrapWithAmpersend(client: x402Client, treasurer: X402Treasurer, networks: Array<string>): x402Client {
// Shared store for correlating payments between hooks and scheme client
const paymentStore = new WeakMap<PaymentRequirements, Authorization>()

// Register TreasurerSchemeClient for v1 protocol on each network
// Using registerV1 because our wallets produce v1 payment payloads
// Cast to any because @x402/core types are v2, but registerV1 accepts v1 at runtime
const schemeClient = new TreasurerSchemeClient(paymentStore)
for (const network of networks) {
client.registerV1(network, schemeClient as any)
}

// Track authorization for status updates
const authorizationByRequirements = new WeakMap<PaymentRequirements, Authorization>()

// beforePaymentCreation: Consult treasurer for payment authorization
client.onBeforePaymentCreation(async (context: PaymentCreationContext) => {
// v1 requirements are passed directly to treasurer (no conversion needed)
const requirements = context.selectedRequirements as unknown as PaymentRequirements

const authorization = await treasurer.onPaymentRequired([requirements], {
method: "http",
params: {
resource: context.paymentRequired.resource,
},
})

if (!authorization) {
return { abort: true, reason: "Payment declined by treasurer" }
}

// Store for scheme client to retrieve
paymentStore.set(requirements, authorization)
// Store for status tracking
authorizationByRequirements.set(requirements, authorization)

return
})

// afterPaymentCreation: Notify treasurer payment is being sent
client.onAfterPaymentCreation(async (context: PaymentCreatedContext) => {
const requirements = context.selectedRequirements as unknown as PaymentRequirements
const authorization = authorizationByRequirements.get(requirements)
if (authorization) {
await treasurer.onStatus("sending", authorization, {
method: "http",
params: {
resource: context.paymentRequired.resource,
},
})
}
})

// onPaymentCreationFailure: Notify treasurer of error
client.onPaymentCreationFailure(async (context: PaymentCreationFailureContext) => {
const requirements = context.selectedRequirements as unknown as PaymentRequirements
const authorization = authorizationByRequirements.get(requirements)
if (authorization) {
await treasurer.onStatus("error", authorization, {
method: "http",
params: {
resource: context.paymentRequired.resource,
error: context.error.message,
},
})
}

// Don't recover - let the error propagate
return
})

return client
}
1 change: 1 addition & 0 deletions typescript/packages/ampersend-sdk/src/x402/http/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { wrapWithAmpersend } from "./adapter.ts"
3 changes: 3 additions & 0 deletions typescript/packages/ampersend-sdk/src/x402/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ export { NaiveTreasurer, createNaiveTreasurer } from "./treasurers/index.ts"
// X402Wallet implementations
export { AccountWallet, SmartAccountWallet, createWalletFromConfig } from "./wallets/index.ts"
export type { SmartAccountConfig, WalletConfig, EOAWalletConfig, SmartAccountWalletConfig } from "./wallets/index.ts"

// HTTP adapter for x402 v2 SDK
export { wrapWithAmpersend } from "./http/index.ts"
Loading