CredifiZero / Agent docsBack to Zero →
CREDIFI ZERO · AGENT GUIDE

Credifi Zero agent guide.

Authenticate with a wallet. Receive credit through a vouch. Request a loan through Credifi’s API.

Overview

Build an agent that uses Credifi Zero to borrow against vouched credit and receive funds in its own wallet. The agent logs in with a wallet signature, accepts a vouch, reviews a quote, and requests a loan through Credifi’s API. Credifi checks and approves the request, then submits the transaction that originates the loan from an onchain lending vault. The vault funds the loan to the vouched wallet address.

Before you borrow

  • Use an Ethereum externally owned account (EOA) that your agent can sign messages and transactions with. Smart-contract wallets such as Safe can’t be used for API login yet.
  • A sponsor can vouch for your wallet address before or after you sign up. Log in with that same wallet to prove control, see the incoming offer, and accept it. Use the same address to receive the loan.
  • Ask a sponsor with available vouch capacity to offer you credit. You don’t need a personal credit limit to receive vouched credit.
  • Accept the vouch and authorize your wallet for its market.
  • Check whether your account requires email/SMS 2FA before borrowing (see below).
  • Keep enough native gas tokens on the chain for wallet authorization and repayment.

The minimum loan is $5 for accounts with only vouched or earned credits, no bank-link history, and no approved base credit limit. Accounts with a bank link or an approved base limit have a $100 minimum. Read minimumLoanAmount in GET /api/v3/user/flags for your account’s minimum. Each request also depends on available credit, market availability, and liquidity.

Accounts for agents

Anon credit is an approved base credit limit from wallet-based eligibility checks. It is separate from vouched or earned credits.

Accounts with a linked bank or an approved Anon base credit limit require email or SMS verification (2FA) before borrowing. This also applies when the loan uses vouch or earned credits.

Read GET /api/v3/user/flags. If loanTwoFactorRequired is false, borrow without an email/SMS code. If it is true, your agent can complete 2FA through the API using access to the account’s registered email or SMS inbox.

If your existing account requires 2FA and you want an agent without an inbox dependency, register a separate agent wallet with no linked bank or approved base credit limit, then vouch for it from the existing account if you have available vouch capacity. The new agent must log in and accept the offer. Removing a bank connection does not remove the 2FA requirement; use the flag returned by the backend.

Money units

WhereUnit$500 example
Vouch amountMicro / reservedMicro and quote fee.feeMicroInteger micro-dollars (1 dollar = 1,000,000)500000000
Loan quote / create amount and API balancesDollars500
Token approvals and onchain repaymentsInteger token base units; use the token's decimals()500000000n for 500 USDC with 6 decimals

Micro-dollars and token base units are different units even when a 6-decimal token happens to use the same number. Keep onchain amounts as bigint; do not convert them to JavaScript floating-point numbers. This guide uses numbers without separators: 500000000 means 500 dollars in micro-dollars. In JavaScript, 500_000_000 is the same value, with underscores only for readability; JSON and environment variables must use 500000000. The n suffix is used only for JavaScript bigint values, never in an API JSON body.

Terms used in this guide

  • Earned credits: borrowing or vouching capacity credited after qualifying loans are repaid, based on a share of paid interest; both borrowers and sponsors can earn credits. Read earnedMicro in GET /api/vouch/status. These credits are separate from cash referral rewards.
  • Vouch: an offer that reserves a sponsor's credit to back a loan for your wallet.
  • EVC (Ethereum Vault Connector): the onchain system that manages account and operator permissions for the lending vaults. Authorizing Credifi permits its adaptor to operate your EVC subaccounts.
  • Credifi1155: the onchain loan contract; its makeLoanPayment method forwards repayment to the Credifi ERC20 adaptor, which collects the tokens and updates the vault debt.
  • Credifi adaptor: the contract connecting Credifi loans to the lending vaults. It reads live loan debt and spends the repayment tokens you approve.

Wallet authentication

Your agent logs in by signing a message with its wallet and exchanging the signature for a Credifi bearer token, which it sends with every API request. New accounts may hit a browser verification challenge during signup; if the API returns SIGNUP_CHALLENGE_REQUIRED, see the next section.

  1. Request the login message for your checksummed wallet address.
  2. Sign the exact returned message with that wallet.
  3. Exchange the message and signature for a session token.

The nonce expires after five minutes and is consumed on successful verification. The returned token currently lasts 24 hours. Request and sign a new message when the token expires.

If signup requires browser verification

A new wallet may receive 403 SIGNUP_CHALLENGE_REQUIRED before its account is created. Complete signup with the same wallet in Credifi’s browser app, then run API login again. This one-time signup check is separate from loan 2FA.

Request and sign a fresh login message when retrying. The rejected attempt already consumed its nonce, so the old signature will not work. The helper below fetches a new message on every call.

// signer: an ethers-compatible EOA signer from your signing service.
// apiBase: the Credifi API base URL for your integration environment.
async function walletLogin(apiBase, signer) {
  const address = await signer.getAddress();
  const challengeResponse = await fetch(
    `${apiBase}/api/auth/wallet-login?address=${encodeURIComponent(address)}`
  );
  if (!challengeResponse.ok) throw new Error('Login challenge failed');
  const { message } = await challengeResponse.json();
  const signature = await signer.signMessage(message);

  const loginResponse = await fetch(`${apiBase}/api/auth/wallet-login`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ message, signature }),
  });
  const result = await loginResponse.json();
  if (!loginResponse.ok) {
    const message = result.code === 'SIGNUP_CHALLENGE_REQUIRED'
      ? 'Complete browser signup verification, then retry with a fresh login message.'
      : result.error || 'Wallet login failed';
    const error = new Error(message);
    error.code = result.code;
    error.details = result.details;
    error.status = loginResponse.status;
    throw error;
  }
  const { token } = result;
  return { address, token };
}

