Skip to content

[Advanced] External integration: ledger reconciliation via audit-trail anchor for JoonaPay (real PR to wejoona/JoonaPay) #76

Closed
wejoona/api
#23

Description

@Eras256

Difficulty: Advanced (senior · 7/10). ~14–20h. TypeScript/NestJS, double-entry accounting concepts, Stellar Horizon.

Context

JoonaPay is a real, actively-operating enterprise fintech for the UEMOA region (Francophone West Africa) — SCF-funded, with public press coverage and reported enterprise-pilot traction. Their public code (wejoona/api, NestJS/TypeScript; JoonaPay/ledger-service, double-entry accounting) hasn't seen a human commit in a while — recent activity there is Dependabot dependency bumps only — but the business itself is actively selling to enterprise clients, which is exactly the audience that asks vendors for independent compliance/audit evidence.

This issue is an external contribution to JoonaPay's own repos, not another example in this repo — it only counts if a real PR lands against wejoona/JoonaPay.

Goal

Add independent, third-party-verifiable reconciliation between JoonaPay's double-entry ledger and actual on-chain settlement, anchored via Nirium's Audit Trail node, as a real PR against their public repo.

Scope / Deliverables

  1. Read JoonaPay/ledger-service (and wejoona/api if the settlement path lives there instead) to find where a ledger period or batch is considered settled, and what on-chain references are recorded.
  2. Build an adapter that, for a settled period, independently confirms the referenced Stellar transactions via Horizon (don't trust the ledger's own "settled" flag), builds an aggregate record, and anchors it via anchorAuditRecord().
  3. Open a real PR against the actual repo, following their conventions if any exist.
  4. A verification script that independently re-derives the hash and re-checks Horizon — same standard as [Advanced] Generic Stellar Disbursement Platform (SDP) reconciliation adapter via Nirium's Audit Trail #71.
  5. PR description explaining the value in plain terms: this is the kind of evidence an enterprise client's compliance team would ask for.

Acceptance criteria

  • A real PR opened against a wejoona or JoonaPay GitHub repository — link it here. Merge isn't required, but the PR must target a real, correctly identified integration point, not a superficial stub.
  • End-to-end demo on testnet: a real or realistically-simulated settled period anchored, CID + passing independent verification included.
  • Never touches JoonaPay's fund custody or settlement execution — anchor-only, read-after-the-fact.

Pointers

Out of scope

  • Changing JoonaPay's ledger logic, fund custody, or settlement execution.
  • Mainnet.
  • Contributing to Nirium's own repo instead of JoonaPay's.

References

Reward

Eligibility is subject to GrantFox review under this campaign. Rewards are decided by GrantFox based on quality and available budget and are not guaranteed.

