Angel Campa
Colour theme

WorkThe .NET year

SkillLedger

Let professionals trade work for work instead of money, settling in an internal credit that was encrypted, signed on every transaction, and held in escrow until a milestone cleared.

Ran on
Railway (API), Cloudflare Workers (web, via OpenNext)
Ran
August 2025 to July 2026
Domain
Professional skill-trading marketplace
Source
skill-exchange-api-skillledger-snapshot

Retired

Built with

  • .NET 9
  • EF Core
  • ASP.NET Core Identity
  • PostgreSQL
  • SignalR
  • Next.js
  • Stripe
  • xUnit

Hosted services

  • Neon (PostgreSQL)
  • Redis
  • Stripe

The decision

SkillLedger encrypted its wallet balances at the column level (AES-256-GCM ciphertext, not a plaintext integer), so the database itself could never read a balance. That choice moved overdraft protection out of SQL and into the application: balances could not be summed, indexed or constrained at the database layer, so every balance check ran as C# logic inside a Serializable transaction instead.

In exchange, a leaked database export would have exposed no balances, and every transaction row carried an HMAC-SHA256 signature, with a KeyIdentifier column reserved for key rotation.

A self-commissioned audit of that wiring later found gaps between the design and the running code: Program.cs never bound the key-vault configuration, so the flag gating a real Azure key stayed false in every environment and both signing keys resolved to hashes of hardcoded string constants; the branch that does reach Key Vault fetches a real key object and then discards it, deriving the AES key as a hash of the key’s name and the current calendar month instead, so wiring the flag correctly would not have fixed it; and the key-rotation method behind the KeyIdentifier column logged “not yet implemented” and returned true, reporting success on a rotation that never ran. None of it touched the column-level encryption itself: the database still never held a plaintext balance.

Architecture

Three .NET projects (Api, Core, and Infrastructure) sat in front of a Next.js frontend, over PostgreSQL. Program.cs was the composition root, and every one of its 93 service registrations was inline.

The project moved from SQL Server to Postgres in February 2026 and collapsed everything before that into one InitialPostgresCreate, so three EF Core migrations covered the 70-table schema.

Escrow held credit between agreement and delivery, and its six-state lifecycle was modelled as methods on the entity itself rather than scattered conditionals. Active and PartiallyReleased are the hub of the machine, and the repository draws it as two graphs rather than one, because all seven of their outbound transitions on a single graph put the labels close enough to merge:

money moving forward
  (start) -> Active                      client funds the project
  Active -> PartiallyReleased            a milestone is released
  PartiallyReleased -> PartiallyReleased the next milestone
  Active -> Completed                    one release covers the total
  PartiallyReleased -> Completed         releases reach the total
  Active, PartiallyReleased -> Cancelled

the account paused or contested
  Active, PartiallyReleased -> Disputed
  Active, PartiallyReleased -> Frozen
  Frozen -> Disputed
  Disputed, Frozen -> Active             nothing released yet
  Disputed, Frozen -> PartiallyReleased  something already released

Every arrow is a real call site rather than an inferred state. The four release edges are gated on CanBeReleased, and the two resume edges carry a guard on the amount already released, which is why Frozen and Disputed return to PartiallyReleased instead of Active once anything has gone out. The four transitions that move credit (funding, milestone release, full release and cancel) are each written from inside a Serializable transaction, and each release or refund writes a row through the same signed transaction log: the state machine and the ledger are two views of one write. The four that only change state (dispute, resolve, freeze, unfreeze) save without one.

By the numbers

Each figure carries where it came from and when it was measured. Open one to see the command behind it, where the repository recorded a command.

Application source
115,872 lines across 380 C# files
Provenance

Source portfolio/METRICS.md § Source size

git ls-files 'src/*.cs' | wc -l ; lines 'src/*.cs'

Measured

Tests
122,933 lines across 217 C# files
Provenance

Source portfolio/METRICS.md § Source size

git ls-files 'tests/*.cs' | wc -l ; lines 'tests/*.cs'

Measured

Declared backend test cases
at least 4,136 declared [Fact] test cases, plus 88 [Theory] and 358 [InlineData] cases
Provenance

Source portfolio/METRICS.md § Tests

Measured

Data model
70 tables, 64 DbSet<>, 62 entity files, 3 EF Core migrations
Provenance

Source portfolio/METRICS.md § Data model

git grep -c 'DbSet<' -- src/SkillLedger.Infrastructure/Data/SkillLedgerDbContext.cs

Measured

API surface
36 controllers, 70 Infrastructure services, 52 Core interfaces, 93 DI registrations in one 1,037-line Program.cs
Provenance

Source portfolio/METRICS.md § Components

Measured

Commit history
944 commits, 2025-08-29 to 2026-07-08
Provenance

Source portfolio/METRICS.md § History, exported from the private source repository; this snapshot is one commit

cat docs/source-history.json

Measured

Self-commissioned code audit
78 defects logged across severity tiers from a full-codebase security and bug audit he ran against his own code
Provenance

Source docs/BUG_REPORT.md § Full codebase security and bug audit

Measured

Live-URL Playwright sweep
A Playwright sweep he built and ran against the deployed site found 13 bugs, including a 404 on every dynamic route
Provenance

Source web/FOUND_BUGS.md

Measured

Read the repositoryskill-exchange-api-skillledger-snapshot

Its portfolio/ directory holds the architecture, metrics and testing documents these figures were taken from.