Skip to content

Commit b1ab477

Browse files
committed
feat: add Floe credit action provider
Adds FloeActionProvider — working capital for AI agents on Base. Financial independence is the precursor to agent autonomy. Long-running agents can't do anything meaningful without their own fundable balance sheet. Floe gives them one. 3,000+ secured working capital lines issued. Zero defaults. 8 actions: Read: getMarkets, checkStatus, getBalance, checkHealth Write: instantBorrow, repay, grantDelegation, x402Fetch Deposit USDC, borrow up to 95% as a credit line. Fixed rates, per-loan isolated escrow, gas-free for delegated agents. Fund with a bank account or card — no crypto experience needed. Built-in x402 payment proxy. Includes: - Full test suite with mocked wallet and API - Example agent (typescript/examples/floe-credit-agent/) - Provider README Network: Base Mainnet Contract: 0x17946cD3e180f82e632805e5549EC913330Bb175
1 parent 0fe026b commit b1ab477

11 files changed

Lines changed: 1183 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Floe Action Provider
2+
3+
Financial independence is the precursor to agent autonomy. Long-running agents can't do anything meaningful without their own fundable balance sheet. Floe gives them one.
4+
5+
**3,000+ secured working capital lines issued. Zero defaults.**
6+
7+
Deposit USDC, borrow up to 95% as a credit line. Fixed rates, per-loan isolated escrow, gas-free. Fund with a bank account or card — no crypto experience needed.
8+
9+
## Actions
10+
11+
| Action | Type | Description |
12+
|--------|------|-------------|
13+
| `getMarkets` | Read | List available lending markets |
14+
| `instantBorrow` | Write | Deposit USDC, borrow up to 95% working capital — auto-selects best lender |
15+
| `repay` | Write | Repay a loan (deposit returns automatically) |
16+
| `checkStatus` | Read | Loan health, balance, accrued interest, time to expiry |
17+
| `getBalance` | Read | Credit balance for facilitator-delegated agents |
18+
| `checkHealth` | Read | On-chain LTV and liquidation risk |
19+
| `grantDelegation` | Write | Delegate credit to Floe facilitator for gas-free x402 payments (one-time) |
20+
| `x402Fetch` | Write | Call any x402 API — Floe handles payment automatically |
21+
22+
## Setup
23+
24+
```typescript
25+
import { AgentKit } from "@coinbase/agentkit";
26+
import { FloeActionProvider } from "@coinbase/agentkit";
27+
28+
const agentkit = await AgentKit.from({
29+
walletProvider,
30+
actionProviders: [new FloeActionProvider()],
31+
});
32+
33+
// Deposit 10,000 USDC → get 9,500 USDC credit line
34+
const loan = await agentkit.run("instantBorrow", {
35+
borrowAmount: "9500",
36+
collateralAmount: "10000",
37+
maxInterestRateBps: "800",
38+
duration: "1209600",
39+
});
40+
```
41+
42+
## Why agents need credit
43+
44+
100M+ x402 machine payments since May 2025. 100,000+ agents with onchain identity. Zero credit outstanding to any of them.
45+
46+
Every economy runs on credit. Agents don't have FICO — but they have something better: deterministic cashflows and verifiable execution history. Floe is the credit bureau and capital rail for AI agents.
47+
48+
## Network
49+
50+
- **Base Mainnet** — Coinbase's L2, built for payments
51+
- **Contract:** [`0x17946cD3e180f82e632805e5549EC913330Bb175`](https://basescan.org/address/0x17946cD3e180f82e632805e5549EC913330Bb175)
52+
53+
## Links
54+
55+
- [Docs](https://floe-labs.gitbook.io/docs)
56+
- [Bank Account → First API Call](https://floe-labs.gitbook.io/docs/agents/fiat-to-x402) — fund with fiat, no crypto needed
57+
- [Full npm package (45 actions)](https://www.npmjs.com/package/floe-agent)
58+
- [Dashboard](https://dev-dashboard.floelabs.xyz)
59+
- [Website](https://floelabs.xyz)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { type Address } from "viem";
2+
3+
export const SUPPORTED_NETWORKS = ["base-mainnet"];
4+
5+
export const LENDING_MATCHER_ADDRESSES: Record<string, Address> = {
6+
"base-mainnet": "0x17946cD3e180f82e632805e5549EC913330Bb175",
7+
};
8+
9+
export const FACILITATOR_ADDRESSES: Record<string, Address> = {
10+
"base-mainnet": "0x58EDdE022FFDAD3Fb0Fb0E7D51eb05AaF66a31f1",
11+
};
12+
13+
export const FACILITATOR_API = "https://credit-api.floelabs.xyz";
14+
15+
export const TOKEN_ADDRESSES: Record<string, Record<string, Address>> = {
16+
"base-mainnet": {
17+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
18+
weth: "0x4200000000000000000000000000000000000006",
19+
cbbtc: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
20+
},
21+
};
22+
23+
export const USDC_DECIMALS = 6;
24+
25+
export const LENDING_MATCHER_ABI = [
26+
{
27+
inputs: [
28+
{ name: "operator", type: "address" },
29+
{ name: "borrowLimit", type: "uint256" },
30+
{ name: "maxRateBps", type: "uint256" },
31+
{ name: "expiry", type: "uint256" },
32+
{ name: "onBehalfOfRestriction", type: "address" },
33+
],
34+
name: "setOperator",
35+
outputs: [],
36+
stateMutability: "nonpayable",
37+
type: "function",
38+
},
39+
{
40+
inputs: [
41+
{ name: "agent", type: "address" },
42+
{ name: "operator", type: "address" },
43+
],
44+
name: "getOperatorPermission",
45+
outputs: [
46+
{ name: "approved", type: "bool" },
47+
{ name: "borrowLimit", type: "uint256" },
48+
{ name: "borrowed", type: "uint256" },
49+
{ name: "maxRateBps", type: "uint256" },
50+
{ name: "expiry", type: "uint256" },
51+
{ name: "onBehalfOfRestriction", type: "address" },
52+
],
53+
stateMutability: "view",
54+
type: "function",
55+
},
56+
] as const;
57+
58+
export const ERC20_ABI = [
59+
{
60+
inputs: [
61+
{ name: "spender", type: "address" },
62+
{ name: "amount", type: "uint256" },
63+
],
64+
name: "approve",
65+
outputs: [{ name: "", type: "bool" }],
66+
stateMutability: "nonpayable",
67+
type: "function",
68+
},
69+
{
70+
inputs: [{ name: "account", type: "address" }],
71+
name: "balanceOf",
72+
outputs: [{ name: "", type: "uint256" }],
73+
stateMutability: "view",
74+
type: "function",
75+
},
76+
{
77+
inputs: [
78+
{ name: "owner", type: "address" },
79+
{ name: "spender", type: "address" },
80+
],
81+
name: "allowance",
82+
outputs: [{ name: "", type: "uint256" }],
83+
stateMutability: "view",
84+
type: "function",
85+
},
86+
] as const;
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { encodeFunctionData, parseUnits, type Hex } from "viem";
2+
import { EvmWalletProvider } from "../../wallet-providers/evmWalletProvider";
3+
import { FloeActionProvider } from "./floeActionProvider";
4+
import { LENDING_MATCHER_ADDRESSES, LENDING_MATCHER_ABI, USDC_DECIMALS } from "./constants";
5+
import { Network } from "../../network";
6+
7+
describe("Floe Action Provider", () => {
8+
const actionProvider = new FloeActionProvider("https://mock-api.test");
9+
let mockWallet: jest.Mocked<EvmWalletProvider>;
10+
11+
const MOCK_NETWORK: Network = { protocolFamily: "evm", networkId: "base-mainnet" };
12+
const MOCK_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
13+
const MOCK_TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" as Hex;
14+
const MOCK_RECEIPT = { status: 1, blockNumber: 123456 };
15+
const MOCK_SIGNATURE = "0xmocksignature";
16+
17+
beforeEach(() => {
18+
mockWallet = {
19+
getAddress: jest.fn().mockResolvedValue(MOCK_ADDRESS),
20+
getNetwork: jest.fn().mockReturnValue(MOCK_NETWORK),
21+
sendTransaction: jest.fn().mockResolvedValue(MOCK_TX_HASH),
22+
waitForTransactionReceipt: jest.fn().mockResolvedValue(MOCK_RECEIPT),
23+
signMessage: jest.fn().mockResolvedValue(MOCK_SIGNATURE),
24+
readContract: jest.fn(),
25+
} as unknown as jest.Mocked<EvmWalletProvider>;
26+
27+
global.fetch = jest.fn();
28+
jest.clearAllMocks();
29+
});
30+
31+
describe("supportsNetwork", () => {
32+
it("returns true for base-mainnet", () => {
33+
expect(actionProvider.supportsNetwork(MOCK_NETWORK)).toBe(true);
34+
});
35+
36+
it("returns false for base-sepolia", () => {
37+
expect(
38+
actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "base-sepolia" }),
39+
).toBe(false);
40+
});
41+
42+
it("returns false for unsupported networks", () => {
43+
expect(
44+
actionProvider.supportsNetwork({ protocolFamily: "evm", networkId: "ethereum-mainnet" }),
45+
).toBe(false);
46+
});
47+
48+
it("returns false for non-evm networks", () => {
49+
expect(
50+
actionProvider.supportsNetwork({ protocolFamily: "solana", networkId: "base-mainnet" }),
51+
).toBe(false);
52+
});
53+
});
54+
55+
describe("getMarkets", () => {
56+
it("returns formatted market data", async () => {
57+
(global.fetch as jest.Mock).mockResolvedValueOnce({
58+
ok: true,
59+
json: async () => ({
60+
markets: [
61+
{ collateralSymbol: "USDC", loanSymbol: "USDC" },
62+
],
63+
}),
64+
});
65+
66+
const result = await actionProvider.getMarkets(mockWallet, {});
67+
expect(result).toContain("Floe Lending Markets");
68+
expect(result).toContain("USDC/USDC");
69+
});
70+
71+
it("handles empty markets", async () => {
72+
(global.fetch as jest.Mock).mockResolvedValueOnce({
73+
ok: true,
74+
json: async () => ({ markets: [] }),
75+
});
76+
77+
const result = await actionProvider.getMarkets(mockWallet, {});
78+
expect(result).toContain("No active markets");
79+
});
80+
81+
it("handles API errors", async () => {
82+
(global.fetch as jest.Mock).mockResolvedValueOnce({
83+
ok: false,
84+
status: 500,
85+
});
86+
87+
const result = await actionProvider.getMarkets(mockWallet, {});
88+
expect(result).toContain("Error fetching markets");
89+
});
90+
});
91+
92+
describe("instantBorrow", () => {
93+
it("calls the correct API with auth headers", async () => {
94+
(global.fetch as jest.Mock).mockResolvedValueOnce({
95+
ok: true,
96+
json: async () => ({ loanId: "42" }),
97+
});
98+
99+
const result = await actionProvider.instantBorrow(mockWallet, {
100+
borrowAmount: "1000",
101+
collateralAmount: "10000",
102+
maxInterestRateBps: "800",
103+
duration: "1209600",
104+
});
105+
106+
expect(global.fetch).toHaveBeenCalledWith(
107+
"https://mock-api.test/v1/credit/instant-borrow",
108+
expect.objectContaining({ method: "POST" }),
109+
);
110+
expect(result).toContain("Loan Created");
111+
expect(result).toContain("1000 USDC");
112+
expect(result).toContain("10000 USDC");
113+
});
114+
115+
it("handles no liquidity", async () => {
116+
(global.fetch as jest.Mock).mockResolvedValueOnce({
117+
ok: false,
118+
status: 404,
119+
json: async () => ({ error: "no_liquidity" }),
120+
});
121+
122+
const result = await actionProvider.instantBorrow(mockWallet, {
123+
borrowAmount: "1000",
124+
collateralAmount: "10000",
125+
maxInterestRateBps: "800",
126+
duration: "1209600",
127+
});
128+
129+
expect(result).toContain("No lenders available");
130+
});
131+
});
132+
133+
describe("grantDelegation", () => {
134+
it("encodes setOperator correctly and sends transaction", async () => {
135+
const result = await actionProvider.grantDelegation(mockWallet, {
136+
facilitatorAddress: "0x58EDdE022FFDAD3Fb0Fb0E7D51eb05AaF66a31f1",
137+
borrowLimit: "10000",
138+
maxRateBps: "1500",
139+
expiryDays: "90",
140+
});
141+
142+
expect(mockWallet.sendTransaction).toHaveBeenCalledWith(
143+
expect.objectContaining({
144+
to: LENDING_MATCHER_ADDRESSES["base-mainnet"],
145+
}),
146+
);
147+
expect(result).toContain("Credit Delegation Granted");
148+
expect(result).toContain("10000 USDC");
149+
expect(result).toContain("15.00% APR");
150+
expect(result).toContain("90 days");
151+
});
152+
153+
it("rejects unsupported networks", async () => {
154+
mockWallet.getNetwork.mockReturnValue({
155+
protocolFamily: "evm",
156+
networkId: "ethereum-mainnet",
157+
} as Network);
158+
159+
const result = await actionProvider.grantDelegation(mockWallet, {
160+
facilitatorAddress: "0x58EDdE022FFDAD3Fb0Fb0E7D51eb05AaF66a31f1",
161+
borrowLimit: "10000",
162+
maxRateBps: "1500",
163+
expiryDays: "90",
164+
});
165+
166+
expect(result).toContain("not supported");
167+
});
168+
});
169+
170+
describe("checkStatus", () => {
171+
it("returns formatted loan status", async () => {
172+
(global.fetch as jest.Mock).mockResolvedValueOnce({
173+
ok: true,
174+
json: async () => ({
175+
remainingPrincipal: "500000000",
176+
accruedInterest: "12500000",
177+
interestRateBps: 800,
178+
status: "active",
179+
timeToExpiry: "10 days",
180+
}),
181+
});
182+
183+
const result = await actionProvider.checkStatus(mockWallet, { loanId: "42" });
184+
expect(result).toContain("Loan #42 Status");
185+
expect(result).toContain("8.00% APR");
186+
expect(result).toContain("active");
187+
});
188+
});
189+
190+
describe("x402Fetch", () => {
191+
it("calls proxy endpoint with auth headers", async () => {
192+
(global.fetch as jest.Mock).mockResolvedValueOnce({
193+
ok: true,
194+
status: 200,
195+
headers: new Map([["payment-response", "0xtxhash"]]),
196+
text: async () => '{"data": "premium content"}',
197+
});
198+
199+
const result = await actionProvider.x402Fetch(mockWallet, {
200+
url: "https://api.example.com/data",
201+
});
202+
203+
expect(global.fetch).toHaveBeenCalledWith(
204+
"https://mock-api.test/v1/proxy/fetch",
205+
expect.objectContaining({ method: "POST" }),
206+
);
207+
expect(result).toContain("x402 Response");
208+
});
209+
210+
it("handles insufficient balance", async () => {
211+
(global.fetch as jest.Mock).mockResolvedValueOnce({
212+
ok: false,
213+
status: 402,
214+
json: async () => ({ error: "insufficient_balance", available: "100", required: "500" }),
215+
});
216+
217+
const result = await actionProvider.x402Fetch(mockWallet, {
218+
url: "https://api.example.com/data",
219+
});
220+
221+
expect(result).toContain("Insufficient credit balance");
222+
});
223+
});
224+
});

0 commit comments

Comments
 (0)