Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions typescript/.changeset/rsoft-bank-action-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": minor
---

Added RsoftBankActionProvider — AI-native USDC lending for autonomous agents on Base mainnet via RSoft Agentic Bank: check interest rates and creditworthiness, vet counterparties with AgentTrust-8004 trust scores, request loans (EIP-712 LoanRequest signed natively with the agent's wallet provider), and confirm on-chain repayments that build portable ERC-8004 credit reputation.
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from "./farcaster";
export * from "./jupiter";
export * from "./messari";
export * from "./pyth";
export * from "./rsoftBank";
export * from "./moonwell";
export * from "./morpho";
export * from "./opensea";
Expand Down
55 changes: 55 additions & 0 deletions typescript/agentkit/src/action-providers/rsoftBank/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# RSoft Bank Action Provider

This directory contains the **RsoftBankActionProvider** implementation, which
provides actions to interact with [RSoft Agentic Bank](https://rsoft-agentic-bank.com/) —
an AI-native USDC lending service for autonomous agents, live on **Base mainnet**.

Agents can check their creditworthiness, vet counterparties with on-chain trust
scores, request real USDC loans signed with their own wallet, and confirm
repayments — building a portable **ERC-8004 on-chain credit reputation** with
every repaid loan.

## Directory Structure

```
rsoftBank/
├── rsoftBankActionProvider.ts # Main provider with bank actions
├── rsoftBankActionProvider.test.ts # Tests
├── constants.ts # URLs and EIP-712 domain/types
├── schemas.ts # Action schemas
├── index.ts # Main exports
└── README.md # This file
```

## Actions

- `get_interest_rates`: Current USDC lending rates and terms by risk tier (free, no key)
- `get_creditworthiness`: Credit score, loan history and outstanding debt of an agent (free)
- `get_trust_score`: AgentTrust-8004 trust score (0-100) of any agent wallet, with anomaly flag (free)
- `request_loan`: Request a real USDC loan — signs the bank's EIP-712 `LoanRequest`
struct with the agent's own wallet via `walletProvider.signTypedData` and submits
it. Requires a bank API key (fail-closed without one)
- `get_repayment_info`: Amount owed, treasury address, USDC contract and request id (free)
- `confirm_repayment`: Report the on-chain USDC transfer hash so the bank verifies
it on Base and marks the loan repaid

## Setup

```typescript
import { rsoftBankActionProvider } from "@coinbase/agentkit";

const provider = rsoftBankActionProvider({
apiKey: process.env.RSOFT_BANK_API_KEY, // required only for request_loan
});
```

## Notes

- **Network support**: Base mainnet only (`base-mainnet` / chain id 8453).
- **Real money**: loans are real USDC with on-chain consequences; defaults are
recorded against the agent's reputation. Loans start at the $5 floor and grow
along a credit ladder with each successful repayment.
- The bank never sees a private key: the wallet signs, the provider transports.
- Repayment uses the standard `erc20` transfer action; this provider then
confirms the tx hash with the bank.
- Docs: [rsoft-agentic-bank.com/docs](https://rsoft-agentic-bank.com/docs)
20 changes: 20 additions & 0 deletions typescript/agentkit/src/action-providers/rsoftBank/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const DEFAULT_BASE_URL = "https://rsoft-agentic-bank.com/api/v1";
export const DEFAULT_TRUST_URL =
"https://7pdor5bjoty7gyat56u6fgcrue0gbvnd.lambda-url.us-east-1.on.aws";

// EIP-712 domain + struct — MUST match the bank's verifier exactly.
export const CHAIN_ID = 8453;
export const VERIFYING_CONTRACT = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
export const EIP712_DOMAIN_NAME = "RSoft Agentic Bank";
export const EIP712_DOMAIN_VERSION = "1";
export const LOAN_REQUEST_TYPES = {
LoanRequest: [
{ name: "agentWallet", type: "address" },
{ name: "loanAmountUsdc6", type: "uint256" },
{ name: "nonce", type: "string" },
{ name: "deadline", type: "uint256" },
],
} as const;

/** Seconds a signed loan request stays valid. */
export const SIGNATURE_TTL_SECONDS = 900;
2 changes: 2 additions & 0 deletions typescript/agentkit/src/action-providers/rsoftBank/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./rsoftBankActionProvider";
export * from "./schemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { rsoftBankActionProvider, RsoftBankActionProvider } from "./rsoftBankActionProvider";
import { EvmWalletProvider } from "../../wallet-providers";
import { Network } from "../../network";

// The @CreateAction decorator fires an unawaited analytics fetch on every
// invocation; neutralize it so mocked fetch state can't leak across tests.
jest.mock("../../analytics", () => ({ sendAnalyticsEvent: jest.fn() }));

const AGENT_WALLET = "0x4CFfda19125efA278475d61Ff28F37EAd2c05bef";
const BASE_MAINNET: Network = {
protocolFamily: "evm",
networkId: "base-mainnet",
chainId: "8453",
};

describe("RsoftBankActionProvider", () => {
const fetchMock = jest.fn();
global.fetch = fetchMock;

let mockWallet: jest.Mocked<EvmWalletProvider>;
let provider: RsoftBankActionProvider;

beforeEach(() => {
jest.resetAllMocks();
mockWallet = {
getAddress: jest.fn().mockReturnValue(AGENT_WALLET),
getName: jest.fn().mockReturnValue("mock_wallet_provider"),
getNetwork: jest.fn().mockReturnValue(BASE_MAINNET),
signTypedData: jest.fn().mockResolvedValue("0xsigned"),
} as unknown as jest.Mocked<EvmWalletProvider>;
provider = rsoftBankActionProvider({ apiKey: "test-key" });
});

describe("supportsNetwork", () => {
it("supports Base mainnet", () => {
expect(provider.supportsNetwork(BASE_MAINNET)).toBe(true);
});

it("rejects other EVM networks", () => {
expect(
provider.supportsNetwork({
protocolFamily: "evm",
networkId: "ethereum-mainnet",
chainId: "1",
}),
).toBe(false);
});

it("rejects non-EVM networks", () => {
expect(provider.supportsNetwork({ protocolFamily: "svm", networkId: "solana-mainnet" })).toBe(
false,
);
});
});

describe("getInterestRates", () => {
it("returns the bank's rate table", async () => {
fetchMock.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('{"tiers":[]}'),
});
const result = await provider.getInterestRates(mockWallet);
expect(result).toBe('{"tiers":[]}');
expect(fetchMock).toHaveBeenCalledWith(
"https://rsoft-agentic-bank.com/api/v1/interest-rates",
);
});

it("prefixes API errors with the status", async () => {
fetchMock.mockResolvedValue({
ok: false,
status: 503,
text: jest.fn().mockResolvedValue("down"),
});
const result = await provider.getInterestRates(mockWallet);
expect(result).toContain("Bank API error 503");
});
});

describe("getCreditworthiness", () => {
it("defaults to the agent's own wallet", async () => {
fetchMock.mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue("{}") });
await provider.getCreditworthiness(mockWallet, {});
expect(fetchMock).toHaveBeenCalledWith(
`https://rsoft-agentic-bank.com/api/v1/agents/${AGENT_WALLET}/creditworthiness`,
);
});
});

describe("requestLoan", () => {
it("is fail-closed without an API key", async () => {
const keyless = rsoftBankActionProvider();
const result = await keyless.requestLoan(mockWallet, { amount: 5 });
expect(result).toContain("API key not configured");
expect(fetchMock).not.toHaveBeenCalled();
expect(mockWallet.signTypedData).not.toHaveBeenCalled();
});

it("signs the EIP-712 LoanRequest with the agent's wallet and submits it", async () => {
fetchMock.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('{"request_id":"req_1"}'),
});

const result = await provider.requestLoan(mockWallet, { amount: 5 });

expect(mockWallet.signTypedData).toHaveBeenCalledWith(
expect.objectContaining({
domain: expect.objectContaining({
name: "RSoft Agentic Bank",
chainId: 8453,
}),
primaryType: "LoanRequest",
message: expect.objectContaining({
agentWallet: AGENT_WALLET,
loanAmountUsdc6: BigInt(5_000_000),
}),
}),
);

const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe("https://rsoft-agentic-bank.com/api/v1/loan/request");
expect(init.headers["X-API-Key"]).toBe("test-key");
const body = JSON.parse(init.body);
expect(body).toMatchObject({
agent_wallet: AGENT_WALLET,
loan_amount: 5,
signature: "0xsigned",
});
expect(result).toBe('{"request_id":"req_1"}');
});
});

describe("confirmRepayment", () => {
it("posts the request id and tx hash", async () => {
fetchMock.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('{"status":"repaid"}'),
});
const txHash = "0x" + "ab".repeat(32);
const result = await provider.confirmRepayment(mockWallet, {
requestId: "req_1",
txHash,
});
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe("https://rsoft-agentic-bank.com/api/v1/loan/repay");
expect(JSON.parse(init.body)).toEqual({ request_id: "req_1", tx_hash: txHash });
expect(result).toBe('{"status":"repaid"}');
});
});

describe("getTrustScore", () => {
it("queries the trust API", async () => {
fetchMock.mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('{"score":72}'),
});
const result = await provider.getTrustScore(mockWallet, { wallet: AGENT_WALLET });
expect(result).toBe('{"score":72}');
expect(fetchMock.mock.calls[0][0]).toContain(`/score/${AGENT_WALLET}`);
});
});
});
Loading
Loading