A pay as you go library for Node.js and Stripe on MongoDb
Tags: payment, startup, sveltekit, saas
Published: 2023-09-29 09:12:17

# UserCredits - Simplify Pay-As-You-Go Features for Your Project

As an entrepreneur working on [CvLink](http://cvlink.in), I understand the challenges of adding payment and pay-as-you-go features to a project. It's a time-consuming process, and I wanted to build a library in public, documenting the reasons behind my decisions and giving readers the opportunity to provide feedback early in the process.

The library is hosted on [GitHub](https://github.com/ziedHamdi/UserCredits).

While there are existing solutions like [LemonSqueezy](https://www.lemonsqueezy.com/) for implementing pay-as-you-go features, they can be overly complex for simple needs. You may just want to allow users to track their token consumption and pay for them, either before or after usage, without the complexity and cost of a full-fledged service.

## What's Typically Needed

To better understand the scope, let's summarize the essential requirements:

- **Tokens**: An abstraction of money to decouple from real currency. For example, users can buy 10 tokens for $10 or 20 tokens for $18.
- Real-time tracking of token consumption or balance.
- A way for customers to pay for the tokens they've used or to purchase tokens upfront (depending on your billing model).
- Currency conversion to display token offers in multiple currencies.
- The ability to refund customers for unused tokens.

## Concepts

Taking a straightforward approach, here are the core concepts:

- **Offers**: Represent how many tokens users get for their money. This includes scenarios like one-time offers, monthly subscriptions, yearly subscriptions, and discounts based on subscriptions. Offers can also override others with the same `overridingKey`.
- **Orders**: Represent customers' intentions to purchase an offer. We track their status until the payment is successful.
- **User Credits**: Provide information about a user's token balance and consumption.
- **Token Timetable**: A detailed log of each token consumption, including the date, time, and the service used.

This seems to cover all the essentials for implementing a pay-as-you-go feature in a startup.

## Technologies

Projects differ in their database and payment choices, so we expect the need for adapters on these axes. Therefore, it's crucial to keep concepts separate from implementations.

Testing scenarios for buying, consuming, and checking credit balances should be dissociated from the specific technologies used, making them adaptable to different platforms.

## Implementation

Here's how I've implemented these concepts:

![Implementation Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pp7joqikjos7lnwm7j9w.png)

### Offer

An offer specifies its type (e.g., one-time offer or subscription) and a repetition cycle. The `parentOffer` property specifies relationships, such as discounts for subscribers. Offers with the same `overridingKey` value override each other.

Multiple sub-offers can share the same `overridingKey`, representing different discount levels. For example, a free user could buy 10 AI credits for the regular price of $10, a **"Starter"** user could get a 30% discount, and a **"Early adopter"** user could have a 50% discount. These offers all have the same overriding key, e.g., `overridingKey="10_AI_Credits"`:

1. The $10 offer at the root level (`parentOffer=null`) for default visibility.
2. The 30% off offer, a child of the "Starter" offer with a `weight=1`.
3. The 50% off offer, a child of the **"Early adopter"** offer with a `weight=2`.

This way, an **"Early adopter"**, subscribed as a **"Starter"**, gets better privileges by paying 50% less for AI tokens; while keeping the access rules associated to **"Starter"** accounts.

```typescript
export interface IOffer {
  cycle: "once" | "weekly" | "monthly" | "yearly";
  kind: "subscription" | "tokens" | "expertise";
  name: string;
  parentOffer: unknown;
  price: number;
  tokenCount: number;
  weight: number;
}
```

### Order

Orders are tied to users intending to purchase an offer. We track their status until payment success. The `status` field is duplicated to simplify order reading, and the token count is duplicated for robustness.

```typescript
export interface OrderStatus {
  date: Date;
  message: string;
  status: "pending" | "paid" | "refused";
}

export interface IOrder<K extends object> {
  history: [OrderStatus];
  offerId: K;
  status: "pending" | "paid" | "refused";
  tokenCount: number;
  userId: K;
}
```

### Token Timetable

Token consumption and additions are logged in the token timetable for users to track their token activity.

```typescript
export interface ITokenTimetable {
  createdAt: Date;
  tokens: number;
  userId: string;
}
```

### User Credits

User credits summarize all transactions, including subscriptions and token balances. Subscriptions duplicate order status for efficient reading.

```typescript
export interface ISubscription {
  expires: Date;
  offerId: unknown;
  starts: Date;
  status: "pending" | "paid" | "refused";
}

export interface IUserCredits {
  subscriptions: (unknown extends ISubscription ? unknown : never)[];
  tokens: number;
  userId: unknown;
}
```

### Service

The payment interface serves as a facade to manage these concepts. Customers can create orders, execute payments, and check their token balance.

```typescript
export interface IPayment<T extends object> {
  createOrder(offerId: unknown, userId: unknown): Promise<IOrder<T>>;
  execute(order: IOrder<T>): Promise<IUserCredits>;
  orderStatusChanged(
    orderId: unknown,
    status: "pending" | "paid" | "refused",
  ): Promise<IOrder<T>>;
  remainingTokens(userId: unknown): Promise<IUserCredits>;
}
```

## Implementation

- MongoDB was chosen for the initial implementation, but migrating to other databases will be straightforward.
- The payment part is yet to be implemented, with Stripe as the first choice, but flexibility for other implementations.
- Screens will be developed using SvelteKit, but the library is designed to be adaptable to React, Angular, and Vue as well.

I welcome early contributors to this project, so feel free to [contact me](https://twitter.com/zhamdi) if you'd like to contribute or provide feedback.

Thank you for your interest in the UserCredits library! 🚀