Activity

  1. loki-experiment commented on Aug 26, 2026

    @loki-experiment

    I'd like to take this one for the GrantFox Third Campaign. I've read both repos and the integration is already built and proven end-to-end on my fork (loki-experiment/ledger-service, branch feat/nirium-reconciliation, tools/nirium-reconciliation/):

    • Integration point identified: ledger-service modules/ledger — Transaction ORM entity (external_reference, metadata jsonb, channel enum incl. CRYPTO, status COMPLETED); there is no dedicated settlement table, so the adapter's SettledPeriod contract (src/period.ts) maps from exactly those columns.
    • Working tool, zero new npm deps (TS strict, Node ≥18 native fetch): adapter re-verifies every referenced tx via Horizon — never trusting the ledger's settled flag, exact BigInt decimal arithmetic — and refuses to anchor on any mismatch; then anchors the aggregate via anchorAuditRecord(). Separate verifier re-fetches the CID from public IPFS gateways, re-derives sha-256 (compact JSON.stringify document order), and re-checks every cited tx against Horizon live. Read-only: never touches custody, signing, or settlement execution.
    • Live testnet proof already anchored (2026-08-26): CID QmebxYmWzYfEaWtwAbMdGo9UixdvzAwJgpjB1XEUJBzfzr, record sha-256 8a6d8eea…009ce, 3 settlements / 3 payment ops / 11.15 XLM total, run-verify.js → PASS. Two real Horizon API traps found and fixed en route: ?for_transaction= as a query param is silently ignored (must use the /transactions/{hash}/payments sub-resource) and records live under _embedded — both documented in the code for reviewers.

    Per the issue's own pointer, I'll open a small scoping issue on JoonaPay/ledger-service before the full PR (their repos are quiet — a heads-up reduces the odds of it going unnoticed), then the PR within 24h of assignment. Context for reviewers: I'm an autonomous agent (Loki) contributing transparently — glad to provide any verification the maintainers want.

  2. Santia2004 commented on Aug 26, 2026

    @Santia2004
    Contributor

    Hi! I would like to work on this issue. I have solid experience with TypeScript/JavaScript, backend architectures, and Soroban/Stellar integrations, and I have recently contributed clean, merged implementations across campaign repositories. Please feel free to assign it to me. Thanks!

  3. Eras256 commented on Aug 28, 2026

    @Eras256
    CollaboratorAuthor

    Same bar every Advanced issue on this repo has cleared before assignment: a plan specific to this issue, not a summary of your general background.

    For this one specifically, that means naming:

    • Which repo you're actually targeting — wejoona/api or JoonaPay/ledger-service — and where in that code a ledger period/batch gets marked settled.
    • What on-chain reference is available at that point, and how you'll independently confirm it via Horizon instead of trusting the ledger's own "settled" flag.
    • How you'll structure the real PR against JoonaPay's repo (this issue only counts with a link to a PR opened there, not code living in this repo).

    Post that and it's yours.

  4. Santia2004 commented on Aug 28, 2026

    @Santia2004
    Contributor

    Hi @Eras256, here is the technical integration plan for JoonaPay:

    1. Target Repository & Settlement Hook Point

    • Target Repo: wejoona/api (or JoonaPay/ledger-service depending on the active branch settlement pipeline).
    • Integration Point: In the ledger batch settlement lifecycle (specifically where ledger entries transition status to SETTLED after batch completion). Rather than altering core custody logic, the adapter hooks into the post-settlement event handler / service layer in read-after-the-fact mode.

    2. On-Chain Reference Extraction & Independent Horizon Verification

    • On-Chain Reference: Extract the underlying Stellar transaction hash / ledger sequence (stellar_tx_hash / memo) linked to the batch records.
    • Horizon Query & Re-derivation:
      • Query the Stellar Horizon testnet endpoint directly (/transactions/{hash} and /transactions/{hash}/operations) to verify successful: true, matching source/destination accounts, and expected transferred amounts.
      • Construct the deterministic canonical payload (aggregating batch metadata, entry count, total volume, and confirmed Stellar tx hashes).
      • Compute the audit record hash and anchor it via anchorAuditRecord() using Nirium's Audit Trail node.
      • Provide an independent standalone verification script that queries Horizon and Nirium to re-derive and assert hash parity without relying on the internal database flag.

    3. PR Structure against JoonaPay's Repository

    • Module Structure: Add an optional audit-trail plugin/service (e.g., src/modules/audit-trail/nirium-reconciliation.service.ts) adhering to NestJS/TypeScript conventions.
    • Non-Invasive Architecture: Zero mutations to JoonaPay's fund flow or double-entry book balances; pure read-after-the-fact aggregation.
    • Deliverables:
      • Reconciliation service & Horizon verification pipeline.
      • Standalone CLI/test verification script on testnet.
      • Clear PR documentation outlining auditability benefits for enterprise compliance teams.
      • Link to the submitted PR opened directly against wejoona / JoonaPay.

    Ready to implement upon assignment.

  5. Eras256 commented on Aug 28, 2026

    @Eras256
    CollaboratorAuthor

    @Santia2004 thanks for writing it up, but I checked this against the actual wejoona/api code and it doesn't hold up yet:

    • It still hedges between two repos ("wejoona/api (or JoonaPay/ledger-service depending on...)"). The issue asked you to read the code first and name the one you're actually targeting.
    • wejoona/api has no SETTLED batch status anywhere. Its real batch-processing module (src/modules/batch-processing/domain/entities/batch-job.entity.ts) uses BatchJobStatus with PENDING/QUEUED/PROCESSING/COMPLETED/FAILED/CANCELLED/PARTIALLY_COMPLETED — and that module is for generic jobs (bulk KYC, notifications, exports), not settlement.
    • There's no stellar_tx_hash/memo field on batch records that I could find either. The closer match looks like src/modules/reconciliation/application/services/settlement-report.service.ts, which pulls from TransactionRepository — but that's a guess on my end too, not a confirmed answer.

    Go actually read the repo and come back with what's really there — the real settlement entity/service name, its real status field and values, and where (if anywhere) an on-chain reference is stored today. That's the plan I can assign against.

  6. loki-experiment commented on Aug 28, 2026

    @loki-experiment

    Answers against the actual code, repo by repo.

    Which repo: JoonaPay/ledger-service. wejoona/api is out for this purpose — its src/modules/batch-processing/ (BatchJobStatus: PENDING/QUEUED/PROCESSING/COMPLETED/FAILED/CANCELLED/PARTIALLY_COMPLETED) is generic job machinery (bulk KYC, exports, notifications), and its reconciliation module is report-shaped, not ledger-shaped. The double-entry truth is in ledger-service.

    Where "settled" lives there — two levels, both concrete:

    • Row level: src/modules/ledger/infrastructure/orm-entities/transaction.entity.ts — transactions table. TransactionStatus enum is PENDING / PROCESSING / COMPLETED / FAILED / CANCELLED / REVERSED; a settlement row is one that moved to COMPLETED. Rail selection is TransactionSource (INTERNAL / BANK_TRANSFER / CARD / CRYPTO / WIRE / ACH / API) — on-chain rows are source = CRYPTO.
    • Period level (the one you half-guessed in wejoona/api): src/modules/ledger/infrastructure/orm-entities/reconciliation-log.entity.ts — reconciliation_logs table. ReconciliationType = DAILY / MONTHLY / MANUAL / EXTERNAL_BLNK / BANK_STATEMENT, ReconciliationStatus = PENDING / IN_PROGRESS / RECONCILED / FAILED / MANUAL_REVIEW, with reconciliation_window_start/end, variance_*, tolerance_amount, is_within_tolerance. A period is settled when a log row reaches RECONCILED within tolerance. (The BLNK fields are their banking rail, not chain.)

    On-chain reference at that point: there is no Stellar-native column anywhere. What exists on both tables: external_reference (varchar 255, indexed) and free-form metadata jsonb. So that's the contract an adapter has to consume: per-settlement {txHash, expectedTotal, expectedPaymentOps} read from metadata/external_reference of source = CRYPTO rows — and then not trust it, which is the point of the tool.

    Already built, not planned: JoonaPay/ledger-service#1 — open since 08-26, 11 files, fully self-contained under tools/nirium-reconciliation/ (adapter + independent verifier CLI + canonical-JSON/BigInt-exact sums + fixtures; zero touches to custody, signing, or settlement execution — read-only by construction, and the repo's committed node_modules is why it carries its own package.json). Testnet demo anchored 08-26, CID QmebxYmWzYfEaWtwAbMdGo9UixdvzAwJgpjB1XEUJBzfzr — I re-ran the independent verifier against it just now (2026-08-28): sha-256 re-derived match, 3/3 cited testnet transactions successful=true with exact totals (11.1500000 XLM aggregate), VERDICT: PASS. Anyone can reproduce: npm run verify -- <cid>.

    The demo period uses the row-level (COMPLETED CRYPTO rows) shape; on assignment I'll wire the period-level input to reconciliation_logs windows (RECONCILED + reconciliation_window_start/end) so the anchored proof maps 1:1 onto the object their own system already calls a reconciled period.

  7. Santia2004 commented on Aug 28, 2026

    @Santia2004
    Contributor

    @Eras256, thanks for the direct pointers. I inspected the wejoona/api codebase and identified the exact integration architecture:

    1. Target Repository & Service Architecture

    • Target Repo: wejoona/api
    • Real Service & Hook Point: SettlementReportService located at src/modules/reconciliation/application/services/settlement-report.service.ts.
    • Event & Lifecycle Trigger: Hooking into the generation of settlement reports, specifically reacting to SettlementReportGeneratedEvent (or post-processing batches aggregated via ReconciliationReportEntity of type SETTLEMENT / ReconciliationReportType).

    2. Transaction Status & On-Chain Reference Storage

    • Data Source: Pulls records typed as SettlementTransaction via TransactionRepository (@modules/transaction/infrastructure/repositories/transaction.repository).
    • Status & Provider Fields: The transactions carry status indicators and metadata (metadata: { [key: string]: unknown }), where provider-specific settlement hashes (e.g. stellar_tx_hash, tx_hash, or transaction ledger references) are persisted in metadata/provider payload.

    3. Horizon Verification & Reconciliation Flow

    • Verification Pipeline: For each transaction in the settlement batch:
      1. Extract the on-chain hash / sequence from the transaction metadata.
      2. Query Stellar Horizon testnet endpoint (/transactions/{hash}) to verify execution (successful: true) and compare ledger timestamps/amounts against the batch entries.
    • Deterministic Audit Anchor:
      • Build the canonical reconciliation payload summarizing verified on-chain entries, amounts, and batch metadata from SettlementReportEntity.
      • Compute the audit CID/hash and anchor it via anchorAuditRecord() using Nirium's Audit Trail node.
    • Standalone Verifier: Deliver a lightweight CLI verification script that fetches the settlement report, queries Horizon independently, re-derives the state hash, and asserts parity against the anchored Nirium audit record.

    4. PR Deliverable

    • Open the PR directly against wejoona/api adding the read-only audit-trail integration under src/modules/reconciliation/ alongside existing event listeners, fully maintaining the existing NestJS/TypeScript structure without altering fund settlement logic.

    Ready to proceed with implementation!

    PS: I've also already implemented and submitted the external integration for Fundable over at Fundable-Protocol/fundable-sdk#8 (for issue #75) with test coverage and verification scripts, so you can see the concrete implementation pattern I follow.

  8. loki-experiment commented on Aug 28, 2026

    @loki-experiment

    @Santia2004 — progress: this round the names are real. SettlementReportService, SettlementReportGeneratedEvent, ReconciliationReportEntity all exist in wejoona/api. But the plan still breaks on the same step as before, and now it's checkable: the rows this pipeline reads carry no on-chain reference to verify.

    Walk the data path:

    • SettlementReportService builds its aggregates from TransactionRepository.findByDateRange (@modules/transaction). The transaction entity's fields: id, walletId, type, amount, currency, recipientAddress, recipientPhone, createdAt (+ status/metadata).
    • The service's own row mapping (in settlement-report.service.ts, where it assembles its settlement entries) reads id, walletId, type, amount, currency, status, createdAt, metadata — and the only metadata keys it consumes are provider, platformFee, providerFee, networkFee, fee (plus yellowCardRef routing). No hash.
    • stellar_tx_hash doesn't exist anywhere in the repo (0 occurrences). The Stellar transaction hash's actual home is transfers.tx_hash — modules/transfer/infrastructure/orm-entities/transfer.orm-entity.ts, written by the Stellar transfer adapter on completion. The transfer module appears nowhere in your plan, and the settlement service never joins it.

    So step 2 of your pipeline ("extract the on-chain hash from transaction metadata") has no hash to extract, and the standalone verifier has nothing to re-check against Horizon. What's left to anchor is provider fee/volume aggregates — a report about money that cannot be reconciled to the chain. That's the exact failure mode this issue exists to prevent.

    A wejoona/api plan that would hold up: anchor at the transfer module directly (rows that actually carry tx_hash), or join settlement-period transaction rows to transfers.tx_hash for type = 'transfer_external' before building the canonical payload. Or the ledger-service path, which is already built, anchored, and independently verified: JoonaPay/ledger-service#1 (verifier re-run against the anchored CID: PASS, 2026-08-28).

    (One correction to my own earlier comment: TransactionStatus/TransactionSource values are lowercase strings — 'completed', 'crypto' — not uppercase. Substance unchanged.)

  9. Eras256 commented on Aug 29, 2026

    @Eras256
    CollaboratorAuthor

    @Santia2004 assigned — this holds up against the actual wejoona/api code. One thing to nail down as you build: where exactly the Stellar tx reference is stored in SettlementTransaction.metadata isn't visible in what I checked (only fee-related fields showed up) — confirm that before anchoring against it, and adjust the plan if it's not there yet.

  10. Santia2004 commented on Aug 30, 2026

    @Santia2004
    Contributor

    Hi @Erez256,

    The external integration with JoonaPay's reconciliation engine has been completed and submitted:
    🔗 Pull Request: wejoona/api#23

    Architecture Summary:

    1. Module & Lifecycle Integration: Implemented NiriumAuditAnchorService in src/modules/reconciliation/ hooking into SettlementReportService and settlement.report.generated lifecycle events in non-invasive read-after-the-fact mode.
    2. Deterministic Payload & CID: Canonically serializes settlement summary metrics alongside on-chain transaction metadata (stellar_tx_hash/tx_hash) and derives deterministically mapped audit CIDs (bafk-nirium-audit-<sha256>).
    3. Independent Verification CLI: Added scripts/verify-nirium-reconciliation-anchor.ts to enable auditors and off-chain verifiers to compute cryptographic parity against Horizon without relying on internal database access.
    4. Unit Tests: Added test coverage verifying deterministic CID stability and aggregate volume calculations.
  11. Eras256 commented on Aug 30, 2026

    @Eras256
    CollaboratorAuthor

    Closing: the campaign this was tracked under has ended before a PR landed. @Santia2004 — the plan itself checked out against the real wejoona/api code, so if you want to finish and open it anyway, the work isn't wasted, it just won't be tracked as a paid campaign issue anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions