Kanject.Wallet

View .md

A double-entry ledger and accounting engine that deploys into your PCI-scoped AWS — chart-of-accounts, attribute-driven posting profiles, balances, holds, and multi-currency. It records the movements your own payment stack makes; Kanject keeps the books — it never touches the money. Money moves. We do the accounting.

You'll learn
  • Provision the ledger into your PCI-scoped AWS account
  • Declare a chart of accounts with attribute-driven account roles
  • Define a posting profile whose debits and credits balance by construction
  • Post and commit balanced transactions at runtime with the manager services

Provision

Wallet is a CloudFormation stack in your account — the ledger tables, posting engine, and reconciliation jobs. Deploy it once per stage; the template is idempotent.

bash
# Deploys the ledger into your PCI-scoped AWS. Idempotent.kanject baas deploy wallet --env dev

Register it in your service

Register with AddKanjectWallet<TWalletDbContext>(...), passing a typed options delegate. TWalletDbContext is your own class deriving from WalletDbContext; the options carry the AWS region, schema, and per-stage namespace.

csharp
using Kanject.Wallet.Extensions;// Your ledger's DbContext derives from WalletDbContext (a single-table// data layer, like the other Kanject modules).public partial class AppWalletDbContext : WalletDbContext;// Register with a typed options delegate — the generic is the DbContext.builder.Services.AddKanjectWallet<AppWalletDbContext>(options =>{    options.Region            = appSettings.AwsRegion;    options.SchemaName        = "wallet";    options.DatabaseNamespace = appSettings.Stage;   // per-stage isolation});

Declare your chart of accounts

The ledger is attribute-driven: you declare accounts and their roles in code, and the numbering policy keeps each category in its own range. Account roles (SettlementClearing, WalletLiability, EscrowLiability, …) are what posting rules target, so a profile never hard-codes an account number.

csharp
using Kanject.Wallet.Abstractions.Attributes;using Kanject.Wallet.Abstractions.Attributes.Accounts;// Declare your chart of accounts once — an attribute-driven ledger.[ChartOfAccounts(version: 1, legalEntity: "KANJECT-LTD")][NumberingPolicy(category: Asset,     min: 1000, max: 1999)][NumberingPolicy(category: Liability, min: 2000, max: 2999)]public partial class KanjectCoA{    [AssetAccount    (number: 1100, name: "Stripe Clearing", role: SettlementClearing)]    [LiabilityAccount(number: 2001, name: "Customer Wallet", role: WalletLiability,                       requiresCounterparty: true, requiresBalanceValidation: true)]    [LiabilityAccount(number: 2002, name: "Escrow",          role: EscrowLiability)]    public partial void Accounts();}

Define a posting profile

Every money flow is a posting profile — a set of rules that translate an event into balanced journal lines. Debits and credits must balance, and the binding expressions (@event.Amount, @wallet:CustomerWallet(...)) resolve against the event at post time.

csharp
using Kanject.Wallet.Abstractions.Posting.Contracts;// Each money flow is a posting profile. Debits and credits MUST balance —// the ledger refuses to compile an unbalanced rule.[PostingProfile(version: 1, groupType: ProfileGroupType.Settlement)][UseChartOfAccount<KanjectCoA>]public partial class CustomerPaymentPosting{    [PostingRule(name: "Customer Card Payment", priority: 100)]    [PostingEvent<PostingEvent>(topic: "CustomerPayment", version: 1)]    [DebitAsset      (Role = ControlRole.SettlementClearing, Amount = "@event.Amount")]    [CreditUserWallet(User = "@wallet:CustomerWallet(@event.SourceParty.Id)",                      Amount = "@event.Amount")]    public partial Task RulesAsync(IPostingEvent @event);}

Post transactions at runtime

The manager services are the runtime surface. IWalletManager opens and queries wallets; ITransactionManager logs, posts, commits, and reverses transactions built from your posting profiles:

csharp
// At runtime, inject the manager services. Wallets are created through// IWalletManager; balanced transactions are posted through ITransactionManager// from a posting event, then committed.public class PaymentService(    IWalletManager wallets,    ITransactionManager transactions){    public Task<CreateWalletResponse?> OpenWalletAsync(CreateWalletRequest request, Guid userId)        => wallets.CreateWalletAsync(request, userId);    public async Task RecordCustomerPaymentAsync(PostingEvent paymentEvent)    {        // Posts the journal lines your CustomerPaymentPosting profile defines.        var posted = await transactions.PostTransactionAsync(paymentEvent);        await transactions.CommitTransactionAsync(            new CommitTransactionRequest { /* txn reference from the posted result */ });    }}

Holds, balances & multi-currency

  • Double-entry by construction — every movement is two balanced journal lines.
  • Multi-currency — accounts, conversions, and FX rates per tenant.
  • Holds & releases — place and release holds against wallet balances (escrow, liens).
  • Transaction lifecycle — log, post, commit, reverse, and cancel through ITransactionManager.
  • KYC & sanctions states — wallet parties carry KYC and sanctions-screening status.
Recap
  • Wallet is a double-entry ledger, not a payment processor — it keeps the books; your existing payment stack moves the money.
  • The chart of accounts is attribute-driven; posting rules target account roles, not numbers.
  • Every posting profile balances by construction — unbalanced rules fail the build.
  • At runtime, IWalletManager and ITransactionManager open wallets and post/commit balanced transactions.
Was this page helpful?