Skip to main content

Overview

This agent skill manages which earn vaults a user’s collateral sits in, automatically rebalancing to maximize yield while keeping the credit line active and healthy.
  • Compares vault yields across supported strategies
  • Rebalances from lower-yield to higher-yield vaults
  • Protects the position by only rebalancing when health factor is safe

API Endpoints Used

EndpointPurpose
GET /credit/protocolGet available earn strategies
GET /credit/accounts/{account}/infoCheck health factor and current position
GET /credit/accounts/{account}/unlockBuild calldata to unlock collateral from current vault
GET /credit/accounts/{account}/lockBuild calldata to lock collateral into new vault (with earn param)

How It Works

1

Check Current Position

The agent checks the user’s credit position to ensure it’s healthy enough to rebalance:
curl -X GET https://api.sprinter.tech/credit/accounts/0xUSER/info
The agent only proceeds if healthFactor > 1.5 — rebalancing temporarily changes the collateral composition, so a larger buffer is needed.
2

Discover Available Strategies

The agent fetches the protocol config to get available earn strategies and their IDs:
curl -X GET https://api.sprinter.tech/credit/protocol
This returns the credit configuration including a strategies field with available earn vaults. The agent compares the user’s current vault yield against alternatives.
3

Rebalance

If a higher-yield vault is available, the agent unlocks collateral from the current vault and re-locks into the new one using the earn param:
curl -X GET 'https://api.sprinter.tech/credit/accounts/0xUSER/unlock?amount=1000000000000000000&asset=0xCOLLATERAL_TOKEN'
Returns { calls: ContractCall[] } — execute to unlock and unwrap from the current vault.
The unlock and lock must be executed sequentially — the collateral must be fully unlocked before it can be re-locked into a new vault.

Implementation

const SPRINTER_API = "https://api.sprinter.tech";
const POLL_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
const MIN_HEALTH_FACTOR = 1.5; // higher threshold during rebalancing
const MIN_YIELD_IMPROVEMENT = 0.5; // only rebalance if >= 0.5% APY improvement

interface ContractCall {
  to: string;
  data: string;
  value: string;
}

interface VaultPosition {
  strategy: string;
  amount: string;
  asset: string;
  apy: number;
}

async function getCreditInfo(account: string) {
  const res = await fetch(`${SPRINTER_API}/credit/accounts/${account}/info`);
  if (!res.ok) throw new Error(`Credit info failed: ${res.status}`);
  return res.json();
}

async function getProtocolConfig() {
  const res = await fetch(`${SPRINTER_API}/credit/protocol`);
  if (!res.ok) throw new Error(`Protocol config failed: ${res.status}`);
  return res.json();
}

async function buildUnlockCalls(
  account: string,
  amount: string,
  asset: string
): Promise<ContractCall[]> {
  const res = await fetch(
    `${SPRINTER_API}/credit/accounts/${account}/unlock?amount=${amount}&asset=${asset}`,
  );
  if (!res.ok) throw new Error(`Unlock calldata failed: ${res.status}`);
  const data = await res.json();
  return data.calls;
}

async function buildLockCalls(
  account: string,
  amount: string,
  asset: string,
  earn?: string
): Promise<ContractCall[]> {
  let url = `${SPRINTER_API}/credit/accounts/${account}/lock?amount=${amount}&asset=${asset}`;
  if (earn) url += `&earn=${earn}`;

  const res = await fetch(url);
  if (!res.ok) throw new Error(`Lock calldata failed: ${res.status}`);
  const data = await res.json();
  return data.calls;
}

async function executeCalls(calls: ContractCall[], signer: any): Promise<string> {
  let lastTxHash = "";
  for (const call of calls) {
    const tx = await signer.sendTransaction({
      to: call.to,
      data: call.data,
      value: call.value || "0",
    });
    const receipt = await tx.wait();
    if (!receipt || receipt.status !== 1) throw new Error(`Tx reverted: ${tx.hash}`);
    lastTxHash = tx.hash;
  }
  return lastTxHash;
}

async function optimizeYield(
  account: string,
  currentPosition: VaultPosition,
  signer: any
) {
  // 1. Check health factor
  const info = await getCreditInfo(account);
  const hf = parseFloat(info.data.USDC.healthFactor);

  if (hf < MIN_HEALTH_FACTOR) {
    console.log(`Health factor ${hf.toFixed(2)} too low to rebalance — skipping`);
    return;
  }

  // 2. Get available strategies from protocol config
  const config = await getProtocolConfig();
  const strategies = config.strategies ?? [];

  // 3. Find the best vault by yield
  const bestVault = strategies
    .filter((s: any) => s.id !== currentPosition.strategy)
    .sort((a: any, b: any) => b.apy - a.apy)[0];

  if (!bestVault) {
    console.log("No alternative vaults available");
    return;
  }

  const improvement = bestVault.apy - currentPosition.apy;
  if (improvement < MIN_YIELD_IMPROVEMENT) {
    console.log(
      `Best alternative: ${bestVault.apy}% APY (+${improvement.toFixed(2)}%) — below threshold`
    );
    return;
  }

  console.log(
    `Rebalancing: ${currentPosition.apy}% → ${bestVault.apy}% APY (+${improvement.toFixed(2)}%)`
  );

  // 4. Unlock from current vault
  const unlockCalls = await buildUnlockCalls(
    account,
    currentPosition.amount,
    currentPosition.asset
  );
  const unlockTx = await executeCalls(unlockCalls, signer);
  console.log(`Unlocked from ${currentPosition.strategy}: ${unlockTx}`);

  // 5. Lock into new vault with earn param
  const lockCalls = await buildLockCalls(
    account,
    currentPosition.amount,
    currentPosition.asset,
    bestVault.id
  );
  const lockTx = await executeCalls(lockCalls, signer);
  console.log(`Locked into ${bestVault.id}: ${lockTx}`);
}

// Run the optimizer loop
setInterval(() => {
  const currentPosition: VaultPosition = {
    strategy: "CURRENT_STRATEGY_ID",
    amount: "1000000000000000000",
    asset: "0xCOLLATERAL_TOKEN",
    apy: 4.2,
  };
  optimizeYield("0xUSER_ADDRESS", currentPosition, signer).catch(console.error);
}, POLL_INTERVAL_MS);

Configuration

Poll Interval

Default: every 1 hour. Vault yields change slowly, so frequent polling isn’t necessary. Hourly checks balance responsiveness with API usage.
Default: 1.5. Higher than the health monitor threshold (1.3) because rebalancing temporarily changes collateral composition. The extra buffer prevents accidental liquidation during the unwrap/wrap window.
Default: 0.5% APY. The agent only rebalances if the improvement exceeds this threshold. This prevents excessive on-chain transactions for marginal gains — each rebalance costs gas.

Supported Vaults

The agent can rebalance between any earn strategies returned by the protocol config:
curl -X GET https://api.sprinter.tech/credit/protocol
The strategies field in the response contains available earn vaults with their IDs — use these as the earn param when calling /lock. See Risk Management for the full collateral tier table and LTV details per vault type.

Credit Engine

How collateral value and LTVs affect your credit line.

Health Monitor

Pair with the health monitor to protect positions during rebalancing.

Credit API Reference

Earn vault wrap/unwrap endpoints with interactive playground.