Skip to content

Joob Pool Configuration (Contract-Based)

🚧 In Development

v3.0 dimension model applied. Updated mapping below. v2.x payload (with pool_type, lp_issuance_model, collateral_type enum) is deprecated.

Pool creation parameters extracted from SSA contracts (EV3: FJOS/ShardLab, EV4: Henon/Hashed) and Investment Memo, mapped to v3.0 dimensions.

🚧 Automated DPD→OJK NAV writedown is deferred (2026-07-01)

The v3-13 automated pipeline that would ingest per-loan DPD and apply the OJK write-off schedule into NAV is not built — blocked on Joob per-loan data (the guest API exposes only aggregate DPD buckets — 30/60/90 on the fund route, 30/60 plus /risk/nonperforming on the master route — and no per-loan principal). Today report.scheduler.fetch records DPD/RNI/NPL as observations for display only; loan_writeoffs has no writer; and NAV writedowns are done manually via POST /nav-changes. See Writedown & NAV → Automated DPD Writedown.

Separately, the display/ingest bucket model is being generalized from fixed 30/60/90 to jurisdiction-defined dynamic buckets (pool_dpd_buckets child table) so non-Joob regimes (e.g. OJK 90/120/180) can be received — see v3-59 (🚧 to build).

Use this when creating the Joob Pool

These values map directly to pools table columns under the v3.0 dimension model. Two scenarios documented:

  1. Joob New Investors Pool — new investors deposit USDC, Aset mints LP, the partner remainder routed to Joob's fund_wallet.
  2. Joob Legacy Holdings (mirrored custody) — existing ShardLab/Hashed RPS positions shown in the platform but not investable. custody_mode = 'MIRROR' (0199). ⚠️ This scenario said is_display_only = true until 0203; that column is now is_showcase and carries only the v3-40 marketing tier, which would hide the Fund Data tab this pool exists to show.

Guest API surfaces — fund vs master (doc "Master Only", integrated 2026-08-19)

The guest API serves the same four concepts under two route families, and a pool picks one via pools.external_entity_type (FUND default | MASTER, migration 0194). Every mapping today is FUND; nothing switches by itself.

