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:
- Joob New Investors Pool — new investors deposit USDC, Aset mints LP, the partner remainder routed to Joob's
fund_wallet. - Joob Legacy Holdings (mirrored custody) — existing ShardLab/Hashed RPS positions shown in the platform but not investable.
custody_mode = 'MIRROR'(0199). ⚠️ This scenario saidis_display_only = trueuntil 0203; that column is nowis_showcaseand 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}) | |
|---|---|---|
| Discovery | whoami.fundIdsAuthorized | whoami.masterIdsAuthorized — a master appears only while it holds one of your funds as an active sub-fund |
/summary | fund + snapshot + fxRate | adds master (compositionVersion, subfundCount), position (this LP's ownershipShare, grossReturn, converted), and fxRate.mixed/lpCurrency/fundCurrency |
/history row | source (chain|ledger) | trigger (daily|attach|detach|manual|backfill) + compositionVersion, merged to one row per UTC day |
/risk/dpd/{b} | 30 · 60 · 90 | 30 · 60 only — 90 answers 400 (dpd-exclude-90) |
| 90+ population | bucket 90 | /risk/nonperforming → pendingWriteOff (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/totalRniare nullable — null is "not recorded", never zero. Live master history returns 20 null-value days out of 225./historyis capped at 366 days per call and rejects afrombefore the entity's start;tocannot 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
503kill switch exists per route (masterSummaryEnabled/masterHistoryEnabled/masterDpdEnabled) with noRetry-After— hence exponential backoff.
Scenario 1: Joob New Investors Pool (v3.0 active pool)
| Dimension | Value | Source | Notes |
|---|---|---|---|
name | "Joob IDN Private Credit 1" | Investment Memo | |
chain_id | 1 (Ethereum mainnet) | Joob/Henon | USDC mainnet via OneSafe |
fund_wallet | Joob's OneSafe address | Joob ops | partner remainder of deposits routed here |
is_showcase | false | — | Investable pool |
partner_id | Joob UUID | platform config | |
| NAV processing | Aset oracle Lambda | v3-15 | Aset 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. |
| Tranche | Standalone (no tranche_group_id) | v3-14 | Single pool, no Senior/Junior. For tranched products, use separate pools with tranche_group_id. |
maturity_model | FIXED_TERM | SSA Schedule 2 | 12-month term |
lockup_days | 90 | SSA Schedule 2 §4.2 | 3-month lockup |
maturity_days | 365 | SSA Schedule 2 §4.1 | 12 months |
target_size | 500000 | SSA EV3 + EV4 confirmed | USD 250K (ShardLab) + USD 250K (Hashed) = 500K |
target_size_max | null | — | No hard cap yet |
min_investment | 100000 | Investment Memo | USD 100K |
apy_rate | 15 | SSA §2.3 (13–17% range) | Midpoint |
apy_disclosure | "Target 13–17% APY; actual varies with asset utilization, product mix, and NPL" | Investment Memo | |
reserve_bps | 1000 | v3.0 default | Redemption 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_type | YIELD_BASED | SSA §4.2 | 50% dividend forfeiture |
penalty_rate_bps | 5000 | SSA §4.2 | 50% of accrued yield (5000 bps) |
yield_frequency | "QUARTERLY" | SSA §1.2 | |
yield_trigger | (dropped v3-20) | SSA §1.1–1.2 | Column 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 Memo | Free text in v3.0 |
collateral_ratio | 100 | Investment Memo | 100% backed |
kyc_level_required | KYC | platform default | Individual retail OK |
jurisdiction_whitelist | [] | — | All jurisdictions allowed |
accepted_currencies | {USDC} | Joob | USDC on Ethereum mainnet |
redemption_gating_bps | null | — | No per-epoch cap |
is_paused | false | — | |
is_emergency_frozen | false | — | |
npl_threshold_days | 90 | Joob call 2026-07-24 | Loans ≥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-24 | Loan 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_status | DRAFT → UPCOMING → ACTIVE | scheduler | After 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 cashfund_valuedirectly instead of reconstructing NAV asprincipal + cash + accrued − loss. Theaccrued_incomerequest 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 realizedcumulative_loss(Joob's separate "written-off / in recovery" line). Sonpl_threshold_days = 90andwrite_off_policy ≈ 180do not collapse: there is a real 90–180 DPD on-book NPL window, and the risk badge's leading NPL signal fires normally.
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.
| Dimension | Value | Notes |
|---|---|---|
custody_mode | 'MIRROR' | No capital path in or out through us; read model identical to any other pool |
is_showcase | false | The v3-40 marketing tier would hide Performance + Fund Data — not this |
chain_id | 8217 (Kaia) | Existing RPS token |
lp_token_address | 0x3f9c024E948Ae2A6554331563D5197f7E6441403 | External RPS contract |
fund_wallet | n/a | No deposits |
| NAV source | Joob 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 dimensions | n/a | No deposits / redemptions through Aset |
Display-only pools show: balance, NAV, accrued yield. No deposit / redeem CTAs.
Pool Create Payload (v3.0)
{
"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_dateTBD 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.x | v3.0 |
|---|---|
pool_type: FUND_POOL | (derived from fund_wallet + partner_id) |
lp_issuance_model: FUND_ISSUED | Removed — Aset always mints LP |
lp_token_address: 0x3f9c...Kaia | Only for the custody_mode = 'MIRROR' legacy pool |
collateral_type: FULLY_COLLATERALIZED | collateral_description + collateral_ratio: 100 |
penalty_type: CUSTOM | penalty_type: YIELD_BASED, penalty_rate_bps: 5000 |
yield_trigger: MANUAL_CLAIM | Field dropped v3-20 — manual claim is the default mechanic for all v3.0 pools |
operating_currency: EFIDR / fx_rate: 16730 | Off-chain Lambda concern; not stored on pools table |