PHPackages                             chemaclass/unspent - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. chemaclass/unspent

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

chemaclass/unspent
==================

A PHP library for UTXO-like bookkeeping using unspent entries.

v1.2.0(3w ago)151MITPHPPHP &gt;=8.4CI passing

Since Jan 14Pushed 1w agoCompare

[ Source](https://github.com/Chemaclass/unspent)[ Packagist](https://packagist.org/packages/chemaclass/unspent)[ Docs](https://github.com/Chemaclass/unspent)[ RSS](/packages/chemaclass-unspent/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (8)Versions (4)Used By (0)

Unspent
=======

[](#unspent)

[![PHP 8.4+](https://camo.githubusercontent.com/25012e98b640e282284d84348b9c496c315c270928666c27e504e5fa10802102/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e342b2d3737374242343f6c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://www.php.net/)[![MIT License](https://camo.githubusercontent.com/784362b26e4b3546254f1893e778ba64616e362bd6ac791991d2c9e880a3a64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e737667)](LICENSE)[![Scrutinizer Code Quality](https://camo.githubusercontent.com/f281727a0668f038c0b21fe7ebb041d6aa3d30c3f535d0bfad4c10fd88292cfe/68747470733a2f2f7363727574696e697a65722d63692e636f6d2f672f4368656d61636c6173732f756e7370656e742f6261646765732f7175616c6974792d73636f72652e706e673f623d6d61696e)](https://scrutinizer-ci.com/g/Chemaclass/unspent/?branch=main)[![Code Coverage](https://camo.githubusercontent.com/9f4efc4a0e8ed2145a976e4e3c59f5c80bd1dfe2192978f5efa07aad2644df63/68747470733a2f2f7363727574696e697a65722d63692e636f6d2f672f4368656d61636c6173732f756e7370656e742f6261646765732f636f7665726167652e706e673f623d6d61696e)](https://scrutinizer-ci.com/g/Chemaclass/unspent/?branch=main)[![Mutation testing badge](https://camo.githubusercontent.com/f2da5e60191f88a8b0b948edf83987e4dcbb1d611ce4e98521952bf0419d70c6/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f7374796c653d666c61742675726c3d687474707325334125324625324662616467652d6170692e737472796b65722d6d757461746f722e696f2532466769746875622e636f6d2532464368656d61636c617373253246756e7370656e742532466d61696e)](https://dashboard.stryker-mutator.io/reports/github.com/Chemaclass/unspent/main)

**Track value like physical cash in your PHP apps.** Every unit has an origin, can only be spent once, and leaves a complete audit trail.

```
$ledger = Ledger::inMemory();
$ledger->credit('alice', 100)->transfer('alice', 'bob', 25);
echo $ledger->totalUnspentByOwner('bob'); // 25
```

Zero-dependency PHP 8.4+ library implementing the UTXO model, inspired by Bitcoin and decoupled as a standalone library.

Table of Contents
-----------------

[](#table-of-contents)

- [Why?](#why)
- [When to use it](#when-to-use-it)
- [Install](#install)
- [Quick Start](#quick-start)
- [Output types](#output-types)
- [Examples](#examples)
- [Documentation](#documentation)
- [Learning Path](#learning-path)
- [FAQ](#faq)
- [Contributing](#contributing)

Why?
----

[](#why)

Traditional balance tracking (`balance: 500`) is just a number you mutate. There's no history, no proof of where it came from, and race conditions can corrupt it.

**Unspent** tracks value like physical cash. You can't photocopy a $20 bill - you spend it and get change back. This gives you:

- **Double-spend prevention** — a unit can only be spent once, ever
- **Complete audit trail** — trace any value back to its origin
- **Immutable history** — state changes are additive, never mutated
- **Advanced locks** — timelocks, multisig, hash-locked outputs (HTLCs)
- **Zero external dependencies** — pure PHP 8.4+

When to use it
--------------

[](#when-to-use-it)

NeedTraditional BalanceUnspentSimple spendingEasyOverkill"Who authorized this?"Requires extra loggingBuilt-in"Trace this value's origin"Requires event sourcingBuilt-inConcurrent spending safetyRace conditionsAtomicConditional spending rulesCustom logic neededLock systemRegulatory audit trailReconstruct from logsNative**Use Unspent when** value moves between parties, you need to prove who authorized what, or audit trail is a requirement.

**Skip it when** you only need a simple counter, single-user balance, or no audit requirements.

### Limitations

[](#limitations)

LimitationDetails**Integer bounds**Amounts bounded by `PHP_INT_MAX` (~9.2e18). Wrap for arbitrary precision.**Single-node model**Single-node operation. For distributed consensus, add Raft/blockchain infrastructure.**No built-in rate limiting**Your application must implement rate limiting.**Memory for large datasets**In-memory ~1MB/1k outputs. Use store-backed mode for &gt;100k outputs.**Not for sub-second precision**Timestamps not enforced; not a real-time trading engine.Install
-------

[](#install)

```
composer require chemaclass/unspent
```

Quick Start
-----------

[](#quick-start)

### In-Memory (prototyping)

[](#in-memory-prototyping)

```
use Chemaclass\Unspent\Ledger;

$ledger = Ledger::inMemory();
$ledger->credit('alice', 1000)
    ->credit('bob', 500)
    ->transfer('alice', 'bob', 200)
    ->transfer('alice', 'bob', 100, fee: 5)
    ->debit('bob', 50);

$ledger->totalUnspentByOwner('alice');  // 695
$ledger->totalUnspentByOwner('bob');    // 750
```

MethodDescription`credit($owner, $amount)`Mint new value to owner`transfer($from, $to, $amount, $fee)`Move value between owners`debit($owner, $amount, $fee)`Burn value from owner### SQLite persistence (production)

[](#sqlite-persistence-production)

```
use Chemaclass\Unspent\Persistence\Sqlite\SqliteRepositoryFactory;
use Chemaclass\Unspent\Output;

$repo = SqliteRepositoryFactory::createFromFile('ledger.db');

$ledger = $repo->find('my-ledger')
    ?? Ledger::withGenesis(Output::ownedBy('alice', 1000));

$ledger->transfer('alice', 'bob', 200);
$repo->save('my-ledger', $ledger);
```

### Coin Control (full input/output control)

[](#coin-control-full-inputoutput-control)

```
use Chemaclass\Unspent\Tx;

$ledger = Ledger::withGenesis(
    Output::ownedBy('alice', 500, 'alice-savings'),
    Output::ownedBy('alice', 300, 'alice-checking'),
);

$ledger->apply(Tx::create(
    spendIds: ['alice-checking'],
    outputs: [
        Output::ownedBy('bob', 200),
        Output::ownedBy('alice', 100, 'alice-change'),
    ],
    signedBy: 'alice',
));
```

Use Coin Control for: specific output selection, custom IDs, multiple recipients, complex fees.

### Batch operations

[](#batch-operations)

```
$ledger->consolidate('alice', fee: 10);

$ledger->batchTransfer('alice', [
    'bob' => 100,
    'charlie' => 200,
    'dave' => 300,
], fee: 5);
```

### Transaction Mempool

[](#transaction-mempool)

Stage transactions for validation before commit:

```
use Chemaclass\Unspent\Mempool;

$mempool = new Mempool($ledger);
$mempool->add($tx1);
$mempool->add($tx2);
// $mempool->add($conflictingTx); // Throws OutputAlreadySpentException
$mempool->commit();
```

Output types
------------

[](#output-types)

MethodUse case`Output::open(100)`No lock - pure bookkeeping`Output::ownedBy('alice', 100)`Server-side auth (sessions, JWT)`Output::signedBy($pubKey, 100)`Ed25519 crypto (trustless)`Output::timelocked('alice', 100, $time)`Vesting, delayed payments`Output::multisig(2, ['a','b','c'], 100)`Joint accounts, escrow`Output::hashlocked($hash, 100)`Atomic swaps, HTLCs`Output::lockedWith($lock, 100)`Custom lock implementationsExamples
--------

[](#examples)

Runnable demos under [`example/`](example/):

```
php example/run               # List all
php example/run game          # Run a demo (also: loyalty, accounting, events, btc, wallet, locks, sqlite)
php example/run game --reset  # Reset state
```

AliasDemonstrates`game`In-game currency, ownership, double-spend, fees`loyalty`Customer rewards, minting, redemption, audit`accounting`Department budgets, multi-party auth, reconcile`events`Order lifecycle as state transitions`btc`Bitcoin simulation, mining, fees, consolidation`wallet`Ed25519 signatures, trustless verification`locks`Custom time-locked outputs, serialization`sqlite`SQLite persistence, querying, historySee [example/README.md](example/README.md) for full output samples and the web API demo.

Documentation
-------------

[](#documentation)

Start at [`docs/README.md`](docs/README.md) for the full index.

TopicWhat you'll learn[Core Concepts](docs/concepts.md)How outputs, transactions, and the ledger work[Ownership](docs/ownership.md)Locks (owner, timelock, multisig, hashlock), authorization[History](docs/history.md)Tracing value through transactions[Fees &amp; Minting](docs/fees-and-minting.md)Implicit fees, coinbase transactions[Selection Strategies](docs/selection-strategies.md)FIFO, largest-first, exact-match, random, custom[Persistence](docs/persistence.md)JSON, SQLite, custom storage[Scalability](docs/scalability.md)In-memory vs store-backed mode[Events](docs/events.md)PSR-14 event dispatching, integrations[Migration Guide](docs/migration.md)Moving from balance-based systems to UTXO[Troubleshooting](docs/troubleshooting.md)Common issues and solutions[API Reference](docs/api-reference.md)Ledger, Output, Tx, Mempool, UtxoAnalyticsLearning Path
-------------

[](#learning-path)

LevelTopicDocsExample1. BasicsOutputs, transactions[Concepts](docs/concepts.md)`php example/run game`2. OwnershipLocks, authorization[Ownership](docs/ownership.md)`php example/run wallet`3. PersistenceSQLite storage[Persistence](docs/persistence.md)`php example/run sqlite`4. ScaleMode selection[Scalability](docs/scalability.md)-5. AdvancedTimelocks, multisig, HTLC[Ownership](docs/ownership.md)`php example/run locks`6. OperationsBatch, mempool, analytics[API Reference](docs/api-reference.md)-FAQ
---

[](#faq)

**Can two outputs have the same ID?**No. Output IDs must be unique across the ledger. If you omit the ID, a unique one is auto-generated (128-bit random entropy). Custom IDs that collide throw `DuplicateOutputIdException`. This mirrors Bitcoin's `txid:vout` model.

**When should I use in-memory vs store-backed mode?**ScenarioRecommendation&lt; 100k total outputs`Ledger::inMemory()` or `Ledger::withGenesis(...)`&gt; 100k total outputs`Ledger::withRepository($repository)`Need full history in memory`Ledger::inMemory()`Memory-constrained environment`Ledger::withRepository($repository)`See [Scalability docs](docs/scalability.md).

**How are fees calculated?**Fees are implicit, like in Bitcoin. The difference between inputs and outputs is the fee:

```
$ledger->apply(Tx::create(
    spendIds: ['input-100'],       // Spending 100
    outputs: [Output::open(95)],   // Creating 95
));
// Fee = 100 - 95 = 5 (implicit)
```

See [Fees &amp; Minting docs](docs/fees-and-minting.md).

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for setup, TDD workflow, quality gates, and commit format.

```
composer install     # Installs dependencies + pre-commit hook
composer check:quick # Fast feedback: cs-fixer + phpunit
composer test        # Full: cs-fixer + rector + phpstan + phpunit
```

Docker workflow available via `make help`.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance97

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~95 days

Total

3

Last Release

25d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3d166420c6770c5941e10bd68b2d26501eabb432e280d8e6eba0a344bcc1e5ae?d=identicon)[Chemaclass](/maintainers/Chemaclass)

---

Top Contributors

[![Chemaclass](https://avatars.githubusercontent.com/u/5256287?v=4)](https://github.com/Chemaclass "Chemaclass (123 commits)")

---

Tags

accountingbookkeepingevent-stormingledgerlibraryphputxoEd25519authorizationbitcoinimmutablewalletAccountingcryptocurrencyaudit-trailbookkeepingtransactionledgerutxodouble-spend

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/chemaclass-unspent/health.svg)

```
[![Health](https://phpackages.com/badges/chemaclass-unspent/health.svg)](https://phpackages.com/packages/chemaclass-unspent)
```

###  Alternatives

[league/oauth2-server

A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.

6.7k151.3M327](/packages/league-oauth2-server)[league/oauth2-client

OAuth 2.0 Client Library

3.8k132.3M1.4k](/packages/league-oauth2-client)[paragonie/sodium_compat

Pure PHP implementation of libsodium; uses the PHP extension if it exists

932149.1M197](/packages/paragonie-sodium-compat)[league/oauth1-client

OAuth 1.0 Client Library

995114.9M127](/packages/league-oauth1-client)[league/oauth2-google

Google OAuth 2.0 Client Provider for The PHP League OAuth2-Client

42524.2M200](/packages/league-oauth2-google)[auth0/auth0-php

PHP SDK for Auth0 Authentication and Management APIs.

41022.5M98](/packages/auth0-auth0-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
