Skip to content

RevFramework – Currency FAQ

This FAQ answers decision questions about using the Currency system: how to integrate it, which extension point to use, and what is supported vs. discouraged.

It does not repeat the README content. If you’re looking for APIs, examples, or behaviour details, see the Currency READMEs.


How do I integrate Currency into my project?

Minimal runtime setup

  1. Add SceneCurrencyService to the scene.
  2. (Recommended) Add CurrencyServiceBootstrap and configure Caps / Audit / Authority as needed.
  3. Resolve the service via CurrencyResolve.ServiceFrom(this) wherever you need it.

Inventory-backed setup (optional)

  • Use CurrencyInventoryFactories.* helpers (requires REV_INVENTORY_PRESENT).
  • Wrap with Caps / Audit / Authority using CurrencyFactories.

Rule of thumb

Resolve services; do not depend on concrete implementations.


Is this the right extension point?

I want to change balances

Use: ICurrencyService (resolved via CurrencyResolve).

Avoid: binding to the concrete SceneCurrencyService type.

SceneCurrencyService is the default scene implementation, but it is not part of the supported public API surface (its Instance accessor is internal). Resolve ICurrencyService via CurrencyResolve so you talk to the composed decorator stack rather than the bare scene service.


I want to enforce min/max limits

Use: CurrencyPolicy + CurrencyFactories.WithCaps(...).

Avoid: clamping values manually in gameplay or UI code.


I want audit logs (who changed what and why)

Use: CurrencyFactories.WithAudit(...) and query via CurrencyAudit.Get(...).

Avoid: custom logging wrapped around currency calls.

Avoid: querying the composed service when anything sits above the audit layer. CurrencyAudit.Get casts the service you give it and does not walk the chain, so WithCaps, WithEscrow and the RequireEscrow guard all hide the reader. Every shipped escrow factory puts escrow outermost, so on those stacks the query returns empty while auditing records normally. Keep a reference to the audited layer and query that — see Audit (Optional Capability) in the Public API reference.


I want atomic multi-step operations (shops, trades)

Use: CurrencyTxn or CurrencyPurchase helpers.

For stronger guarantees with escrow-enabled stacks, use CurrencyHoldTxn.

Avoid: chaining multiple Credit / Debit calls without orchestration.

Transactions provide:

  • ordered execution
  • best-effort rollback
  • optional batch event emission

Do not put a |rid: token in a transaction's sourceId

CurrencyTxn.Begin(...) and CurrencyHoldTxn.Begin(...) pass their sourceId to every staged leg verbatim — they do not derive a distinct id per leg. The idempotency decorator keys per operation (kind, currency, request id, amount), not per transaction, so two legs that share a kind and a currency collide on that key:

  • different amounts → the second leg fails with IdempotencyMismatch
  • equal amounts → the second leg is silently dropped as a replay, and the transaction commits having moved half of what you staged

A two-Debit gold purchase in one CurrencyTxn with a rid in the sourceId therefore charges once, not twice, and reports success. Keep the rid out of the transaction sourceId and dedupe at your own transaction boundary — see I want multiplayer-safe retries below.

Rollback may be blocked by outer decorators such as policy, authority, escrow requirements, or idempotency.


I want escrow / holds

Use: CurrencyFactories.WithEscrow(...).

If you need to enforce escrow usage:

Enable CurrencyPolicy.RequireEscrow.

This adds a guard that blocks direct Debit / Transfer when escrow is not present.

RequireEscrow does not add escrow

RequireEscrow does not add escrow automatically.
It only blocks operations when escrow is missing.

If you need both authority and escrow (e.g. with RequireEscrow enabled), use CurrencyFactories.WithCapsAuditAuthorityEscrow(...) (or ...EscrowAndPump(...)) — it composes escrow and authority coherently. Do not enable RequireEscrow on the plain WithCapsAuditAuthority* stacks: they include no escrow, so the guard denies every Debit / Transfer with ServiceMissing.


I want multiplayer-safe retries

Use: CurrencyFactories.WithIdempotency(...) and include:

|rid:<token>

inside the sourceId.

