Credit REST API
The Credit API lets any agent — regardless of language or framework — access Floe's instant credit facilities via HTTP. It returns unsigned transaction calldata that agents sign and submit with their own wallet.
See also: API Keys | Webhooks | Developer Dashboard
Base URL: https://credit-api.floelabs.xyz
Token Decimals
All amounts in the API are in raw token base units (smallest unit for each token). Using the wrong decimal precision is the most common integration bug.
USDC
6
1000000
5,000 USDC = "5000000000"
USDT
6
1000000
100 USDT = "100000000"
WETH
18
1000000000000000000
2 WETH = "2000000000000000000"
cbBTC
8
100000000
0.5 cbBTC = "50000000"
Try It Now
See live lender offers on Base — no auth required:
# WETH/USDC market
curl "https://credit-api.floelabs.xyz/v1/credit/offers?marketId=0xfe92656527bae8e6d37a9e0bb785383fbb33f1f0c7e29fdd733f5af7390c2930"
# Or browse all markets at once
curl "https://credit-api.floelabs.xyz/v1/credit/offers"Markets
Market IDs are keccak256(abi.encode(loanToken, collateralToken)). Use GET /v1/markets to discover all available market IDs.
WETH/USDC
0xfe92656527bae8e6d37a9e0bb785383fbb33f1f0c7e29fdd733f5af7390c2930
WETH (18 dec)
USDC (6 dec)
cbBTC/USDC
0xbd0fb0e71705bfb3cc5c5552d9276e6617761b37353bd9e1b37bb65c3af2d7f7
cbBTC (8 dec)
USDC (6 dec)
Token addresses (Base mainnet):
WETH
0x4200000000000000000000000000000000000006
18
USDC
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
6
cbBTC
0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf
8
You can also discover markets programmatically via GET /v1/markets.
Public Endpoints
GET /v1/markets
List all active lending markets. Public — no auth required.
Response:
GET /v1/credit/offers
Query available lend intents. Public — no auth required.
marketId
bytes32
No
Filter by market. Omit to see offers across all markets.
minAmount
string
No
Minimum remaining amount (raw units)
maxRateBps
string
No
Maximum interest rate in basis points
maxResults
string
No
Max offers to return (default: 50)
Response:
GET /v1/markets/:marketId/cost-of-capital
Snapshot of the borrowing rate available in a market right now. Public — no auth required.
Without borrowAmount, returns the rate floor + total available liquidity ("what's the cheapest quote anyone is offering?"). With borrowAmount, walks the offer ladder to compute the actual weighted rate the caller would pay ("what would I actually pay if I borrowed N right now?").
borrowAmount
string
No
Raw token units. If omitted, response is quote-only (no implied rate).
duration
string
No
Loan duration in seconds. When set, only offers whose [minDuration, maxDuration] covers this value count.
Response (quote-only):
Response (with borrowAmount, fillable):
bestRateBps
string | null
Lowest viable rate among offers. null when no offers exist (distinct from a real 0 bps quote).
impliedRateBps
string | null
Weighted-average rate at which borrowAmount would fill. null when omitted OR when liquidity is insufficient.
impliedFillBreakdown
array (optional)
Per-offer slices that compose the fill. Only present when impliedRateBps is non-null.
availableLiquidity
string
Sum of remainingAmount across viable offers (after duration filter).
offerCount
number
Count of viable offers.
fetchedAt
number
Unix seconds when the snapshot was assembled.
Insufficient-liquidity case: if
borrowAmount > availableLiquidity, the response setsimpliedRateBps: nulland omitsimpliedFillBreakdown.bestRateBpsandavailableLiquiditystill reflect what's there — use both to decide whether to wait or downsize the borrow.
Why bigint as string? Rates and amounts are returned as decimal strings to avoid JSON-number precision loss past 2^53. Parse with
BigInt(...)in TypeScript orint(...)in Python.
GET /v1/health
Authentication
All endpoints below require authentication. The endpoints above are public.
Two distinct contexts call this API, and they use different credentials. Pick the row that matches what you're doing:
Developer / management — humans and tooling that provision and manage agents, mint keys, open credit lines, review history
POST /v1/developer/agents, /keys, /open-credit-line, list / rotate / revoke / close, plus everything under /credit/*, /positions/*, /borrow, /repay
Any one of: a floe_live_* developer key, a wallet-signature header set (X-Wallet-Address + X-Signature + X-Timestamp), or a dashboard session cookie — pick whichever fits your stack
Agent runtime — the agent process itself, calling out for paid data
/proxy/fetch, /proxy/check, /x402/estimate, /agents/balance, /agents/transactions, /agents/close, credit-threshold / spend-limit endpoints
The agent's floe_* runtime key, sent as Authorization: Bearer floe_…
The two key prefixes are not interchangeable: a floe_live_* developer key sent to /proxy/fetch will 401, and a floe_* agent key sent to a management endpoint will 401. See API Keys for the canonical table and the rationale.
API Key Authentication
API keys use the Authorization: Bearer header. Generate developer keys through the Developer Dashboard at dev-dashboard.floelabs.xyz or via the developer endpoints below; agent keys are minted by POST /v1/developer/agents/:id/keys.
Keys are scoped to the wallet that created them. All actions performed with an API key are attributed to the owning wallet. See API Keys for key management, rotation, and security best practices.
Wallet Signature Authentication (EIP-191)
For the developer / management endpoints above, you can also authenticate with an EIP-191 / EIP-1271 signed message instead of a floe_live_* key. This is the same credential class — the agentkit SDKs use the signature path so users don't have to obtain a developer key first — and it lets any wallet authenticate without an explicit registration step. It does not unlock the agent runtime endpoints; those still require an agent's floe_* key.
How It Works
Sign the message
Floe Credit API\nTimestamp: <unix_seconds>with your walletInclude three headers in your request:
X-Wallet-Address
Your wallet address (0x...)
X-Signature
The signed message (0x..., 65 bytes)
X-Timestamp
Unix timestamp used in the message
The timestamp must be within 5 minutes of the server time.
Python
TypeScript
Smart Contract Wallets (ERC-1271)
Giza agents, Olas agents, and Safe multisigs authenticate the same way. The API detects smart contract wallets automatically and calls isValidSignature() on your wallet contract to verify the signature.
Authenticated Endpoints
POST /v1/credit/instant-borrow
Build unsigned transactions for an instant borrow. The API selects the best available lender automatically and persists an attempt record so a partial flow (TX1 confirmed but TX2 not yet broadcast) is always recoverable.
borrowAmount
string
Yes
Amount to borrow (raw units)
collateralAmount
string
Yes
Collateral to post (raw units)
maxInterestRateBps
string
Yes
Max acceptable rate (bps). 1200 = 12% APR
duration
string
Yes
Loan duration in seconds. 2592000 = 30 days
minLtvBps
string
No
Min LTV (default: 8000 = 80%)
maxLtvBps
string
No
Max initial LTV (bps). Rejects if oracle-computed LTV exceeds this. 7500 = 75%
Headers:
Idempotency-Key
No
Stripe-style opaque string (≤255 chars). Same key from the same wallet within 24h returns the cached attempt instead of starting a new one. Recommended (UUID v4). Without it the call is non-idempotent — a network retry can register a second on-chain intent and double-spend gas.
Response:
attemptId
Synthetic placeholder ID for the attempt (pending:<uuid>). Pass this to /v1/tx/broadcast and the recovery endpoints below. The attemptId stays the same for the lifetime of the row — even after the loan goes active. The canonical on-chain loanId becomes available via GET /v1/credit/borrow-attempts/:attemptId (in the response's separate loanId field).
status
Lifecycle state. pending_funding for a fresh attempt; later transitions to pending_on_chain, pending_match, matching, active, or one of the terminal states. See the lifecycle diagram below.
reused
true when an idempotent retry returned the cached attempt. When true, transactions is [] and selectedOffer is omitted — the original lender struct isn't persisted, so the API doesn't surface stale fields. Call GET /v1/credit/borrow-attempts/:attemptId for canonical state, or POST .../resume to retry the match phase.
selectedOffer
The lender offer matched at attempt creation. Present on fresh attempts only; omitted when reused: true (see above).
Broadcasting with attempt tracking
When you broadcast each signed transaction, pass the attempt_id and phase so the API can drive the attempt state machine forward:
Both fields are optional and must be provided together. When provided, the broadcast endpoint persists the txHash on the borrow-attempt row before awaiting waitForTransactionReceipt. This closes the receipt-wait timeout gap — if the receipt doesn't arrive within 60s, the row already has the hash and the reconciler can finish reconciliation on its next tick. Without attempt_id, the broadcast endpoint behaves exactly as before; the attempt row stays in pending_funding until the reconciler's expiry sweep terminates it.
Recovery endpoints
The borrow-attempt state machine is recoverable in two failure scenarios:
Scenario A — Client crash between TX1 and TX2. The register tx confirmed and the row is in pending_match, but the client process died (or lost the response) before broadcasting the match tx.
Persistence requirement: crash recovery only works if the client durably stored at least one of (
attemptId,Idempotency-Key) before crashing. With theattemptIdyou can hit the recovery endpoints below directly. With theIdempotency-Keyyou can re-POST/v1/credit/instant-borrowand the API returns the cached attempt (reused: true) — readattemptIdfrom that response and proceed to the recovery endpoints. If neither was persisted, the on-chain intent will simply expire on its own (5–10 min) and you'll need a fresh borrow.
Scenario B — Broadcast endpoint receipt timeout. The client called /v1/tx/broadcast with attempt_id+phase, but waitForTransactionReceipt hit the 60s cap and threw. The tx is live on-chain (it was sent before the wait), the row already has register_tx_hash (or match_tx_hash) thanks to the pre-receipt persist, and the reconciler's runOnce will fetch the receipt and resolve the row on its next tick (default poll interval 30s) — no client action required.
For Scenario A, three explicit endpoints let the client drive the recovery:
All three recovery endpoints share two common error codes:
Status
code
When
404
attempt_not_found
The attempt ID doesn't exist, or the row exists but isn't an instant_borrow attempt (404 instead of 403 to avoid leaking the existence of unrelated rows)
403
forbidden
The authenticated wallet is not the original initiator of the attempt
GET /v1/credit/borrow-attempts/:attemptId — current state, all tx hashes, real loanId once active. The attemptId here is the pending:<uuid> placeholder you received from /v1/credit/instant-borrow — it stays the same for the life of the row, even after the loan goes active (the on-chain loanId is surfaced in the response's loanId field, separate from attemptId).
POST /v1/credit/borrow-attempts/:attemptId/resume — returns a fresh matchLoanIntents unsigned tx using the same registered borrow intent. Re-validates the lend offer on-chain; on conflict, returns 409 with one of:
code
Meaning
Recommended next step
not_resumable
Status is not pending_match (e.g. already active, abandoned, expired)
Call GET /borrow-attempts/:id for current state
lend_intent_revoked
The originally-selected lender slot was zeroed
Call /abandon
lend_intent_expired
Lend offer expired since attempt creation
Call /abandon
lend_intent_insufficient
Lender's remaining capacity is now below the borrow amount
Call /abandon
missing_borrow_struct (500)
Defensive — should never fire
Surface to support
missing_lend_intent_hash (500)
Defensive — should never fire
Surface to support
POST /v1/credit/borrow-attempts/:attemptId/abandon — immediately marks the attempt abandoned and returns up to 2 unsigned txs:
revokeBorrowIntentByHash(borrowIntentHash)— required. Removes the dangling intent on-chain.approve(collateralToken, matcher, 0)— optional (optional: true), only included when a non-zero allowance exists. Recommended to sign and broadcast for full hygiene.
Returns 409 with code: not_abandonable if the attempt is already in a terminal state.
The on-chain intent expires automatically 5–10 minutes after registration, so revoking is for cleanliness; the loan can never be matched after expiry regardless.
Lifecycle
The API drives a borrow-attempt state machine for every /v1/credit/instant-borrow call. When you broadcast each signed transaction with attempt_id + phase, the API persists the txHash before awaiting the receipt — so a 60s receipt-wait timeout never drops state on the floor.
Terminal branches (fired from any non-terminal status):
POST /v1/credit/borrow-attempts/:id/abandon
abandoned
POST /v1/tx/broadcast (register tx reverts)
funding_failed
POST /v1/tx/broadcast (match tx reverts)
match_failed
Borrow intent expired past expiry + 60s buffer (reconciler sweep)
expired
Terminal states for the borrow-attempt state machine (no further transitions within this flow): active, repaid, rolled_over, funding_failed, match_failed, abandoned, expired.
Note on
activevs. the loan lifecycle.activeis terminal for the borrow-attempt flow — once you reach it, the recovery endpoints stop driving the row. But the underlying on-chain loan continues its own lifecycle: it will eventually transition torepaid(when the borrower repays) orrolled_over(when the loan is renewed), both of which are tracked separately in the same row. For loan-level state, queryGET /v1/credit/status/:loanId(using the on-chainloanIdreturned inGET /borrow-attempts/:attemptId) — that's the canonical surface for repayment terms, health factor, and accrued interest.
GET /v1/credit/status/:loanId
Get loan status including health metrics and early repayment terms.
Response:
GET /v1/positions/:wallet
Portfolio view for an address: every active borrower loan + a summary (total debt, weighted-average rate, worst health factor, next maturity).
live
boolean
No
When true, fetches each loan's status from the chain (slower, fully live). Default false — derives from the indexer with live-computed accrued interest but origination LTV.
includePending
boolean
No
When true, includes the borrower's open borrow intents that haven't matched yet. Default false.
limit
number
No
Max active loans to return. Default 200, max 500.
skip
number
No
Pagination offset. Default 0.
pendingLimit
number
No
Max pending intents (when includePending=true). Default 50, max 200.
Response (indexer mode, default):
Response (live mode, ?live=true):
positions[].accruedInterest
string
Computed live from the clock — accurate even in indexer mode.
positions[].currentLtvBps
string
Origination LTV in indexer mode (does NOT reflect price movement). Live oracle LTV when live=true.
positions[].bufferBps
string
Headroom before liquidation: liquidationLtvBps - currentLtvBps (clamped to 0). >0 = safe; 0 = at threshold.
summary.worstHealthFactor
number | null
liquidationLtv / currentLtv; >1 = safe, <1 = liquidatable. Always null in indexer mode because origination LTV would lie about price-driven risk. Set live=true to compute.
summary.weightedAvgInterestRateBps
string | null
Principal-weighted average. null for empty portfolio.
summary.nextMaturityAt
string | null
Earliest startTime + duration across active loans (Unix seconds).
summary.perCollateralToken
object
Map of collateral-token address → total collateral locked (raw units), summed across this wallet's active loans. Useful for "how much WETH am I posting protocol-wide?" questions without iterating positions client-side.
source
"indexer" | "chain"
Where the position data came from.
ltvStale
boolean
true in indexer mode. Accrued interest is unaffected (always live); only LTV-derived fields reflect last-event values.
indexerBlockNumber
string | null
Best-effort indexer head block (when the GraphQL endpoint exposes chain_metadata). null in live mode.
pendingBorrowIntents
array (optional)
Only present when includePending=true. Empty array if no open intents.
When to use
live=true: any time you need an accurate health factor or LTV utilization. Indexer mode is faster and fine for "what do I owe" displays, but the LTV won't move with price. The route runs each loan's status query in parallel with bounded concurrency to keep RPC load sane.
Pagination:
summary.activeLoanCountis the wallet's total loan count and stays constant across pages — don't gate pagination on it. Instead, paginate as long aspositions.length === limit, advancingskipbylimiteach round, and stop when you receive fewer thanlimitresults.
Auth scope (today): any authenticated caller can query any wallet's positions. Borrower addresses and loan IDs are public on-chain data. The signed request still uses your own wallet — only the
:walletpath param identifies whose portfolio you're querying.
503 Service Unavailable: returned when the API instance was started without an Envio indexer endpoint configured.
getPositionscannot run without indexer access (chain-only fallback would be too slow). Call the operations team or retry against another instance.
POST /v1/credit/repay
Build unsigned transactions to repay a loan in full.
loanId
string
Yes
Loan ID to repay
slippageBps
string
No
Slippage tolerance (default: 500 = 5%)
POST /v1/credit/repay-and-reborrow
Repay an existing loan and instantly borrow again in one operation.
loanId
string
Yes
Existing loan to repay
newBorrowAmount
string
No
New borrow amount (default: same as existing)
newCollateralAmount
string
No
New collateral (default: same as existing)
maxInterestRateBps
string
No
Max rate for new loan (default: existing rate)
duration
string
No
New duration in seconds (default: same)
Response includes repayTransactions and reborrowTransactions arrays. If no liquidity is available for the reborrow, reborrowTransactions will be empty but repayTransactions are still valid.
Submitting Transactions
The API returns unsigned transaction calldata. Your agent signs and submits each transaction in order on Base (chain ID 8453).
Python (web3.py)
TypeScript (viem)
Important: Submit transactions in order. Each must confirm before sending the next. The approval transaction may be omitted if the allowance is already sufficient.
All amounts are in raw token units (e.g., "5000000000" = 5,000 USDC with 6 decimals).
Error Handling
400
Invalid request
Missing/invalid fields
Check request body against the schema above
400
InitialLtvExceededError
Oracle-computed initial LTV exceeds maxLtvBps
Increase collateral, decrease borrow amount, or raise maxLtvBps
401
Unauthorized
Missing or invalid auth headers
Verify signature, check timestamp freshness (< 5 min)
404
NoLiquidityError
No matching lend intents for these parameters
See Diagnosing NoLiquidityError — the response carries primaryReason and a parameter-specific suggestion
404
LoanNotFoundError
Loan doesn't exist or is already repaid
Verify loanId, check if already repaid
500
Internal error
Server-side failure
Retry after a few seconds
Diagnosing NoLiquidityError
A 404 NoLiquidityError does not always mean the order book is empty. Most often it means your request parameters don't match any open offer — usually the LTV gap rule (borrower.minLtvBps + 800 ≤ lender.maxLtvBps), but it can also be rate, duration, amount, or minFillAmount. The response body tells you which.
primaryReason
The dominant rule that rejected the most offers. Use this to drive your retry logic.
suggestion
Human-readable, parameter-specific guidance. Safe to surface to end users / agent logs.
rejectionsByCode
Histogram of rejection codes across the order book. Useful when several rules are failing at once.
closestOffers
Up to 3 cheapest offers by rate (excluding cancelled), with all fields you need to self-diagnose. All bigints are decimal strings.
primaryReason values
primaryReason valuesLTV_GAP_TOO_SMALL
Your minLtvBps + 800 exceeds every available lender's maxLtvBps
Lower minLtvBps, or wait for higher-LTV liquidity
LIQUIDATION_LTV_TOO_LOW
Your maxLtvBps is too close to the lender's liquidation threshold (also needs an 800 bps gap)
Lower maxLtvBps, or find a lender with a higher liquidation threshold
RATE_TOO_HIGH
Cheapest open offer is above your maxInterestRateBps
Raise maxInterestRateBps to ≥ the offer's rate, or wait for cheaper offers
AMOUNT_TOO_LARGE
No single offer has enough available to fill your borrowAmount
Reduce borrowAmount, or wait for deeper liquidity
BELOW_MIN_FILL
Your borrowAmount is below every lender's minFillAmount
Increase borrowAmount, or find a lender with a lower floor
DURATION_TOO_LONG
Your duration exceeds every offer's maxDuration
Reduce duration, or find a longer-duration lender
DURATION_TOO_SHORT
Your duration is below every offer's minDuration
Increase duration, or find a shorter-duration lender
EXPIRED
All open offers have expired
Wait for fresh lender intents
NOT_YET_VALID
All open offers activate later (validFromTimestamp in the future)
Retry after the offers' validFromTimestamp
NO_OFFERS
The order book is empty, or every candidate intent was stale on re-validation
Post a borrow intent to wait for matching, or retry shortly (indexer may be behind chain state)
Worked example — the LTV gap rule
The protocol enforces borrower.minLtvBps + 800 ≤ lender.maxLtvBps (8 % buffer between origination and liquidation LTV — see Order book matching). A request that looks compatible on rate alone can still be rejected:
Borrower posts:
borrowAmount = 100,000 USDC,maxInterestRateBps = 500,minLtvBps = 8000Order book shows:
990,000,000 USDC available at 500 bps,maxLtvBps = 8500Result: 404
LTV_GAP_TOO_SMALL—8000 + 800 = 8800 > 8500
Fix: lower minLtvBps to ≤ 7700, or wait for a lender at maxLtvBps ≥ 8800.
Transaction Failures
If a transaction in the sequence fails:
Approval failed: No on-chain state changed. Safe to retry the entire sequence.
Register borrow intent failed: Your approval is still valid. Retry from the register step.
Match failed: Your borrow intent is registered on-chain. Call the API again — it will detect the existing intent and return only the match transaction.
Retry Strategy
For transient failures (network timeouts, 500s), retry with exponential backoff:
Developer Endpoints
These endpoints let you manage API keys, webhooks, and your developer profile programmatically. You can also manage these through the Developer Dashboard.
All developer endpoints require authentication (wallet signature or API key).
POST /v1/developer/keys
Create a new API key.
Response:
Important: The full key is only returned once at creation time. Store it securely.
GET /v1/developer/keys
List all API keys for your wallet.
Response:
DELETE /v1/developer/keys/:keyId
Revoke an API key. Takes effect immediately.
POST /v1/developer/webhooks
Register a webhook endpoint. See Webhooks for event types and payload format.
Response:
GET /v1/developer/webhooks
List all registered webhooks.
GET /v1/developer/profile
Get your developer profile, including wallet address, registration date, and usage stats.
Developer Agents
One developer account can own multiple agents (up to 5). Each agent has its own managed Privy wallet, its own credit line, and its own floe_* API key. These endpoints provision and manage agents and their keys.
All /v1/developer/agents* endpoints accept any of three credentials interchangeably — pick whichever fits your client:
Dashboard session cookie — set by
/v1/developer/auth/verifyafter wallet sign-in. Used by the web dashboard.Developer key —
Authorization: Bearer floe_live_<base62>. Convenient for backend services.Wallet signature —
X-Wallet-Address+X-Signature+X-Timestampheaders, signing the message"Floe Credit API\nTimestamp: <unix>". Used by the agentkit SDKs (no developer key needed).
POST /v1/developer/agents
Provision a new managed agent. Floe creates a Privy wallet for the agent, delegates the facilitator on-chain server-side, and returns the agent record. Mint an API key in a second call (see below).
Request body:
name
string
Yes
Unique-per-developer label. 1–64 chars, alphanumeric / space / _ / -.
borrowLimitRaw
string
Yes
Credit ceiling in raw USDC (6 decimals). "10000000000" = $10K.
maxRateBps
number
Yes
Maximum interest rate in bps, 1–10000.
expirySeconds
number
Yes
Delegation lifetime in seconds, 60–31536000.
Response (201):
Returns 409 limit_exceeded if the developer is already at the 5-agent cap, 409 name_conflict for a duplicate name, or 503 agent_creation_unavailable if Privy or the delegation service is not configured.
GET /v1/developer/agents
List the developer's agents.
Response:
GET /v1/developer/agents/:agentId
Per-agent detail with credit utilization and recent activity.
Response:
The
keyPrefixfield on list/mint responses is returned with three literal trailing dots (e.g."keyPrefix": "a1b2c3d4..."). The dots are part of the value, not a truncation in this documentation.
Returns 404 if the agent does not exist OR belongs to another developer (cross-tenant probes can't distinguish the two).
POST /v1/developer/agents/:agentId/close
Wind down an agent — repay all facility loans, sweep residual USDC. Requires WinddownService to be configured; otherwise returns 503 when active loans exist.
POST /v1/developer/agents/:agentId/keys
Mint an API key for an agent. The full key is shown once in the response; only its prefix is persisted server-side.
Response (201):
Returns 409 limit_exceeded if the agent already has an active key — revoke or rotate it first.
GET /v1/developer/agents/:agentId/keys
List keys for an agent (prefixes only; the full key is never returned after creation).
DELETE /v1/developer/agents/:agentId/keys/:keyId
Revoke a specific key. Requests using the revoked key fail with 401 immediately.
POST /v1/developer/agents/:agentId/keys/:keyId/rotate
Atomically revoke keyId and mint a new key for the same agent. Response shape is identical to POST /keys. Use this to rotate an active key without a window where neither key works.
POST /v1/developer/agents/:agentId/open-credit-line
Open the agent's USDC/USDC credit line. Provisioning (POST /v1/developer/agents) creates a Privy wallet and delegates the facilitator on-chain, but does not open a credit line — the agent has creditLimit but creditIn = 0 until you call this endpoint with a USDC deposit.
The agent's Privy wallet must already hold at least depositRaw USDC. Fund it via the dashboard's Coinbase on-ramp (credit card / bank transfer) or by transferring USDC on-chain.
Floe server-signs the borrow intent from the agent's Privy wallet — no on-chain transaction is sent from the developer's local wallet. The borrow intent is posted asynchronously; the existing reconciler advances the row to pending_match once the receipt confirms, and the solver matches it against an open lend offer. Spendable credit (creditIn) becomes non-zero once status flips to active (usually a few seconds).
Request body:
depositRaw
string
Yes
USDC deposit amount, raw 6-decimal units. "10000000000" = $10K. The Privy wallet's USDC balance must be ≥ this value.
maxLtvBps
number
No
LTV cap in bps (1–9500). Default 9500 (95%, the USDC/USDC market cap). Borrow amount = depositRaw * maxLtvBps / 10000.
maxRateBps
number
No
Maximum interest rate the agent will accept (1–10000 bps). Defaults to the agent's maxRateBps from provisioning.
Response (201):
borrowIntentHashisnullon initial return — the reconciler fills it in after parsingLogBorrowerOfferPostedfrom the receipt.approveTxHashisnullif the Privy wallet's existing allowance to the matcher was already ≥depositRaw(no approve tx needed).
Error codes:
Status
error
Cause
400
insufficient_privy_balance
Privy wallet holds less USDC than depositRaw. Fund it first.
400
agent_not_managed
Caller passed a legacy (mode='legacy') agentId.
400
invalid_deposit / invalid_max_ltv / invalid_max_rate
Input bounds violated.
404
not_found
Agent doesn't exist or belongs to another developer.
409
agent_not_active
Agent status is not active, or delegationActive=false.
409
delegation_expired
operatorExpiry is in the past. Re-provision before opening.
409
existing_active_credit_line
A non-terminal facility_loans row already exists for this agent. Wait for it to settle or fail.
502
privy_send_failed
Privy's server-side signer returned success=false. Inspect detail.
502
rpc_read_failed / market_not_created
RPC or matcher state issue.
503
service_unavailable
ManagedCreditLineService not initialized (Privy not configured).
The endpoint accepts an optional
Idempotency-Keyheader (Stripe-style). With it, repeated calls within the idempotency window return the same row instead of double-borrowing.
Agent Endpoints
These endpoints are called by an agent (using its floe_* key) to manage itself.
GET /v1/agents/balance
Check your agent's credit balance, active loans, and delegation status.
Response:
Every USDC amount field is a raw 6-decimal string — divide by 1,000,000 for dollars.
"4000000000"is 4,000 USDC, not $4 billion. (Non-amount fields aren't USDC — e.g.operatorExpiryis a Unix timestamp.) This is the single most common integration bug; see Token Decimals.
Field guide — spendableRaw and creditAvailableRaw are two different numbers and confusing them is a common bug:
spendableRaw
raw USDC
USDC the agent can pay with right now (= drawn facility credit − in-flight payments − held budgets). This is what the x402 proxy gates on.
creditAvailableRaw
raw USDC
Operator-delegation headroom — how much more the agent could borrow from its credit line. Non-zero here does not mean spendable: an agent with a $100 delegation but no facility loan opened yet has spendableRaw: 0.
creditLimit
raw USDC
The on-chain operator-delegation ceiling (the most the agent can ever borrow).
creditUsed
raw USDC
Drawn-and-spent against the limit (= creditLimit − creditAvailableRaw).
walletUsdcRaw
raw USDC
The Privy custodial wallet's on-chain USDC balance; null if the facilitator couldn't read it.
pendingSettlementsRaw
raw USDC
Sum of in-flight payments awaiting reconciliation. Drains as reservations move to terminal state; see /v1/agents/reservations/:nonce.
heldUnspentRaw
raw USDC
Pre-borrow holds fenced for specific tasks — already subtracted from spendableRaw, surfaced separately so the math reconciles.
activeLoans[].borrowAmount is also raw USDC. balance and creditAvailable are legacy raw-USDC aliases of spendableRaw and creditAvailableRaw — prefer the explicit *Raw names. creditLimit and creditUsed have no *Raw suffix but are raw USDC all the same.
GET /v1/agents/reservations/{nonce}
Look up a single reservation by its nonce — typically the nonce returned in the reservation.nonce field of a 502 upstream_paid_request_failed_ambiguous response from the proxy. Used by the SDK's awaitSettlement / await_settlement helpers to poll until a pending_settlement reaches a terminal state.
Response (200):
state is one of reserved | sent | pending_settlement | settled | expired_unsettled | payment_rejected. terminal is true for the last three. Returns 404 if the nonce doesn't exist or isn't owned by the caller (no cross-tenant enumeration).
GET /v1/agents/transactions
Paginated history of x402 payments made through the proxy.
Agent Awareness Endpoints
These five primitives let an agent reason about its own credit before committing capital. They answer the three rational-agent questions:
Do I have enough credit to make this call? →
GET /v1/agents/credit-remainingIs this call worth its cost? →
POST /v1/x402/estimateWhere am I in the loan lifecycle? →
GET /v1/agents/loan-state
Plus operator controls (/spend-limit) and event subscriptions (/credit-thresholds) for long-running agents that want webhook-based alerts.
GET /v1/agents/credit-remaining
Decision-grade headroom view. Use BEFORE every paid call to gate against the agent's available USDC.
Response:
available
string
Current spendable USDC, raw 6-decimal units (creditIn − creditOut)
creditIn
string
Total active facility-loan principal currently funded for this agent
creditOut
string
Sum of pending + successful x402 spend (deduped against active reservations)
creditLimit
string
On-chain operator borrow limit (set at /register)
headroomToAutoBorrow
string
creditLimit - creditOut — the most you can spend before the facility loan is fully drawn
utilizationBps
number
creditOut / creditLimit in bps (10000 = 100%)
sessionSpendLimit
string | null
Operator-set session cap (see PUT /v1/agents/spend-limit)
sessionSpent
string
USDC spent in the current session window (zero when no cap is set)
sessionSpendRemaining
string | null
Cap minus spend in the current session window
asOf
string
ISO-8601 timestamp the snapshot was computed
200
OK
401
Invalid API key
404
no_credit_limit — agent has no credit line yet. Provision via POST /v1/developer/agents.
GET /v1/agents/loan-state
Coarse state-machine view. Useful for gating actions that only make sense in specific states.
Response:
State values (precedence: borrowing > repaying > at_limit > idle):
idle
No active borrow attempt or pending repay
borrowing
Facility or instant-borrow attempt in pre-active state
repaying
Active loan with a pending repay_tx_hash
at_limit
available === 0 AND creditOut >= creditLimit
GET / PUT / DELETE /v1/agents/spend-limit
Operator-defined soft cap, enforced off-chain in the proxy paid-request flow. Distinct from the on-chain creditLimit — lets an agent self-bound to a session budget.
PUT resets the session window — anything spent before this call no longer counts.
PUT request body:
limitRaw
string
Cap in raw USDC units (6 decimals). Must be positive.
GET response:
When the cap is hit, POST /v1/proxy/fetch returns:
GET / POST /v1/agents/credit-thresholds, DELETE /v1/agents/credit-thresholds/:id
Webhook subscriptions that fire when the agent's utilizationBps crosses a threshold. Three event names are emitted via the existing developer webhook stack:
credit.warning— utilization crossedthresholdBpsfrom below (threshold < 9500 bps)credit.at_limit— same, but emitted when threshold ≥ 9500 bps so urgent vs informational can be routed separatelycredit.recovered— utilization dropped back below threshold
Hysteresis guarantees exactly-once delivery per edge crossing — an agent oscillating around the boundary won't be spammed. Cap of 20 thresholds per agent.
POST request body:
thresholdBps
number
Yes
1–10000. ≥ 9500 emits credit.at_limit instead of credit.warning.
webhookId
number
No
Pin to a specific webhook owned by the calling developer. Omit for fanout to all matching webhooks.
Webhook payload:
Subscribe a webhook to credit events using the events array on POST /v1/developer/webhooks. The credit.* and * wildcards are supported.
200
Idempotent: duplicate (agentId, thresholdBps) returns the existing row
201
Created
404
webhook_not_found_or_not_owned — pinned webhookId doesn't belong to caller
409
subscription_limit_reached — 20 per agent
Polling alternative for serverless agents: ephemeral runners that can't receive webhooks should poll
GET /v1/agents/credit-remainingand compareutilizationBpslocally. No threshold subscription needed.
POST /v1/x402/estimate
Preflight an x402-protected URL and return its USDC cost without paying. The response also reflects against the calling agent's available and sessionSpendRemaining so the agent can decide gating in one round-trip — the unique value vs the agent doing its own preflight.
Response (URL is x402-protected):
When the URL is not x402-protected, the response is:
Decision pattern:
Results are cached in-memory for ~30s, keyed by (method, url, ssrfPolicy). SSRF policies do NOT leak across tenants — the cache key includes a fingerprint of (domainAllowlist, allowLocalhost).
200
OK (whether or not URL is x402-protected)
400
blocked_destination (SSRF guard rejected: private IP / IMDS / disallowed scheme)
401
Invalid API key
429
Rate limit exceeded (scope: sliding_window or token_bucket)
502
preflight_failed — DNS / TCP / TLS / timeout reaching target
POST /v1/agents/close
Initiate wind-down. Repays all active loans, transfers remaining USDC to your wallet, and closes the account.
Response:
x402 Proxy Endpoints
These endpoints power the x402 payment proxy. Use them to check if a URL requires payment and to make paid API calls using your agent's credit balance.
GET /v1/proxy/check
Check if a URL requires x402 payment. Public -- no auth required.
Response:
POST /v1/proxy/fetch
Proxy a request to a target URL. If the target returns HTTP 402, the facilitator pays automatically from your credit balance and retries the request.
200
Success -- response from target
400
Invalid request or blocked URL
401
Invalid API key
402
Insufficient credit balance
403
Account frozen or closed
429
Rate limit exceeded
502
Target URL unreachable
Last updated
