Blog

How to grant monthly subscription credits in C# with Subscrio

Scanned manuscript and restored photograph drawing 20 and 16 credits from one ledger.

FolioWorks helps archives turn scanned collections into usable digital material. Its monthly plan includes 1,200 credits: OCR costs two credits per page, while restoring a photograph costs eight. A simple request counter cannot express those different prices.

In this guide we will grant monthly subscription credits in C#, price both operations, and check the balance after a mixed batch of archive work.

The plan uses recurring credit grants with rollover: unused credits remain in the customer wallet when the next allocation arrives. Each feature has its own consumption rule, so different operations can spend different amounts from that balance.

Define the customer promise

Item Archive credits
Monthly plan grant 1,200
OCR page 2
Restored photograph 8
Unused balance Retained

Run the example

The complete FolioWorks sample contains the setup and executable assertions used below. Use Windows with the .NET SDK and SQL Server Express LocalDB. The runner connects with Windows integrated authentication, creates an isolated database, and removes it when finished.

git clone https://github.com/subscrio/samples.git
cd samples/examples/monthly-subscription-credits-csharp
sqllocaldb start MSSQLLocalDB
dotnet restore --locked-mode
dotnet run --no-restore

The snippets run in order within that program. Check prints the returned value and asserts that it matches the expected result. The outputs below come from the executable sample. ExpectError<T> is its assertion helper: it requires the requested exception and prints true when that check passes.

Initialize Subscrio

The database helper supplies this connection-string form. Select the SQL Server provider and install the schema before creating the catalog.

Server=(localdb)\MSSQLLocalDB;Database=SubscrioBlog_<generated name>;Integrated Security=true;TrustServerCertificate=true
var clock = new DemoClock();
using var app = new Subscrio.Core.Subscrio(
    new SubscrioConfig
    {
        Database = new DatabaseConfig
        {
            ConnectionString = database.ConnectionString,
            DatabaseType = DatabaseType.SqlServer
        },
        Clock = clock
    }
);
await app.InstallSchemaAsync();

The full runner supplies the imports, database helper, and cleanup. DemoClock is a sample helper implementing Subscrio’s IClock; setting its UtcNow property changes the time used by the checks below. Its injectable clock lets the boundary checks advance time without waiting. It controls Subscrio’s entitlement/accounting evaluation; it does not change the database server clock.

Create the product

FolioWorks groups OCR and photograph restoration under one archive product.

await app.Products.CreateProductAsync(new("folioworks", "FolioWorks"));

Define the entitlement

OCR is a yes-or-no entitlement. The false default means a customer needs a qualifying plan before the application accepts document processing.

await app.Features.CreateFeatureAsync(new("ocr", "ocr", "toggle", "false"));

Associate the feature

Make OCR available to plans for the archive product.

await app.Products.AssociateFeatureAsync(
    "folioworks",
    "ocr",
    new FeatureResolutionOptions(SubscriptionRule: "most_generous")
);

Create the plan

Create the Archive plan under FolioWorks. Its feature values will enable archive work; a separate credit grant will fund that work.

await app.Plans.CreatePlanAsync(new("folioworks", "standard", "Archive"));

Set the included value

The archive plan permits OCR. Credit pricing is configured separately below.

await app.Plans.SetFeatureValueAsync("standard", "ocr", "true");

Create the billing cycle

The monthly cycle identifies the catalog offer. Creating it does not charge the customer.

await app.BillingCycles.CreateBillingCycleAsync(
    new("standard", "standard-cycle", "Monthly", "months", DurationValue: 1)
);

Create the customer

The sample uses the key customer for the FolioWorks purchaser. Creating this record identifies the recipient; the subscription will assign their plan.

await app.Customers.CreateCustomerAsync(new("customer", "FolioWorks customer"));

Define photograph restoration

Restoration has its own yes-or-no entitlement, separate from OCR.

await app.Features.CreateFeatureAsync(
    new("restore-photo", "Restore photograph", "toggle", "false")
);

Associate restoration with FolioWorks

Make restoration available to plans for the archive product.

await app.Products.AssociateFeatureAsync(
    "folioworks",
    "restore-photo",
    new FeatureResolutionOptions(SubscriptionRule: "most_generous")
);

Include restoration in the plan

The paid plan permits both OCR and photograph restoration.

