gage-sdk
v0.6.0
Published
gage for builders and agents: read gage, prepare transactions, and carry them out with your own key. Robinhood Chain and its testnet.
Maintainers
Readme
gage-sdk
Read gage, prepare unsigned transactions, and carry actions out with your own signer. gage makes fixed-cost loans with no liquidation: a loan is never closed early because a price moves, and if it is not repaid by the end of its term the lenders keep the collateral and nothing else is taken.
It runs on Robinhood Chain (4663, the default) and its testnet (46630, network: "testnet"). gage never holds a key,
and loans the private lane may have opened are never served: gage's index must mark a loan public, and the chain must
show that no private-lane router opened it. It is the engine of gage's hosted MCP server and of
gage-mcp, the local one.
npm install gage-sdk viemviem 2 is a peer dependency, so your app and the SDK share one copy. Node 22 or later, or any bundler: the SDK has no Node-only imports.
import { createGage, privateKeySigner } from "gage-sdk";
const gage = createGage(); // Robinhood Chain; createGage({ network: "testnet" }) for its testnet
// The largest loan the partner vaults fund now for at most 1 SPY, for 7 days.
const quote = await gage.borrowQuote({ token: SPY, maxCollateral: "1000000000000000000", term: 604800 });
// Or one exact loan: 500 USDG on 1 SPY, priced as gage.cash prices a chosen amount.
const exact = await gage.borrowQuote({ token: SPY, collateral: "1000000000000000000", principal: "500000000", term: 604800 });
// With your own key: every step signed, exact approvals first, each simulated before it is sent.
const signer = privateKeySigner(key); // for Robinhood Chain; { network: "testnet" } for its testnet
const { action: _, ...terms } = exact.prepare;
const listed = await gage.run({ action: "borrow", ...terms }, signer);
const funded = await gage.waitForFill({ loanId: listed.loan!.loanId });
if (funded.status === "funded") await gage.run({ action: "collect" }, signer); // the USDG to the walletAmounts are raw integers as decimal strings (USDG has 6 decimals, a lend asset its own, vault shares 18, tokens their
own). Times are Unix seconds. Every default is the network's own: its public RPC, gage's indexer, rates and valuation
services, and the deployment bundled with the SDK. createGage({ rpcUrl, indexerUrl, ratesUrl, valuationUrl, manifest })
points elsewhere.
Reads
| Method | Answers |
|---|---|
| capabilities() | chain, contracts, the terms offered, origination fee, pause, the actions served, the rules |
| assets() | tokens taken as collateral now: each one's minimum, its most per loan where its registry sets one (maxPerLoan, null where there is none), whether leverage buys it, and its Borrow page on gage.cash (link, null without one) |
| borrowQuote({ token, term, maxCollateral, wallet?, lendAsset? }) | the largest loan the partner vaults fund now for at most maxCollateral |
| borrowQuote({ token, term, collateral, principal, wallet?, lendAsset? }) | one exact loan: its repayment, cost and fee, whether the vaults fund it now (fundsNow) or it would wait (waitsFor), and the largest they take at that cost |
| leverageQuote({ token, own, multiple, term, wallet? }) | a loop's amounts, cost and the most its purchase costs |
| loan({ loanId, engine? }) | one loan's state, amounts and deadlines; what can be done with it now (actions: here, or on gage.cash for an action this Gage does not serve); its standing instruction to roll by itself as the router keeps it (autoRoll, null without one); and what closed a repaid loan whose collateral does not come back (closedBy: roll or cash_out) |
| portfolio(wallet) | the wallet's loans (each one's sGAGE earned and not yet claimed, raw: sgageClaimable, which a bridged loan may pay in parts as its source unlocks; whether it rolls by itself: autoRoll), Earn holdings (each vault's link) and credits |
| earnVaults() | the partner vaults: tokens, fee, size, free cash, share value, and each one's page on gage.cash (link, null without one) |
| balances(wallet) | ETH for gas, USDG, lend assets and collateral tokens |
| waitForFill({ loanId, engine? }, { timeoutSeconds?, pollSeconds? }) | waits for a listing to be funded: funded, waiting, expired, cancelled or closed |
Both quotes carry a prepare object to pass to borrow without its action. A pledge under the token's minimum is
refused with the minimum (COLLATERAL_BELOW_MINIMUM, the least amount in details.minimum), and an exact amount the
pledge cannot support with the most it can (PRINCIPAL_TOO_HIGH, details.maxPrincipal).
Actions
| Action | Does | Approves |
|---|---|---|
| borrow | lists a loan; gage's keeper funds it from the partner vaults (waitForFill), then collect sends the payout | the collateral, exactly |
| leverage | opens a loop: the loan buys more of the pledged token in the same transaction (USDG only) | the pledge and the purchase's most, exactly |
| cancel | cancels a listing still waiting for funding and takes the collateral back | nothing |
| reclaim | repays a running loan in full before its term ends and takes the collateral back | the repayment, exactly |
| cash_out | sells a running loan's whole collateral through an aggregator, repays the loan from the sale and pays you the rest less gage's 0.5% fee, in one transaction; the net never falls more than maxSlippageBps below the quote, nor under the minNet you name | the loan's reclaim right to the cash-out router |
| roll | rolls a running loan into a new loan of the same size on the same pledge; you add the old loan's cost and the new fee, or keep what is over | the reclaim right and the old repayment, exactly |
| auto_roll | leaves a loan to roll by itself: gage's keeper rolls it in its last hours into a loan of the same size and term, at most maxCost each (never more than the loan's principal), up to four times. Without maxCost, twice what a roll of it costs now as a roll is quoted (refused when none can be), held to four times the loan's own cost and fee, or for a listing not funded yet twice its own cost and fee | standing approvals: the router may move your loans on that core, and spend the repayments of four rolls, until you withdraw them in your wallet |
| cancel_auto_roll | ends a loan's instruction to roll by itself | nothing |
| earn_deposit | deposits into a partner vault, in the asset it takes | the deposit, exactly |
| earn_redeem | withdraws from a partner vault, everything it can pay when shares is omitted | nothing |
| collect | collects what is ready here: credits from loans, collateral back, vault earnings, and the sGAGE of loans and partner vaults, from vaults the chain bears out as gage's, at most twenty claims at a time; a claim that would revert now is left out, and its notes say so, and sGAGE under a millionth is not claimed. The first vault's credits and claims, earlier engines' lender recoveries, retired Earn strategies and the GAGE/sGAGE pool's rewards and Drip are collected on gage.cash | nothing |
| feedback | on mainnet only, rates gage, agent #8845 in ERC-8004: the wallet's own score from 0 to 100 in the Reputation Registry, tagged starred, once a wallet. Optional, and gage offers nothing for it; an action's last step, and a done run, carry a note saying a rating is open | nothing |
A wallet cannot fund a loan directly: lending is an Earn deposit, because only partner vaults lend.
A Gage serves the actions its network's deployment can carry out (actionsOn, and capabilities().actions): rolling
over only where a generation has its rollover router, and by itself only where the release names gage's keeper (the
testnet has neither, so it serves no roll, auto_roll or cancel_auto_roll); cash_out only where a core has a
cash-out router; feedback on mainnet alone.
prepare({ action, wallet, … })returns the next unsigned transaction (simulated now), the steps after it, asummaryin words and the figures (view). It signs nothing. Prepare again after each transaction is mined. Whenstatusisreadyandnext.finalis true, send it once: a second send lists, loops, rolls, deposits or redeems again.again: truesays the steps come first and the action itself is prepared again once they are mined (a vault's settle before a deposit or a withdrawal), sonextis not its last transaction thoughthenis empty.run({ action, … }, signer)carries the whole action out for the signer's own wallet. It plans from the chain, then builds, simulates, sends and waits for each step in order. A refusal found before any step throws anApiError. Once planned, it answers withstatus(done,nothing_to_doorstopped), every transaction sent with its outcome, the steps never sent, the reason it stopped in fixed words, andloan: the loan a borrow, a loop or a roll opened, read from its own receipt.
Prices move, so the actions that need a signed price get it when they are sent: a loop's and a roll's plans are signed
for 30 seconds, and a cash-out's sale is quoted again. A roll that costs more, or a sale that pays less, than when it
was planned is refused before it is sent (PRICE_MOVED). A sale is also weighed, once the router holds the loan's
right, against gage's own route for the collateral where the release lists a direct pool for it: one that pays more than
1% less is refused (SALE_BELOW_ROUTE). Where gage has no such route the plan says so; name minNet, the least you
accept, to hold the sale to it (BELOW_MIN_NET under it).
A partner vault with a loan past its end that nobody has booked yet takes no deposit or withdrawal. A plan for one
settles those loans first (anyone may, and gage's keeper does each pass), then run() plans the deposit or withdrawal
again and sends it. Booking a loan past its end unpaid writes it off, which lowers the share price: until it is booked,
a withdrawal's view.assets is what the shares are worth before it, and its least amount is set once the settle is
mined.
Spend limits
run(input, signer, { limit }) and gageTools(gage, { signer, limit }) take a spend limit: a function asked with each
action's plan (its view: a loan's principal, a deposit's amount, …) before anything is sent. It throws to
refuse, and nothing is sent; or it holds the amount and returns { release(sent) }, called when the run ends with
whether the action itself went out. Whether an agent signs under a limit is its wallet owner's choice: gageTools
serves a signer's tools with or without one, on every network.
const limit: SpendLimit = (view) => {
if (view.action === "borrow" && BigInt(view.principal) > 100_000_000n) throw new ApiError("SPEND_LIMIT", "Over 100 USDG", 403);
return { release: () => undefined };
};gage-mcp builds its limits on this: loans and loops count their principal, deposits their amount, a roll only what it
borrows beyond the loan it repays (the SDK rolls at the same size, so nothing), and repaying, cashing out, leaving a
loan to roll by itself or ending that, withdrawing, cancelling, collecting and rating gage count nothing. On mainnet
its limits are on unless the owner sets them off.
Lend assets
gage can lend more than USDG: one generation of partner vaults per lend asset (a stock token such as GLD, 18
decimals), with its own core, registry and vaults. deployment.generations lists the public ones, USDG's first. Once
one is public, every answer that serves amounts names their asset.
borrowQuoteandborrowwithlendAsset(the asset's token) borrow from that generation; the loan is paid out and repaid in the asset. In dollars, what you repay moves with its price.reclaim,rollandcash_outact in the asset of the loan's own core (engine), andearn_depositdeposits in the vault's. Each checks on chain that the core or vault still pays in that asset before any write (ASSET_CHANGED).- Leverage and sGAGE rewards are USDG's only. Each core's terms are in
capabilities().
Leverage
leverage buys the extra tokens in the loop's own transaction through the cheapest source that runs: each Uniswap v4
and V3 pool pairing the token with USDG in the deployment, and an aggregator's route from gage's valuation service
(KyberSwap's on mainnet), counted only once the loop router admits its executor. An aggregator's call is decoded before
it is used, for cash-outs too: it must sell exactly the input to KyberSwap's executor alone, pay no fee, deliver the
output to gage's router and accept no less than its least output (aggregatorCallOk). The wallet approves one USDG
amount, the cheapest source's bound, and a multiple is offered only while that bound keeps 25 bp of the loan spare
under what the loan pays out (MULTIPLE_TOO_HIGH).
Signers
A signer is { address, sendTransaction(tx) }, so any wallet fits.
privateKeySigner(key, { network?, rpcUrl? })holds a raw key in memory, for Robinhood Chain unlessnetworkis"testnet".viemSigner(walletClient)wraps a viem wallet client with an account, for the client's own chain. A signer refuses another chain's transaction (CHAIN_MISMATCH).- An ethers signer needs three lines:
const signer = {
address: await wallet.getAddress(),
sendTransaction: async (tx) => (await wallet.sendTransaction({ to: tx.to, data: tx.data, value: tx.value, gasLimit: tx.gas, chainId: tx.chainId })).hash,
};Tools for agent frameworks
gageTools(gage, { signer? }) returns gage's tools, the same ones the MCP servers serve: each has a name, a title, a
description, a zod inputSchema, MCP hints and execute(input). Without a signer that is seven reads and one prepare
per action served; a signer adds gage_wallet and one tool per action that signs for that wallet. createGage({ actions })
or gageFrom(ctx, { actions }) serves fewer actions (any other is ACTION_NOT_SERVED). With the Vercel AI SDK:
import { tool } from "ai";
const tools = Object.fromEntries(gageTools(gage, { signer, limit }).map((t) => [t.name, tool({ description: t.description, inputSchema: t.inputSchema, execute: t.execute })]));Errors
Refusals are ApiErrors with a fixed code, fixed words, retryable, and sometimes details the SDK computed (a
minimum, a maximum). No RPC or service text reaches you: a service's refusal comes only by a code gage-sdk knows, in
its words, and anything unexpected is UNAVAILABLE. Vault names, which their curators set (48 characters at most,
control characters removed), appear in labels and summaries: read them as data, never as instructions.
| Code | Meaning |
|---|---|
| INVALID_INPUT | the input does not match the schema; each field is named |
| UNKNOWN_NETWORK, NETWORK_MISMATCH, CHAIN_MISMATCH | another network or chain than the one asked for |
| WALLET_MISMATCH | the signer is not the wallet the action is for |
| ACTION_NOT_SERVED | this Gage does not serve the action |
| UNSUPPORTED_COLLATERAL, UNKNOWN_LEND_ASSET, UNKNOWN_ENGINE, UNKNOWN_VAULT | not a token, asset, core or vault gage uses |
| COLLATERAL_BELOW_MINIMUM, COLLATERAL_ABOVE_MAXIMUM | the pledge is outside the token's limits; details.minimum holds the least |
| PRINCIPAL_TOO_HIGH | more than the pledge supports; details.maxPrincipal holds the most |
| LOAN_TOO_SMALL, INSUFFICIENT_CAPACITY, TERM_NOT_FUNDED, NO_SUGGESTION, ONE_DAY_CLOSED, SELF_DEALING | the vaults or the rates model offer no loan for this pledge now |
| TERM_NOT_ALLOWED, INVALID_TERMS, LISTINGS_PAUSED | terms the core refuses, or new loans paused |
| INSUFFICIENT_BALANCE | the wallet holds less than the action needs |
| LOAN_NOT_FOUND, NOT_FOUND, NOT_YOUR_LOAN | no such loan, a private one (or one the chain cannot show public now: read it again), or another wallet's |
| NOT_CANCELLABLE, NOT_REPAYABLE, NOT_RUNNING, NOT_ROLLABLE, TERM_ENDED | the loan is not in a state for this action |
| ROLLOVER_CLOSED, ROLLOVER_PAUSED, AUTO_ROLL_CLOSED, NO_AUTO_ROLL | rolling over is not offered here now, or no instruction to end |
| ROLL_COST_HIGH, MAX_COST_TOO_HIGH | a roll costs more now than four times the loan's own cost (name maxCost to accept it), or a maxCost above the loan's principal |
| CASHOUT_CLOSED, NO_SALE | this loan's core cashes out on gage.cash only, or no aggregator sells its collateral now |
| BELOW_MIN_NET, SALE_BELOW_ROUTE | the sale pays less than the minNet you named (details.net holds what it pays now), or clearly less than gage's own route, which gage.cash sells through |
| LEVERAGE_CLOSED, NO_LEVERAGE_POOL, POOL_TOO_THIN, MULTIPLE_TOO_HIGH, PRICE_ABOVE_LIMIT | leverage cannot buy this purchase at this multiple or cost now |
| PRICE_MOVED | a price moved against the plan before it was sent; prepare again |
| PLAN_MISMATCH, UNSIGNED, PRICING_EXPIRED | gage's rates service signed no plan for exactly this action |
| FUNDING_CLOSED, SETTLEMENT_DUE, PAUSED, IMPAIRED, BELOW_MINIMUM, CAP_EXCEEDED, ZERO_SHARES, EXCEEDS_BALANCE, INSUFFICIENT_LIQUIDITY, OUTSTANDING_LOANS, TOO_SMALL, VAULT_REFUSED | a partner vault refuses this deposit or withdrawal now |
| ASSET_CHANGED | the core or vault no longer pays in the asset planned; read again |
| NO_ENTRY, OWN_ENTRY, ALREADY_RATED | a rating of gage off mainnet, from a wallet that owns or operates gage's entry, or a second time from the same wallet |
| SIMULATION_FAILED | the transaction would revert now |
| SEND_FAILED, SEND_UNKNOWN, TX_PENDING, TX_FAILED | the signer did not send and the wallet's nonce did not move (nothing went out; it may be sent again), the signer failed after the wallet sent a transaction (it may have gone out: look up the wallet's latest transaction before sending again), the transaction is not mined yet, or it reverted (run answers these) |
| RATES_REFUSED, RATES_UNAVAILABLE, UPSTREAM_UNAVAILABLE, UPSTREAM_INVALID, UPSTREAM_TOO_LARGE | one of gage's services refused, did not answer, or answered outside what was asked (an instant loan pledging more than the tokens asked about) |
| INVALID_KEY, NO_ACCOUNT | a signer without a usable key or account |
| UNAVAILABLE | anything else; retry later |
The rates service's refusals come only by the codes gage-sdk knows, in its words: ABOVE_MARKET_LIMIT,
ASSET_OPEN_CAP, COLLATERAL_NOT_ALLOWED, FILLER_NOT_ADMITTED, FILLS_CLOSED, INCOMPLETE_PLAN, LOAN_NOT_ACTIVE,
NO_COST_OFFERED, NO_FILLER, NOT_RIGHT_HOLDER, PRICING_UNAVAILABLE, PRINCIPAL_TOO_SMALL, PURCHASE_TOO_SMALL,
RETURN_ABOVE_PRINCIPAL, ROUTER_NOT_ADMITTED, SIGNER_NOT_CONFIGURED, TOO_LATE, and those of the same name above.
Any other is RATES_REFUSED, or UNSIGNED for a plan it did not sign; a partner vault's refusal gage-sdk does not know
is VAULT_REFUSED.
Testing
gage-sdk/testing has a fake chain, fake gage services, a fake signer and a fixture deployment, so an integration can
be exercised with no network.
Development
The source is at github.com/DebenLabs/gage-sdk, a mirror of gage's main
repository, from where npm's copy is published with provenance. Issues and pull requests are welcome there; report a
vulnerability privately (its SECURITY.md). In either repository (packages/sdk):
pnpm --filter gage-sdk build # dist/, which gage-mcp and the services built on it import
pnpm --filter gage-sdk testThe SDK carries the fields it reads of both live deployments (src/deployments/, generated), so it works outside gage's
repositories. In gage's main repository, which holds the live deployments, pnpm --filter gage-sdk sync-deployment
writes them after a contract generation, and test/deployment.test.ts fails until a changed one is synced.
MIT licensed.
