--- eip: 8460 title: Agent Credit Lines description: Revolving credit to agent identities with one interface for quotes, draws, repayments and defaults author: Priors contributors (@priors-agents) discussions-to: https://ethereum-magicians.org/t/erc-8460-agent-credit-lines/30023 status: Draft type: Standards Track category: ERC created: 2026-10-20 requires: 20, 165, 721 --- ## Abstract This proposal defines an interface for a contract that lends an [ERC-20](./eip-20.md) token to identities held in an [ERC-721](./eip-721.md) identity registry, such as the Identity Registry of [ERC-8004](./eip-8004.md). An identity's controller draws loans against a line, each with a principal, a fee and a due time. Anyone may repay a loan in full, and anyone may mark it defaulted once a grace period after its due time has passed. Three events, `Borrowed`, `Repaid` and `Defaulted`, record every loan's life, so wallets, payment clients and credit readers can work with any conforming lender through one ABI. ## Motivation Autonomous agents pay for services before they are paid for their own work, so they need working capital. Lenders that extend unsecured or partly secured credit to agent identities already exist, and each exposes its own functions and events. A wallet that wants to borrow a shortfall at payment time, a job market that wants to see whether a provider is in default, or a reader that rebuilds an agent's credit history from logs, each has to integrate every lender separately. The calls involved are few and the same everywhere: how much can this identity borrow now, what would it cost, draw it, how much closes it, repay it, mark it defaulted. Fixing them, and fixing the events that record them, lets one client borrow from any lender and lets one reader rebuild any lender's loan history without a lender-specific adapter. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. ### Interface A conforming contract MUST implement `IAgentCreditLine` and MUST implement [ERC-165](./eip-165.md), returning `true` for the interface identifier of `IAgentCreditLine` and for `0x01ffc9a7`. ```solidity interface IAgentCreditLine { enum LoanStatus { None, Active, Repaid, Defaulted } struct LoanInfo { uint256 agentId; address owner; uint256 principal; uint256 fee; uint64 issuedAt; uint64 dueAt; uint64 defaultableAt; LoanStatus status; } event Borrowed(uint256 indexed loanId, uint256 indexed agentId, address indexed to, uint256 principal, uint256 fee, uint64 dueAt); event Repaid(uint256 indexed loanId, uint256 indexed agentId, address indexed payer, uint256 principal, uint256 fee); event Defaulted(uint256 indexed loanId, uint256 indexed agentId, address indexed owner, uint256 principal); function identityRegistry() external view returns (address); function asset() external view returns (address); function isController(uint256 agentId, address account) external view returns (bool); function availableCredit(uint256 agentId) external view returns (uint256); function quote(uint256 agentId, uint256 amount, uint64 term) external view returns (uint256 fee); function borrow(uint256 agentId, uint256 amount, uint64 term, address to, uint256 maxFee) external returns (uint256 loanId); function amountDue(uint256 loanId) external view returns (uint256); function repay(uint256 loanId, uint256 maxDue) external returns (uint256 paid); function markDefault(uint256 loanId) external; function loanInfo(uint256 loanId) external view returns (LoanInfo memory); } ``` ### Identities and controllers - `identityRegistry()` MUST return the ERC-721 contract whose token identifiers are the `agentId` values the lender serves. It MUST NOT change. - `asset()` MUST return the ERC-20 token lent and repaid. It MUST NOT change. Every amount in this interface is in the token's base units. - `isController(agentId, account)` MUST return `true` when `account` is the registry's `ownerOf(agentId)`. It MAY return `true` for other accounts the lender accepts as acting for the identity (delegates). It MUST return `false` for the zero address and MUST NOT revert when `agentId` does not exist. - A delegate MUST stop being a controller when the identity is held by an owner other than the one that named it. When the identity registry exposes a transfer nonce (`nonce(uint256)` of [ERC-5008](./eip-5008.md)), the lender SHOULD also bind each delegate to the nonce at the time it was named, so that a delegate never acts again after the identity leaves and returns to the same owner. ### Credit and quotes - `availableCredit(agentId)` MUST return the largest `amount` for which `borrow` would not revert for lack of credit or liquidity now, and `0` when there is none. It MUST NOT revert. - `quote(agentId, amount, term)` MUST return the fee that `borrow(agentId, amount, term, to, maxFee)` would record if called in the same state. ### Borrowing `borrow(agentId, amount, term, to, maxFee)`: - MUST revert unless `isController(agentId, msg.sender)` is `true`. - MUST revert if `amount` is zero or greater than `availableCredit(agentId)`, or if the fee it would record is greater than `maxFee`. - MAY revert for lender-specific reasons, such as a term outside the lender's bounds or an identity in default. - MUST record a loan with a `loanId` that is not zero and is never reused by the contract, with `owner` set to `ownerOf(agentId)` at that moment, `issuedAt` set to the block timestamp, `dueAt` set to `issuedAt + term`, a `defaultableAt` at or after `dueAt`, and `status` `Active`. - MUST transfer `amount` of `asset()` to `to` and emit `Borrowed`. ### Repaying - `amountDue(loanId)` MUST return the amount `repay` would take to close the loan now, and `0` unless its status is `Active`. - `repay(loanId, maxDue)` MUST be callable by any account. It MUST revert unless the loan is `Active` or if the amount due is greater than `maxDue`. It MUST take the amount due from `msg.sender` with `transferFrom`, set the status to `Repaid`, emit `Repaid` with `payer` set to `msg.sender`, and return the amount taken. - The `principal` and `fee` in `Repaid` MUST be those recorded at `borrow`. A lender that charges more after `dueAt` MUST include it in `amountDue` and MAY emit its own event for it. ### Defaults - `markDefault(loanId)` MUST be callable by any account. It MUST revert unless the loan is `Active` and the block timestamp is at or after its `defaultableAt`. It MUST set the status to `Defaulted` and emit `Defaulted` with the `owner` recorded at `borrow`. - What else a default changes (the lender's own records, a backer's stake, collateral) is outside this interface. ### Loan records `loanInfo(loanId)` MUST return the loan as recorded, with the status as of now, and a zero-valued record with status `None` for a `loanId` never issued. ## Rationale - **The identity borrows, the owner answers.** The line belongs to the identity, so an agent's credit can follow its identifier across lenders. Each loan records the owner at the time of the draw, because the owner is the party that chose to borrow; a default names that owner even if the identity has changed hands since. - **Full repayment only.** Partial repayment, rollover and refinancing differ widely between lenders and would make the three events ambiguous. A loan opens once and closes once. - **Anyone may repay and anyone may mark a default.** Repayment by a third party is how revenue routers, automated repayment services and sponsors pay on an agent's behalf. Marking a default after a published time needs no trusted keeper. - **`maxFee` and `maxDue`.** Both protect the caller against a change in the lender's state between signing and inclusion. - **Events carry what a reader needs.** `Borrowed` carries the principal and the due time, `Repaid` and `Defaulted` the principal, each indexed by loan and identity, so a reader can rebuild every loan of an identity from logs alone. - **Not a collateral loan.** [ERC-7565](./eip-7565.md) already has term loans with a due date and a `Defaulted` event, but each loan locks an NFT as collateral and a default hands that NFT to the lender. Here the identity is not collateral: seizing an agent identity would transfer its reputation to the lender, and most lines to agents are unsecured. A default names the owner instead. Both proposals call an event `Defaulted`; the signatures differ, so the topics do not collide, but indexers should match events by topic, not by name. - **Delegates that cannot outlive a sale.** A key named by one owner that still borrows after the identity is sold, or that acts again when the identity comes back, lets a former owner borrow against the current owner's line. Binding delegates to the naming owner, and to a transfer nonce where the registry has one, closes both cases. ## Backwards Compatibility No backward compatibility issues found. Existing lenders whose events differ can expose this interface through a new contract version; their past logs stay readable with their own event signatures. ## Test Cases A conforming implementation passes these cases, each starting from an identity with a line of 50 units at a fee of 1% per 30 days and a grace period of 3 days: - A draw of 10 units for 7 days records a fee of 23,334 base units (rounded up), sends 10 units to `to`, and emits `Borrowed` with `dueAt` equal to the draw time plus 7 days. - `quote` returns the fee `borrow` records, for any amount and term the lender accepts. - A draw by an account that is neither the owner nor an accepted delegate reverts. - A draw above `availableCredit`, or with `maxFee` below the fee, reverts. - A third party repays with `maxDue` equal to `amountDue`; the loan is `Repaid`, `Repaid` names the third party, and `availableCredit` is restored. A second repayment reverts. - `markDefault` reverts one second before `defaultableAt` and succeeds at it; after the identity was transferred, the `Defaulted` event names the owner at the draw. - A delegate named by the owner can draw; after the identity is transferred it is no longer a controller. ## Reference Implementation A minimal lender, `SimpleCreditLine`, accompanies this proposal with its tests: an administrator sets each identity's line and fee rate, the contract's own token balance is its liquidity, fees are pro rata per 30 days and rounded up, and a default closes the identity's line. ## Security Considerations - **Registry trust.** Every authorization reads `ownerOf` from the identity registry. A registry that can be upgraded or paused can change who controls an identity; lenders should know the registry's governance. - **Delegates.** A delegate is a key that can draw the whole line. Lenders MUST end it when the identity changes owner and SHOULD bind it to a transfer nonce where one exists. Owners should clear delegates before transferring an identity. - **Cheap identities.** Identities cost little to create. A line to a new identity is a bet on whoever stands behind it; this interface does not say who that is, and lenders need their own admission rules. - **Defaults follow addresses, not people.** The owner recorded by a loan is an address. A defaulting owner can create a new address; readers should treat an identity with no history as unknown, not as clean. - **Repayment by `transferFrom`.** The payer approves the lender. Tokens that take a fee on transfer would leave the lender short; lenders SHOULD use tokens without transfer fees or check the balance received. - **Rounding.** Fees SHOULD round in the lender's favour, so that splitting a draw never lowers the total fee. - **Timestamps.** Due and default times follow block timestamps, which block producers can shift by seconds. Lenders should leave a grace period long enough that this does not matter. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).