await app.Plans.SetFeatureValueAsync("standard", "restore-photo", "true");

Define the credit currency

Both operations spend archive credits from the customer wallet.

await app.Credits.CreateCurrencyAsync(new("archive-credits", "Archive credits"));

Configure the recurring credit grant

The plan issues 1,200 credits on its monthly grant cadence. This example retains unused credits and does not expire grants.

await app.Credits.SetPlanGrantAsync(
    "standard",
    "archive-credits",
    new(1200, "monthly")
);

Price the two operations

Set the cost of one unit of each feature. OCR units are pages; restoration units are photographs.

await app.Credits.SetConsumptionRuleAsync("ocr", "archive-credits", 2);
await app.Credits.SetConsumptionRuleAsync("restore-photo", "archive-credits", 8);

Give the customer a subscription

The agreement subscription connects this customer to the Archive plan through standard-cycle. That plan supplies feature access and the recurring grant policy configured above.

await app.Subscriptions.CreateSubscriptionAsync(
    new("agreement", "customer", "standard-cycle", ActivationDate: clock.UtcNow)
);

Issue the due grant

Call the grant processor from your application scheduler when you want grants issued proactively. Balance reads and credit spending also reconcile due grants. Repeated processing does not issue the same monthly grant twice. Available below is the spendable credit balance, so the first allocation returns 1,200.

await app.Credits.ProcessScheduledGrantsAsync("customer");
Check(
    "Monthly credits",
    (await app.Credits.GetBalanceAsync("customer", "archive-credits")).Available,
    1200L
);
var repeated = await app.Credits.ProcessScheduledGrantsAsync("customer");
Check("Repeated grants", repeated.Issued, 0);

Output:

Monthly credits: 1200
Repeated grants: 0

Spend credits on archive work

Feature access and wallet affordability are separate decisions. Check the customer entitlement before consuming credits; ConsumeAsync then enforces the available balance.

async Task Process(string feature, long units, string jobKey)
{
    if (
        !await app.FeatureChecker.GetValueForCustomerAsync<bool>(
            "customer",
            "folioworks",
            feature,
            false
        )
    )
        throw new InvalidOperationException("Feature is not included");
    await app.Credits.ConsumeAsync(new("customer", feature, units, jobKey));
}
await Process("ocr", 10, "collection-ocr");
await Process("restore-photo", 2, "collection-photos");
Check(
    "Credits after archive work",
    (await app.Credits.GetBalanceAsync("customer", "archive-credits")).Available,
    1164L
);

Output:

Credits after archive work: 1164

Keep the entitlement check when credits remain

If restoration leaves the plan, a funded wallet must not bypass that decision. The same application function rejects the work without consuming credits.

await app.Plans.SetFeatureValueAsync("standard", "restore-photo", "false");
await ExpectError<InvalidOperationException>(
    () => Process("restore-photo", 1, "unavailable-photo"),
    "Restoration denied"
);
Check(
    "Unspent credits preserved",
    (await app.Credits.GetBalanceAsync("customer", "archive-credits")).Available,
    1164L
);

Output:

Restoration denied: true
Unspent credits preserved: 1164

Verify monthly credit rollover

This plan retains unused credits, so a later monthly allocation adds to the 1,164 remaining. Advance the injected clock instead of waiting for the grant date.

clock.UtcNow = DateTime.UtcNow.AddMonths(1).AddSeconds(1);
await app.Credits.ProcessScheduledGrantsAsync("customer");
Check(
    "Credits after next monthly grant",
    (await app.Credits.GetBalanceAsync("customer", "archive-credits")).Available,
    2364L
);

Output:

Credits after next monthly grant: 2364

Use the result in your application

The mixed batch costs 36 credits, leaving 1,164. A later monthly grant adds another 1,200 because this plan retains unused credits. Restoration can still be denied while that wallet is funded: a credit balance pays for an operation but does not decide which features the customer owns.

Run the complete FolioWorks example to reproduce the checks. The Subscrio credit entitlements guide explains the related model.

For a related TypeScript example, see a shared wallet across TypeScript products. A related next step is prepaid credit packs in C#.

Written by

Jasen Fici

Founder, Subscrio

Bootstrapped founder. Built Velaro and StatusCast. Now building Subscrio, an entitlement engine for software products.

Screenshot preview