Skip to content
Draft
7 changes: 6 additions & 1 deletion packages/account-sdk/src/browser-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { base } from './interface/payment/base.browser.js';
import { CHAIN_IDS, TOKENS } from './interface/payment/constants.js';
import { getPaymentStatus } from './interface/payment/getPaymentStatus.js';
import { pay } from './interface/payment/pay.js';
import { payWithToken } from './interface/payment/payWithToken.js';
import { subscribe } from './interface/payment/subscribe.js';
import type {
InfoRequest,
Expand All @@ -17,6 +18,8 @@ import type {
PaymentResult,
PaymentStatus,
PaymentStatusOptions,
PayWithTokenOptions,
PayWithTokenResult,
SubscriptionOptions,
SubscriptionResult,
} from './interface/payment/types.js';
Expand Down Expand Up @@ -50,14 +53,16 @@ export type {
export { PACKAGE_VERSION as VERSION } from './core/constants.js';
export { createBaseAccountSDK } from './interface/builder/core/createBaseAccountSDK.js';
export { getCryptoKeyAccount, removeCryptoKey } from './kms/crypto-key/index.js';
export { base, CHAIN_IDS, getPaymentStatus, pay, subscribe, TOKENS };
export { base, CHAIN_IDS, getPaymentStatus, pay, payWithToken, subscribe, TOKENS };
export type {
InfoRequest,
PayerInfo,
PaymentOptions,
PaymentResult,
PaymentStatus,
PaymentStatusOptions,
PayWithTokenOptions,
PayWithTokenResult,
SubscriptionOptions,
SubscriptionResult,
};
75 changes: 75 additions & 0 deletions packages/account-sdk/src/core/telemetry/events/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,81 @@ export const logPaymentCompleted = ({
);
};

export const logPayWithTokenStarted = ({
token,
chainId,
correlationId,
}: {
token: string;
chainId: number;
correlationId: string | undefined;
}) => {
logEvent(
'payment.payWithToken.started',
{
action: ActionType.process,
componentType: ComponentType.unknown,
method: 'payWithToken',
correlationId,
signerType: 'base-account',
token,
chainId,
},
AnalyticsEventImportance.high
);
};

export const logPayWithTokenCompleted = ({
token,
chainId,
correlationId,
}: {
token: string;
chainId: number;
correlationId: string | undefined;
}) => {
logEvent(
'payment.payWithToken.completed',
{
action: ActionType.process,
componentType: ComponentType.unknown,
method: 'payWithToken',
correlationId,
signerType: 'base-account',
token,
chainId,
},
AnalyticsEventImportance.high
);
};

export const logPayWithTokenError = ({
token,
chainId,
correlationId,
errorMessage,
}: {
token: string;
chainId: number;
correlationId: string | undefined;
errorMessage: string;
}) => {
logEvent(
'payment.payWithToken.error',
{
action: ActionType.error,
componentType: ComponentType.unknown,
method: 'payWithToken',
correlationId,
signerType: 'base-account',
token,
chainId,
errorMessage,
},
AnalyticsEventImportance.high
);
};

export const logPaymentStatusCheckStarted = ({
testnet,
correlationId,
Expand Down
2 changes: 2 additions & 0 deletions packages/account-sdk/src/core/telemetry/logEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ type CCAEventData = {
testnet?: boolean;
status?: string;
periodInDays?: number;
token?: string;
chainId?: number;
};

type AnalyticsEventData = {
Expand Down
12 changes: 12 additions & 0 deletions packages/account-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ export {

export { PACKAGE_VERSION as VERSION } from './core/constants.js';

// Payment interface exports
export {
base,
CHAIN_IDS,
getPaymentStatus,
getSubscriptionStatus,
pay,
payWithToken,
prepareCharge,
subscribe,
TOKENS,
Expand All @@ -41,10 +43,20 @@ export type {
PrepareChargeCall,
PrepareChargeOptions,
PrepareChargeResult,
PrepareRevokeCall,
PrepareRevokeOptions,
PrepareRevokeResult,
RevokeOptions,
RevokeResult,
SubscriptionOptions,
SubscriptionResult,
SubscriptionStatus,
SubscriptionStatusOptions,
PayWithTokenOptions,
PayWithTokenResult,
PaymasterOptions,
TokenPaymentSuccess,
TokenInput,
} from './interface/payment/index.js';

export {
Expand Down
60 changes: 51 additions & 9 deletions packages/account-sdk/src/interface/payment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,56 @@ if (payment.success) {
}
```

## Token Payments (`payWithToken`)

Use `payWithToken` to send any ERC20 token on Base or Base Sepolia by specifying the token and paymaster configuration.

```typescript
import { payWithToken } from '@base-org/account';

const payment = await payWithToken({
amount: '1000000', // base units (wei)
token: 'USDC', // symbol or contract address
testnet: false, // Use Base mainnet (false) or Base Sepolia (true)
to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51',
paymaster: {
url: 'https://paymaster.example.com',
},
payerInfo: {
requests: [{ type: 'email' }],
},
});

console.log(`Token payment sent! Transaction ID: ${payment.id}`);
```

**Token payment notes**

- `amount` must be provided in the token's smallest unit (e.g., wei).
- `token` can be an address or a supported symbol (USDC, USDT, DAI).
- `testnet` toggles between Base mainnet (false) and Base Sepolia (true). Only Base and Base Sepolia are supported.
- `paymaster` is required. Provide either a `url` for a paymaster service or a precomputed `paymasterAndData`.
- The returned `payment.id` is a transaction hash, just like the `pay()` function. Pass this ID to `getPaymentStatus` along with the same `testnet` value.

## Checking Payment Status

You can check the status of a payment using the transaction ID returned from the pay function:
You can check the status of a payment using the ID returned from either `pay()` or `payWithToken()`:

```typescript
import { getPaymentStatus } from '@base/account-sdk';

// Check payment status
const status = await getPaymentStatus({
id: payment.id,
testnet: true
// Assume tokenPayment/usdcPayment are the results from the examples above.

// Token payments - use the same testnet value as the original payment
const tokenStatus = await getPaymentStatus({
id: tokenPayment.id, // e.g., "0x1234..."
testnet: false, // Same testnet value used in payWithToken
});

// USDC payments via pay() still require a testnet flag.
const usdcStatus = await getPaymentStatus({
id: usdcPayment.id,
testnet: true,
});

switch (status.status) {
Expand All @@ -50,6 +89,11 @@ switch (status.status) {
}
```

The status object now includes:

- `tokenAmount`, `tokenAddress`, and `tokenSymbol` for any detected ERC20 transfer.
- `amount` (human-readable) when the token is a whitelisted stablecoin (USDC/USDT/DAI).

## Information Requests (Data Callbacks)

You can request additional information from the user during payment using the `payerInfo` parameter:
Expand Down Expand Up @@ -144,10 +188,8 @@ The payment result is always a successful payment (errors are thrown as exceptio

### `getPaymentStatus(options: PaymentStatusOptions): Promise<PaymentStatus>`

#### PaymentStatusOptions

- `id: string` - Transaction ID (userOp hash) to check status for
- `testnet?: boolean` - Whether to check on testnet (Base Sepolia). Defaults to false (mainnet)
- `id: string` - Payment ID to check (transaction hash returned from `pay()` or `payWithToken()`).
- `testnet?: boolean` - Whether the payment was on testnet (Base Sepolia). Must match the value used in the original payment.
- `telemetry?: boolean` - Whether to enable telemetry logging (default: true)

#### PaymentStatus
Expand Down
82 changes: 77 additions & 5 deletions packages/account-sdk/src/interface/payment/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import type { Address } from 'viem';

/**
* Chain IDs for supported networks (Base only)
*/
export const CHAIN_IDS = {
base: 8453,
baseSepolia: 84532,
} as const;

/**
* Token configuration for supported payment tokens
* Token configuration for USDC-only payment APIs (e.g., pay()).
* For other stables or arbitrary tokens, use payWithToken.
*/
export const TOKENS = {
USDC: {
Expand All @@ -12,13 +23,74 @@ export const TOKENS = {
} as const;

/**
* Chain IDs for supported networks
* Canonical placeholder used by wallet providers to represent native ETH
*/
export const CHAIN_IDS = {
base: 8453,
baseSepolia: 84532,
export const ETH_PLACEHOLDER_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' as const;

/**
* Registry of whitelisted stablecoins that can be referenced by symbol
* when calling token-aware payment APIs (Base and Base Sepolia only).
*/
export const STABLECOIN_WHITELIST = {
USDC: {
symbol: 'USDC',
decimals: 6,
addresses: {
[CHAIN_IDS.base]: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
[CHAIN_IDS.baseSepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
} satisfies Partial<Record<number, Address>>,
},
USDT: {
symbol: 'USDT',
decimals: 6,
addresses: {
[CHAIN_IDS.base]: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2',
} satisfies Partial<Record<number, Address>>,
},
DAI: {
symbol: 'DAI',
decimals: 18,
addresses: {
[CHAIN_IDS.base]: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb',
} satisfies Partial<Record<number, Address>>,
},
EURC: {
symbol: 'EURC',
decimals: 6,
addresses: {
[CHAIN_IDS.base]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
} satisfies Partial<Record<number, Address>>,
},
} as const;

type StablecoinKey = keyof typeof STABLECOIN_WHITELIST;

/**
* Lookup map from token address to stablecoin metadata.
*/
export const STABLECOIN_ADDRESS_LOOKUP = Object.entries(STABLECOIN_WHITELIST).reduce<
Record<
string,
{
symbol: StablecoinKey;
decimals: number;
chainId: number;
}
>
>((acc, [symbol, config]) => {
for (const [chainId, address] of Object.entries(config.addresses)) {
if (!address) {
continue;
}
acc[address.toLowerCase()] = {
symbol: symbol as StablecoinKey,
decimals: config.decimals,
chainId: Number(chainId),
};
}
return acc;
}, {});

/**
* ERC20 transfer function ABI
*/
Expand Down
Loading
Loading