FUND (/api/guest/fund/{id})MASTER (/api/guest/master/{id})
Discoverywhoami.fundIdsAuthorizedwhoami.masterIdsAuthorized — a master appears only while it holds one of your funds as an active sub-fund
/summaryfund + snapshot + fxRateadds master (compositionVersion, subfundCount), position (this LP's ownershipShare, grossReturn, converted), and fxRate.mixed/lpCurrency/fundCurrency
/history rowsource (chain|ledger)trigger (daily|attach|detach|manual|backfill) + compositionVersion, merged to one row per UTC day
/risk/dpd/{b}30 · 60 · 9030 · 60 only — 90 answers 400 (dpd-exclude-90)
90+ populationbucket 90/risk/nonperformingpendingWriteOff (on book) + writtenOff (charged off)

What the master route reports is not this pool's fund. Live: master 2 consolidates 2 sub-funds, so its DPD-30 exposure is 719,308,325 EFIDR against fund 1's 1,075,925, and master.totalSubscribed is 17,525,000,000 against the fund's 8,950,000,000. position.ownershipShare (0.2386 live) is the LP's share of master AUM — a fraction, never a percentage. Switching a pool to MASTER therefore changes what its reported value covers, including sub-funds this pool has no direct grant to. It is a deliberate decision, not a migration default.

Contract details that bite:

  • Every response is HTTP 200, success or failure; the outcome is the body's code. Branching on the transport status reads a 401/403/400/503 as a success carrying no data.
  • snapshot, startDate, endDate, snapshotDate, and (master) fundValue/totalRni are nullable — null is "not recorded", never zero. Live master history returns 20 null-value days out of 225.
  • /history is capped at 366 days per call and rejects a from before the entity's start; to cannot be in the future (UTC). All three answer 400 rather than clamping.
  • Master DPD/nonperforming are keyed by month (YYYY-MM), not by day.
  • Amounts are already scaled to human units (decimal strings), not wei — no extra 1e18 division.
  • No webhooks (polling only), no client-side caching, and a 503 kill switch exists per route (masterSummaryEnabled / masterHistoryEnabled / masterDpdEnabled) with no Retry-After — hence exponential backoff.

Scenario 1: Joob New Investors Pool (v3.0 active pool)

DimensionValueSourceNotes
name"Joob IDN Private Credit 1"Investment Memo
chain_id1 (Ethereum mainnet)Joob/HenonUSDC mainnet via OneSafe
fund_walletJoob's OneSafe addressJoob opspartner remainder of deposits routed here
is_showcasefalseInvestable pool
partner_idJoob UUIDplatform config
NAV processingAset oracle Lambdav3-15Aset always adjudicates NAV (no nav_data_source dimension). Automated DPD→OJK writedown (v3-13) is deferred/unbuilt — see note below; DPD is cached for display only and NAV is set manually via POST /nav-changes.
TrancheStandalone (no tranche_group_id)v3-14Single pool, no Senior/Junior. For tranched products, use separate pools with tranche_group_id.
maturity_modelFIXED_TERMSSA Schedule 212-month term
lockup_days90SSA Schedule 2 §4.23-month lockup
maturity_days365SSA Schedule 2 §4.112 months
target_size500000SSA EV3 + EV4 confirmedUSD 250K (ShardLab) + USD 250K (Hashed) = 500K
target_size_maxnullNo hard cap yet
min_investment100000Investment MemoUSD 100K
apy_rate15SSA §2.3 (13–17% range)Midpoint
apy_disclosure"Target 13–17% APY; actual varies with asset utilization, product mix, and NPL"Investment Memo
reserve_bps1000v3.0 defaultRedemption liquidity reserve held in the Pool contract (1000 bps = 10%). Not a first-loss buffer — R8. First-loss for this standalone pool is the manager equity buffer (R6), whose rate is still pending contract confirmation
penalty_typeYIELD_BASEDSSA §4.250% dividend forfeiture
penalty_rate_bps5000SSA §4.250% of accrued yield (5000 bps)
yield_frequency"QUARTERLY"SSA §1.2
yield_trigger(dropped v3-20)SSA §1.1–1.2Column removed — yield is manual-claim by default; FJO declares, Aset triggers depositYield after
collateral_description"100% collateralized by Indonesian loan receivables (BCA Wholesale Loans, EWA, etc.)"Investment MemoFree text in v3.0
collateral_ratio100Investment Memo100% backed
kyc_level_requiredKYCplatform defaultIndividual retail OK
jurisdiction_whitelist[]All jurisdictions allowed
accepted_currencies{USDC}JoobUSDC on Ethereum mainnet
redemption_gating_bpsnullNo per-epoch cap
is_pausedfalse
is_emergency_frozenfalse
npl_threshold_days90Joob call 2026-07-24Loans ≥90 DPD are non-performing but stay on book (feed npl_ratio); write-off is separate & later (below)
write_off_policy"Write off at ~6 months (≈180 DPD)"Joob call 2026-07-24Loan removed from book → realized loss → cumulative_loss (Joob's "written-off / in recovery" line). Separate from the 90-DPD NPL mark. See Fund value basis
lifecycle_statusDRAFTUPCOMINGACTIVEschedulerAfter deployment

Yield calculation

Joob's total_rni is gross of fees. Aset Lambda must compute net for Pool.settleYield():

net_yield = total_rni
          − admin_fee (1% / year, quarterly)
          − performance_fee (20% of return above 15% hurdle, at maturity)

Performance fee accrues at maturity only; quarterly distribution uses gross_rni − admin_fee only.

Fund value basis & loss recognition — Joob call 2026-07-24

  • current_fund_value = cash-received basis, and Aset mirrors it as-is. Joob's reported fund value counts only interest actually received, not approved-but-uncollected (accrued / 미수) interest. Because the approved→received lag is only ~1–2 days (≈ one day of interest, ~0.04% at 15% APY — below NAV display precision), Aset ingests the cash fund_value directly instead of reconstructing NAV as principal + cash + accrued − loss. The accrued_income request is dropped; its snapshot column (migration 0068) stays dormant — revisit only if a material (e.g. quarterly) accrual mismatch appears. Joob is moving fund value (like DPD) to an EOD daily 09:00 refresh so the ingest lands once per day.
  • NPL at 90, write-off at ~180 DPD (separate). Joob classifies a loan non-performing at 90 DPD — it stays on book (in the DPD-90 bucket, feeding npl_ratio) — and writes it off at ~6 months (≈180 DPD) into realized cumulative_loss (Joob's separate "written-off / in recovery" line). So npl_threshold_days = 90 and write_off_policy ≈ 180 do not collapse: there is a real 90–180 DPD on-book NPL window, and the risk badge's leading NPL signal fires normally.

See Writedown & NAV → NPL vs Cumulative Loss and v3-90.

Penalty type clarification

SSA §4.2 penalty formula: B = 50% of dividends paid up to redemption date. In v3.0 vocabulary this is penalty_type = YIELD_BASED with penalty_rate_bps = 5000 (50%).

If no dividends have been paid at redemption time: NPL losses (up to 5% of subscription) get added to redemption price per SSA proviso. Handle off-chain in the Aset Lambda before calling Pool.approveRedemption().

Scenario 2: Joob Legacy Holdings (display-only)

Pre-existing ShardLab and Hashed RPS positions exist on Kaia (0x3f9c024E948Ae2A6554331563D5197f7E6441403). Shown on the platform for investor visibility but not deposit/redeem capable through Aset.

DimensionValueNotes
custody_mode'MIRROR'No capital path in or out through us; read model identical to any other pool
is_showcasefalseThe v3-40 marketing tier would hide Performance + Fund Data — not this
chain_id8217 (Kaia)Existing RPS token
lp_token_address0x3f9c024E948Ae2A6554331563D5197f7E6441403External RPS contract
fund_walletn/aNo deposits
NAV sourceJoob API (read-only mirror)Recorded, not proposed: the derive step writes nav_history with attested_off_chain = true (0201) and sends no updateNAV
All flow dimensionsn/aNo deposits / redemptions through Aset

Display-only pools show: balance, NAV, accrued yield. No deposit / redeem CTAs.

Pool Create Payload (v3.0)

json
{
  "name": "Joob IDN Private Credit 1",
  "chain_id": 1,
  "fund_wallet": "0x...joob_onesafe_address...",
  "is_showcase": false,
  "partner_id": "<joob-uuid>",
  "maturity_model": "FIXED_TERM",
  "lockup_days": 90,
  "maturity_days": 365,
  "target_size": 500000,
  "min_investment": 100000,
  "apy_rate": 15,
  "apy_disclosure": "Target 13–17% APY; actual varies with asset utilization, product mix, and NPL",
  "reserve_bps": 1000,
  "penalty_type": "YIELD_BASED",
  "penalty_rate_bps": 5000,
  "yield_frequency": "QUARTERLY",
  "collateral_description": "100% collateralized by Indonesian loan receivables",
  "collateral_ratio": 100,
  "kyc_level_required": "KYC",
  "jurisdiction_whitelist": [],
  "accepted_currencies": ["USDC"],
  "lifecycle_status": "DRAFT"
}

Open items

  • target_size = USD 500K (ShardLab 250K + Hashed 250K confirmed). Current admin panel capacity ($534,997.01) reflects EFIDR/USD FX conversion from earlier estimate — should be reconciled to 500K on next sync.
  • Active TVL ($573K in admin) includes 2× $10K test deposits from PM + backend dev (not real investor capital). True investor capital = $500K.
  • start_date / end_date TBD based on actual SSA Completion Date.
  • fund_wallet — confirm Joob's OneSafe Ethereum mainnet address before deployment.
  • partner_id — create partner entity first.

v2.x → v3.0 mapping

v2.xv3.0
pool_type: FUND_POOL(derived from fund_wallet + partner_id)
lp_issuance_model: FUND_ISSUEDRemoved — Aset always mints LP
lp_token_address: 0x3f9c...KaiaOnly for the custody_mode = 'MIRROR' legacy pool
collateral_type: FULLY_COLLATERALIZEDcollateral_description + collateral_ratio: 100
penalty_type: CUSTOMpenalty_type: YIELD_BASED, penalty_rate_bps: 5000
yield_trigger: MANUAL_CLAIMField dropped v3-20 — manual claim is the default mechanic for all v3.0 pools
operating_currency: EFIDR / fx_rate: 16730Off-chain Lambda concern; not stored on pools table