Skip to content

Latest commit

 

History

424 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

idem

Double-entry ledger for institutions settling cross-border payments on stablecoin rails.

CI License: FSL-1.1-ALv2 GitHub Stars


Idem is an API-first, event-sourced double-entry ledger built for fintechs and PSPs that move money across fiat and stablecoin rails simultaneously. It models both FiatEntry and OnChainEntry (EVM, Solana, Tron) in a single unified transaction, enforces debits-equal-credits at the domain layer, and reads on-chain transfers automatically via Alchemy and QuickNode webhooks. It is not a payment processor — it records and reconciles money movement, it does not initiate it.


Demo

Idem demo: agentic execution via MCP

Claude Code driving reconcile_batch, rollback_workflow, and get_agent_audit_log end-to-end through Idem's MCP server.


Why Idem

  • Fiat and on-chain in one double-entry model. A single transaction can contain a PIX debit and a USDC credit on Base. No separate reconciliation step between your fiat ledger and a blockchain indexer.
  • On-chain entries auto-post. Alchemy (EVM) and QuickNode (Solana) webhooks drive chain event ingestion. Tron is polled via Tronscan REST. Transfers to watched addresses create ledger entries automatically, idempotently keyed by chainId:txHash.
  • Scope-based API key auth with PostgreSQL RLS as the backstop. Every request is validated against bcrypt-hashed keys cached in Redis. Even if auth is bypassed, PostgreSQL row-level security prevents cross-tenant data access.
  • Multi-replica safe out of the box. Outbox polling and Tron chain polling are guarded by ShedLock-backed distributed locks. Single-instance deployments run without any coordination overhead.

Quick start

Prerequisites: JDK 21, Maven 3.9+, Docker

# Start PostgreSQL 16 and Redis 7
make up          # or: docker compose up -d

# Build all modules (skips tests for speed)
make build       # or: ./mvnw install -DskipTests

# Seed the dev tenant and print your ADMIN API key
make seed

# Run the application
./mvnw spring-boot:run -pl app -Dspring-boot.run.profiles=dev

The API is available at http://localhost:8081. Interactive OpenAPI docs: http://localhost:8081/swagger-ui.html.

make seed creates a dev tenant (idempotent) and generates a fresh ADMIN-scoped API key printed to stdout. Copy the IDEM_API_KEY=sk_live_... value — it is shown exactly once.

Production: API keys are created via POST /api/v1/api-keys using your ADMIN key. The raw value is returned once at creation and never stored.

Create accounts

Accounts are chart-of-accounts buckets, not wallets — the transaction example below needs two of them. type drives the account's normal balance side (DEBIT for ASSET/EXPENSE, CREDIT for LIABILITY/EQUITY/REVENUE).

curl -X POST http://localhost:8081/api/v1/accounts \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $IDEM_API_KEY" \
  -d '{"name": "Nostro USD", "currency": "USD", "type": "ASSET"}'

curl -X POST http://localhost:8081/api/v1/accounts \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $IDEM_API_KEY" \
  -d '{"name": "Customer Revenue", "currency": "USD", "type": "REVENUE"}'
{ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Nostro USD", "description": null, "currency": "USD", "type": "ASSET", "normalBalance": "DEBIT", "createdAt": "2025-01-01T00:00:00Z" }

name, currency (ISO 4217), and type are required; description is optional. Copy the id from each response into the debit/credit accountId fields below.

Post a transaction

curl -X POST http://localhost:8081/api/v1/transactions \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $IDEM_API_KEY" \
  -H "Idempotency-Key: tx-$(uuidgen)" \
  -d '{
    "lines": [
      {
        "accountId": "<debit-account-uuid>",
        "entryType": "DEBIT",
        "monetaryEntry": {
          "type": "FIAT",
          "amount": 1000.00,
          "currency": "USD",
          "rail": "WIRE"
        }
      },
      {
        "accountId": "<credit-account-uuid>",
        "entryType": "CREDIT",
        "monetaryEntry": {
          "type": "FIAT",
          "amount": 1000.00,
          "currency": "USD",
          "rail": "WIRE"
        }
      }
    ]
  }'

<debit-account-uuid> / <credit-account-uuid> are the id values from the two accounts created above.

{ "transactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }

An on-chain entry looks like:

{
  "accountId": "<account-uuid>",
  "entryType": "CREDIT",
  "monetaryEntry": {
    "type": "ONCHAIN",
    "amount": 1000.00,
    "token": "USDC",
    "chainId": "EVM",
    "txHash": "0xabc...",
    "blockNumber": 19500000,
    "walletAddress": "0xabc...",
    "tokenContract": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
  }
}

Query balance

curl http://localhost:8081/api/v1/accounts/<account-uuid>/balance \
  -H "X-API-Key: sk_test_devkey00000000000000000000"
{
  "accountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "currency": "USD",
  "amount": 1000.00,
  "normalBalance": "DEBIT",
  "computedAt": "2025-01-01T00:00:00Z",
  "onChainBalances": [
    { "token": "USDC", "amount": 2.50 }
  ]
}

Point-in-time balance: append ?asOf=2025-01-01T00:00:00Z.

amount is the fiat balance in the account's declared currency — it only sums FiatEntry lines. On-chain (stablecoin) entries posted to the same account are reported separately in onChainBalances, one entry per token net across all chains, and are never combined with the fiat amount: a token amount and a fiat amount are not fungible units. An account only receiving fiat entries returns onChainBalances: [].

Kotlin SDK

val client = IdemClient(
    baseUrl = "http://localhost:8081",
    apiKey  = "sk_test_devkey00000000000000000000",
)

val tx = client.postTransaction(
    PostTransactionRequest(
        lines = listOf(
            JournalLineRequest(
                accountId = debitAccountId,
                entryType = EntryType.DEBIT,
                monetaryEntry = FiatEntryRequest(
                    amount     = BigDecimal("1000.00"),
                    currency   = FiatCurrency.USD,
                    rail       = PaymentRail.WIRE,
                ),
            ),
            JournalLineRequest(
                accountId = creditAccountId,
                entryType = EntryType.CREDIT,
                monetaryEntry = FiatEntryRequest(
                    amount     = BigDecimal("1000.00"),
                    currency   = FiatCurrency.USD,
                    rail       = PaymentRail.WIRE,
                ),
            ),
        ),
    ),
    idempotencyKey = UUID.randomUUID().toString(),
)

More end-to-end examples (settlement lifecycle, reconciliation, MCP agent workflows) live in idem-examples — a separate, MIT-licensed repo meant for copying straight into your own project.

MCP server (AI agent integration)

Plug Idem directly into Claude Desktop, Claude Code, or any MCP-compatible agent runtime:

{
  "mcpServers": {
    "idem": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://<your-host>/sse"],
      "env": {
        "MCP_HEADER_X_API_KEY": "sk_agent_..."
      }
    }
  }
}

Available tools: postTransaction, getBalance, listEntries, describeAccount, reconcileBatch (require AGENTS_EXECUTE scope), rollbackWorkflow (requires AGENTS_ROLLBACK scope), and getAgentAuditLog (requires AGENTS_AUDIT_READ scope). Full connection guide: docs/mcp-server.md. Using Claude? Install docs/skills/idem-ledger/SKILL.md for a lean, on-demand-loaded cheat sheet instead.


API reference

Method Path Scope required Description
POST /api/v1/accounts ACCOUNTS_WRITE Create an account
POST /api/v1/transactions TRANSACTIONS_WRITE Post a balanced double-entry transaction
GET /api/v1/accounts/{id}/balance ACCOUNTS_READ Current or point-in-time balance
GET /api/v1/accounts/{id}/entries ACCOUNTS_READ Paginated reverse-chronological entry timeline
GET /api/v1/accounts/{id}/statement ACCOUNTS_READ Statement with opening/closing balances
POST /internal/webhooks/alchemy Alchemy Notify inbound (HMAC-validated)
POST /internal/webhooks/quicknode QuickNode Streams inbound (HMAC-validated)

Full OpenAPI spec available at /v3/api-docs when the app is running.

Available scopes: TRANSACTIONS_READ, TRANSACTIONS_WRITE, ACCOUNTS_READ, ACCOUNTS_WRITE, AGENTS_EXECUTE, AGENTS_AUDIT_READ, RECONCILIATION_READ, RECONCILIATION_WRITE, COMPLIANCE_EXPORT, WEBHOOK_MANAGE, ADMIN


Architecture

Idem is a modular monolith (Spring Modulith). Module boundaries are enforced at compile time — violations fail the build.

app ──┬── api ──────────────┬── application ── core
      ├── infrastructure ───┘
      └── mcp

sdk-kotlin  (standalone HTTP client, no internal module deps)

Dependency rule: app → {api, infrastructure, mcp} → application → core. The core module has zero framework dependencies — pure Kotlin, compiles without Spring on the classpath.

MonetaryEntry sealed class is the central design decision: a single journal line carries either a FiatEntry (amount, currency, rail, bankReference) or an OnChainEntry (amount, token, chainId, txHash, blockNumber, walletAddress, tokenContract). The double-entry invariant — debits == credits per currency per transaction — is enforced in Transaction.validate() and never bypassed.

All side effects (audit log, webhook outbox) are written in the same @Transactional as the primary operation. No event bus. Webhook delivery runs via a @Scheduled outbox poller with exponential backoff (5s → 30s → 2m → 10m → 1h, max 5 attempts).

New to Idem? Start with the end-to-end walkthrough — docs/architecture-overview.md — which follows a transaction from input through double-entry validation, chain ingestion, reconciliation, webhooks, and queries, with diagrams.

Technical documentation for individual components lives in docs/.


Tech stack

Component Version
Kotlin 1.9.25
JVM 21
Spring Boot 3.5.15
Spring Modulith 1.4.11
PostgreSQL 16
Redis 7
Web3j (EVM chain reader) 4.12.0
ShedLock (distributed scheduling) 6.6.0
Flyway managed by Spring Boot parent
springdoc-openapi 2.8.9

Project status

Idem is under active development. The core ledger engine, API key authentication, chain readers (EVM, Solana, Tron), webhook outbox, reconciliation, Kotlin SDK, PolicyGuard (agentic policy evaluation, backed by a per-tenant/per-agent PolicyRepository), the agent audit trail (HMAC-signed AgentAuditEvent, written before execution), rollback via compensating transactions (saga pattern), and the full seven-tool MCP server (post_transaction, get_balance, list_entries, describe_account, reconcile_batch, rollback_workflow, get_agent_audit_log) are complete. Not yet implemented: Travel Rule (IVMS 101), LGPD export, and Keycloak dashboard login.

Not yet recommended for production use without independent review.

Live roadmap: GitHub Milestones · Open Issues


Contributing

See CONTRIBUTING.md for development setup, module rules, code style, the PR process, and the DCO sign-off requirement.

Quick version:

git commit -s -m "feat: describe the change"   # DCO sign-off, required on every commit
./mvnw verify                                   # full build + tests + JaCoCo coverage gate, required before PR

Tests that touch PostgreSQL or Redis use Testcontainers — Docker must be running.


Telemetry

Idem collects anonymous, non-identifying usage data (a random installation UUID, bucketed tenant/entry counts, JVM version) sent once per week to telemetry.idem.finance. No transaction data, wallet addresses, amounts, or PII are ever collected.

Opt out via application.yaml or environment variable:

idem:
  telemetry:
    enabled: false
IDEM_TELEMETRY_ENABLED=false

Release verification

GPG signature (Maven artifacts)

Published Maven Central artifacts — finance.idem:idem-core and finance.idem:idem-sdk-kotlin — are GPG-signed.

Field Value
Owner Idem Finance <flaubert165@gmail.com>
Key ID 0ABC39374C2B51EC
Fingerprint 3E33 3148 F633 F474 9F6A 4DF3 0ABC 3939 4C2B 51EC
Key server keys.openpgp.org
gpg --keyserver keys.openpgp.org --recv-keys 3E333148F633F4749F6A4DF30ABC39374C2B51EC
gpg --verify idem-core-0.1.0.jar.asc idem-core-0.1.0.jar

Stewardship: the private key is held by Idem Finance and used exclusively by the release.yml GitHub Actions workflow (via the GPG_PRIVATE_KEY/GPG_PASSPHRASE repo secrets) to sign artifacts on tag push. It is not held in plaintext on any individual's machine.

Rotation plan: the key is rotated at minimum every 2 years, or immediately if compromise is suspected. On rotation: a new key pair is generated, the new public key is published to keys.openpgp.org, this table is updated with the new fingerprint, and the old key is revoked on the keyserver. Artifacts signed under a prior key remain verifiable against that key's fingerprint in the corresponding release's git history.

SHA256 checksums for every release's signed jars and native binary are also attached to the corresponding GitHub Release as checksums.txt.

Cosign signature (container images)

Docker images are signed with Sigstore Cosign via keyless signing (GitHub Actions OIDC), published to both GHCR and Docker Hub:

  • ghcr.io/idem-finance/idem — JVM image only
  • id3mfin4nc3/idem — JVM (latest, {version}) and GraalVM native (native, native-{version}) variants
cosign verify \
  --certificate-identity-regexp="https://github.com/idem-finance/idem/.github/workflows/release.yml@refs/tags/" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  id3mfin4nc3/idem:v0.1.0

License

FSL-1.1-Apache-2.0 — free to use, modify, and self-host for any purpose that does not compete with Idem as a managed service. Converts to Apache 2.0 automatically two years after each release. Full license text: fsl.software.

If you build a product on Idem and make it available to others, include an acknowledgement in your documentation: "Powered by Idem".


Links

About

The ledger layer the stablecoin payment stack was missing. Double-entry, event-sourced, agentic-first. Fiat + on-chain. Built for cross-border.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages