agentleFS
adxptivedadxptived/Rust-Skills/skills/rust-fintech/SKILL.md

rust-fintech

Skill1 starsChanged 6 months agoMITView on GitHub

Rust Fintech and financial system design guidelines. Use when: - User works with money, financial transactions, ledger accounts, or currency - Handling high-precision calculations (e.g. rust_decimal) - Enforcing auditability, immutability, and event sourcing in ledger designs - Writing code requiring checked arithmetic to prevent overflow losses - Building payment processing, settlement, or reconciliation systems - Implementing FX conversion, interest calculations, or fee computation - Designing idempotent payment flows or webhook verification

The file

Show its rules and text

We split this skill into the rules it gives an agent

14 rules. About 0 are common, 13 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈13

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never use floats (f32, f64) to represent monetary values due to rounding errors and precision loss.

    line 28
  2. Distinctive:

    Ensure that entries balance exactly: the sum of debits must equal the sum of credits.

    line 77
  3. Distinctive:

    Always use checked, wrapping, or saturating arithmetic methods rather than standard operators if there's any chance of input overflow.

    line 127
  4. Distinctive:

    Prefer integer minor units for ledgers and settlement.

    line 135
  5. Distinctive:

    Every posted transaction must be:

    line 147
  6. Distinctive:

    Never use floats for money.

    line 172
  7. Distinctive:

    Keep FX rates with source, timestamp, precision, and effective interval.

    line 175
  8. Distinctive:

    Test duplicate idempotency keys.

    line 304
  9. Distinctive:

    Test reversal entries sum to zero.

    line 305
  10. Distinctive:

    Test currency mismatch errors.

    line 306
  11. Distinctive:

    Test rounding at domain boundaries with known trap values.

    line 309
  12. Distinctive:

    Test FX rate staleness detection.

    line 310
  13. Distinctive:

    All arithmetic uses checked operations.

    line 337
  14. Distinctive:

    Test suite covers currency mismatch, overflow, and rounding edge cases.

    line 338

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Quick Navigation

  • references/decimals.md
  • references/ledger.md

Rust Financial & FinTech Systems Guide

Best practices for building accurate, compliant, and deterministic financial systems in Rust.

Core Rules & Constraints

1. Zero Floating-Point Tolerance

Never use floats (f32, f64) to represent monetary values due to rounding errors and precision loss. Instead, use rust_decimal::Decimal.

use rust_decimal::Decimal;
use rust_decimal_macros::dec;

// Bad: f64 is subject to rounding inaccuracies
let price: f64 = 19.99;

// Good: Exact fixed-point precision
let price: Decimal = dec!(19.99);

2. Money Value Object

Encapsulate currency and amount into a single type-safe domain boundary. Prevent addition or arithmetic operations between mismatched currencies.

use rust_decimal::Decimal;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Currency {
    USD,
    EUR,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Money {
    amount: Decimal,
    currency: Currency,
}

impl Money {
    pub fn new(amount: Decimal, currency: Currency) -> Self {
        Self { amount, currency }
    }

    pub fn add(&self, other: &Money) -> Result<Money, &'static str> {
        if self.currency != other.currency {
            return Err("Currency mismatch");
        }
        let new_amount = self.amount.checked_add(other.amount)
            .ok_or("Arithmetic overflow")?;
        
        Ok(Money::new(new_amount, self.currency.clone()))
    }
}

3. Double-Entry Integrity

Ensure that entries balance exactly: the sum of debits must equal the sum of credits.

pub struct LedgerEntry {
    pub account_id: String,
    pub amount: Decimal, // Positive for debit, negative for credit
}

pub struct Transaction {
    pub id: uuid::Uuid,
    pub entries: Vec<LedgerEntry>,
}

impl Transaction {
    pub fn validate(&self) -> Result<(), &'static str> {
        let sum: Decimal = self.entries.iter().map(|e| e.amount).sum();
        if sum.is_zero() {
            Ok(())
        } else {
            Err("Transaction is unbalanced: total sum of entries must equal zero")
        }
    }
}

4. Immutable Event Sourcing

Ledger entries must be append-only and immutable. Rather than modifying states in-place, record discrete event entries over time.

pub enum AccountEvent {
    Deposited { amount: Decimal, timestamp: chrono::DateTime<chrono::Utc> },
    Withdrawn { amount: Decimal, timestamp: chrono::DateTime<chrono::Utc> },
}

pub struct Account {
    id: String,
    balance: Decimal,
}

impl Account {
    pub fn apply(&mut self, event: &AccountEvent) {
        match event {
            AccountEvent::Deposited { amount, .. } => self.balance += amount,
            AccountEvent::Withdrawn { amount, .. } => self.balance -= amount,
        }
    }
}

5. Safe Math

Always use checked, wrapping, or saturating arithmetic methods rather than standard operators if there's any chance of input overflow.

  • checked_add / checked_sub
  • saturating_add / saturating_sub

Financial Correctness Rules

Money Representation

Prefer integer minor units for ledgers and settlement. Use decimal types for pricing, rates, tax, and calculations where scale must be preserved.

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AmountMinor(i64);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Currency { Usd, Eur, Gbp }

Ledger Invariants

Every posted transaction must be:

  • balanced per currency;
  • immutable after posting;
  • idempotent at external boundaries;
  • traceable to actor/request/source;
  • reversible through compensating entries, not mutation.

Idempotency Pattern

pub struct IdempotencyKey(String);

