35 detailed issues spanning Contracts (Rust/Soroban), API (NestJS), Mobile (Expo/React Native), and DevOps.
Each issue is self-contained and ready for a contributor to pick up. Read the linked context files before starting.
Repo: StepFi-app/StepFi-Contracts Labels: contracts, testing, hard Difficulty: hard
The repay_installment() function in contracts/creditline-contract/src/lib.rs lacks isolated unit tests. The only existing coverage is incidental via end-to-end flow tests, leaving error paths (double-pay, out-of-bounds, unauthorized, zero-amount) untested. Without these, regressions in repayment logic can silently break borrower balances.
Repayment correctness is the most safety-critical operation in StepFi — a bug here can either lock learners out of repaying or allow them to pay twice. Sponsors lose trust if installments are mis-accounted, and the reputation contract derives scoring from these calls. This must be airtight before mainnet.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/creditline-contract/src/lib.rs
- contracts/creditline-contract/src/tests.rs
- Add a helper
setup_loan_with_schedule(env, borrower, amount, n_installments)at the top oftests.rsthat initializes the contract, creates a loan, and approves it. - Test
repay_installment_happy_path: pay installment 0, assertInstallment.paid == true, assert outstanding balance decremented by exact amount. - Test
repay_installment_double_pay_rejected: pay installment 0, then call again — assertContractError::InstallmentAlreadyPaidis returned. - Test
repay_installment_out_of_bounds: call with indexinstallments.len()— assertContractError::InvalidInstallmentIndex. - Test
repay_installment_non_borrower_rejected: call from a wallet other than the loan borrower — assert auth panic viaenv.mock_auths()mismatch. - Test
repay_installment_zero_amount_rejected: ensure zero-amount payments are explicitly rejected withContractError::InvalidAmount.
contracts/creditline-contract/src/tests.rscontracts/creditline-contract/src/errors.rs(ifInvalidInstallmentIndex/InstallmentAlreadyPaidnot yet defined)
- 5 new
#[test]functions covering each scenario above -
cargo test -p creditline-contractpasses with all new tests green - Each test uses
Env::default()andmock_all_auths()where appropriate - No existing test is modified or weakened
- Test names follow the
repay_installment_<scenario>convention - Coverage report shows
repay_installment()branch coverage ≥ 90%
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-Contracts Labels: contracts, feature, medium Difficulty: medium
Loans created by create_loan() in contracts/creditline-contract/src/lib.rs are stuck in LoanStatus::Pending because no transition function exists. Without approve_loan(), no learner can ever receive funds — the protocol is effectively read-only end-to-end.
The Pending → Active gate is what separates "applied for credit" from "owes money". Sponsors need to see only Active loans count toward pool utilization, and learners only accrue obligations after admin approval. This unblocks the entire mobile-app borrowing flow.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/creditline-contract/src/lib.rs
- contracts/creditline-contract/src/events.rs
- Add
pub fn approve_loan(env: Env, loan_id: u64) -> Result<(), ContractError>to the contractimpl. - First line:
require_auth()on the admin address loaded from storage (StorageKey::Admin). - Load loan via
storage::get_loan(&env, loan_id); returnContractError::LoanNotFoundif missing. - Verify
loan.status == LoanStatus::Pending; otherwise returnContractError::InvalidLoanState. - Mutate to
LoanStatus::Active, write back withstorage::set_loan(&env, &loan), thenextend_ttl(). - Emit a
LOANAPPROVEDevent viaevents::emit_loan_approved(&env, loan_id, loan.borrower). Add corresponding emit helper inevents.rs. - Add 3 unit tests: happy path, non-admin rejection, non-pending state rejection.
contracts/creditline-contract/src/lib.rscontracts/creditline-contract/src/events.rscontracts/creditline-contract/src/errors.rscontracts/creditline-contract/src/tests.rs
-
approve_loan()is the second public entry point aftercreate_loan()and follows the same code structure - All 3 unit tests pass
-
LOANAPPROVEDevent is documented in events.rs with a comment - No silent state transitions — every error path returns a typed
ContractError - Loan TTL is extended after status mutation
- Admin auth is the FIRST line of the function
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-Contracts Labels: contracts, feature, hard Difficulty: hard
The RepaymentInstallment struct in contracts/creditline-contract/src/types.rs carries no due_date, and repay_installment() does not penalize late payments. Borrowers can effectively pay months late with zero consequence, which breaks the reputation score's underlying assumption that on-time payment matters.
Late fees are the protocol's only economic enforcement mechanism short of liquidation. Without them, sponsors price all risk into the base APR, hurting on-time learners. The fee must flow back into the liquidity pool so sponsors are made whole.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/creditline-contract/src/types.rs
- contracts/creditline-contract/src/lib.rs
- Add
pub due_date: u64toRepaymentInstallmentintypes.rs. Populate at loan creation:due_date = approval_ts + (n+1) * installment_period_secs. - Add
pub late_fee_bps: u32toProtocolParametersintypes.rs, default 500 bps (5%). - In
repay_installment(), computenow = env.ledger().timestamp(). Ifnow > installment.due_date, computelate_fee = installment.amount * late_fee_bps / 10_000and require the caller to transferinstallment.amount + late_fee. - Route the late fee to the liquidity pool via cross-contract call, not the borrower's loan balance.
- Emit a
LATEFEEPAIDevent with(loan_id, installment_index, fee_amount). - Add 4 unit tests: on-time path (no fee), 1-day-late, exact-due-date boundary, custom-bps test.
contracts/creditline-contract/src/types.rscontracts/creditline-contract/src/lib.rscontracts/creditline-contract/src/storage.rscontracts/creditline-contract/src/events.rscontracts/creditline-contract/src/tests.rs
-
due_dateis set on every installment at loan creation -
late_fee_bpsis configurable and persisted inProtocolParameters - Late fees route to the liquidity pool, not the loan balance
- On-time payments incur zero fee (exact
now == due_dateis on-time) - 4 new tests pass
- Migration note added: existing loans without
due_dateare gracefully handled (assume 0 = on-time)
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-Contracts Labels: contracts, feature, hard Difficulty: hard
There is no on-chain mechanism for verified mentors to vouch for learners. The reputation contract currently has no off-board signal for new wallets with zero loan history, so first-time learners face artificially high interest rates with no path to bootstrap trust.
Mentor vouching is StepFi's cold-start fix. A verified educator or community lead can stake their own reputation behind a learner, unlocking lower-rate credit for previously unscored users. This is the protocol's social capital layer.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/reputation-contract/src/lib.rs
- Create a new crate at
contracts/vouching-contract/mirroring the layout ofcreditline-contract(Cargo.toml,src/{lib,types,storage,events,errors,tests}.rs). - Storage:
VerifiedMentors: Map<Address, bool>,Vouches: Map<(Address mentor, Address learner), VouchRecord { ts, boost_amount, active }>. pub fn vouch(env, mentor: Address, learner: Address):require_auth(&mentor), checkVerifiedMentors[mentor] == true, writeVouchRecord { ts: now, boost: protocol.vouch_boost, active: true },extend_ttl(), cross-call reputation contractadd_boost(learner, boost_amount).pub fn revoke_vouch(env, mentor, learner):require_auth(&mentor), setactive=false, cross-call reputationremove_boost(learner, boost_amount).pub fn get_vouches(env, learner) -> Vec<VouchRecord>: read-only.pub fn set_mentor(env, mentor, verified: bool): admin-only.- Add events:
MENTORVOUCHED,VOUCHREVOKED,MENTORVERIFIED. - Write tests for all 4 mutating functions including duplicate-vouch rejection and unverified-mentor rejection.
contracts/vouching-contract/Cargo.tomlcontracts/vouching-contract/src/lib.rscontracts/vouching-contract/src/types.rscontracts/vouching-contract/src/storage.rscontracts/vouching-contract/src/events.rscontracts/vouching-contract/src/errors.rscontracts/vouching-contract/src/tests.rs- Workspace
Cargo.toml(add member) contracts/reputation-contract/src/lib.rs(exposeadd_boost/remove_boost)
- New crate builds cleanly with
cargo build -p vouching-contract - All 4 mutating functions begin with
require_auth() - All persistent writes followed by
extend_ttl() - Cross-contract calls to reputation are tested with a mock
- Minimum 8 unit tests
- Events are emitted on every state change
- Workspace
Cargo.tomlincludes the new crate
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-Contracts Labels: contracts, infra, medium Difficulty: medium
None of the 5 contracts (creditline, liquidity-pool, reputation, vendor-registry, token-mock) currently expose an upgrade() entry point. Once deployed to testnet, any bug fix requires a full redeploy and address change — breaking every client that references the old address.
Soroban supports in-place WASM upgrades preserving contract address and storage. Without this, every contract bugfix invalidates the API's contract IDs and the mobile app's hardcoded references. Upgradeability is the difference between a one-shot deploy and a maintainable protocol.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/*/src/lib.rs
- Define a shared snippet (in each contract's
lib.rs):pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), ContractError> { let admin: Address = env.storage().instance().get(&StorageKey::Admin).ok_or(ContractError::NotInitialized)?; admin.require_auth(); env.deployer().update_current_contract_wasm(new_wasm_hash); Ok(()) }
- Add to all 5 contracts: creditline, liquidity-pool, reputation, vendor-registry, token-mock.
- Emit a
CONTRACTUPGRADEDevent with(old_version, new_version, ts)— add aget_version() -> u32function returning current version, bumped by upgrade. - Persist
StorageKey::Versionin each contract's instance storage, default 1. - Write one unit test per contract: non-admin call rejected, admin call succeeds (mock the wasm hash).
- Document the upgrade flow in
contracts/README.md.
contracts/creditline-contract/src/lib.rscontracts/liquidity-pool-contract/src/lib.rscontracts/reputation-contract/src/lib.rscontracts/vendor-registry-contract/src/lib.rscontracts/token-mock-contract/src/lib.rs- Each contract's
events.rs,storage.rs,errors.rs contracts/README.md
- All 5 contracts expose
upgrade(env, new_wasm_hash) - All 5 contracts expose
get_version()returning au32 - Admin auth check is the FIRST executable line of
upgrade() - 5 new tests pass (one per contract)
-
CONTRACTUPGRADEDevent emitted on successful upgrade - Upgrade flow documented in README
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-Contracts Labels: contracts, refactor, medium Difficulty: medium
Across all contracts, storage.rs files use .expect("loan not found") and similar patterns when reading persistent storage. These produce opaque VM traps with no error code, making the API unable to distinguish "loan does not exist" from "contract panicked unexpectedly".
Every contract error must be a typed ContractError so the API can translate it to a proper HTTP status code. Right now, any missing storage read returns a 500 to the mobile app instead of a 404, which makes the user-facing error stories incoherent.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/*/src/storage.rs
- contracts/*/src/errors.rs
- Audit every
.expect(...)and.unwrap()call across all 5storage.rsfiles. Userg -n "expect\(|unwrap\(\)" contracts/*/src/storage.rs. - For each, change the function signature to return
Result<T, ContractError>. - Replace
.expect("loan not found")with.ok_or(ContractError::LoanNotFound)?. - Update callers in
lib.rsto propagate with?. - Add missing error variants where needed (e.g.
PoolNotInitialized,VendorNotFound). - Add a regression test per contract: call a getter before
initialize()and assertNotInitializedis returned (not a panic).
contracts/creditline-contract/src/storage.rs,errors.rs,lib.rs,tests.rscontracts/liquidity-pool-contract/src/storage.rs,errors.rs,lib.rs,tests.rscontracts/reputation-contract/src/storage.rs,errors.rs,lib.rs,tests.rscontracts/vendor-registry-contract/src/storage.rs,errors.rs,lib.rs,tests.rscontracts/token-mock-contract/src/storage.rs,errors.rs,lib.rs,tests.rs
- Zero
.expect()or.unwrap()calls remain in anystorage.rs - All getter functions return
Result<T, ContractError> - 5 new regression tests prove typed errors are returned instead of panics
- All 93 existing tests still pass
- Cargo clippy returns no new warnings
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-Contracts Labels: contracts, safety, medium Difficulty: medium
Public functions that read from persistent storage will panic with a VM trap if invoked before initialize() has been called. There is no is_initialized() guard, so any deploy-without-init race condition produces opaque failures rather than a clear NotInitialized error.
The API frontend retries on transient errors. A panic on uninitialized state looks transient — but it isn't. Wrapping every public read with a typed init check lets the API surface a clear "contract not initialized" message and stop retrying.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/*/src/lib.rs
- Add
pub fn is_initialized(env: &Env) -> boolhelper in each contract'slib.rsthat checksenv.storage().instance().has(&StorageKey::Admin). - At the top of every public function (mutating or read-only) that touches persistent storage, add:
if !Self::is_initialized(&env) { return Err(ContractError::NotInitialized); }. - Exclude
initialize()itself and any pure helper that doesn't read storage. - Add
ContractError::NotInitialized = 1variant inerrors.rsif missing. - Write one regression test per contract that calls a getter before initialize and asserts
NotInitialized. - Document the convention in
code-standards.md.
- All 5
contracts/*/src/lib.rs - All 5
contracts/*/src/errors.rs - All 5
contracts/*/src/tests.rs context/code-standards.md
- Every public function that reads storage starts with an
is_initializedcheck -
ContractError::NotInitializedexists in all 5 error enums - 5 regression tests pass
- No existing tests fail
- code-standards.md documents the convention
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-Contracts Labels: contracts, safety, medium Difficulty: medium
Only creditline-contract consistently calls extend_ttl() after persistent storage writes. The liquidity-pool and vendor-registry contracts write to persistent storage but never extend TTL, meaning their data will expire and the contract will appear to "lose" deposits or registered vendors.
Soroban persistent entries expire if not refreshed. A sponsor's pool balance disappearing because of TTL would be catastrophic — funds become unrecoverable. This is a latent bug that only surfaces after weeks of inactivity on testnet but would be irrecoverable on mainnet.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- contracts/liquidity-pool-contract/src/lib.rs
- contracts/vendor-registry-contract/src/lib.rs
- Audit liquidity-pool: locate every
env.storage().persistent().set(...). After each, callenv.storage().persistent().extend_ttl(&key, MIN_TTL, MAX_TTL)whereMIN_TTL = 100_000,MAX_TTL = 1_000_000(define inconstants.rs). - Same for vendor-registry: every
set(...)followed byextend_ttl(). - Centralize the constants in a shared
contracts/common/src/ttl.rsif not yet present; otherwise define locally per contract. - Add a
storage::write_and_extend(env, key, value)helper to enforce the pattern at the source. - Write a test per contract: deposit to pool, advance ledger by
MIN_TTL - 1, read — assert value still present. - Update
code-standards.mdto make the helper mandatory.
contracts/liquidity-pool-contract/src/storage.rscontracts/liquidity-pool-contract/src/lib.rscontracts/vendor-registry-contract/src/storage.rscontracts/vendor-registry-contract/src/lib.rscontracts/liquidity-pool-contract/src/tests.rscontracts/vendor-registry-contract/src/tests.rscontext/code-standards.md
- Every persistent
set()in both contracts is followed byextend_ttl() - TTL constants are named and centralized
- 2 new tests prove data survives near-expiry
- Helper function exists and is used by all writes
- code-standards.md mandates the pattern
- cargo build passes with zero errors
- cargo test — all 93 existing tests still pass
- require_auth() is FIRST line of every mutating function
- extend_ttl() called after EVERY persistent storage write
- New unit tests written for every new function
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, blockchain, hard Difficulty: hard
src/modules/liquidity/liquidity.service.ts currently returns hardcoded placeholder XDR strings for deposit() and withdraw() operations. The real LiquidityContractClient exists in src/blockchain/contracts/ but is not injected, so the mobile app's sponsor flow cannot actually deposit on-chain.
This is the sponsor-side blocker for end-to-end testing. Without real XDR generation, sponsors can sign but the resulting transaction is unsubmittable garbage. Every other sponsor feature (portfolio, APY, withdrawals) depends on this being real.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/liquidity/liquidity.service.ts
- src/blockchain/contracts/liquidity-contract.client.ts
- Inject
LiquidityContractClientintoLiquidityServiceconstructor. - Read
LIQUIDITY_POOL_CONTRACT_IDfromConfigServiceand pass to the client. - Replace placeholder in
buildDepositXdr(walletAddress, amount): callclient.buildUnsignedXdr('deposit', [walletAddress, amount])and return the base64 XDR string. - Same for
buildWithdrawXdr(walletAddress, shares): callclient.buildUnsignedXdr('withdraw', [walletAddress, shares]). - Add error handling: contract simulation errors should return a 400 with the typed Soroban error code mapped to a user-facing message.
- Update e2e test
test/e2e/liquidity.e2e-spec.tsto assert the returned XDR is a valid base64-encoded Stellar transaction (useTransactionBuilder.fromXDR()).
src/modules/liquidity/liquidity.service.tssrc/modules/liquidity/liquidity.module.tssrc/blockchain/contracts/liquidity-contract.client.tssrc/blockchain/blockchain.module.tstest/e2e/liquidity.e2e-spec.ts.env.example
- No placeholder XDR strings remain in the liquidity service
- Returned XDR parses successfully via
TransactionBuilder.fromXDR -
LIQUIDITY_POOL_CONTRACT_IDis read viaConfigService, notprocess.env - Contract simulation errors map to typed HTTP errors
- E2E test passes against a running Soroban testnet RPC
- Unit tests for the service mock the client and assert the right method is invoked
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, feature, medium Difficulty: medium
The VendorsModule has a repository and service skeleton but exposes no creation endpoint. The mobile app's loan wizard relies on a vendor list, but admins currently have no API path to register a new vendor — vendor data has to be inserted directly via SQL.
Vendors are the only entities a learner can transact with — they're the "merchants" of StepFi. Onboarding the first 10–20 schools and bootcamps requires admins to be able to add vendors through Postman or the admin tool, not psql.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/vendors/
- Create
dto/create-vendor.dto.tswith class-validator decorators:name: string @IsString @MinLength(2),type: VendorType (enum: school|bootcamp|certification|tool),country: string @Length(2,2),website: string @IsUrl,description: string @IsOptional @MaxLength(500). - Add
POST /api/v1/vendorstovendors.controller.ts, guarded by@UseGuards(JwtAuthGuard, AdminGuard), decorated with@ApiTags('vendors'),@ApiOperation,@ApiResponse(201/400/401/403). - In
vendors.service.tsaddcreateVendor(dto: CreateVendorDto): Promise<Vendor>— calls Supabase repo, returns the inserted row. - Add
GET /api/v1/vendors(paginated, public) andGET /api/v1/vendors/:id(public, 404 if missing). - Write a migration
supabase/migrations/NNN_create_vendors_table.sqlif the vendors table does not yet exist. - Add e2e test covering: admin can create, non-admin gets 403, invalid DTO gets 400, GET returns paginated list.
src/modules/vendors/vendors.controller.tssrc/modules/vendors/vendors.service.tssrc/modules/vendors/vendors.repository.tssrc/modules/vendors/dto/create-vendor.dto.tssrc/modules/vendors/dto/vendor.dto.tssupabase/migrations/NNN_create_vendors_table.sql(if needed)test/e2e/vendors.e2e-spec.ts
- POST /vendors is admin-guarded and returns 403 for non-admins
- DTO validation rejects malformed payloads with 400
- GET /vendors returns paginated results with
total,page,limit - GET /vendors/:id returns 404 for unknown IDs
- All endpoints have Swagger decorators
- E2E tests cover all 4 scenarios
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, feature, medium Difficulty: medium
When a wallet hits POST /auth/verify for the first time, no row is inserted into learner_profiles. The mobile app then calls GET /learners/me and receives 404, producing a null-pointer error on the home screen rendering.
Every authenticated wallet should map to exactly one learner profile. The current race means the very first action a new user takes after signing in is to immediately hit an error state. Auto-create-on-first-login is the standard pattern.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/auth/auth.service.ts
- src/modules/learners/learners.service.ts
- In
auth.service.ts#verify(), after successful signature validation and before issuing JWT, calllearnersService.ensureProfile(walletAddress). - Add
LearnersService#ensureProfile(wallet: string): Promise<LearnerProfile>: query by wallet; if found, return; else insert with defaults{ wallet_address: wallet, display_name: null, role: 'learner', created_at: now }. - The operation must be idempotent — two concurrent verifies for the same wallet must produce exactly one row (use unique constraint on
wallet_address). - Inject
LearnersServiceintoAuthService(handle circular dependency withforwardRef). - Add migration adding
UNIQUE(wallet_address)tolearner_profilesif not present. - Add unit test: first verify creates profile; second verify returns existing profile.
src/modules/auth/auth.service.tssrc/modules/auth/auth.module.tssrc/modules/learners/learners.service.tssrc/modules/learners/learners.module.tssupabase/migrations/NNN_learner_profiles_unique_wallet.sql(if missing)test/unit/auth.service.spec.ts
- First-time verify creates a learner_profiles row
- Second verify for the same wallet does NOT create a duplicate
- GET /learners/me succeeds immediately after first verify
- DB has a unique constraint on
wallet_address - Unit tests cover both first-time and repeat paths
- Concurrent verifies for the same wallet produce one row
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, testing, good first issue Difficulty: good first issue
src/modules/auth/auth.service.ts has zero unit tests. All authentication paths (nonce generation, signature verification, token rotation) are untested. Any regression here logs every user out and is undetectable until production.
Auth is the most security-sensitive module. A silent bug in token rotation could leak session lifetimes or allow refresh-token replay attacks. Unit tests are the cheapest line of defense.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/auth/auth.service.ts
- Create
test/unit/auth.service.spec.tswith aTest.createTestingModulesetup mockingSupabaseService,JwtService, andConfigService. - Test
getNonce(wallet): returns a 32-char nonce, inserts a row innonceswithwallet,nonce,expires_at. - Test
verify(wallet, signature): valid signature path returns{ accessToken, refreshToken }; invalid signature throwsUnauthorizedException. - Test
refresh(refreshToken): valid token returns new pair; refresh token is rotated (old one marked revoked). - Test nonce dedup: two
getNonce(wallet)calls within expiry window return the same nonce. - Achieve ≥ 90% branch coverage on
auth.service.ts.
test/unit/auth.service.spec.ts
- At least 5 distinct
describeblocks covering each method - All Supabase calls are mocked — no real DB hit
- Test runs in under 2 seconds
-
npm test -- auth.servicepasses - Branch coverage ≥ 90% on the service
- No
anytypes in the test file
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, testing, good first issue Difficulty: good first issue
src/modules/learners/learners.service.ts has no unit tests. Profile read/write paths are unverified, so a typo in the Supabase select chain would silently return empty profiles.
Profile data drives the home screen and reputation display. A broken getProfile makes the entire learner experience appear empty. Cheap unit tests prevent silent breakage.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/learners/learners.service.ts
- Create
test/unit/learners.service.spec.tsusingTest.createTestingModulewith mocked Supabase client. - Test
getProfile(wallet): returns a profile for a valid wallet; returns 404 (NotFoundException) for an unknown wallet. - Test
updateProfile(wallet, dto): persists changes and returns the updated row; rejects unknown wallet with 404. - Test
ensureProfile(wallet): returns existing profile if present; creates and returns new if missing. - Mock the supabase chain via
from().select().eq().single()chained returns. - Achieve ≥ 85% branch coverage.
test/unit/learners.service.spec.ts
- All 3 service methods tested in distinct describes
- Both success and failure paths covered
- No real DB calls
-
npm test -- learners.servicepasses - Coverage ≥ 85% branches
- No
anytypes
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, testing, good first issue Difficulty: good first issue
src/modules/vendors/vendors.service.ts has no unit tests. Listing, filtering, and creation logic is unverified, so pagination off-by-one bugs or filter mismatches go undetected.
The vendor list powers the loan application wizard — if it returns wrong results, learners borrow against the wrong merchant. Tests anchor expected behavior before the controller in Issue 10 is exercised.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/vendors/vendors.service.ts
- Create
test/unit/vendors.service.spec.tswithTest.createTestingModuleand mocked repo. - Test
listVendors({ page, limit, type? }): returns paginated{ data, total, page, limit }; respects page+limit; filters bytypewhen provided. - Test
getVendor(id): returns the vendor; throwsNotFoundExceptionfor unknown id. - Test
createVendor(dto): persists and returns inserted row with generated id andcreated_at. - Edge case:
listVendorswithpage=0orlimit=0falls back to defaults. - Achieve ≥ 85% branch coverage.
test/unit/vendors.service.spec.ts
- All 3 service methods covered
- Pagination math is correct (off-by-one verified)
- Type filter is exercised with valid + invalid values
-
npm test -- vendors.servicepasses - Coverage ≥ 85%
- No
anytypes
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, testing, good first issue Difficulty: good first issue
The VouchingService (mirror of the new on-chain vouching contract) has no unit tests. Vouch creation, retrieval, duplicate rejection, and expiry are all unverified at the service layer.
Vouching is a trust-bootstrap mechanism — a silent bug here could let one mentor inflate a learner's reputation unboundedly. Tests anchor the invariants before this goes live.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/vouching/vouching.service.ts
- Create
test/unit/vouching.service.spec.ts. - Test
createVouch(mentor, learner): inserts avouchesrow with statusactive; rejects withConflictExceptionif an active vouch already exists for the same pair. - Test
getVouches(learner): returns the list of active vouches for the learner; returns empty array if none. - Test
expireVouches(): scans for rows withexpires_at < now()and setsstatus = 'expired'; idempotent on re-run. - Mock Supabase client throughout.
- Achieve ≥ 85% coverage.
test/unit/vouching.service.spec.ts
- 3 distinct describe blocks
- Duplicate vouch rejection verified
- Expiry cleanup verified for the boundary case
expires_at == now -
npm test -- vouching.servicepasses - Coverage ≥ 85%
- No
anytypes
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, testing, good first issue Difficulty: good first issue
src/modules/sponsors/sponsors.service.ts has no unit tests. Pool stats aggregation, deposit XDR construction, and withdrawal validation are all unverified.
Sponsors are the funding side of the protocol — any silent miscalculation in pool stats undermines trust. Tests are essential before sponsors put real funds in.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/sponsors/sponsors.service.ts
- Create
test/unit/sponsors.service.spec.ts. - Test
getPool(): returns aggregated{ totalDeposited, totalShares, utilizationBps, apyBps }; mocks repo + contract client. - Test
buildDepositXdr(wallet, amount): calls the liquidity client with correct args; returns the XDR; rejects amount ≤ 0. - Test
buildWithdrawXdr(wallet, shares): rejects shares belowMIN_WITHDRAWAL_SHARES; otherwise returns valid XDR. - Test that pool utilization is computed as
(borrowed / totalDeposited) * 10_000rounded to bps. - Achieve ≥ 85% coverage.
test/unit/sponsors.service.spec.ts
- All 3 service methods covered
- Minimum-withdrawal validation tested
- Pool math verified against a known fixture
-
npm test -- sponsors.servicepasses - Coverage ≥ 85%
- No
anytypes
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, jobs, medium Difficulty: medium
Vouches have an expires_at column but no background process marks them inactive once that timestamp passes. Stale vouches accumulate and continue inflating reputation scores indefinitely.
Vouches are time-bounded by design — a mentor vouching today shouldn't still affect a learner's score two years later without renewal. Without expiry, the trust signal decays into noise.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/jobs/nonce-cleanup/
- Create
src/jobs/vouch-cleanup/vouch-cleanup.module.ts,vouch-cleanup.processor.ts,vouch-cleanup.service.tsmirroring the nonce-cleanup layout. - Register a hourly repeatable job with BullMQ:
every: 60 * 60 * 1000ms, namedvouch-cleanup. - Processor
handleCleanup(): callsvouchingService.expireVouches()and logs the affected row count. - Register the module in
app.module.ts. - Add structured log line via
Logger:{ job: 'vouch-cleanup', expired: N, took_ms: T }. - Add integration test that seeds 3 vouches (1 expired, 2 active) and asserts only 1 is marked expired.
src/jobs/vouch-cleanup/vouch-cleanup.module.tssrc/jobs/vouch-cleanup/vouch-cleanup.processor.tssrc/jobs/vouch-cleanup/vouch-cleanup.service.tssrc/app.module.tstest/integration/vouch-cleanup.spec.ts
- Job runs every 60 minutes via BullMQ scheduler
- Processor logs the count of expired vouches per run
- Module is registered in
app.module.ts - Integration test seeds + verifies cleanup
- Job is idempotent (a second immediate run touches zero rows)
- No business logic in the processor — delegates to service
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, testing, good first issue Difficulty: good first issue
The POST /auth/refresh endpoint exists in auth.controller.ts but test/e2e/auth.e2e-spec.ts only covers nonce + verify. Token-rotation behavior (single-use refresh tokens) is unverified end-to-end.
Refresh-token rotation is a security-critical pattern: a reused refresh token must be rejected, otherwise stolen tokens give attackers indefinite access. Without an e2e test, this regression could slip into production silently.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/auth/auth.controller.ts
- test/e2e/auth.e2e-spec.ts
- Extend
test/e2e/auth.e2e-spec.tswith a newdescribe('POST /auth/refresh')block. - Test happy path: sign in, receive
{ accessToken, refreshToken }, call refresh with the refresh token, receive a new pair, assert tokens differ from previous. - Test expired refresh token: forge an expired token (sign with past
exp), assert 401. - Test rotation/reuse detection: use refresh token once successfully, then attempt to reuse the same one — assert 401 and that the user's session is revoked entirely.
- Test malformed token: send garbage, assert 401.
- Use the e2e test harness that already exists (Supabase test schema + Nest test app).
test/e2e/auth.e2e-spec.ts
- 4 new test cases in a single describe block
- Rotation reuse case verifies session revocation
- All cases run against a real (test) Supabase schema
-
npm run test:e2epasses - No flake on 10 consecutive runs
- No
anytypes
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, performance, medium Difficulty: medium
GET /api/v1/reputation/:wallet calls the reputation Soroban contract on every request. Each call costs an RPC round trip (~300–800ms). Mobile screens that show reputation on render produce noticeable lag.
Reputation scores change only on loan events (creation, repayment, late fee). Between events, the score is stable for hours or days — perfect for a short Redis cache. This is the single biggest UX win for the home screen.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/reputation/reputation.service.ts
- Inject
Redisclient intoReputationService. - Add
REPUTATION_CACHE_TTL_SECto.env.examplewith default300. - In
getScore(wallet): firstGET reputation:{wallet}from Redis; if hit, return parsed; if miss, call contract, write to Redis with TTL, return. - Add
invalidate(wallet)method thatDEL reputation:{wallet}. - In
LoansService.repay()andLoansService.create(), callreputationService.invalidate(borrowerWallet)after every contract write. - Add unit tests covering cache-hit, cache-miss, and invalidation paths.
src/modules/reputation/reputation.service.tssrc/modules/reputation/reputation.module.tssrc/modules/loans/loans.service.ts.env.exampletest/unit/reputation.service.spec.ts
- Cache hit returns in < 10 ms
- Cache miss populates Redis with the configured TTL
- Loan creation + repayment invalidate the cache for that wallet
-
REPUTATION_CACHE_TTL_SECis configurable via env - 3 unit tests cover hit/miss/invalidate
- No stale read after invalidation in any test
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, observability, good first issue Difficulty: good first issue
@sentry/nestjs is installed in package.json but SentryModule.forRoot() is not registered in app.module.ts, and main.ts does not initialize Sentry. Unhandled exceptions are logged to stdout only — invisible in production.
Render's free-tier logs roll over fast. Without an external error sink, real production issues vanish before the team sees them. Sentry is the table-stakes minimum.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- package.json
- src/main.ts
- src/app.module.ts
- Call
Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 0.1 })at the very top ofmain.ts, beforeNestFactory.create. - Register
SentryModule.forRoot()inapp.module.ts. - Add
SentryGlobalFiltertoAPP_FILTERso all unhandled exceptions report to Sentry. - Add
SENTRY_DSN=andNODE_ENV=developmentto.env.example. - Verify integration locally with a deliberate
throw new Error('sentry test')in a test endpoint — confirm event lands in Sentry. - Add a README section "Error tracking" describing how to enable Sentry.
src/main.tssrc/app.module.ts.env.exampleREADME.md
- Sentry initializes before Nest bootstrap
- Unhandled exceptions in any controller reach Sentry
- App still starts when
SENTRY_DSNis empty (no-op fallback) -
traces_sample_rateis configurable via env - README documents the env vars
- No
anytypes introduced
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, security, medium Difficulty: medium
POST /auth/nonce and POST /auth/verify have no rate limit. An attacker can hammer signature verification (CPU-expensive) or nonce issuance (DB write) to exhaust resources or fish for valid signatures.
Auth endpoints are the most exposed surface. Free-tier Render has limited CPU and DB connections; one bad actor can degrade service for all real users. @nestjs/throttler is the standard mitigation.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/auth/auth.controller.ts
- Confirm
@nestjs/throttleris installed; if not, add it and registerThrottlerModule.forRoot([{ ttl: 60_000, limit: 5 }])inapp.module.ts. - Add
@UseGuards(ThrottlerGuard)toAuthControllerif not globally enabled. - Decorate
POST /auth/noncewith@Throttle({ default: { limit: 5, ttl: 60_000 } }). - Decorate
POST /auth/verifywith same. - Configure throttler keyGenerator to use client IP + wallet address combined, so one IP cannot exhaust limits for multiple wallets.
- Add e2e test: 6 rapid requests from the same IP receive 429 on the 6th.
src/modules/auth/auth.controller.tssrc/app.module.tstest/e2e/auth.e2e-spec.ts
- 5 requests within 60 seconds succeed; 6th returns 429
- Limits are configurable via env (
AUTH_THROTTLE_LIMIT,AUTH_THROTTLE_TTL_MS) - Throttler key includes IP — multiple wallets from one IP share the limit
- E2E test passes
- No regression in other endpoints
- Swagger response 429 documented on both endpoints
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, performance, good first issue Difficulty: good first issue
GET /api/v1/loans in loans.controller.ts returns every row in the loans table with no LIMIT. As the protocol grows, this becomes a slow query and a payload bomb on the mobile client.
The mobile loan history screen needs paginated access — infinite scroll, not a single 50MB JSON response. Adding pagination now is cheap; retrofitting it after the UI exists is expensive.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/loans/loans.controller.ts
- Create
dto/loan-list-query.dto.tswithpage: number @IsInt @Min(1) @Default(1),limit: number @IsInt @Min(1) @Max(50) @Default(10). - Update
LoansController.list(@Query() q: LoanListQueryDto)signature. - Update
LoansService.list(q)to applyrange((page - 1) * limit, page * limit - 1)on Supabase, and return{ data, total, page, limit }. - Add
Total-Countresponse header for clients that prefer headers over body. - Update Swagger:
@ApiQuery({ name: 'page', required: false }), etc. - Add e2e test: insert 12 loans, request page=2 with limit=10 — assert returns 2 items and
total=12.
src/modules/loans/loans.controller.tssrc/modules/loans/loans.service.tssrc/modules/loans/dto/loan-list-query.dto.tstest/e2e/loans.e2e-spec.ts
- Default page=1, limit=10 when query params omitted
-
limit > 50rejected with 400 - Response body shape:
{ data, total, page, limit } - Swagger documents query params
- E2E test verifies pagination math
- No regression on existing consumers (data field still an array)
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: backend, integrations, good first issue Difficulty: good first issue
There is no GET /.well-known/stellar.toml endpoint. Wallets and indexers that auto-discover Stellar projects (Lobstr directory, StellarExpert) cannot find StepFi's federation file from the API domain.
stellar.toml is the discoverability standard for Stellar projects — it advertises org info, contract IDs, and curated asset metadata. Wallets read it to display the project name and verify contract addresses.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- src/modules/health/health.controller.ts
- Add a
StellarTomlController(or extendHealthController) exposingGET /.well-known/stellar.toml. - Set
Content-Type: text/plain; charset=utf-8andAccess-Control-Allow-Origin: *. - Body content: org name, website, github, contracts block listing
CREDITLINE_CONTRACT_ID,LIQUIDITY_POOL_CONTRACT_ID,REPUTATION_CONTRACT_ID,VENDOR_REGISTRY_CONTRACT_ID, all read fromConfigService. - Cache the response in memory with 1-hour TTL (cheap to regenerate, but no need to do it per request).
- Add Swagger decorators marking it as a static metadata endpoint.
- Add e2e test asserting status 200, content-type text/plain, and contract IDs present.
src/modules/health/stellar-toml.controller.tssrc/modules/health/health.module.tstest/e2e/stellar-toml.e2e-spec.ts
- GET /.well-known/stellar.toml returns 200 with text/plain body
- Body includes all 4 contract IDs
- CORS allows
*for this endpoint specifically - Cached in memory for 60 minutes
- E2E test verifies content
- Swagger lists the endpoint
- npm run build passes with zero TypeScript errors
- No new
anytypes introduced anywhere - Full Swagger @ApiOperation + @ApiResponse decorators on any new endpoints
- New migration file created for any schema changes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, blockchain, hard Difficulty: hard
The mobile app has no wallet integration. Users cannot connect Lobstr, xBull, or Freighter to sign transactions. Every flow that requires signing (loan apply, repay, deposit) is currently dead-end.
WalletConnect v2 is the Stellar-wallet standard. Without it, the entire transactional surface of StepFi-App is non-functional. This is the gate to running any e2e flow.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- services/auth.service.ts
- Install
@walletconnect/web3walletand Stellar adapter; configure project ID viaEXPO_PUBLIC_WALLETCONNECT_PROJECT_ID. - Create
services/wallet.service.tsexposing:initWalletConnect(),connectWallet(): Promise<{ address, sessionId }>,signXdr(xdr: string): Promise<string>,disconnectWallet(). - Show a QR-code modal in the connect flow using
react-native-qrcode-svg; on mobile, deep-link to wallet app if installed. - Create
stores/wallet.store.ts(Zustand):{ address, sessionId, isConnected, connect, disconnect }. Persist to SecureStore. - Handle session expiry — listener that calls
disconnect()on session_delete event. - Add a
useWallet()hook wrapping the store for use in screens.
services/wallet.service.tsstores/wallet.store.tshooks/useWallet.tscomponents/wallet/ConnectModal.tsxapp.config.ts(env var)package.json
- User can connect Lobstr/xBull/Freighter via QR scan
- Session persists across app restarts (SecureStore)
-
signXdrreturns a signed XDR or throws on user rejection - Disconnect clears all wallet state
- Session expiry auto-clears state without crash
- No hardcoded hex colors in any new UI
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, ui, medium Difficulty: medium
app/(auth)/sign-in.tsx is a placeholder. There is no onboarding flow introducing StepFi's value proposition, features, or reputation tiers before asking the user to connect a wallet — leading to immediate drop-off.
First-time users have no context for "connect wallet" — they need a 30-second onboarding explaining what StepFi does and why they should trust it. The sliding 4-step pattern matches industry norms (Robinhood, Cash App).
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- constants/colors.ts
app/(auth)/sign-in.tsx: aFlatListwithpagingEnabled,horizontal,showsHorizontalScrollIndicator={false}, 4 slides.- Slide 1 (Welcome): logo + tagline "Climb your credit stairs" + staircase illustration. Use
colors.background.primaryandcolors.text.primary. - Slide 2 (Features): 3 rows with Lucide icons (
GraduationCap,TrendingUp,Shield) and descriptions. - Slide 3 (Reputation Tiers): 4 horizontally scrollable cards for Starter / Bronze / Silver / Gold with tier-color borders from
colors.tiers.*. - Slide 4 (Connect Wallet): wallet option buttons (Lobstr, xBull, Freighter) that call the
useWallethook from Issue 24. - Page indicator dots at the bottom; "Skip" button top-right jumps to slide 4.
app/(auth)/sign-in.tsxcomponents/onboarding/OnboardingSlide.tsxcomponents/onboarding/PageIndicator.tsxcomponents/onboarding/WalletOptions.tsx
- 4 slides render and paginate smoothly
- All colors come from
constants/colors.ts— zero hardcoded hex - All icons are Lucide React Native
- Skip button works from any slide
- Connecting a wallet on slide 4 navigates to
role-select - Works on
npx expo export --platform web
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, ui, good first issue Difficulty: good first issue
After wallet connection, there is no role-selection step. The app currently assumes everyone is a learner, which breaks the sponsor experience entirely.
Learner and sponsor flows are fundamentally different — different home screens, different actions, different tab bars. The role must be picked once at onboarding and persisted.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- constants/colors.ts
app/(auth)/role-select.tsx: two large tappable cards, vertically stacked, 80% screen width, 240px tall each.- Learner card:
GraduationCapicon (Lucide, 48px), title "I'm a Learner", subtitle "Build credit for school", border colorcolors.tiers.silver(blue). - Sponsor card:
TrendingUpicon, title "I'm a Sponsor", subtitle "Fund learners, earn yield", border colorcolors.semantic.success(green). - Tap stores
roleinstores/user.store.tsand animates a checkmark badge on the selected card. - CTA button at bottom: "Continue" — disabled until a role is selected, navigates to
app/(auth)/register.tsx. - Loading / error / empty states unused here, but ensure no flicker on mount.
app/(auth)/role-select.tsxcomponents/auth/RoleCard.tsxstores/user.store.ts
- Two cards render with correct icons and borders from colors.ts
- Tapping a card marks it selected with checkmark
- Continue is disabled until selection
- Selection persists in user.store.ts
- No hardcoded hex
-
npx expo export --platform webpasses
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, ui, hard Difficulty: hard
app/(tabs)/index.tsx is a placeholder. The learner has no home dashboard showing credit available, active loans, or upcoming payments — the core screen of the app.
The home screen is the first thing a learner sees every session. It must surface: credit ceiling, current debt, next payment due, recent activity. Without it, the app has no anchor screen.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- services/loans.service.ts
- services/reputation.service.ts
- Header row: greeting "Hi, {first-letters-of-wallet}" + truncated wallet address (e.g.
GABC...XYZ) with copy icon. - Credit available card (full width): big number "Available: $X", progress bar (used / ceiling), tier label from reputation.
- Quick actions row: 4 round buttons with Lucide icons (
PlusApply,CreditCardPay,HistoryHistory,UsersVouches), each navigates to its respective screen. - Active loans horizontal scroll: cards showing vendor logo, amount, next due date.
- Upcoming payments list: 3-row vertical list with date, amount, "Pay now" button.
- Pull-to-refresh re-fetches via
hooks/useLoansandhooks/useReputation. Loading skeletons + empty state ("No active loans yet — apply for credit") + error state with retry.
app/(tabs)/index.tsxhooks/useLoans.tshooks/useReputation.tscomponents/home/CreditCard.tsxcomponents/home/QuickActionsRow.tsxcomponents/home/ActiveLoanCard.tsxcomponents/home/UpcomingPaymentRow.tsx
- All 4 sections render with real data from services
- No fetch logic in the screen file — only hooks
- Loading skeletons render during fetch
- Empty state shows when no loans
- Error state shows retry button
- No hardcoded hex; all Lucide icons
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, ui, hard Difficulty: hard
There is no loan application flow. Learners cannot actually borrow — app/(tabs)/apply.tsx is empty.
This is the second most important screen after home. Without it, the app is read-only. The wizard pattern (multi-step bottom sheet) matches the user mental model of "filling out an application".
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- services/loans.service.ts
- services/vendors.service.ts
- Step 1 — Vendor: search bar + category filter chips (school/bootcamp/cert/tool). Tapping a vendor advances to step 2.
- Step 2 — Details: amount slider (min/max from credit ceiling), installment-schedule selector (3/6/12 months), live preview of monthly payment + total interest using
useReputationinterest rate. - Step 3 — Review: summary card listing vendor, amount, schedule, interest, total; warning banner about late-fee policy; CTA "Sign with Wallet" calls
wallet.signXdrfrom Issue 24. - Step 4 — Success: green checkmark, tx-hash row (tap to copy / open in StellarExpert), next payment summary, CTA "Back to home".
- Step transitions animated horizontally; back button preserves prior state.
- Loading + error states on step 3 sign action; empty vendor list state on step 1.
app/(tabs)/apply.tsxcomponents/loan-wizard/Step1Vendor.tsxcomponents/loan-wizard/Step2Details.tsxcomponents/loan-wizard/Step3Review.tsxcomponents/loan-wizard/Step4Success.tsxhooks/useVendors.tshooks/useLoanQuote.ts
- All 4 steps render and transition
- Back button preserves prior step state
- Step 2 preview recalculates on slider drag
- Step 3 sign error shows toast + keeps user on step 3
- Empty vendor list shows empty state
- All colors from colors.ts; all icons Lucide
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, ui, medium Difficulty: medium
There is no UI for a learner to see their reputation score, tier, or path to the next tier. The score is opaque, undermining the gamified "climb the stairs" core mechanic.
Reputation is the protocol's loyalty loop. Making it visible and progressive (showing distance to next tier) drives behavior — on-time payments, vouching, building history. Hidden, it's just a number.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- services/reputation.service.ts
- Animated circular progress ring (200×200 px) with the score 0–100 in the center, color from
colors.tiers.<current>. - Tier badge below ring: tier name + tier color, e.g. "🥈 Silver Tier".
- Stats row (3 columns): interest rate, credit limit, loans repaid.
- "How to reach next tier" section: progress bar showing distance to next threshold and bullet list of actions ("Pay 2 more loans on time", "Get 1 mentor vouch").
- Score history line chart (last 6 months) using
react-native-chart-kitorvictory-native— y-axis 0–100, x-axis month labels. - Pull-to-refresh, loading + empty + error states.
app/(tabs)/reputation.tsxcomponents/reputation/ScoreRing.tsxcomponents/reputation/TierBadge.tsxcomponents/reputation/NextTierProgress.tsxcomponents/reputation/ScoreHistoryChart.tsxhooks/useReputation.ts
- Ring animates from 0 to score on mount
- Tier color matches
colors.tiers.<tier> - Chart renders 6 data points
- No tier hardcoded — read from data
- All icons Lucide; no hardcoded hex
- Empty state when wallet has no history
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, ui, medium Difficulty: medium
app/(tabs)/settings.tsx is empty. Users cannot edit their profile, switch roles, manage notifications, or disconnect their wallet.
Settings is the universal escape hatch — fix profile typos, log out, switch sides. Many users will spend less than 1 minute total here but it must exist or the app feels half-finished.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- stores/auth.store.ts
- stores/user.store.ts
- Profile card: avatar (initials), display name, wallet address with copy icon, edit button.
- Learner profile card: school, program, income type — editable fields stored via
learners.service. - Role switcher: pill toggle (Learner ↔ Sponsor) bound to
user.store. Switch triggers a re-route to the correct tab layout. - App section: notification toggle, language picker (English/Spanish/French).
- Security section: connected wallet (with disconnect button calling
walletService.disconnectWallet), session expiry timer. - About section: version, terms, privacy policy, support email.
app/(tabs)/settings.tsxcomponents/settings/ProfileCard.tsxcomponents/settings/RoleSwitcher.tsxcomponents/settings/SettingRow.tsxhooks/useUserProfile.ts
- Profile fields persist via the learners service
- Role switcher updates the tab layout immediately
- Disconnect clears wallet state and routes to sign-in
- Notification + language preferences persist locally
- All icons Lucide; no hardcoded hex
- Loading/error states on profile fetch
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: mobile, ui, medium Difficulty: medium
Sponsor users have no portfolio view. There's no screen showing deposit value, share price, APY, or pool utilization. Sponsors can't see what they've funded.
Sponsors need a Robinhood-style portfolio view — total value, daily change, action buttons. Without it, sponsoring is invisible and they have no reason to come back.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- context/ui-context.md
- services/sponsors.service.ts
- Total Deposited card: big number "$X", APY badge ("8.4% APY"), 24h change indicator (+/- color).
- Pool stats row (3 columns): your shares, share price, interest earned to date.
- Pool health bar: visualization of locked vs available liquidity, with utilization percentage.
- Recent activity list: deposits + withdrawals + interest accruals with timestamps.
- Floating action button (FAB): "Deposit" — opens a bottom sheet flow that calls
sponsorsService.buildDepositXdrthenwalletService.signXdr. - Pull-to-refresh, loading + empty + error states.
app/(tabs)/sponsor-home.tsxcomponents/sponsor/TotalDepositedCard.tsxcomponents/sponsor/PoolStatsRow.tsxcomponents/sponsor/PoolHealthBar.tsxcomponents/sponsor/ActivityList.tsxcomponents/sponsor/DepositSheet.tsxhooks/useSponsorPortfolio.ts
- Portfolio renders with mocked-then-real data via the sponsors service
- Deposit FAB opens a working signing flow
- Pool health bar visualizes utilization correctly
- Activity list paginates beyond 10 entries
- All icons Lucide; no hardcoded hex
- Empty state shown for new sponsor (zero deposits)
- No hardcoded hex colors — use constants/colors.ts only
- No API calls in screen files — use services/ only
- Loading, error, AND empty states handled
- Lucide React Native used for ALL icons
- npx expo export --platform web passes
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: devops, ci, good first issue Difficulty: good first issue
The StepFi-API repo has no GitHub Actions workflow. PRs can merge with broken TypeScript builds or failing tests because nothing enforces them.
Open-source contributors will submit PRs blind. CI is the gate that prevents broken builds from landing in main. Without it, the maintainer has to manually run npm run build on every PR.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- Create
.github/workflows/ci.yml. - Triggers:
pull_requestto main +pushto main. - Job
build-test: ubuntu-latest, Node 20 viaactions/setup-node@v4withcache: 'npm'. - Steps: checkout → setup node →
npm ci→npm run build→npm test. - Cache
node_moduleskeyed onpackage-lock.jsonhash. - Add CI status badge to README pointing at the workflow.
.github/workflows/ci.ymlREADME.md
- Workflow runs on every PR to main
- Failing build blocks merge
- node_modules cached between runs
- Total run time under 5 minutes
- Status badge visible at top of README
- No secrets exposed in workflow logs
- CI passes on a test PR before merging
- Zero secrets committed to the repo
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: devops, ci, good first issue Difficulty: good first issue
The mobile app repo has no CI workflow. A typo in any component can break the Expo build and only be discovered when EAS build fails hours later.
Expo builds are slow and rate-limited. Catching compile errors at PR time via a cheap expo export --platform web is dramatically faster than waiting for an EAS run.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- Create
.github/workflows/ci.ymlin StepFi-App. - Triggers:
pull_requestandpushto main. - Job
web-build: ubuntu-latest, Node 20, cache npm. - Steps: checkout → setup-node →
npm ci→npx expo export --platform web. - Upload web build as artifact for inspection.
- Status badge in README.
.github/workflows/ci.ymlREADME.md
- Workflow runs on every PR to main
- Failed Expo export blocks merge
- node_modules cached
- Web bundle uploaded as artifact
- Status badge in README
- Run time under 7 minutes
- CI passes on a test PR before merging
- Zero secrets committed to the repo
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-App Labels: devops, ci, medium Difficulty: medium
There is no automated EAS build pipeline. Every release requires the maintainer to manually run eas build --platform android --profile production and upload the APK, which delays releases and risks human error.
Releases gated by manual steps don't happen on schedule. Automating production builds on v* tag push means tagging a commit produces a downloadable APK with zero further action.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- eas.json
- Create
.github/workflows/eas-build.yml. - Trigger:
pushwithtags: ['v*']. - Job
production-android: ubuntu-latest, Node 20. - Steps: checkout → setup-node →
npm ci→ installeas-cli→eas build --platform android --profile production --non-interactive --no-wait. - Auth via
EXPO_TOKENsecret (must be added in repo settings — document in workflow comment). - After job, run
eas build:list --jsonand download APK, upload as release asset viaactions/upload-release-asset.
.github/workflows/eas-build.ymleas.json(verify production profile exists)README.md(release process section)
- Pushing tag
v0.1.0triggers an EAS production build - APK is attached to the corresponding GitHub release
-
EXPO_TOKENsecret is documented as required - No secrets leaked in logs
- eas.json has a production profile with
buildType: apkoraab - Release process documented in README
- CI passes on a test PR before merging
- Zero secrets committed to the repo
- context/progress-tracker.md updated
Repo: StepFi-app/StepFi-API Labels: devops, observability, good first issue Difficulty: good first issue
The Render free-tier instance hosting StepFi-API spins down after inactivity. First request after sleep takes 30+ seconds, and outages are silent — no one knows the service is down until a user complains.
A simple periodic ping keeps the instance warm and acts as a heartbeat monitor. If the ping fails, a GitHub issue is auto-created so the maintainer sees the outage even without external monitoring tools.
Read these context files first:
- context/architecture-context.md
- context/code-standards.md
- context/progress-tracker.md
- Create
.github/workflows/health-check.yml. - Trigger:
schedule: cron '0 */6 * * *'(every 6 hours) +workflow_dispatch(manual). - Step 1:
curl -sf -o /dev/null -w "%{http_code}" https://stepfi-api.onrender.com/api/v1/health— capture status. - Step 2: if status != 200, use
actions/github-scriptto create an issue titled "Health check failed at {timestamp}" with the HTTP status in the body. Include labelincident. - Step 3: dedupe — if an open
incidentissue already exists, comment on it instead of creating a duplicate. - Document the ping endpoint in README.
.github/workflows/health-check.ymlREADME.md
- Workflow runs every 6 hours via cron
- Manual trigger works
- Non-200 response opens a GitHub issue with label
incident - Duplicate issues are not created — comments on existing ones
- Ping URL and label documented in README
- Workflow uses no committed secrets
- CI passes on a test PR before merging
- Zero secrets committed to the repo
- context/progress-tracker.md updated
End of issues — 35 total.