Avoid: relying on non-audit overloads — idempotency applies only to audit-aware overloads.

Avoid: putting that |rid: token in a CurrencyTxn / CurrencyHoldTxn sourceId — see the warning under I want atomic multi-step operations above.

This layer is per-operation, not per-transaction. The key is kind | currency | rid | amount, so a rid is safe to reuse only across calls you genuinely want treated as the same operation. Giving each leg of a multi-step flow its own id is the caller's responsibility — nothing in the framework derives them for you:

  • Per-operation retries — issue each call yourself and give each one its own rid, e.g. "shop|rid:" + purchaseId + ":fee" and "shop|rid:" + purchaseId + ":item".
  • Whole-transaction retries — leave the rid out of the currency sourceId entirely and dedupe at your own boundary. Economy does exactly this: it owns whole-saga dedup on its requestId and deliberately neutralises this decorator per leg (rid: is renamed to req:) so the two boundaries do not fight.

Note:

Replay short-circuits may not emit wallet events or audit entries, depending on decorator order.


I want authority (server / host only)

Use: CurrencyFactories.WithAuthority(...) with an ICurrencyAuthority implementation.

Single-player

Use CurrencyAuthorityBinder (alwaysTrue = true).

Avoid

Calling currency mutations from non-authoritative contexts.


Can I do X with Y?

Can I use Currency without Inventory?

Yes.

Inventory adapters are optional. Currency works with SceneCurrencyService alone.


Can I use Inventory items as currency?

Yes.

Use CurrencyInventoryFactories.* (when the inventory module is present).


Can I save / load balances?

Yes.

Use:

  • CurrencyPersistence helpers (per-owner)
  • CurrencyJsonSave (scene-wide utility)

Persistence restore respects the composed decorator stack (policy, authority, etc.).


Can I show balances in UI?

Yes.

Optional UI components:

  • CurrencyBar (UGUI)
  • CurrencyBarTMP
  • CurrencyBarUITK

Or subscribe to:

ICurrencyService.OnWalletChanged

Can I exchange currencies?

Yes.

Use CurrencyExchangeTable + CurrencyFactories.BuildExchange(...).

Always TryQuote before TryExchange.


Can I change balances directly from UI code?

No

UI should observe currency state, not mutate it.

All balance changes must go through ICurrencyService.


Can I call SetBalance in multiplayer?

Yes — but only from the authoritative side.

SetBalance writes an absolute value and bypasses debit/credit semantics.

It still flows through policy, authority, and other decorators.

Authoritative side only

Do not call SetBalance on clients unless your authority model allows it.

Prefer debit/credit flows when modelling gameplay actions


What should I avoid?

Avoid

  • ❌ Newing decorator classes directly
  • ❌ Bypassing CurrencyFactories
  • ❌ Hardcoding currency logic in UI
  • ❌ Polling for balance changes
  • ❌ Treating CurrencyDefinition as gameplay logic
  • ❌ Assuming escrow is automatic
  • ❌ Assuming decorator order does not matter

What guarantees does Currency provide?

Currency guarantees:

  • deterministic currency mutations
  • explicit decorator composition
  • orchestrated multi-operation flows
  • event-driven observation
  • a stable, minimal public API

Currency does not guarantee:

  • full transactional atomicity across arbitrary decorator stacks
  • networking replication
  • automatic escrow usage

See SystemGuaranteesMatrix.md for the formal guarantee definitions.


When should I contact support?

Before contacting support, please check:

  • Are you resolving the service via CurrencyResolve?
  • Is your decorator order intentional?
  • Are you using audit-aware overloads when expecting idempotency?
  • Is RequireEscrow enabled without composing escrow?
  • Are wallet owners assigned a valid StableId (for persistence)?

If reporting an issue, include:

  • Unity version
  • Active decorators
  • Repro steps
  • Relevant CurOpResult codes / messages

Summary

Currency is designed to be:

  • modular
  • explicit
  • authority-aware
  • transaction-capable
  • UI-agnostic

If you resolve via CurrencyResolve, compose via CurrencyFactories, and keep logic out of UI, you are using the system correctly.