Keep the signing key in your signing service and the bearer token private. Never send the private key to the API. For a new wallet, the first successful login (after any signup challenge) creates the account.

Request helper

The examples use Node.js 20+ and ethers v6 (npm install ethers). Save the JavaScript blocks in order in one .mjs file. The setup and vouch blocks make API requests as soon as they run. The authorization, borrowing, and repayment helpers do nothing until you call them with your own approval callbacks.

Set AGENT_RPC_URL and AGENT_PRIVATE_KEY for your agent’s EOA on the vouch’s chain. The production API is https://backend.credi.fi; set CREDIFI_API_BASE to use another environment. The optional sponsor block uses a separate SPONSOR_PRIVATE_KEY. You can swap the example ethers Wallet for a hosted signing service without changing anything else.

Define the request helper once. Pass market for quote and loan requests, which need the chain and market headers.

function createApi(apiBase, token) {
  return async function request(path, { method = 'GET', body, market } = {}) {
    const response = await fetch(`${apiBase}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
        ...(market
          ? {
              'x-chain-id': String(market.chainId),
              'x-market-id': market.marketId,
            }
          : {}),
      },
      ...(body ? { body: JSON.stringify(body) } : {}),
    });
    const text = await response.text();
    const json = text ? JSON.parse(text) : {};
    if (!response.ok) {
      const error = new Error(json.error || 'Credifi request failed');
      error.code = json.code;
      error.details = json.details;
      error.status = response.status;
      throw error;
    }
    return json;
  };
}

Initialize the agent client

import { ethers } from 'ethers';

const apiBase = process.env.CREDIFI_API_BASE || 'https://backend.credi.fi';
if (!process.env.AGENT_RPC_URL || !process.env.AGENT_PRIVATE_KEY) {
  throw new Error('Configure AGENT_RPC_URL and AGENT_PRIVATE_KEY first');
}
const provider = new ethers.JsonRpcProvider(process.env.AGENT_RPC_URL);
const agentSigner = new ethers.Wallet(process.env.AGENT_PRIVATE_KEY, provider);
const { address: agentAddress, token: agentToken } = await walletLogin(apiBase, agentSigner);
const agentApi = createApi(apiBase, agentToken);

agentApi is the authenticated request function used throughout the agent examples. agentAddress is the wallet returned by login. If signup returns SIGNUP_CHALLENGE_REQUIRED, finish the browser step above and rerun login with a fresh message; do not continue without a token.

Receive a vouch

Get vouched credit

The sponsor needs a registered, verified wallet and available vouch capacity. Your agent does not need a Credifi account yet: the sponsor can send the offer to its wallet address first. When your agent signs up and verifies that same wallet, the offer appears in GET /api/vouch/status and in the app’s received vouches. Accept it to make the credit available for borrowing. Signup alone does not accept the offer or take out a loan.

The sponsor sends an offer to the recipient’s wallet address. Amounts on the vouch endpoints are integer micro-dollars, so $500 is 500000000. The server assigns the chain and market, and the sponsor doesn’t set the APR.

// Optional: run this block in the sponsor's signing environment.
// The recipient may sign up before or after this offer is created.
if (!process.env.SPONSOR_PRIVATE_KEY) throw new Error('Configure the sponsor signer first');
const sponsorSigner = new ethers.Wallet(process.env.SPONSOR_PRIVATE_KEY, provider);
const { token: sponsorToken } = await walletLogin(apiBase, sponsorSigner);
const sponsorApi = createApi(apiBase, sponsorToken);
// Persist this ID and reuse it when retrying this same offer.
const vouchRequestId = crypto.randomUUID();
const sentOffer = await sponsorApi('/api/vouch/commitments', {
  method: 'POST',
  body: {
    requestId: vouchRequestId,
    recipientWalletAddress: agentAddress,
    amountMicro: 500000000,
  },
});
// sentOffer: { id, status: "offered" }
console.log('Offer ID for VOUCH_OFFER_ID:', sentOffer.id);

Your agent finds the offer among its incoming vouches and checks it before accepting. Set VOUCH_OFFER_ID to the offer ID, VOUCH_SPONSOR_ADDRESS to the sponsor wallet you expect, and VOUCH_AMOUNT_MICRO to the agreed amount (for example, 500000000 for $500). Obtain these expectations from your sponsor or agent configuration, not from the offer being checked.

In GET /api/vouch/status, each offered commitment includes:

  • sponsorWalletAddress: the sponsor wallet address; sponsorId is the sponsor’s account ID, not its wallet address.
  • borrowerWalletAddress: the intended recipient wallet. The offer is tied to this address even before the recipient signs up.
  • borrowerId: the recipient’s Credifi account ID after verified wallet ownership is established; it is null for an offer awaiting signup or wallet verification.
  • amountMicro: the original offered amount in micro-dollars.
  • reservedMicro: the credit currently reserved for the commitment, also in micro-dollars.
  • remainingMicro: the unused credit available for another loan, in micro-dollars. Reserved credit can also include backing for existing loans.

The example checks the offer ID, recipient, sponsor, original amount, and reserved credit before sending the acceptance request. Accepting records consent to use the credit; it doesn’t take out a loan or move funds.

const expectedOfferId = process.env.VOUCH_OFFER_ID;
if (!expectedOfferId) throw new Error('Set VOUCH_OFFER_ID to the sponsor offer ID');
if (!process.env.VOUCH_SPONSOR_ADDRESS) throw new Error('Set VOUCH_SPONSOR_ADDRESS');
const expectedSponsorAddress = ethers.getAddress(process.env.VOUCH_SPONSOR_ADDRESS);
const expectedAmountMicro = Number(process.env.VOUCH_AMOUNT_MICRO);
if (!Number.isSafeInteger(expectedAmountMicro) || expectedAmountMicro < 5000000) {
  throw new Error('Set VOUCH_AMOUNT_MICRO to the agreed amount in micro-dollars');
}
const status = await agentApi('/api/vouch/status');
if (!status.enabled) throw new Error('Vouch credit is unavailable');
const incomingOffer = status.commitments.find(
  c =>
    c.id === expectedOfferId &&
    c.status === 'offered' && c.borrowerWalletAddress?.toLowerCase() === agentAddress.toLowerCase()
);
if (!incomingOffer) throw new Error('No incoming vouch found');
if (incomingOffer.sponsorWalletAddress?.toLowerCase() !== expectedSponsorAddress.toLowerCase()) {
  throw new Error('Vouch sponsor does not match the expected wallet');
}
if (incomingOffer.amountMicro !== expectedAmountMicro) {
  throw new Error('Vouch amount does not match the agreed amount');
}
if (incomingOffer.reservedMicro !== expectedAmountMicro) {
  throw new Error('Vouch reserved credit does not match the agreed amount');
}

await agentApi('/api/vouch/action', {
  method: 'POST',
  body: { commitmentId: incomingOffer.id, action: 'accept' },
});
const acceptedStatus = await agentApi('/api/vouch/status');
const acceptedCommitment = acceptedStatus.commitments.find(
  c => c.id === incomingOffer.id && c.status === 'accepted'
);
if (!acceptedCommitment) throw new Error('Vouch acceptance is not reflected yet');
const { markets: availableMarkets } = await agentApi('/api/v2/markets/list');
const market = availableMarkets.find(
  m => String(m.chainId) === String(acceptedCommitment.chainId) &&
       m.marketId === acceptedCommitment.marketId
);
if (!market) throw new Error('The accepted vouch market is unavailable');

The code fetches the status again after accepting. The accepted commitment gives you the chainId, marketId, and remainingMicro used in the next steps.

Use your vouch

One vouch can back multiple loans up to its total amount. Each loan must fit within the commitment’s unused remainingMicro and use an eligible market and its recipient wallet. Credifi won’t combine several vouches, or a vouch and your personal credit, to fund a single loan.

When a loan is created, its backing is deducted from remainingMicro. The commitment can remain active while its unused amount funds another loan. Either party can cancel the unused amount, including after a loan starts. Backing for existing loans stays reserved until each loan is fully repaid or recorded as defaulted. Repayment releases that loan’s backing to the voucher; it does not restore the recipient’s spent vouch amount.

Authorize the wallet

Before the first loan in a market, the borrower wallet must authorize the Credifi adaptor as an EVC operator. This is an onchain transaction, so it needs gas on that chain. It’s a separate permission from the login signature.

The steps:

  1. Read GET /api/v2/markets/list and find the market assigned to the accepted vouch. It supplies chainId, marketId, and vaultId.
  2. Call GET /api/v3/user/flags?includeEvcAuthorizations=true. Read evcAuthorizations["<chainId>-<vaultId>"] for the selected vault.
  3. If isAuthorized is already true, continue to the quote. Otherwise, the entry supplies txnData with to, data, and value.
  4. Check the transaction against your signing policy, then sign and broadcast it with your agent’s wallet on that chain. Wait for confirmation and read the authorization status again.
// market: an entry from /api/v2/markets/list for the accepted vouch.
// signer: the same wallet used for API login, connected to that chain.
// approveAuthorization: your policy check of the EVC contract, configured
// Credifi adaptor, wallet address prefix, and operator permissions in txnData.
async function authorizeWallet(agentApi, signer, market, approveAuthorization) {
  const path = '/api/v3/user/flags?includeEvcAuthorizations=true';
  const key = `${market.chainId}-${market.vaultId}`;
  const flags = await agentApi(path);
  const authorization = flags.evcAuthorizations?.[key];
  if (!authorization) throw new Error('Authorization status unavailable');
  if (authorization.isAuthorized) return;
  if (!authorization.txnData) throw new Error('Authorization transaction unavailable');

  const network = await signer.provider.getNetwork();
  if (String(network.chainId) !== String(market.chainId)) {
    throw new Error('Signer is on a different chain');
  }
  if (!(await approveAuthorization(authorization.txnData, market))) {
    throw new Error('Authorization rejected by signing policy');
  }
  const tx = await signer.sendTransaction({
    to: authorization.txnData.to,
    data: authorization.txnData.data,
    value: BigInt(authorization.txnData.value),
  });
  const receipt = await tx.wait();
  if (!receipt || receipt.status !== 1) throw new Error('Authorization failed');
  const updated = await agentApi(path);
  if (!updated.evcAuthorizations?.[key]?.isAuthorized) {
    throw new Error('Authorization not yet reflected; recheck before borrowing');
  }
}

Use authorizeWallet(agentApi, agentSigner, market, approveAuthorization) with the market found after accepting the vouch and your own signing-policy callback. The combined example below calls this before requesting a loan.

The authorization lets the Credifi adaptor operate every EVC subaccount under the wallet’s address prefix, not just the one used for this loan, so your signing policy needs to allow that scope. It stays valid for later loans. A new vault, or a permission you revoked, may need another authorization transaction. The sponsor never gets permission to sign for your wallet.

Quote and borrow

By this point the vouch is accepted and the wallet is authorized. Check GET /api/v3/user/flags. When loanTwoFactorRequired is true, complete email/SMS 2FA before creating the loan; an agent can do this through the API if it can retrieve the code.

  1. Read authenticated markets and select the accepting market attached to the vouch.
  2. Request a quote for the exact amount in dollars.
  3. Check the quoted rate, fee, repayment schedule, and expiry against the agent’s policy.
  4. Submit the quote ID and the vouched recipient address to create the loan.
  5. Check the loan status and disbursement record. An HTTP success response alone does not mean funds have arrived.

Complete 2FA when required

  1. Read loanTwoFactorAvailableChannels from GET /api/v3/user/flags and choose an available channel.
  2. Send POST /api/user/challenge-request with { "channel": "email" } or { "channel": "sms" }. The backend chooses the account’s registered contact; you cannot supply a different destination.
  3. Retrieve the new six-digit code using your email/SMS integration.
  4. Send POST /api/user/challenge-verify with { "code": "123456" }. Only proceed after success. A failed verification returns HTTP 400.

The code is delivered outside Credifi’s API; your readCode callback must retrieve that newly requested code from the correct inbox. Completed loan 2FA is valid for ten minutes. loanTwoFactorRequired remains true even after successful verification because it describes the account’s policy, not the completed challenge. The loan endpoint checks completion when you submit the loan.

async function completeLoan2fa(agentApi, { channel, readCode } = {}) {
  const flags = await agentApi('/api/v3/user/flags');
  if (flags.loanTwoFactorRequired === false) return;
  if (flags.loanTwoFactorRequired !== true) throw new Error('Loan 2FA status unavailable');
  if (typeof readCode !== 'function') {
    throw new Error('Configure email/SMS code retrieval or vouch for a separate agent wallet');
  }
  const selectedChannel = channel ?? flags.loanTwoFactorAvailableChannels?.[0];
  if (!flags.loanTwoFactorAvailableChannels?.includes(selectedChannel)) {
    throw new Error('No usable loan 2FA channel; review the account contact details');
  }
  await agentApi('/api/user/challenge-request', {
    method: 'POST', body: { channel: selectedChannel },
  });
  const code = await readCode({ channel: selectedChannel });
  if (typeof code !== 'string' || !/^\d{6}$/.test(code)) {
    throw new Error('Expected the newly received six-digit 2FA code');
  }
  await agentApi('/api/user/challenge-verify', {
    method: 'POST', body: { code },
  });
}

The borrowing helper below calls this before requesting a quote. Pass twoFactor: { channel, readCode } for an account that requires 2FA; omit it for a credits-only account. It does not create a loan when code retrieval or verification fails. Each request refreshes GET /api/vouch/status and selects the commitment by ID so the helper checks its current unused amount.

async function requestLoan(agentApi, { address, commitment, amount, maxAprPercent, approveQuote, twoFactor, authorizeMarket }) {
  const flags = await agentApi('/api/v3/user/flags');
  if (![5, 100].includes(flags.minimumLoanAmount)) {
    throw new Error('Minimum loan amount unavailable');
  }
  const commitmentId = commitment?.id;
  const vouchStatus = await agentApi('/api/vouch/status');
  if (!vouchStatus.enabled || !Array.isArray(vouchStatus.commitments)) {
    throw new Error('Vouch credit is unavailable');
  }
  commitment = vouchStatus.commitments.find(c => c.id === commitmentId);
  if (!commitment || !['accepted', 'active'].includes(commitment.status)) {
    throw new Error('Vouch is not accepted');
  }
  if (commitment.borrowerWalletAddress?.toLowerCase() !== address.toLowerCase()) {
    throw new Error('Vouch belongs to a different wallet');
  }
  if (
    !Number.isFinite(amount) ||
    amount < flags.minimumLoanAmount ||
    !Number.isSafeInteger(commitment.remainingMicro) ||
    Math.round(amount * 1000000) > commitment.remainingMicro
  ) {
    throw new Error('Amount is outside the vouch limit');
  }

  const { markets } = await agentApi('/api/v2/markets/list');
  const market = markets.find(
    m =>
      m.userAccessible &&
      m.acceptsNewBorrowers &&
      String(m.chainId) === String(commitment.chainId) &&
      m.marketId === commitment.marketId
  );
  if (!market) throw new Error('The vouch market is unavailable');
  if (authorizeMarket) await authorizeMarket(market);
  await completeLoan2fa(agentApi, twoFactor);

  const quote = await agentApi('/api/v2/loans/preview-schedule', {
    method: 'POST',
    market,
    body: { amount },
  });
  // interestRate is a percentage: 15 means 15% APR.
  if (quote.loan.interestRate > maxAprPercent) throw new Error('APR exceeds policy');
  if (quote.fee.feeMicro !== 0) throw new Error('Expected a zero-fee loan');
  if (new Date(quote.quoteExpiresAt).getTime() <= Date.now()) {
    throw new Error('Request a fresh quote');
  }
  if (!(await approveQuote(quote))) throw new Error('Quote rejected by policy');
  return agentApi('/api/v4/loans/create', {
    method: 'POST',
    market,
    body: { amount, disburseTo: address, quoteId: quote.quoteId },
  });
}

This helper combines authorization and borrowing. It refreshes the vouch and validates the requested amount and market before asking for wallet authorization. Authorization finishes before loan 2FA starts, keeping the ten-minute verification window available for quoting and loan creation. It runs only when you call it with your own approval callbacks:

async function borrowWithAcceptedVouch({ amount = 500, approveAuthorization, approveQuote, twoFactor }) {
  const flags = await agentApi('/api/v3/user/flags');
  if (flags.loanTwoFactorRequired !== false && flags.loanTwoFactorRequired !== true) {
    throw new Error('Loan 2FA status unavailable');
  }
  if (flags.loanTwoFactorRequired && typeof twoFactor?.readCode !== 'function') {
    throw new Error('Configure email/SMS code retrieval or vouch for a separate agent wallet');
  }
  return requestLoan(agentApi, {
    address: agentAddress,
    commitment: acceptedCommitment,
    amount,
    maxAprPercent: 25,
    approveQuote,
    twoFactor,
    authorizeMarket: selectedMarket =>
      authorizeWallet(agentApi, agentSigner, selectedMarket, approveAuthorization),
  });
}

Call borrowWithAcceptedVouch({ amount: 300, approveAuthorization, approveQuote }) for an account without loan 2FA. The helper defaults to $500 if you omit amount; pass the amount you intend to borrow on every later call. For an account that requires 2FA, also pass twoFactor: { channel: "email", readCode } after defining your code-retrieval callback and choosing an available channel. Read the returned loan and reconcile its disbursement before treating funds as received.

approveAuthorization and approveQuote are callbacks you write. Each should return true only for transactions and terms your policy allows. approveQuote receives the full quote, so it can check quote.loan.loanTerms, quote.paymentSchedule, and anything else before the loan is created.

fee is the quote's origination-fee breakdown, not an additional repayment charge. Credifi Zero currently charges no origination fee: fee.feeMicro is 0 and fee.netProceedsMicro equals fee.principalMicro. The example rejects a nonzero fee to enforce that expectation. Interest still accrues at the quoted rate, and your chain's gas fees are separate.

The client-side checks above are for your agent’s own safety. They don’t reserve credit. The server checks credit and eligibility again when the loan is created, and a vouched loan can only pay out to the wallet on its commitment.

Responses and retries

New crypto loans funded entirely by vouched or earned credits skip manual fraud review when the account qualifies for the loan 2FA exemption: no linked bank and a known zero approved base credit limit. First-time or new IP addresses do not create a review hold for those loans. The server still checks credit backing, wallet authorization, account restrictions, market availability, and liquidity.

Accounts with a linked bank or an approved base limit retain their existing verification and review requirements. Loans already pending review are not automatically released by this policy.

Loan statuses:

  • approved: the loan is approved but the payout may not be finished. Check its progress before assuming funds were sent.
  • active: check the disbursement record and the confirmed transaction before treating the funds as spendable.
  • pending_fraud_review: the loan is under review. Don’t submit another loan to get around it.

API errors expose code and optional details; the request helper preserves them as error.code and error.details, along with error.status. Match the following literal codes:

Older backend versions may return ETHOS_LOAN_2FA_REQUIRED or ETHOS_LOAN_2FA_EMAIL_REQUIRED during an upgrade. Handle them exactly like ANON_LOAN_2FA_REQUIRED or ANON_LOAN_2FA_EMAIL_REQUIRED, respectively, by completing the same email/SMS verification flow.

error.codeWhat your agent should do
ANON_LOAN_2FA_EMAIL_REQUIRED, ANON_LOAN_2FA_REQUIRED, BANK_LINKED_LOAN_2FA_CONTACT_REQUIRED, BANK_LINKED_LOAN_2FA_REQUIREDComplete email/SMS 2FA through the challenge endpoints before borrowing. If the account has no usable contact or your agent cannot read its inbox, resolve the contact requirement or vouch for a separate credits-only agent wallet.
EVC_UNAUTHORIZEDRun authorizeWallet(agentApi, agentSigner, market, approveAuthorization) again, then retry after authorization is confirmed.
LOAN_BELOW_MINIMUMRead minimumLoanAmount from GET /api/v3/user/flags and request at least that amount.
LOAN_EXCEEDS_CREDITRead details.maxAmount (dollars). Refresh GET /api/vouch/status for commitments and GET /api/v2/user-balance for credit.availableCredit (micro-dollars), then obtain a new quote within the limit.
VOUCH_EXCEEDS_CAPACITYRefresh vouch status; one eligible accepted commitment must cover the amount, market, and recipient. Request less or obtain a new vouch.
VOUCH_ACCOUNT_FROZEN, USER_BORROWING_BLOCKEDStop borrowing. The account is restricted; do not retry until the restriction is resolved.
VOUCH_SPONSOR_SUSPENDEDStop using the sponsor’s backing; refresh status and obtain eligible backing before another request.
VOUCH_MARKET_UNAVAILABLERefresh markets and vouch status; the requested market is unavailable for this offer.
INSUFFICIENT_LIQUIDITYThe vault cannot fund that amount now. Refresh market liquidity before requesting and reviewing a new quote.
VOUCH_RECIPIENT_WALLET_MISMATCHUse the recipient wallet recorded on the commitment.
VOUCH_PILOT_DISABLED, VOUCH_UNAVAILABLEStop this attempt; vouch access or the commitment is unavailable. Refresh status before proceeding.
LOAN_QUOTE_REQUIREDThe quote is missing, expired, or no longer matches the request. Request a fresh quote and review its terms.
LOAN_QUOTE_CONSUMEDThe quote has already been used. Read the existing loan (using details.loanId when present); do not submit a replacement loan.

For UNSPECIFIED, missing codes, or unknown codes, stop and record the response for investigation. Do not infer a retry policy from message text. This list isn’t exhaustive.

If a create request times out, check GET /api/v3/loans/list before retrying. The create endpoint has no idempotency key, so retrying blindly with a new quote can create a second loan.

Repay

Your agent’s wallet pays the loan onchain. Afterward, a call to Credifi’s API updates the recorded balance and payment history. The API never moves funds or signs payments for you.

  1. Read GET /api/v3/loans/list and select the loan by its API id. Use its disbursement.chainId, marketId, loanOwner, and blockchainLoanId for repayment.
  2. Match that market with GET /api/v2/markets/list, then read GET /api/v4/chain/info?all=true. The response is keyed by chain ID. Find the vault with id === market.vaultId; merge chain.addresses with vault.addresses. The last entry for CREDIFI_ADAPTOR and CREDIFI1155 gives the current contract address and ABI version. vault.borrowAsset names the token entry under the merged addresses, with address, symbol, and decimals.
  3. On the loan's chain, call the adaptor's getLoanDetails(loanOwner, blockchainLoanId) to read currentDebt and the loan's borrowVault. Call that vault's asset() to identify the repayment token, then the token's decimals() for base-unit conversion. Check the token and vault against the configured market and the loan's disbursement.tokenAddress.
  4. Call the token's allowance(payer, adaptorAddress). If necessary, call approve(adaptorAddress, maxSpendBaseUnits) and wait for confirmation. The spender is the adaptor, not Credifi1155. Then call Credifi1155.makeLoanPayment(loanOwner, blockchainLoanId, repayAmount).
  5. Wait for the repayment receipt, refresh through POST /api/chain/indexer, and fetch the API loan again. Also reread getLoanDetails to confirm the live remaining debt. A successful payment can be partial.

Use the blockchain loan ID for contract calls and the API loan ID to select the record. They are not interchangeable. Read the current market configuration rather than copying an address from another chain. The helper below supports the currently configured .v2 contracts; it stops if the ABI version changes.

Repayment helper

Supply apiLoanId from the selected entry in await agentApi('/api/v3/loans/list'), your chain-connected agentSigner, and a maxSpendTokens string that caps the token allowance. Supply amountTokens as a decimal string for a partial payment. Omit it for full payoff: MaxUint256 asks the current adaptor to repay the debt at execution time, while the finite token allowance bounds what it can spend. If that allowance is too small for accrued debt, the transaction reverts; reread debt before retrying. Do not grant an unlimited token allowance.

approveRepayment is your integration's signing-policy callback. It receives the resolved contracts, token, chain, debt, and spend cap and must approve them before the helper sends either transaction. The helper does not broadcast anything when the callback rejects the request.

async function repayLoan(agentApi, signer, {
  apiLoanId, maxSpendTokens, amountTokens, approveRepayment,
}) {
  const loans = await agentApi('/api/v3/loans/list');
  const loanRecord = loans.find(loan => loan.id === apiLoanId);
  if (!loanRecord) throw new Error('Loan not found');
  const disbursement = loanRecord.disbursement;
  if (!disbursement.loanOwner || !disbursement.blockchainLoanId) {
    throw new Error('Loan has not been disbursed onchain');
  }
  const { markets } = await agentApi('/api/v2/markets/list');
  const repaymentMarket = markets.find(m =>
    String(m.chainId) === String(disbursement.chainId) &&
    m.marketId === disbursement.marketId
  );
  if (!repaymentMarket) throw new Error('Loan market not found');
  const network = await signer.provider.getNetwork();
  if (String(network.chainId) !== String(repaymentMarket.chainId)) {
    throw new Error('Signer is on a different chain');
  }
  const chains = await agentApi('/api/v4/chain/info?all=true');
  const chain = chains[String(repaymentMarket.chainId)];
  const vault = chain?.vaults.find(v => v.id === repaymentMarket.vaultId);
  if (!vault) throw new Error('Loan vault configuration unavailable');
  const addresses = { ...chain.addresses, ...vault.addresses };
  const latest = name => addresses[name]?.at(-1);
  const adaptorEntry = latest('CREDIFI_ADAPTOR');
  const loanContractEntry = latest('CREDIFI1155');
  const borrowVaultEntry = latest('BORROW_VAULT');
  const tokenEntry = latest(vault.borrowAsset);
  if (adaptorEntry?.abi !== 'CredifiERC20Adaptor.v2' ||
      loanContractEntry?.abi !== 'Credifi1155.v2' ||
      !borrowVaultEntry || !tokenEntry) {
    throw new Error('Unsupported or incomplete market configuration');
  }
  const adaptor = new ethers.Contract(adaptorEntry.address, [
    'function getLoanDetails(address user,uint256 loanId) view returns ((uint256 loanId,uint256 tokenId,address collateralVault,address borrowVault,uint256 principal,uint256 creditAmount,uint256 timestamp,bytes32 certificateHash,bool active,address subAccount,uint256 dueDate,uint256 interestRate,uint256[8] __reserved) loan,uint256 currentDebt,uint256 collateralBalance)',
    'function credifi1155() view returns (address)',
  ], signer);
  const loanContract = new ethers.Contract(loanContractEntry.address, [
    'function makeLoanPayment(address user,uint256 loanId,uint256 repayAmount)',
  ], signer);
  const blockchainLoanId = BigInt(disbursement.blockchainLoanId);
  const details = await adaptor.getLoanDetails(disbursement.loanOwner, blockchainLoanId);
  if (details.loan.borrowVault.toLowerCase() !== borrowVaultEntry.address.toLowerCase()) {
    throw new Error('Loan vault does not match the market');
  }
  const linkedLoanContract = await adaptor.credifi1155();
  if (linkedLoanContract.toLowerCase() !== loanContractEntry.address.toLowerCase()) {
    throw new Error('Loan contract does not match the adaptor');
  }
  const borrowVault = new ethers.Contract(details.loan.borrowVault, [
    'function asset() view returns (address)',
  ], signer);
  const tokenAddress = await borrowVault.asset();
  if (tokenAddress.toLowerCase() !== tokenEntry.address.toLowerCase() ||
      tokenAddress.toLowerCase() !== disbursement.tokenAddress.toLowerCase()) {
    throw new Error('Repayment token does not match the loan');
  }
  const token = new ethers.Contract(tokenAddress, [
    'function decimals() view returns (uint8)',
    'function allowance(address owner,address spender) view returns (uint256)',
    'function approve(address spender,uint256 value) returns (bool)',
  ], signer);
  const decimals = Number(await token.decimals());
  if (decimals !== tokenEntry.decimals) throw new Error('Token decimals mismatch');
  const maxSpend = ethers.parseUnits(maxSpendTokens, decimals);
  const repayAmount = amountTokens === undefined
    ? ethers.MaxUint256
    : ethers.parseUnits(amountTokens, decimals);
  if (maxSpend <= 0n || repayAmount <= 0n ||
      (amountTokens !== undefined && repayAmount > maxSpend)) {
    throw new Error('Invalid repayment or spend cap');
  }
  if (details.currentDebt === 0n) throw new Error('Loan has no live debt');
  if (amountTokens === undefined && details.currentDebt > maxSpend) {
    throw new Error('Live payoff exceeds the approved spend cap');
  }
  if (!(await approveRepayment({
    apiLoanId, chainId: repaymentMarket.chainId,
    loanOwner: disbursement.loanOwner, blockchainLoanId,
    adaptorAddress: adaptorEntry.address,
    loanContractAddress: loanContractEntry.address,
    tokenAddress, decimals, currentDebt: details.currentDebt, maxSpend, repayAmount,
  }))) throw new Error('Repayment rejected by signing policy');
  const payer = await signer.getAddress();
  const allowance = await token.allowance(payer, adaptorEntry.address);
  // Keep the allowance at this request's finite cap, including reducing an old one.
  if (allowance !== maxSpend) {
    if (allowance > 0n) {
      const reset = await token.approve(adaptorEntry.address, 0n);
      const resetReceipt = await reset.wait();
      if (!resetReceipt || resetReceipt.status !== 1) throw new Error('Allowance reset failed');
    }
    const approval = await token.approve(adaptorEntry.address, maxSpend);
    const approvalReceipt = await approval.wait();
    if (!approvalReceipt || approvalReceipt.status !== 1) throw new Error('Approval failed');
  }
  const transaction = await loanContract.makeLoanPayment(
    disbursement.loanOwner, blockchainLoanId, repayAmount
  );
  const receipt = await transaction.wait();
  if (!receipt || receipt.status !== 1) throw new Error('Repayment failed');
  // If this refresh fails, retry the refresh, not the already confirmed payment.
  await agentApi('/api/chain/indexer', {
    method: 'POST', market: repaymentMarket, body: { indexerType: 'loans' },
  });
  const updatedLoans = await agentApi('/api/v3/loans/list');
  const updatedLoan = updatedLoans.find(loan => loan.id === apiLoanId);
  if (!updatedLoan) throw new Error('Updated loan not found');
  const after = await adaptor.getLoanDetails(disbursement.loanOwner, blockchainLoanId);
  return { txHash: transaction.hash, updatedLoan, currentDebt: after.currentDebt };
}

Here is the full call with a 510-token payoff cap. selectedLoan is the entry you select from GET /api/v3/loans/list, and approveRepayment is your signer policy:

async function payOffSelectedLoan(selectedLoan, approveRepayment) {
  return repayLoan(agentApi, agentSigner, {
    apiLoanId: selectedLoan.id,
    maxSpendTokens: '510',
    approveRepayment,
  });
}

Call payOffSelectedLoan(selectedLoan, approveRepayment) only when a 510-token cap is within your policy; adjust the cap in the example for another limit. Use ethers.formatUnits(currentDebt, decimals) to display debt; keep the actual transaction value as a bigint. Confirm zero live debt and the refreshed API status before treating the loan as paid off.

If the refresh fails after a confirmed payment, retry the refresh and check the loan before sending another payment. Credit and vouch balances update only after the loan record does.

New integrations should use the indexer refresh. The older POST /api/payments/direct-crypto endpoint (see the API reference) does the same job and is still used by the Credifi app.

Credits and rewards

Credits grow when qualifying loans are fully repaid and their onchain repayments are confirmed. The calculation uses interest actually paid, not the loan principal, the quoted APR, or interest that has not been paid. Partial repayments do not grant these credits or rewards yet.

Credits increase your capacity

Earned credits let you borrow using your own capacity or vouch for another wallet. They are recorded in dollars of capacity; they are not tokens deposited in your wallet and cannot be withdrawn as cash.

Vouched credit is different: it can back multiple loans within its unused amount. Repaying a loan releases that loan’s backing to the voucher. Any unused vouch amount remains available until it is drawn or cancelled; repaying a loan does not refill it. The recipient keeps the credits they earned from repayment, but does not keep the original vouch amount as permanent personal credit.

The current allocations are:

Allocation from paid interestVouched loanLoan using your own capacity
Borrower earns credits40%65%
Voucher earns credits15%None
Voucher earns a dollar reward10%None

These percentages describe the credit and reward allocations. Credit allocations increase capacity; only the voucher's dollar reward is an onchain payout. Amounts are rounded down to whole micro-dollars.

Example: a vouched loan is repaid

Alice has $1,200 in earned credits and vouches $1,000 for an agent's wallet. That reserves $1,000 of her capacity, leaving $200 available for another vouch or her own borrowing. The agent accepts the vouch and borrows $1,000.

Assume the agent fully repays the $1,000 principal plus $100 in actual interest. This is an illustrative interest amount, not a quote for a particular term. Once settlement completes:

WhoResult
Agent / borrowerEarns $40 in credits: 40% of $100.
Alice / voucherEarns $15 in credits: 15% of $100. Her earned-credit balance grows from $1,200 to $1,215.
Alice / voucherEarns a separate $10 reward: 10% of $100. For a USDC loan, this is 10 USDC awaiting payout.
Alice's reserved backingThe $1,000 reservation is released. With no other loans or vouches, her available capacity is now $1,215.

The agent now has $40 of its own earned capacity, assuming no other obligations. The repaid $1,000 vouch is finished; it is not added to that $40. Alice can make a new vouch using her released capacity.

Example: credits grow through your own borrowing

The agent uses $20 of its $40 earned capacity for a new loan, without a vouch. Assume it fully repays $20 principal plus $2 in interest. It earns $1.30 in credits: 65% of $2.

Its earned-credit balance grows from $40 to $41.30. After the loan is settled, the $20 used for that loan is available again. With no other obligations, the agent can borrow against $41.30 or use up to $41.30 to vouch for another wallet. There is no voucher dollar reward on this loan.

Each qualifying repayment can add more earned credits. Available capacity also depends on outstanding loans, credit already reserved for vouches, and account eligibility. Received vouches cannot be passed on as your own vouch capacity.

When dollar rewards are paid

The voucher's reward is calculated when the vouched loan is fully repaid and settled. It is then recorded as payable. The reward is paid separately from Credifi's reward treasury, in the loan's token, to the voucher's account wallet. A payable reward is not proof that funds have arrived; it becomes paid only after its onchain payout is confirmed. Repayment does not automatically send the reward, and there is currently no agent API endpoint to claim it.

If a loan defaults, its reserved backing is consumed and that voucher's new vouches and unpaid rewards are suspended. Unpaid rewards can be forfeited. Repayment credits and rewards are recorded once per settled loan; retrying a refresh does not earn them twice.

Check balances through the API

Read GET /api/vouch/status after refreshing the loan record. Amounts below are integer micro-dollars: 1000000 means $1 of credits or a $1 reward amount, depending on the field.

FieldMeaning
earnedMicroYour accumulated earned credits, including capacity currently in use.
personalAvailableMicroYour own available borrowing capacity after outstanding loans and vouch reservations.
vouchedMicroYour capacity reserved to back outgoing vouches.
availableToVouchMicroCapacity currently available for new vouches.
rewardPayableMicroVoucher dollar rewards awaiting payout.
rewardHeldMicroUnpaid rewards currently held because the voucher is suspended.
rewardPaidMicroVoucher dollar rewards whose payouts have been confirmed.

Use GET /api/v2/user-balance for the overall available borrowing limit, and the accepted commitment in vouch status for a vouched loan. Your own capacity and a received vouch are not added together to fund a single loan.

API reference

Authenticated endpoints use Authorization: Bearer <token>. Quote and creation requests also require x-chain-id and x-market-id.

Authentication

  • GET /api/auth/wallet-login?address=<address> — request a signing message.
  • POST /api/auth/wallet-login — exchange { message, signature } for { token, user }. If it returns SIGNUP_CHALLENGE_REQUIRED, complete browser signup before retrying with a fresh message.

Account requirements

  • GET /api/v3/user/flags — minimumLoanAmount is your account’s minimum loan in dollars ($5 or $100). loanTwoFactorRequired indicates whether email/SMS 2FA applies; loanTwoFactorAvailableChannels lists supported contact channels. If required, complete the challenge before loan creation. The loan endpoint checks eligibility and 2FA completion when you submit a request. Add ?includeEvcAuthorizations=true to retrieve per-vault authorization status and unsigned transaction data; entries are keyed by <chainId>-<vaultId>.

  • POST /api/user/challenge-request — send { channel: "email" } or { channel: "sms" } to request a code at the account’s registered contact. Success returns HTTP 200 with no body.

  • POST /api/user/challenge-verify — submit { code: "123456" }; success returns HTTP 200 with no body, failed verification returns HTTP 400. A code must contain six digits. This completes required loan 2FA; it does not create a loan.

Credit and vouches

  • GET /api/v2/user-balance — credit.availableCredit, credit.totalCreditLimit, and credit.currentUsage are integer micro-dollars. For a vouched loan, also check the accepted commitment in vouch status; the account balance does not replace the per-vouch limit.

  • GET /api/vouch/status — policy availability, capacity, and commitments belonging to the authenticated account.

  • POST /api/vouch/commitments — sponsor offers credit using { requestId, recipientWalletAddress, amountMicro }. The recipient can sign up after the offer is created. Keep the same request ID on a retry of the same offer.

  • POST /api/vouch/action — recipient accepts, or either party cancels an unused commitment or unused remainder, using { commitmentId, action }.

  • GET /api/v2/markets/list — available markets, vault liquidity, and userAccessible for the authenticated account. Each market includes chainId, marketId, and vaultId.

  • GET /api/v4/chain/info?all=true — chain-keyed configuration containing contract addresses and ABI versions, vaults, borrow-token addresses, symbols, and decimals. Use this for transaction discovery; it does not grant borrowing eligibility.

Loans

  • POST /api/v2/loans/preview-schedule — quote { amount }; returns quoteId, quoteExpiresAt, fee, loan, paymentSchedule, and scheduleInfo. loan.loanTerms contains the term and repayment type. paymentSchedule is the array of quoted installments; the loan-list endpoint uses a different schedule field for its servicing record.
  • POST /api/v4/loans/create — submit { amount, disburseTo, quoteId }; returns { loan, rewards }.
  • GET /api/v3/loans/list — reconcile the account’s loan statuses, disbursement, and repayments.
  • POST /api/chain/indexer — refresh loan records after a confirmed onchain payment. Send { indexerType: "loans" } with x-chain-id, then fetch your loans to confirm the updated balance. A successful response may have no JSON body.
  • POST /api/payments/direct-crypto — legacy payment-recording endpoint accepting { loanId, txHash }. The transaction must already have confirmed; this endpoint does not transfer repayment funds.

Credit limits

A vouch is an offer of vouched credit, not a payment to your wallet. Accept the offer to make it available for a loan request. Funds arrive from the onchain lending vault only after you request a loan and Credifi approves its origination.

How much can I borrow?

Up to the unused remainingMicro of one accepted or active vouch, in an eligible market, with a $5 minimum for credits-only accounts or $100 for accounts with a bank link or an approved base limit. Check minimumLoanAmount in GET /api/v3/user/flags. Vouches aren’t pooled, and personal credit isn’t added on top. The quote shows the rate, fee, term, and payment schedule before you commit.

Can I take another loan under the same vouch?

Yes, if that vouch still has enough unused capacity. For example, borrowing $300 from a $500 vouch leaves $200 available for another loan. Refresh vouch status and check remainingMicro before each request. Repaying a loan releases its backing to the voucher; it does not refill the recipient’s vouch amount.

Is a vouch enough to start borrowing immediately?

A vouch provides the credit, but does not take out a loan automatically. You do not need to create an account before someone vouches for your address. Your agent can sign up afterward with that wallet, prove ownership, see and accept the incoming vouch, then authorize the wallet for its market and request a loan.

A credits-only account with no bank-link history or approved base credit limit does not need email/SMS loan 2FA. Check loanTwoFactorRequired: if it is true, complete 2FA through the API using the account’s inbox, or have an eligible sponsor vouch for a separate credits-only agent wallet. A new wallet may need browser signup once. After signup, the agent can borrow and repay through the API and its own signer without using the frontend.

Even then, a vouch doesn’t guarantee funding. Credifi checks available credit, eligibility, market availability, and liquidity on every request.