🛠️Integration Guide

ProgramId: FL3X2pRsQ9zHENpZSKDRREtccwJuei8yg9fwDu9UN69Q

All information for a Lulo account is stored and routed through the UserAccount, which is a PDA that is derived from the owner's wallet address:

export function getFlexUserAccountAddress(owner: PublicKey, programId: PublicKey) {
	return PublicKey.findProgramAddressSync(
		[Buffer.from('flexlend'), owner.toBuffer()],
		programId, // FL3X2pRsQ9zHENpZSKDRREtccwJuei8yg9fwDu9UN69Q

The Lulo userAccount has authority over the deposited funds in each integrated protocol. For Mango, Drift, and Kamino, the authority transfers over directly to the respective user accounts at those protocols.

For Solend and MarginFi, you can find the corresponding Lulo obligation accounts by parsing the userAccount using the below IDL:

pub struct UserAccount {
    pub bump: u8,
    pub _padding: [u8; 7],
    // wallet address that owns this UserAccount
    pub owner: Pubkey,
    // the marginFi userAccount which this UserAccount has authority
    pub mfi_account: Pubkey,
    // the solend obligation which this UserAccount has authority
    pub solend_obligation: Pubkey,

    // 2x Points lift metadata
    pub promotion_seeds: [PromotionSeed; 4],

    pub _padding2: [u64; 12],

Points Lift balance

export async function fetchKaminoPointsLiftBalance(
	program: Program<Flexlend>,
	owner: PublicKey,
	// always USDC
	mintAddress: PublicKey,
): Promise<BN | null> {
	const flexUserPda = getFlexUserPDA(owner, program.programId)
	const flexUser = await program.account.userAccount.fetchNullable(flexUserPda)

	if (flexUser == null) {
		return null

	// default [u8, 19]
	const isEmpty = (authoritySeed: number[]) => {
		return (
			authoritySeed.toString() ===
			[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].toString()

	// check for any valid `promotionSeeds` on the user, there should be either 0 or 1
	const validAuthoritySeed = flexUser.promotionSeeds.find(s => !isEmpty(s.authoritySeed))

	if (!validAuthoritySeed) {
		return null

	// map to UTF-8 and find the address for this promotion authority,
	// which is another PDA that has the authority over the Kamino obligation
	const seedValue = Buffer.from(validAuthoritySeed.authoritySeed).toString('utf8')

	const authorityAddress = PublicKey.findProgramAddressSync(
		[Buffer.from('promotion_authority'), Buffer.from(seedValue.toString())],

	const promotionAuthority = await program.account.promotionAuthority.fetchNullable(
	if (promotionAuthority === null) {
		throw new Error(`invalid promotion seed ${seedValue} for owner ${owner}`)

	// promotion authority tracks user deposits, the obligation on the JLP pool will show 2x the user deposits
	return promotionAuthority.totalDeposits

Last updated