pub async fn post_payment(req: PaymentRequest, key: IdempotencyKey) -> Result<Receipt, Error> {
    // 1. reserve key in transaction
    // 2. if existing response, return it
    // 3. validate and post ledger entries
    // 4. persist response tied to key
    // 5. commit
    todo_domain_example()
}

Decimal/Rounding Rules

  • Never use floats for money.
  • Round exactly once at the domain-approved boundary.
  • Store the rounding policy with calculated results when audit matters.
  • Keep FX rates with source, timestamp, precision, and effective interval.

FX Conversion Pattern

pub struct FxRate {
    pub base: Currency,
    pub quote: Currency,
    pub rate: Decimal,
    pub source: &'static str,
    pub fetched_at: chrono::DateTime<chrono::Utc>,
}

impl FxRate {
    pub fn convert(&self, amount: Decimal) -> Result<Decimal, &'static str> {
        (amount * self.rate).round_dp(4).checked_sub(Decimal::ZERO)
            .ok_or("FX conversion overflow")
    }
}

pub struct FxProvider {
    rates: HashMap<(Currency, Currency), FxRate>,
}

impl FxProvider {
    pub fn convert(&self, from: Currency, to: Currency, amount: Decimal) -> Result<Money, FxError> {
        if from == to {
            return Ok(Money::new(amount, from));
        }

        let rate = self.rates.get(&(from, to))
            .ok_or(FxError::RateNotFound { from, to })?;

        if rate.fetched_at + chrono::Duration::minutes(5) < chrono::Utc::now() {
            return Err(FxError::StaleRate);
        }

        let converted = amount * rate.rate;
        Ok(Money::new(converted.round_dp(2), to))
    }
}

Fee Calculation

pub enum FeeStructure {
    Flat { amount: Decimal },
    Percentage { rate: Decimal, min: Option<Decimal>, max: Option<Decimal> },
    Tiered { tiers: Vec<(Decimal, Decimal)> },
}

impl FeeStructure {
    pub fn calculate(&self, amount: Decimal) -> Result<Decimal, FeeError> {
        match self {
            FeeStructure::Flat { amount: fee } => Ok(*fee),
            FeeStructure::Percentage { rate, min, max } => {
                let raw = (amount * rate).round_dp(2);
                let bounded = match (min, max) {
                    (Some(lo), _) if raw < *lo => *lo,
                    (_, Some(hi)) if raw > *hi => *hi,
                    _ => raw,
                };
                Ok(bounded)
            }
            FeeStructure::Tiered { tiers } => {
                for (threshold, rate) in tiers.iter().rev() {
                    if amount >= *threshold {
                        return Ok((amount * rate).round_dp(2));
                    }
                }
                Ok(Decimal::ZERO)
            }
        }
    }
}

Interest Calculation

pub fn compound_interest(
    principal: Decimal,
    annual_rate: Decimal,
    periods: u32,
    compounds_per_year: u32,
) -> Result<Decimal, &'static str> {
    let rate_per_period = annual_rate / Decimal::from(compounds_per_year);
    let total_periods = periods * compounds_per_year;
    let mut balance = principal;

    for _ in 0..total_periods {
        let interest = (balance * rate_per_period).round_dp(2);
        balance = balance.checked_add(interest)
            .ok_or("Interest calculation overflow")?;
    }

    Ok(balance - principal) // total interest earned
}

Webhook Signature Verification

use hmac::{Hmac, Mac};
use sha2::Sha256;

type HmacSha256 = Hmac<Sha256>;

pub fn verify_webhook_signature(
    payload: &[u8],
    signature: &str,
    secret: &[u8],
) -> Result<(), WebhookError> {
    let expected = hex::decode(signature)
        .map_err(|_| WebhookError::InvalidSignature)?;

    let mut mac = HmacSha256::new_from_slice(secret)
        .map_err(|_| WebhookError::InvalidSecret)?;
    mac.update(payload);

    mac.verify_slice(&expected)
        .map_err(|_| WebhookError::SignatureMismatch)
}

Testing Strategy

  • Property test ledger balance invariants.
  • Test duplicate idempotency keys.
  • Test reversal entries sum to zero.
  • Test currency mismatch errors.
  • Snapshot audit records for required fields.
  • Integration test database constraints.
  • Test rounding at domain boundaries with known trap values.
  • Test FX rate staleness detection.

Anti-Patterns

  • Updating account balances without immutable entries.
  • Storing money as JSON floats.
  • Retrying non-idempotent payment operations.
  • Combining currencies in one integer total.
  • Hiding rounding in display formatting.
  • Using local time for settlement boundaries.
  • Expiring idempotency keys too soon (before settlement completes).
  • Trusting client-provided FX rates without validation.

Review Prompt

For fintech Rust, verify numeric representation, ledger immutability, idempotency, audit trail completeness, database constraints, and property tests for invariants.

Production Checklist

  • Money never uses binary floating point.
  • Ledger updates are append-only and transactionally balanced.
  • Idempotency keys protect retries at every external boundary.
  • Audit records include actor, request/source, timestamp, and correlation ID.
  • Rounding policy is explicit and tested at domain boundaries.
  • Reversals use compensating entries instead of mutation.
  • FX rates have source, timestamp, and staleness checks.
  • Webhook signatures are verified with timing-safe comparison.
  • All arithmetic uses checked operations.
  • Test suite covers currency mismatch, overflow, and rounding edge cases.

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.