
Liquidium Borrow
- 5 installs
- 1 repo stars
- Updated July 29, 2026
- ropl-btc/agent-skills
Runs Liquidium borrow, repay, and portfolio flows over the @liquidium/client SDK via a CLI, defaulting to accountless instant BTC-collateral loans.
About
Wraps the Liquidium non-custodial lending protocol in a guarded CLI covering quotes, instant loan creation, repayment, collateral top-ups, and portfolio dashboards. A developer uses it to build or operate agent-driven crypto lending workflows without connecting a wallet by default.
- Accountless instant loans that need no wallet adapter, using client.instantLoans.create
- Always surfaces LTV, liquidation thresholds, and APY with dry-run safety before any transfer
Liquidium Borrow by the numbers
- 5 all-time installs (skills.sh)
- Ranked #338 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ropl-btc/agent-skills --skill liquidium-borrowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | ropl-btc/agent-skills ↗ |
What it does
Runs Liquidium borrow, repay, and portfolio flows over the @liquidium/client SDK via a CLI, defaulting to accountless instant BTC-collateral loans.
Files
Liquidium Borrow
Liquidium is a decentralized, non-custodial cross-chain lending protocol. The default agent flow is an accountless instant loan: the user provides collateral and destination addresses, Liquidium returns deposit and repayment targets, and the agent stores the loan reference for later status, repayment, or collateral top-up.
Liquidium docs: https://liquidium.fi/docs and SDK docs: https://liquidium.fi/docs/sdk.
Load The Right Reference
- Instant loans, manual wallet users, wallet-access agents, saved loan records, repayment, add-collateral, and status tracking: read
references/instant-loans.md. - Connected-wallet profile flows for supply, borrow, repay, withdraw, and portfolio dashboards: read
references/profile-flows.md. - LTV, APY, health factor, liquidation thresholds, high-LTV warnings, and risk copy: read
references/risk-and-liquidation.md. - SDK method shapes, CLI details, TypeScript examples, and troubleshooting: read
references/sdk-methods.md.
Quick Start CLI
Run the wrapper from this skill directory:
<skill-path>/scripts/liquidium-borrow poolsThe CLI bootstraps a pinned @liquidium/client package into ~/.cache/liquidium-borrow/node on first real CLI use. This requires npm/network access, installs with lifecycle scripts disabled, and is excluded from the packaged skill. Set LIQUIDIUM_CLI_CACHE_DIR to use a different cache location.
Created instant loans are saved by default under ~/.local/share/liquidium-borrow/loans/<ref>.json; local loan records live outside the repo. Set LIQUIDIUM_CLI_DATA_DIR to store records elsewhere. Use --no-save only when the caller has another durable record system.
Common commands:
<skill-path>/scripts/liquidium-borrow max-borrow --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005
<skill-path>/scripts/liquidium-borrow quote --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005 --borrow-amount-decimal 9
<skill-path>/scripts/liquidium-borrow instant-create --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005 --borrow-amount-decimal 9 --borrow-destination 0x2222222222222222222222222222222222222222 --refund-destination bc1qrefunddestination
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action status
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action repay
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action add-collateral
<skill-path>/scripts/liquidium-borrow deposit-status --ref 8Y9AQQ --txid <txid>
<skill-path>/scripts/liquidium-borrow instant-activities --ref 8Y9AQQ --filter active
<skill-path>/scripts/liquidium-borrow loan-list
<skill-path>/scripts/liquidium-borrow loan-show --ref 8Y9AQQ
<skill-path>/scripts/liquidium-borrow loan-tx --ref 8Y9AQQ --kind collateral --txid <txid>
<skill-path>/scripts/liquidium-borrow profile-summary --profile-id <profile-id>The CLI prints JSON for reads and summaries. Use --json for raw SDK output where supported. It does not sign messages or broadcast wallet transactions; use wallet tools only after explicit user confirmation.
Decision Tree
1. If the user wants to borrow without a Liquidium profile or wallet connection, use instant loans. Read references/instant-loans.md. 2. If the user asks "how much can I borrow?", run max-borrow, then explain LTV and liquidation risk. Read references/risk-and-liquidation.md. 3. If the user wants to create, fund, repay, add collateral, or check an instant loan, use instant-create, loan-instructions, deposit-status, instant-get, instant-activities, and local loan records. 4. If the user lost the instant-loan reference, use instant-find --address <address> only to find candidates, then hydrate with instant-get or loan-instructions. 5. If the user wants a portfolio dashboard, repeated borrowing, connected-wallet supply, withdraw, or profile-level repay, use profile flows. Read references/profile-flows.md. 6. If the user wants ETH ERC-20 contract-interaction deposits, use profile-based client.lending.supply({ mechanism: "contractInteraction", ... }) with an EVM RPC and wallet adapter.
Safety Rules
- Creating an instant loan request is not the same as transferring collateral. Before broadcasting any on-chain transfer, sending funds, or signing a wallet message, get explicit user confirmation of asset, amount, destination, chain, fee assumptions, and refund/borrow destination.
- Treat borrow, supply, repay, and withdraw as financially consequential. Validate amounts, LTV, pool status, destination addresses, chain, and base-unit conversions before execution.
- Always explain liquidation risk when discussing borrow capacity or high LTV. Include current LTV, max allowed LTV, liquidation threshold LTV, and estimated collateral liquidation price when available. Default to the short warning from
references/risk-and-liquidation.md; add the high-LTV warning only when the quote is close to max LTV. - For instant loans, never reuse a collateral deposit target as a repayment target. Always refresh loan state and use the target for the exact action: collateral/add-collateral uses
depositTarget; repayment usesrepayment.target. - When returning instant-loan funding instructions, include
depositWindowSeconds, an estimated absolute deposit deadline timestamp when available, and make the final line exactly specify the next transfer:Send <amount> <asset> to <deposit address>. - When the user says they sent collateral, run
deposit-status --ref <ref>; include--txid <txid>if they provide one. Explain that Liquidium detection can take a few minutes after broadcast/confirmation, and report activity status, confirmations, and required confirmations when available. - Include the current borrowing interest rate/APY from the SDK when available. Never display raw scaled rate integers to users.
- For instant loans, explain that the refund address is where collateral is returned after full repayment or when collateral cannot be applied, including failed/late/expired deposit cases. Explain that sending more collateral to the deposit target after the loan starts is a top-up that lowers LTV and improves health.
- For instant loans, explain that partial repayments may reduce debt/LTV but do not trigger collateral withdrawal. The loan must be repaid in full to receive the full collateral amount back.
- Do not expose, request, or store wallet private keys or seed phrases. Use existing wallet providers/adapters.
- If the environment lacks a wallet or transfer tool, return exact transfer instructions rather than pretending to move funds.
Common Mistakes
- Requiring a wallet adapter for instant loans. Instant loans do not need one.
- Calling
client.lending.borrow(...)for the default accountless flow. Useclient.instantLoans.create(...). - Treating address recovery as canonical loan state. Hydrate candidates with
instant-get. - Displaying raw bigint base units or raw fixed-point rates to users.
- Ignoring frozen pools or
validationErrors. - Confusing instant-loan collateral deposit targets with repayment targets.
- Assuming profile-flow
outflow.txidis present immediately after borrow.
node_modules/
interface:
display_name: "Liquidium Borrow"
short_description: "Borrow and supply via Liquidium SDK"
default_prompt: "Use $liquidium-borrow to implement a headless Liquidium instant-loan borrow flow."
{
"name": "liquidium-borrow",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@liquidium/client": "0.1.2"
}
},
"node_modules/@adraffy/ens-normalize": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
"license": "MIT"
},
"node_modules/@dfinity/agent": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-2.4.1.tgz",
"integrity": "sha512-IczFFOUDGfMTdQ83yiCvGtvHr1IIB80lWBP0ZYRLogs6NVt8t6HYcMlu1sgT+9VivhT7iwX4pktPFxxOkO3COw==",
"deprecated": "This package has been deprecated. Use @icp-sdk/core/agent instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@noble/curves": "^1.4.0",
"@noble/hashes": "^1.3.1",
"base64-arraybuffer": "^0.2.0",
"borc": "^2.1.1",
"buffer": "^6.0.3",
"simple-cbor": "^0.4.1"
},
"peerDependencies": {
"@dfinity/candid": "^2.4.1",
"@dfinity/principal": "^2.4.1"
}
},
"node_modules/@dfinity/candid": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-2.4.1.tgz",
"integrity": "sha512-kOaIKfhR2PYN8vD4M0Pc4s/7wb1nKjlTJUw+5E9jh26T03fITIZmaafIuwlX+wmdxwIT9Xoy7PlsxOEpzv203A==",
"deprecated": "This package has been deprecated. Use @icp-sdk/core/candid instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"@dfinity/principal": "^2.4.1"
}
},
"node_modules/@dfinity/cbor": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@dfinity/cbor/-/cbor-0.2.3.tgz",
"integrity": "sha512-5GX+9tAf29r94Pxv8nCzrv2/t+AkOfem/c/G61+FA2Wadq7THvc5cvr/AVLH0LpukVfNSWRRHvl1fkCdiB9MMQ==",
"license": "Apache-2.0"
},
"node_modules/@dfinity/principal": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-2.4.1.tgz",
"integrity": "sha512-Cz6XQVOwq0TXDBClPbcidDd4SqK1lfr1/Kn34ruDD13xVQ4iaP1iCntzS9O97+vGpY/6jwDtKd32Gn5YJ9BQNw==",
"deprecated": "This package has been deprecated. Use @icp-sdk/core/principal instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@noble/hashes": "^1.3.1"
}
},
"node_modules/@dfinity/utils": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/@dfinity/utils/-/utils-2.14.0.tgz",
"integrity": "sha512-WGwME4891K9TU7SXchhUVtdSbU3jUxnndblcrNJH0/QLPuQGnvP/7YhFL1b6MhI5GRpTWh0qLsyvMpEpdw7Faw==",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"@dfinity/agent": "^2.0.0",
"@dfinity/candid": "^2.0.0",
"@dfinity/principal": "^2.0.0"
}
},
"node_modules/@icp-sdk/core": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@icp-sdk/core/-/core-5.4.0.tgz",
"integrity": "sha512-yedhCkHIhCxI65W8id99Mi8fUgSyqKSTbD57AphquNcFV2/3Rjykng0/6H08mHswKydDcv6Y0+e8aPdrznewCw==",
"license": "Apache-2.0",
"dependencies": {
"@dfinity/cbor": "^0.2.3",
"@noble/curves": "^1.9.7",
"@noble/hashes": "^1.8.0",
"@scure/bip32": "^1.7.0",
"@scure/bip39": "^1.6.0",
"asn1js": "^3.0.7"
}
},
"node_modules/@liquidium/client": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@liquidium/client/-/client-0.1.2.tgz",
"integrity": "sha512-osL8IRzLwBd8QPdLiib6NNIzMGCFnlf1wxc0C2JGvlJeZOy9UVConV/Sc9FkPcOkn3aHOfuWwhC+5QXl9w9Ocg==",
"license": "MIT",
"dependencies": {
"@dfinity/ledger-icrc": "2.1.3",
"@dfinity/principal": "2.1.3",
"@icp-sdk/core": "^5.4.0",
"@noble/hashes": "^1.8.0",
"viem": "^2.50.4"
}
},
"node_modules/@liquidium/client/node_modules/@dfinity/agent": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-1.4.0.tgz",
"integrity": "sha512-/zgGajZpxtbu+kLXtFx2e9V2+HbMUjrtGWx9ZEwtVwhVxKgVi/2kGQpFRPEDFJ461V7wdTwCig4OkMxVU4shTw==",
"deprecated": "This package has been deprecated. Use @icp-sdk/core/agent instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@noble/curves": "^1.4.0",
"@noble/hashes": "^1.3.1",
"base64-arraybuffer": "^0.2.0",
"borc": "^2.1.1",
"buffer": "^6.0.3",
"simple-cbor": "^0.4.1"
},
"peerDependencies": {
"@dfinity/candid": "^1.4.0",
"@dfinity/principal": "^1.4.0"
}
},
"node_modules/@liquidium/client/node_modules/@dfinity/candid": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-1.4.0.tgz",
"integrity": "sha512-PsTJVn63ZM4A/6Xs5coI0zMFevSwJ8hcyh38LdH/92n6wi9UOTis1yc4qL5MZvvRCUAD0c3rVjELL+49E9sPyA==",
"deprecated": "This package has been deprecated. Use @icp-sdk/core/candid instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"@dfinity/principal": "^1.4.0"
}
},
"node_modules/@liquidium/client/node_modules/@dfinity/ledger-icrc": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.1.3.tgz",
"integrity": "sha512-MJWm05EJlA596ekG0OfGrW7JTwC++XrNFy8H2VZYVjPzLB22DZNp4oBtxZ4gX6BByPvSru2z0ZLo3a/Jzk9CBA==",
"license": "Apache-2.0",
"peerDependencies": {
"@dfinity/agent": "^1.0.1",
"@dfinity/candid": "^1.0.1",
"@dfinity/principal": "^1.0.1",
"@dfinity/utils": "^2.1.2"
}
},
"node_modules/@liquidium/client/node_modules/@dfinity/principal": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-2.1.3.tgz",
"integrity": "sha512-HtiAfZcs+ToPYFepVJdFlorIfPA56KzC6J97ZuH2lGNMTAfJA+NEBzLe476B4wVCAwZ0TiGJ27J4ks9O79DFEg==",
"deprecated": "This package has been deprecated. Use @icp-sdk/core/principal instead. Migration guide: https://js.icp.build/core/latest/upgrading/v5",
"license": "Apache-2.0",
"dependencies": {
"@noble/hashes": "^1.3.1"
}
},
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
"integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.9.0",
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/abitype": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
"integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/wevm"
},
"peerDependencies": {
"typescript": ">=5.0.4",
"zod": "^3.22.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/asn1js": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
"integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
"license": "BSD-3-Clause",
"dependencies": {
"pvtsutils": "^1.3.6",
"pvutils": "^1.1.5",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/base64-arraybuffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==",
"peer": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true
},
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": "*"
}
},
"node_modules/borc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/borc/-/borc-2.1.2.tgz",
"integrity": "sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"bignumber.js": "^9.0.0",
"buffer": "^5.5.0",
"commander": "^2.15.0",
"ieee754": "^1.1.13",
"iso-url": "~0.4.7",
"json-text-sequence": "~0.1.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/borc/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT",
"peer": true
},
"node_modules/delimit-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz",
"integrity": "sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==",
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC",
"peer": true
},
"node_modules/iso-url": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/iso-url/-/iso-url-0.4.7.tgz",
"integrity": "sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
}
},
"node_modules/isows": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
"integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/json-text-sequence": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz",
"integrity": "sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==",
"license": "MIT",
"peer": true,
"dependencies": {
"delimit-stream": "0.1.0"
}
},
"node_modules/ox": {
"version": "0.14.27",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.14.27.tgz",
"integrity": "sha512-+xhLHo/f+f4BH121/1Pomm/1vgBBda1wYiFpTvjSo8o5OcEj76Pf1hGPJiepoYMTQoTm2SKdSBvWkFWk5l07PA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "^1.11.0",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "1.9.1",
"@noble/hashes": "^1.8.0",
"@scure/bip32": "^1.7.0",
"@scure/bip39": "^1.6.0",
"abitype": "^1.2.3",
"eventemitter3": "5.0.1"
},
"peerDependencies": {
"typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ox/node_modules/@noble/curves": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/pvutils": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
"integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true
},
"node_modules/simple-cbor": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/simple-cbor/-/simple-cbor-0.4.1.tgz",
"integrity": "sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==",
"license": "ISC",
"peer": true
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT",
"peer": true
},
"node_modules/viem": {
"version": "2.52.0",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.52.0.tgz",
"integrity": "sha512-py2QPYe9e1f4DmPJCsXF7zHmyZ0PkJrBxdQZ5dvNXvzy3UzWkUn7dNfC0TMeNm6Qv1tKw3b6qXXExpx6L0oMbw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@noble/curves": "1.9.1",
"@noble/hashes": "1.8.0",
"@scure/bip32": "1.7.0",
"@scure/bip39": "1.6.0",
"abitype": "1.2.3",
"isows": "1.0.7",
"ox": "0.14.27",
"ws": "8.20.1"
},
"peerDependencies": {
"typescript": ">=5.0.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/viem/node_modules/@noble/curves": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/ws": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
{
"private": true,
"type": "module",
"dependencies": {
"@liquidium/client": "0.1.2"
}
}
Instant Loans
Use instant loans when the user wants to borrow without creating a Liquidium profile or connecting a wallet. This is the default Liquidium borrow UX and works well for manual wallets, air-gapped wallets, hardware wallets, exchange accounts, and agent-guided workflows.
Manual Wallet Flow
Use this when the user controls their own wallet and the agent only guides the process.
1. For "how much can I borrow with X collateral?", run max-borrow with live data. 2. Explain collateral asset/chain, borrow asset/chain, estimated maximum borrow, current borrowing interest rate/APY, max LTV, liquidation threshold LTV, estimated collateral liquidation price when available, and that the estimate must be requoted before execution. 3. If the user proposes a borrow amount, run quote and check validationErrors. 4. If LTV is high, recommend a lower borrow amount or more collateral. Read risk-and-liquidation.md for warning copy. 5. When the user wants to proceed, ask for:
- borrow destination address on the borrow asset chain
- refund address on the collateral asset chain
6. Before loan creation, restate collateral asset/chain/amount, borrow asset/chain/amount, borrow destination, refund destination, current borrowing interest rate/APY, current LTV, max allowed LTV, liquidation threshold LTV, estimated collateral liquidation price when available, deposit window, estimated absolute deadline when available, and the liquidation warning. 7. After explicit confirmation, run instant-create. It saves ~/.local/share/liquidium-borrow/loans/<ref>.json. 8. Return the exact funding instructions from the CLI/SDK:
- loan ref and local record path
- collateral amount in display units and base units
- collateral asset and chain
- deposit target
- borrowed amount in display units and base units
- borrow asset and chain
- borrow destination
- refund destination
- current LTV, max LTV, liquidation threshold LTV, estimated collateral liquidation price, current borrowing interest rate/APY
- deposit window, estimated absolute deadline, and status
9. Tell the user to send only the specified collateral asset/chain/amount to the deposit target. The final line of the response should be exactly actionable: Send <amount> <asset> to <deposit address>. 10. Do not tell the user to use the deposit target for repayment. 11. Explain that the refund address receives collateral returns after full repayment, or refunds if collateral cannot be applied, including failed/late/expired deposits.
Wallet-Access Agent Flow
Use this when the agent has access to a wallet tool such as a CLI wallet. The default Liquidium route can still be accountless instant loans; the wallet tool only broadcasts the user's collateral or repayment transaction.
1. Run max-borrow or quote and explain risk. 2. Ask for or derive borrow/refund addresses. Verify the addresses match the requested asset/chain. 3. Restate the full plan and ask for explicit user confirmation before creating the loan. 4. Run instant-create; save the generated ~/.local/share/liquidium-borrow/loans/<ref>.json record. 5. Before sending collateral, restate exact send amount, asset, chain, deposit target, receiving borrow address, expected borrow amount, current LTV, max LTV, liquidation threshold LTV, estimated collateral liquidation price, current borrowing interest rate/APY, and liquidation warning. 6. After explicit confirmation, use the wallet tool to send only the exact collateral amount to loan.depositTarget. 7. Store the txid with loan-tx --ref <ref> --kind collateral --txid <txid>. 8. Poll with deposit-status --ref <ref> --txid <txid> and loan-instructions --ref <ref> --action status until active or until the user asks to stop. 9. For repayment, run loan-instructions --ref <ref> --action repay, ask for explicit confirmation, use the wallet tool to send repayment, then store the txid with loan-tx --ref <ref> --kind repayment --txid <txid>.
Commands
From the skill directory:
<skill-path>/scripts/liquidium-borrow max-borrow --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005
<skill-path>/scripts/liquidium-borrow quote --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005 --borrow-amount-decimal 9
<skill-path>/scripts/liquidium-borrow instant-create --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005 --borrow-amount-decimal 9 --borrow-destination 0x2222222222222222222222222222222222222222 --refund-destination bc1qrefunddestination
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action status
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action repay
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action add-collateral
<skill-path>/scripts/liquidium-borrow deposit-status --ref 8Y9AQQ --txid <txid>
<skill-path>/scripts/liquidium-borrow instant-activities --ref 8Y9AQQ --filter active
<skill-path>/scripts/liquidium-borrow instant-find --address bc1q...
<skill-path>/scripts/liquidium-borrow loan-list
<skill-path>/scripts/liquidium-borrow loan-show --ref 8Y9AQQ
<skill-path>/scripts/liquidium-borrow loan-tx --ref 8Y9AQQ --kind collateral --txid <txid>instant-create, instant-get, and loan-instructions save or update the local loan record unless --no-save is passed.
Repayment
Always refresh immediately before giving repayment instructions:
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action repayRepayment target is loan.repayment.target, not loan.depositTarget. The current full repayment amount includes accrued interest and buffers where the SDK provides them. Interest accrues continuously, so stale repayment amounts can be wrong.
Partial repayments reduce debt and may reduce LTV, but they do not trigger collateral withdrawal. The loan must be repaid in full to receive the full collateral amount back at the refund address. If the user asks to fully close a loan, use the latest full repayment amount from the refreshed loan.
Add Collateral
Use add-collateral instructions when the user wants to lower LTV or improve loan health:
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action add-collateralAdding collateral uses the collateral deposit target. It is not repayment and should not be sent to the repayment target. If the user sends more collateral to the deposit target after the loan has started, it tops up the position, lowers LTV, and improves loan health.
Status And States
Use loan-instructions --action status, instant-get, and instant-activities.
When the user asks whether Liquidium detected their collateral deposit, prefer:
<skill-path>/scripts/liquidium-borrow deposit-status --ref 8Y9AQQ --txid <txid>If the user has no txid, omit --txid. This command combines instantLoans.get with activities.list({ shortRef, filter: "all" }), saves the latest status locally, and reports whether a deposit activity is visible, whether the provided txid matched Liquidium activity, current/required confirmations, and the loan status.
Explain detection this way:
- If deposit activity exists, say Liquidium has detected the collateral deposit and report activity status (
pending,detected,processing,confirmed, orfailed) plus confirmations when available. - If activity is still empty but the user has a blockchain txid, say Liquidium has not surfaced it in the SDK yet and that detection can take a few minutes after broadcast or confirmation. If a Bitcoin explorer/tool is available, verify the tx separately against the deposit address and amount.
- If the loan remains
awaiting_depositbut a deposit activity exists, trust and report the activity feed as the more granular deposit-tracking surface; keep polling until the loan becomesdeposit_detected,active,closed, or failed/refunded.
Explain states this way:
awaiting_deposit: the loan exists and waits for collateral. Show deposit target, deposit window, and estimated absolute deadline when available.deposit_detected: collateral has been seen and the borrow is processing. Keep polling.active: borrow is open. User can repay or add collateral.settling: closing or processing. Avoid duplicate actions.closed: loan is finished. Stop prompting for repayment.
Product docs also mention user-facing states like "deposit too small" and "repaid"; if activities or loan state indicate those, explain what additional collateral or no further action is needed.
If collateral is sent after the deposit window or cannot be applied to the loan, explain that the specified refund address is where collateral refunds/returns are directed. If collateral is sent to the deposit address after the loan has started, explain that it is treated as a collateral top-up, not a refund and not a repayment.
Local Records
Saved records live in ~/.local/share/liquidium-borrow/loans/<ref>.json by default and should include request details, latest loan state, transfer instructions, deposit deadline estimate, risk fields, txids, and timestamps. local loan records live outside the repo.
Use LIQUIDIUM_CLI_DATA_DIR when the host wants records outside the skill directory.
Recovery
If the user lost the reference, use address recovery:
<skill-path>/scripts/liquidium-borrow instant-find --address <borrow-or-refund-address>Candidates are not canonical. Hydrate the selected candidate with instant-get or loan-instructions before showing targets or repayment amounts.
Liquidium product docs mention transaction-ID recovery, but the current SDK path reviewed exposes address recovery, not a public findByTxid helper. Store txids locally with loan-tx for future local lookup.
Profile Flows
Use profile flows when the user wants a connected-wallet Liquidium dashboard: supply, borrow, repay, withdraw, repeated borrowing, positions, linked wallets, or profile-level portfolio health. Do not add profile creation to the default accountless instant-loan flow.
When To Use
- The user wants to manage positions across sessions from one profile.
- The user wants to supply liquidity and earn yield.
- The user wants to borrow repeatedly against existing supplied collateral.
- The user wants to repay or withdraw from a profile position.
- The user wants portfolio summary, health factor, reserves, history, or linked wallets.
- The user explicitly wants an ETH contract-interaction supply flow.
Portfolio Reads
<skill-path>/scripts/liquidium-borrow profile-summary --profile-id <profile-id>
<skill-path>/scripts/liquidium-borrow positions --profile-id <profile-id>SDK calls:
const positions = await client.positions.listPositions(profileId);
const summary = await client.positions.getUserPositionSummary(profileId);
const reserves = await client.positions.getUserReserves(profileId);
const healthFactor = await client.positions.getHealthFactor(profileId);For profile questions, explain that health can change as prices move and interest accrues.
Profile Creation
Use only when wallet signing is available and the user confirms profile creation.
const profileId = await client.accounts.createProfile({
account: walletAddress,
chain: "ETH",
walletAdapter: {
signMessage: async ({ message }) => wallet.signMessage(message),
},
});Prepare/sign/submit variant:
const action = await client.accounts.prepareCreateProfile({ account: walletAddress });
const signature = await wallet.signMessage(action.message);
const profileId = await action.submit({
signature,
chain: "ETH",
account: walletAddress,
});Handle existing profiles by catching the relevant SDK/protocol error and resolving the existing profile instead of retrying.
Supply And Repay By Deposit Address
Use this when the app/profile should show a target and let the user send funds externally.
const supplyFlow = await client.lending.supply({
profileId,
poolId,
action: "deposit",
});
showTarget(supplyFlow.target);
await supplyFlow.submit({ txid });Use action: "repayment" for repayment. Deposit and repayment targets are action-specific; do not reuse a deposit target for repayment.
Wallet-Executed Supply
For BTC or transfer-path ETH stablecoin supply, provide only the wallet adapter methods needed.
const supplyFlow = await client.lending.supply({
profileId,
poolId,
action: "deposit",
amount,
account: walletAddress,
walletAdapter: {
sendBtcTransaction: async ({ toAddress, amountSats }) =>
wallet.sendBtcTransaction({ toAddress, amountSats }),
},
});ETH Contract-Interaction Supply
Use only for account-based ETH stablecoin flows where an EVM wallet should approve and deposit through the SDK.
Requirements:
evmRpcUrlorevmPublicClient- wallet adapter with
sendEthTransaction - token amount in base units
- explicit user confirmation before approval/deposit transactions
const supplyFlow = await client.lending.supply({
mechanism: "contractInteraction",
profileId,
poolId,
action: "deposit",
walletAdapter,
account: evmAddress,
amount,
});Instant loans return their own deposit and repayment targets; do not use this path for accountless instant loans.
Profile-Based Borrow
Use quote-first borrowing for connected profiles:
const quote = client.quote.getQuote(request, pools, prices);
if (quote.validationErrors.length > 0) {
throw new Error(quote.validationErrors.map((error) => error.message).join(" "));
}
const outflow = await client.lending.borrow({
profileId,
poolId: quote.borrowPoolId,
amount: quote.borrowAmount,
receiverAddress,
signerWalletAddress: walletAddress,
signerChain: "ETH",
signerWalletAdapter: {
signMessage: async ({ message }) => wallet.signMessage(message),
},
});Display outflow.id immediately. outflow.txid may be missing until the protocol assigns or broadcasts the chain transaction. Use activities/history or app-level polling if a txid is needed.
Withdraw
Use signed profile withdraw flows when the user wants to remove supplied assets. Confirm destination, asset, amount, chain, and risk impact before signing or submitting.
Safety
- Confirm before every signature or transaction.
- Never ask for private keys or seed phrases.
- For supply, repay, borrow, or withdraw, show asset, chain, amount, destination/target, expected APY/rate where available, and health/risk impact.
- For profile repayment, use the repayment target for
action: "repayment"; do not reuse deposit target.
Risk And Liquidation
Use this when answering borrow-capacity, LTV, health, repayment, add-collateral, or liquidation-risk questions.
Core Concepts
- Liquidium loans are over-collateralized.
- LTV is loan-to-value: borrowed value divided by collateral value.
- Max LTV is the highest allowed starting/validation LTV for the selected collateral/borrow pair.
- Liquidation threshold is the risk threshold where collateral can be sold to repay debt.
- Liquidation price is the estimated collateral asset price where the current debt/collateral pair reaches the liquidation threshold.
- Health factor/portfolio health shows how far a profile position is from liquidation.
- Borrow APY/interest rate is dynamic and can change with pool utilization.
- Interest accrues continuously and compounds; debt grows over time.
- There is no fixed repayment date, but liquidation risk can rise as prices move or debt grows.
Agent Warning Policy
Always mention risk when:
- the user asks how much they can borrow
- the user asks for a high LTV
- the quote is close to max LTV
- the user asks to proceed with a borrow
- the user asks about repayment or adding collateral
Suggested concise warning:
Liquidation risk: if your LTV reaches the liquidation threshold, collateral can be sold to repay the loan.
If LTV is near max:
This is close to the max LTV, so a price move or accrued interest could put the loan at risk.
If giving instant-loan funding instructions:
Send only the specified collateral asset on the specified chain before the deposit deadline.
If giving repayment instructions:
Interest accrues continuously, so refresh the repayment amount before sending.
Instant Loan Specifics
- Run
quotebeforeinstant-create. - Run
max-borrowfor capacity questions. - Do not represent
max-borrowas recommended borrow size; it is a limit estimate from current data. - For stablecoin borrows such as USDC or USDT against another collateral asset, calculate and show the estimated liquidation price for the collateral asset when quote data is available.
- Requote immediately before creation.
- Explain that if LTV moves too high before the collateral deposit is registered, the loan may not open until more collateral is supplied or may be refunded according to the protocol/app behavior.
- Deposit target opens/adds collateral.
- Repayment target pays debt down.
Repayment And Add Collateral
Repaying reduces debt and improves LTV. Adding collateral increases collateral value and improves LTV. They use different targets.
For instant loans, partial repayment can reduce LTV, but it does not trigger collateral withdrawal. Full collateral release requires the debt to be fully repaid. Always refresh with loan-instructions --action repay before telling the user the full repayment amount.
For instant loans, collateral top-ups can be sent to the collateral deposit target even after the loan has started. This is not repayment; it lowers LTV and improves loan health.
Capacity Answer Shape
When a user asks "how much can I borrow with X BTC?", answer with:
- collateral amount, asset, and chain
- borrow asset and chain
- estimated maximum borrow amount
- current/max LTV used by the quote
- liquidation threshold LTV
- estimated collateral liquidation price when borrowing USDC or USDT against another collateral asset
- current borrowing interest rate/APY if available
- warning that it is live market data and must be requoted before execution
- suggestion to borrow less than the maximum for liquidation buffer
Profile Health
For profile flows, use:
client.positions.getHealthFactor(profileId);
client.positions.getUserPositionSummary(profileId);
client.positions.getUserReserves(profileId);Explain health in user terms: higher health is safer; near liquidation is risky; collateral prices, debt value, and liquidation thresholds determine health.
Liquidation Price Estimate
For a stablecoin borrow against a non-stable collateral asset:
estimated liquidation collateral price =
current collateral price * current LTV / liquidation thresholdUse the SDK quote's current LTV and the collateral pool's liquidationThreshold. Treat the result as an estimate only. It changes as debt accrues, stablecoin prices move, collateral prices move, and protocol/oracle data updates.
Liquidium SDK Methods
This reference captures practical method shapes for Liquidium borrow and supply work. Prefer local installed types when available.
Installation And Client
npm install @liquidium/clientimport { LiquidiumClient, LiquidiumError } from "@liquidium/client";
const client = new LiquidiumClient({});Useful config:
const client = new LiquidiumClient({
environment: "mainnet",
apiBaseUrl: "https://your-service.example.com",
evmRpcUrl: "https://mainnet.infura.io/v3/<key>",
timeoutMs: 30_000,
});Use default config for normal mainnet SDK flows. Add evmRpcUrl or evmPublicClient for ETH contract-interaction supply planning. Add apiBaseUrl only for custom service deployments or methods that rely on the Liquidium API service.
Bundled CLI
Prefer the CLI for quick agent operations:
<skill-path>/scripts/liquidium-borrow --help
<skill-path>/scripts/liquidium-borrow pools
<skill-path>/scripts/liquidium-borrow prices
<skill-path>/scripts/liquidium-borrow max-borrow --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005
<skill-path>/scripts/liquidium-borrow quote --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005 --borrow-amount-decimal 9
<skill-path>/scripts/liquidium-borrow instant-get --ref 8Y9AQQ
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action status
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action repay
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action add-collateral
<skill-path>/scripts/liquidium-borrow repay-instructions --ref 8Y9AQQ
<skill-path>/scripts/liquidium-borrow add-collateral-instructions --ref 8Y9AQQ
<skill-path>/scripts/liquidium-borrow instant-find --address bc1q...
<skill-path>/scripts/liquidium-borrow loan-list
<skill-path>/scripts/liquidium-borrow loan-show --ref 8Y9AQQ
<skill-path>/scripts/liquidium-borrow loan-tx --ref 8Y9AQQ --kind collateral --txid <txid>
<skill-path>/scripts/liquidium-borrow profile-summary --profile-id <profile-id>instant-create creates an accountless loan request and returns transfer targets, but it does not move collateral:
<skill-path>/scripts/liquidium-borrow instant-create \
--collateral-asset BTC \
--borrow-asset USDC \
--collateral-amount-decimal 0.0005 \
--borrow-amount-decimal 9 \
--borrow-destination 0x2222222222222222222222222222222222222222 \
--refund-destination bc1qrefunddestinationEnvironment/config flags:
--api-base-urlorLIQUIDIUM_API_BASE_URL--environment--evm-rpc-urlorLIQUIDIUM_EVM_RPC_URL--timeout-msorLIQUIDIUM_TIMEOUT_MSLIQUIDIUM_CLI_CACHE_DIRfor the auto-installed Node dependency cacheLIQUIDIUM_CLI_DATA_DIRfor local loan records
By default, the CLI installs @liquidium/client into ~/.cache/liquidium-borrow/node on first real use. That cache is not part of the skill package and can be deleted safely.
By default, instant-create, instant-get, and loan-instructions save or update local records under ~/.local/share/liquidium-borrow/loans. That directory is gitignored. Use --no-save to disable local record writes.
For operational workflows, read:
instant-loans.mdfor accountless borrow, repay, add-collateral, wallet-agent, status, and recovery flows.profile-flows.mdfor connected-wallet supply, borrow, repay, withdraw, and portfolio flows.risk-and-liquidation.mdfor LTV, APY, interest, health, and liquidation warnings.
Modules
client.instantLoans: accountless instant loans with generated deposit and repayment targets.client.market: pools, prices, and rates.client.quote: pure quote and LTV helpers.client.activities: activity lists and receipt status.client.accounts: profile lifecycle and wallet/profile lookup.client.lending: profile-based supply, borrow, withdraw, repay, and inflow reporting.client.positions: per-pool positions, summaries, health, and aggregate stats.client.history: user and pool history.
Instant Loans
Use for accountless/headless borrowing.
const loan = await client.instantLoans.create({
collateralPoolId,
borrowPoolId,
collateralAsset: "BTC",
borrowAsset: "USDC",
collateralAmount,
borrowAmount,
ltvMaxBps,
depositWindowSeconds: 3_600n,
borrowDestination: { type: "External", address: borrowAddress },
refundDestination: { type: "External", address: refundAddress },
});Restore and track:
const loan = await client.instantLoans.get({ ref });
const sameLoan = await client.instantLoans.get({ loanId });
const activities = await client.activities.list({ shortRef: ref, filter: "active" });
const status = await client.activities.getStatus({ shortRef: ref, id });For user reassurance after they send collateral, use the CLI shortcut:
<skill-path>/scripts/liquidium-borrow deposit-status --ref 8Y9AQQ --txid <txid>It reads current loan state and all instant-loan activities. Deposit inflow activities can expose status, txid, confirmations, and requiredConfirmations before the simplified loan status becomes active.
Address recovery:
const candidates = await client.instantLoans.findByAddress(address);
const loan = await client.instantLoans.get({ loanId: candidates[0].loanId });Do not display transfer targets from findByAddress(...) candidates. Hydrate first.
Target Formatting
Transfer targets are discriminated unions. Render by target.type.
function formatSupplyTarget(target: { type: string; address?: string; account?: string }) {
if (target.type === "nativeAddress") return target.address;
if (target.type === "icrcAccount") return target.account;
throw new Error(`Unsupported target type: ${target.type}`);
}Also validate target metadata such as poolId, asset, chain, and action when present.
Amounts
SDK amounts are bigint base units.
- BTC: satoshis.
- USDC/USDT: token base units according to the pool's
decimals.
Use Pool.decimals from client.market.listPools() for conversion.
function decimalToBaseUnits(value: string, decimals: number): bigint {
const [whole, fraction = ""] = value.split(".");
const padded = fraction.padEnd(decimals, "0").slice(0, decimals);
return BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(padded || "0");
}
function baseUnitsToDecimal(value: bigint, decimals: number): string {
const scale = 10n ** BigInt(decimals);
const whole = value / scale;
const fraction = (value % scale).toString().padStart(decimals, "0");
return `${whole}.${fraction}`.replace(/\.?0+$/, "");
}LTV Validation
Fetch market inputs, then calculate LTV before creating a loan or enabling signed borrow.
const [pools, prices] = await Promise.all([
client.market.listPools(),
client.market.getAssetPrices(),
]);
const ltv = client.quote.calculateLtv(
{ collateralPoolId, borrowPoolId, collateralAmount, borrowAmount },
pools,
prices
);
if (ltv.validationErrors.length > 0) {
throw new Error(ltv.validationErrors.map((error) => error.message).join(" "));
}calculateLtv(...) is synchronous after market data is fetched.
Instant Loan Lifecycle
awaiting_deposit: showloan.depositTargetand deposit deadline.deposit_detected: keep polling; borrow is processing.active: showloan.repayment.amountandloan.repayment.target.settling: keep polling and avoid duplicate actions.closed: show final state and stop prompting for repayment.
Reload before showing repayment instructions so the amount and target are current.
Use loan-instructions --action repay instead of cached records when giving repayment instructions. Use loan-instructions --action add-collateral when the user wants to lower LTV with more collateral.
For instant loans only:
depositTargetis for initial collateral and later collateral top-ups. Top-ups after the loan starts lower LTV and improve health.repayment.targetis for debt repayment.- The refund destination receives collateral returned after full repayment, or collateral refunds when funds cannot be applied, including failed/late/expired deposit cases.
- Partial repayments can reduce debt/LTV but do not withdraw collateral; full collateral return requires full repayment.
quote, max-borrow, instant-create, and loan-instructions expose risk fields where available:
currentLtv/currentLtvBpsmaxAllowedLtv/maxAllowedLtvBpsliquidationThreshold/liquidationThresholdBpsliquidationEstimate.estimatedLiquidationCollateralPriceUsd
For stablecoin borrows such as USDC or USDT against BTC or another non-stable collateral asset, show the estimated liquidation collateral price as an estimate, not a guarantee.
max-borrow, instant-create, and saved loan records expose current borrow-rate fields where available:
expectedBorrowApyfor user displayrawBorrowingRatefor machine use onlyrateDecimalsfor formatting raw rates
Display the formatted APY/rate to users and never show raw scaled rate integers as percentages.
instant-create and loan-instructions expose deposit timing fields where available:
depositDeadline.depositWindowSecondsdepositDeadline.estimatedDepositDeadlineAtdepositDeadline.estimated
The SDK returns the deposit window length, not a canonical absolute deadline in the hydrated loan object. The CLI estimates the absolute deadline from the local creation timestamp plus depositWindowSeconds; present it as an estimate unless a canonical event timestamp is available.
Profile Creation
Use only for connected-wallet profile flows.
const profileId = await client.accounts.createProfile({
account: walletAddress,
chain: "ETH",
walletAdapter: {
signMessage: async ({ message }) => wallet.signMessage(message),
},
});Prepare/sign/submit variant:
const action = await client.accounts.prepareCreateProfile({ account: walletAddress });
const signature = await wallet.signMessage(action.message);
const profileId = await action.submit({
signature,
chain: "ETH",
account: walletAddress,
});Handle existing profiles by catching the relevant SDK/protocol error and resolving the profile instead of retrying.
Profile-Based Supply And Repay
Use when the app intentionally manages a Liquidium profile.
const supplyFlow = await client.lending.supply({
profileId,
poolId,
action: "deposit",
});
if (supplyFlow.type === "transfer") {
showTarget(supplyFlow.target);
}
await supplyFlow.submit({ txid: "<broadcast-txid>" });Use action: "repayment" for repayment. Do not reuse a deposit target for repayment.
Automated wallet transfer path:
const supplyFlow = await client.lending.supply({
profileId,
poolId,
action: "deposit",
amount,
account: walletAddress,
walletAdapter: {
sendBtcTransaction: async ({ toAddress, amountSats }) =>
wallet.sendBtcTransaction({ toAddress, amountSats }),
},
});ETH contract-interaction path:
const supplyFlow = await client.lending.supply({
mechanism: "contractInteraction",
profileId,
poolId,
action: "deposit",
walletAdapter,
account: evmAddress,
amount,
});This path needs evmRpcUrl or evmPublicClient, plus sendEthTransaction.
Profile-Based Borrow
Use only for persistent profile/dashboard integrations.
const quote = client.quote.getQuote(request, pools, prices);
if (quote.validationErrors.length > 0) {
throw new Error(quote.validationErrors.map((error) => error.message).join(" "));
}
const outflow = await client.lending.borrow({
profileId,
poolId: quote.borrowPoolId,
amount: quote.borrowAmount,
receiverAddress,
signerWalletAddress: walletAddress,
signerChain: "ETH",
signerWalletAdapter: {
signMessage: async ({ message }) => wallet.signMessage(message),
},
});Display outflow.id immediately. outflow.txid may be null until broadcast/settlement is available.
Portfolio
const positions = await client.positions.listPositions(profileId);
const position = await client.positions.getPosition(profileId, poolId);
const healthFactor = await client.positions.getHealthFactor(profileId);
const stats = await client.positions.getUserStats(profileId);
const summary = await client.positions.getUserPositionSummary(profileId);
const reserves = await client.positions.getUserReserves(profileId);
const maxRepay = await client.positions.getMaxRepayAmount(profileId, poolId, 50n);Error Handling
try {
return await client.instantLoans.get({ ref });
} catch (error) {
if (error instanceof LiquidiumError) {
throw new Error(error.message);
}
throw error;
}Use exported LiquidiumErrorCode values when the app needs separate handling for timeout, transport, validation, or protocol errors.
Rate Formatting
Rates and risk ratios can be fixed-point values scaled by rateDecimals, often 27. Divide by 10 ** rateDecimals before percentage formatting.
function formatScaledRatePercent(
scaledRate: bigint,
rateDecimals: bigint,
fractionDigits = 2
): string {
const scale = 10n ** rateDecimals;
const displayScale = 10n ** BigInt(fractionDigits);
const rounded = (scaledRate * 100n * displayScale + scale / 2n) / scale;
const whole = rounded / displayScale;
const fraction = rounded % displayScale;
return `${whole}.${fraction.toString().padStart(fractionDigits, "0")}%`;
}Common Debug Checks
- Pool exists and is not frozen.
- Amounts are base units and use the selected pool's decimals.
ltv.validationErrorsorquote.validationErrorsis empty before execution.- Instant loans use
client.instantLoans, notclient.lending.borrow. - Instant-loan create/get do not require wallet adapters.
loan.refis persisted before showing transfer instructions.- Repayment instructions come from the freshly loaded loan, not cached targets.
- Profile supply/repay targets are action-specific.
- ETH contract-interaction supply has
evmRpcUrlorevmPublicClient. - SDK method names match the installed
@liquidium/clientversion.
#!/usr/bin/env node
import { createRequire } from "node:module";
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const __dirname = dirname(fileURLToPath(import.meta.url));
const skillDir = dirname(__dirname);
const homeDir = process.env.HOME || homedir();
if (!homeDir) {
throw new Error("Cannot determine home directory. Set HOME, LIQUIDIUM_CLI_CACHE_DIR, or LIQUIDIUM_CLI_DATA_DIR.");
}
const cacheDir = process.env.LIQUIDIUM_CLI_CACHE_DIR || join(process.env.XDG_CACHE_HOME || join(homeDir, ".cache"), "liquidium-borrow", "node");
const packageJsonPath = join(cacheDir, "package.json");
const packageLockPath = join(cacheDir, "package-lock.json");
const skillPackageJsonPath = join(skillDir, "package.json");
const skillPackageLockPath = join(skillDir, "package-lock.json");
const dataDir = process.env.LIQUIDIUM_CLI_DATA_DIR || join(process.env.XDG_DATA_HOME || join(homeDir, ".local", "share"), "liquidium-borrow");
const loansDir = join(dataDir, "loans");
const commands = new Set([
"pools",
"prices",
"max-borrow",
"quote",
"instant-create",
"instant-get",
"instant-activities",
"deposit-status",
"instant-find",
"loan-instructions",
"repay-instructions",
"add-collateral-instructions",
"loan-list",
"loan-show",
"loan-tx",
"profile-summary",
"positions",
]);
function printHelp() {
console.log(`Liquidium CLI
Usage:
<skill-path>/scripts/liquidium-borrow <command> [options]
Commands:
pools List Liquidium pools
prices Get asset prices
max-borrow Estimate max borrow amount for collateral
quote Validate/preview LTV for an instant loan
instant-create Create an accountless instant loan
instant-get Restore an instant loan by --ref or --loan-id
instant-activities List activity for an instant loan --ref
deposit-status Check whether Liquidium detected collateral deposit
instant-find Find candidate instant loans by address
loan-instructions Show refreshed status/repay/add-collateral instructions
repay-instructions Shortcut for loan-instructions --action repay
add-collateral-instructions Shortcut for loan-instructions --action add-collateral
loan-list List locally saved instant loan records
loan-show Show a locally saved loan record by --ref
loan-tx Append a collateral or repayment txid to a record
profile-summary Read aggregate profile portfolio summary
positions List profile positions
Common options:
--json Emit raw JSON instead of a compact summary
--api-base-url URL Override Liquidium SDK service URL
--environment NAME SDK environment, default mainnet
--evm-rpc-url URL EVM RPC URL for methods that need it
--no-save Do not write/update local loan records
Examples:
<skill-path>/scripts/liquidium-borrow pools
<skill-path>/scripts/liquidium-borrow max-borrow --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005
<skill-path>/scripts/liquidium-borrow quote --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005 --borrow-amount-decimal 9
<skill-path>/scripts/liquidium-borrow instant-create --collateral-asset BTC --borrow-asset USDC --collateral-amount-decimal 0.0005 --borrow-amount-decimal 9 --borrow-destination 0x2222222222222222222222222222222222222222 --refund-destination bc1qrefunddestination
<skill-path>/scripts/liquidium-borrow instant-get --ref 8Y9AQQ
<skill-path>/scripts/liquidium-borrow deposit-status --ref 8Y9AQQ --txid <txid>
<skill-path>/scripts/liquidium-borrow loan-instructions --ref 8Y9AQQ --action repay
<skill-path>/scripts/liquidium-borrow loan-tx --ref 8Y9AQQ --kind collateral --txid <txid>
`);
}
function parseArgs(argv) {
const args = { _: [] };
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith("--")) {
args._.push(arg);
continue;
}
const key = arg.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
args[key] = true;
} else {
args[key] = next;
i += 1;
}
}
return args;
}
function ensureClientPackage() {
mkdirSync(cacheDir, { recursive: true });
if (existsSync(skillPackageJsonPath) && existsSync(skillPackageLockPath)) {
copyFileSync(skillPackageJsonPath, packageJsonPath);
copyFileSync(skillPackageLockPath, packageLockPath);
} else if (!existsSync(packageJsonPath)) {
throw new Error("Missing packaged Liquidium dependency manifest. Reinstall this skill.");
}
const requireFromCache = createRequire(packageJsonPath);
try {
return requireFromCache("@liquidium/client");
} catch {
const result = spawnSync("npm", ["ci", "--omit=dev", "--silent", "--ignore-scripts", "--no-audit", "--no-fund"], {
cwd: cacheDir,
stdio: "inherit",
});
if (result.status !== 0) {
throw new Error("Failed to install @liquidium/client for the Liquidium CLI.");
}
return requireFromCache("@liquidium/client");
}
}
function clientConfig(options) {
const config = {};
if (options.environment) config.environment = options.environment;
if (options["api-base-url"] || process.env.LIQUIDIUM_API_BASE_URL) {
config.apiBaseUrl = options["api-base-url"] || process.env.LIQUIDIUM_API_BASE_URL;
}
if (options["evm-rpc-url"] || process.env.LIQUIDIUM_EVM_RPC_URL) {
config.evmRpcUrl = options["evm-rpc-url"] || process.env.LIQUIDIUM_EVM_RPC_URL;
}
if (options["timeout-ms"] || process.env.LIQUIDIUM_TIMEOUT_MS) {
config.timeoutMs = Number(options["timeout-ms"] || process.env.LIQUIDIUM_TIMEOUT_MS);
if (!Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0) {
throw new Error("--timeout-ms / LIQUIDIUM_TIMEOUT_MS must be a positive number");
}
}
return config;
}
function parseBigIntOption(options, key, required = true) {
const value = options[key];
if (value === undefined || value === true) {
if (required) throw new Error(`Missing --${key}`);
return undefined;
}
if (!/^\d+$/.test(String(value))) {
throw new Error(`--${key} must be an integer string in base units`);
}
return BigInt(value);
}
function decimalToBaseUnits(value, decimals) {
const raw = String(value);
if (!/^\d+(\.\d+)?$/.test(raw)) {
throw new Error(`Decimal amount must be a non-negative number: ${raw}`);
}
const [whole, fraction = ""] = raw.split(".");
const places = Number(decimals);
if (fraction.length > places) {
throw new Error(`Too many decimal places for asset decimals=${places}: ${raw}`);
}
const padded = fraction.padEnd(places, "0");
return BigInt(whole || "0") * 10n ** BigInt(places) + BigInt(padded || "0");
}
function parseAmountOption(options, key, pool) {
const baseValue = parseBigIntOption(options, key, false);
if (baseValue !== undefined) return baseValue;
const decimalValue = stringOption(options, `${key}-decimal`, false);
if (decimalValue !== undefined) return decimalToBaseUnits(decimalValue, pool.decimals);
throw new Error(`Missing --${key} or --${key}-decimal`);
}
function stringOption(options, key, required = true) {
const value = options[key];
if (value === undefined || value === true || value === "") {
if (required) throw new Error(`Missing --${key}`);
return undefined;
}
return String(value);
}
function bigintReplacer(_key, value) {
return typeof value === "bigint" ? value.toString() : value;
}
function emitJson(value) {
console.log(JSON.stringify(value, bigintReplacer, 2));
}
function safeFilePart(value) {
return String(value).replace(/[^a-zA-Z0-9_.-]/g, "_");
}
function loanRecordPath(refOrLoanId) {
return join(loansDir, `${safeFilePart(refOrLoanId)}.json`);
}
function writeLoanRecord(record) {
mkdirSync(loansDir, { recursive: true });
const ref = record.loan?.ref || record.summary?.ref || record.ref || record.loan?.loanId || record.loan?.id;
if (!ref) throw new Error("Cannot save loan record without ref or loan id");
const path = loanRecordPath(ref);
writeFileSync(path, JSON.stringify(record, bigintReplacer, 2));
return path;
}
function addSecondsIso(iso, seconds) {
const started = new Date(iso);
if (Number.isNaN(started.getTime())) return null;
return new Date(started.getTime() + Number(seconds) * 1000).toISOString();
}
function depositDeadline({ loan, record, createdAt }) {
const anchor = record?.createdAt || createdAt;
const windowSeconds =
loan?.depositWindowSeconds ||
record?.loan?.depositWindowSeconds ||
record?.request?.depositWindowSeconds;
if (!anchor || windowSeconds === undefined || windowSeconds === null) {
return {
depositWindowSeconds: windowSeconds || null,
estimatedDepositDeadlineAt: null,
estimated: true,
};
}
return {
depositWindowSeconds: windowSeconds,
estimatedDepositDeadlineAt: addSecondsIso(anchor, windowSeconds),
estimated: true,
};
}
function readLoanRecord(ref) {
const path = loanRecordPath(ref);
if (!existsSync(path)) throw new Error(`No local loan record found for ${ref}`);
return JSON.parse(readFileSync(path, "utf8"));
}
function readLoanRecordIfExists(ref) {
try {
return readLoanRecord(ref);
} catch {
return null;
}
}
function listLoanRecords() {
if (!existsSync(loansDir)) return [];
return readdirSync(loansDir)
.filter((name) => name.endsWith(".json"))
.sort()
.map((name) => {
const path = join(loansDir, name);
try {
const record = JSON.parse(readFileSync(path, "utf8"));
return {
path,
ref: record.summary?.ref || record.loan?.ref || record.ref,
status: record.summary?.status || record.loan?.status,
collateral: record.collateral,
borrow: record.borrow,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
};
} catch (error) {
return {
path,
error: true,
errorMessage: error.message,
};
}
});
}
function mergeLoanRecord(ref, updates) {
const existing = readLoanRecordIfExists(ref) || {};
const merged = {
...existing,
...updates,
createdAt: existing.createdAt || updates.createdAt,
transactions: updates.transactions || existing.transactions,
request: existing.request || updates.request,
collateral: updates.collateral || existing.collateral,
borrow: updates.borrow || existing.borrow,
transferInstructions:
updates.transferInstructions || existing.transferInstructions,
risk: updates.risk || existing.risk,
rate: updates.rate || existing.rate,
loan: updates.loan || existing.loan,
summary: updates.summary || existing.summary,
};
return writeLoanRecord(merged);
}
function targetText(target) {
if (!target) return null;
if (target.type === "nativeAddress") return target.address;
if (target.type === "icrcAccount") return target.account;
if (target.address) return target.address;
if (target.account) return target.account;
return JSON.stringify(target, bigintReplacer);
}
function toBigInt(value, fallback = 0n) {
if (value === undefined || value === null) return fallback;
return typeof value === "bigint" ? value : BigInt(String(value));
}
function minBigInt(...values) {
return values.reduce((min, value) => (value < min ? value : min));
}
function formatBaseUnits(value, decimals) {
const amount = toBigInt(value);
const places = Number(decimals);
const scale = 10n ** BigInt(places);
const whole = amount / scale;
const fraction = (amount % scale).toString().padStart(places, "0").replace(/0+$/, "");
return fraction ? `${whole}.${fraction}` : whole.toString();
}
function formatBps(value) {
const bps = Number(value);
return `${(bps / 100).toFixed(2)}%`;
}
function formatScaledRatePercent(scaledRate, rateDecimals, fractionDigits = 2) {
const rate = toBigInt(scaledRate);
const scale = 10n ** toBigInt(rateDecimals);
const displayScale = 10n ** BigInt(fractionDigits);
const rounded = (rate * 100n * displayScale + scale / 2n) / scale;
const whole = rounded / displayScale;
const fraction = rounded % displayScale;
return `${whole}.${fraction.toString().padStart(fractionDigits, "0")}%`;
}
function scaledRateToBps(rate, rateDecimals) {
const value = toBigInt(rate);
const scale = 10n ** toBigInt(rateDecimals);
return (value * 10_000n + scale / 2n) / scale;
}
function poolRatioToBps(value) {
const ratio = toBigInt(value);
if (ratio <= 10_000n) return ratio;
return scaledRateToBps(ratio, 27n);
}
function estimateLiquidationPrice({ ltv, collateralPool }) {
const liquidationThresholdBps = poolRatioToBps(collateralPool.liquidationThreshold);
const ltvBps = toBigInt(ltv.ltvBps);
if (ltvBps <= 0n || liquidationThresholdBps <= 0n) return null;
const collateralUsd = Number(ltv.collateralUsd) / 1e8;
const collateralAmount = Number(ltv.collateralAmount);
const decimals = Number(collateralPool.decimals);
if (!Number.isFinite(collateralUsd) || !Number.isFinite(collateralAmount) || collateralAmount <= 0) {
return null;
}
const currentPrice = collateralUsd / (collateralAmount / 10 ** decimals);
const liquidationPrice = currentPrice * (Number(ltvBps) / Number(liquidationThresholdBps));
return {
collateralAsset: collateralPool.asset,
collateralChain: collateralPool.chain,
liquidationThresholdBps,
liquidationThreshold: formatBps(liquidationThresholdBps),
estimatedCurrentCollateralPriceUsd: Number(currentPrice.toFixed(2)),
estimatedLiquidationCollateralPriceUsd: Number(liquidationPrice.toFixed(2)),
note:
"Estimated from current quote data. It changes as prices, debt, interest, and oracle data change.",
};
}
function findPool(pools, asset, chain) {
return pools.find((pool) => {
if (pool.asset !== asset) return false;
if (chain && pool.chain !== chain) return false;
return !pool.frozen;
});
}
async function loadMarket(client) {
return Promise.all([client.market.listPools(), client.market.getAssetPrices()]);
}
async function buildInstantRequest(client, options) {
const collateralAsset = stringOption(options, "collateral-asset");
const borrowAsset = stringOption(options, "borrow-asset");
const collateralChain = stringOption(options, "collateral-chain", false);
const borrowChain = stringOption(options, "borrow-chain", false);
const [pools, prices] = await loadMarket(client);
const collateralPool = findPool(pools, collateralAsset, collateralChain);
const borrowPool = findPool(pools, borrowAsset, borrowChain);
if (!collateralPool) throw new Error(`No non-frozen collateral pool for ${collateralAsset}`);
if (!borrowPool) throw new Error(`No non-frozen borrow pool for ${borrowAsset}`);
const collateralAmount = parseAmountOption(options, "collateral-amount", collateralPool);
const borrowAmount = parseAmountOption(options, "borrow-amount", borrowPool);
const ltv = client.quote.calculateLtv(
{
collateralPoolId: collateralPool.id,
borrowPoolId: borrowPool.id,
collateralAmount,
borrowAmount,
},
pools,
prices
);
const validationErrors = ltv.validationErrors || [];
return {
pools,
prices,
collateralPool,
borrowPool,
ltv,
validationErrors,
request: {
collateralPoolId: collateralPool.id,
borrowPoolId: borrowPool.id,
collateralAsset,
borrowAsset,
collateralAmount,
borrowAmount,
ltvMaxBps: parseBigIntOption(options, "ltv-max-bps", false) || ltv.maxAllowedLtvBps,
depositWindowSeconds:
parseBigIntOption(options, "deposit-window-seconds", false) || 3_600n,
borrowDestination: {
type: "External",
address: stringOption(options, "borrow-destination"),
},
refundDestination: {
type: "External",
address: stringOption(options, "refund-destination"),
},
},
};
}
function summarizeLoan(loan) {
return {
ref: loan.ref,
loanId: loan.loanId || loan.id,
status: loan.status,
depositTarget: targetText(loan.depositTarget),
repaymentAmount: loan.repayment?.amount,
repaymentTarget: targetText(loan.repayment?.target || loan.repayTarget),
position: loan.position,
};
}
function amountDisplayForLoanSide(side, amount) {
if (!side?.decimals || amount === undefined || amount === null) return null;
return formatBaseUnits(amount, side.decimals);
}
function buildLoanInstructions(loan, action, record = null) {
const summary = summarizeLoan(loan);
const collateral = record?.collateral;
const borrow = record?.borrow;
const transfer = record?.transferInstructions || {};
const repayment = loan.repayment || {};
const rate = record?.rate || {};
const repayTarget = targetText(repayment.target || loan.repayTarget);
const depositTarget = targetText(loan.depositTarget);
const repayAmountDisplay = amountDisplayForLoanSide(
borrow,
repayment.amount
);
const base = {
action,
ref: summary.ref,
loanId: summary.loanId,
status: summary.status,
collateral,
borrow,
currentPosition: loan.position,
warning:
"Liquidation risk: if your LTV reaches the liquidation threshold, collateral can be sold to repay the loan.",
refundNote:
"For instant loans, the refund address receives collateral returned after full repayment, or collateral refunds when funds cannot be applied, including failed/late/expired deposits.",
localRecordFound: Boolean(record),
depositDeadline: depositDeadline({ loan, record }),
borrowRate: {
expectedBorrowApy: rate.expectedBorrowApy || null,
rawBorrowingRate: rate.rawBorrowingRate || null,
rateDecimals: rate.rateDecimals || null,
},
};
if (action === "repay") {
return {
...base,
instructions:
"Send the borrowed asset to the repayment target. Refresh this instruction immediately before sending because interest accrues continuously.",
repayAmountBaseUnits: repayment.amount,
repayAmountDisplay,
repayAsset: borrow?.asset || repayment.asset,
repayChain: repayment.chain || borrow?.chain,
repayTarget,
fullRepaymentNote:
"For instant loans, partial repayments reduce debt and may improve LTV, but they do not trigger collateral withdrawal. The loan must be repaid in full to receive the full collateral amount back.",
};
}
if (action === "add-collateral") {
return {
...base,
instructions:
"Send additional collateral asset to the collateral deposit target to lower LTV and improve loan health. This also works after the loan has started. Do not send repayment funds to this target.",
collateralAsset: collateral?.asset || transfer.sendCollateralAsset,
collateralChain: collateral?.chain || transfer.sendCollateralChain,
depositTarget,
originalCollateralAmount: transfer.sendCollateralAmount,
originalCollateralAmountBaseUnits: transfer.sendCollateralAmountBaseUnits,
nextStep:
transfer.sendCollateralAmount && depositTarget
? `Send ${transfer.sendCollateralAmount} ${transfer.sendCollateralAsset || ""} to ${depositTarget}`.trim()
: null,
};
}
return {
...base,
instructions: "Current refreshed loan state.",
depositTarget,
repayAmountBaseUnits: repayment.amount,
repayAmountDisplay,
repayTarget,
nextStep:
transfer.sendCollateralAmount && depositTarget
? `Send ${transfer.sendCollateralAmount} ${transfer.sendCollateralAsset || ""} to ${depositTarget}`.trim()
: null,
};
}
function summarizeMarketSide(pool, amount) {
return {
asset: pool.asset,
chain: pool.chain,
poolId: pool.id,
decimals: pool.decimals,
amountBaseUnits: amount,
amountDisplay: formatBaseUnits(amount, pool.decimals),
};
}
function buildDepositStatus({ loan, activities, record = null, txid = null }) {
const summary = summarizeLoan(loan);
const depositTarget = targetText(loan.depositTarget);
const depositActivities = activities.filter(
(activity) =>
activity.direction === "inflow" && activity.kind === "deposit"
);
const matchingActivities = txid
? depositActivities.filter((activity) => {
const txids = [
activity.txid,
...(Array.isArray(activity.txids) ? activity.txids : []),
].filter(Boolean);
return txids.includes(txid);
})
: [];
const bestActivity =
matchingActivities[0] ||
depositActivities.find((activity) => activity.status !== "failed") ||
depositActivities[0] ||
null;
const detected =
depositActivities.length > 0 ||
summary.status !== "awaiting_deposit" ||
toBigInt(loan.position?.collateralAmount, 0n) > 0n;
const processing =
detected &&
!["active", "settling", "closed"].includes(String(summary.status));
return {
ref: summary.ref,
loanId: summary.loanId,
loanStatus: summary.status,
depositDetected: detected,
depositProcessing: processing,
depositTarget,
expectedCollateral: record?.collateral || loan.collateral,
depositDeadline: depositDeadline({ loan, record }),
checkedTxid: txid,
txidMatchedByLiquidium: txid ? matchingActivities.length > 0 : null,
bestDepositActivity: bestActivity,
depositActivities,
confirmations: bestActivity
? {
current: bestActivity.confirmations,
required: bestActivity.requiredConfirmations,
}
: null,
note: detected
? "Liquidium has detected a collateral deposit. Keep polling until the loan becomes active or closed."
: "Liquidium has not surfaced a collateral deposit yet. It can take a few minutes after the Bitcoin transaction broadcasts or confirms before the SDK activity feed updates.",
};
}
function summarizeLoanDetails(loan, built) {
const summary = summarizeLoan(loan);
const createdAt = new Date().toISOString();
const borrowRate = built.borrowPool.borrowingRate
? formatScaledRatePercent(built.borrowPool.borrowingRate, built.borrowPool.rateDecimals)
: null;
return {
summary,
createdAt,
depositDeadline: depositDeadline({ loan, createdAt }),
collateral: summarizeMarketSide(built.collateralPool, built.request.collateralAmount),
borrow: summarizeMarketSide(built.borrowPool, built.request.borrowAmount),
risk: {
currentLtvBps: built.ltv.ltvBps,
currentLtv: formatBps(built.ltv.ltvBps),
maxAllowedLtvBps: built.ltv.maxAllowedLtvBps,
maxAllowedLtv: formatBps(built.ltv.maxAllowedLtvBps),
liquidationThresholdBps: poolRatioToBps(
built.collateralPool.liquidationThreshold
),
liquidationThreshold: formatBps(
poolRatioToBps(built.collateralPool.liquidationThreshold)
),
liquidationEstimate: estimateLiquidationPrice({
ltv: built.ltv,
collateralPool: built.collateralPool,
}),
nearMaxLtv: Number(built.ltv.ltvBps) >= Number(built.ltv.maxAllowedLtvBps) * 0.9,
},
rate: {
expectedBorrowApy: borrowRate,
rawBorrowingRate: built.borrowPool.borrowingRate,
rateDecimals: built.borrowPool.rateDecimals,
},
transferInstructions: {
sendCollateralAmount: formatBaseUnits(built.request.collateralAmount, built.collateralPool.decimals),
sendCollateralAmountBaseUnits: built.request.collateralAmount,
sendCollateralAsset: built.collateralPool.asset,
sendCollateralChain: built.collateralPool.chain,
sendCollateralTo: summary.depositTarget,
receiveBorrowAmount: formatBaseUnits(built.request.borrowAmount, built.borrowPool.decimals),
receiveBorrowAmountBaseUnits: built.request.borrowAmount,
receiveBorrowAsset: built.borrowPool.asset,
receiveBorrowChain: built.borrowPool.chain,
receiveBorrowTo: built.request.borrowDestination.address,
refundCollateralTo: built.request.refundDestination.address,
nextStep: `Send ${formatBaseUnits(built.request.collateralAmount, built.collateralPool.decimals)} ${built.collateralPool.asset} to ${summary.depositTarget}`,
},
};
}
async function buildMaxBorrow(client, options) {
const collateralAsset = stringOption(options, "collateral-asset");
const borrowAsset = stringOption(options, "borrow-asset");
const collateralChain = stringOption(options, "collateral-chain", false);
const borrowChain = stringOption(options, "borrow-chain", false);
const [pools, prices] = await loadMarket(client);
const collateralPool = findPool(pools, collateralAsset, collateralChain);
const borrowPool = findPool(pools, borrowAsset, borrowChain);
if (!collateralPool) throw new Error(`No non-frozen collateral pool for ${collateralAsset}`);
if (!borrowPool) throw new Error(`No non-frozen borrow pool for ${borrowAsset}`);
const collateralAmount = parseAmountOption(options, "collateral-amount", collateralPool);
const rawAvailable = toBigInt(borrowPool.availableLiquidity, 0n);
const borrowCapRemaining =
borrowPool.borrowCap && borrowPool.totalDebt
? toBigInt(borrowPool.borrowCap) - toBigInt(borrowPool.totalDebt)
: rawAvailable;
let high = minBigInt(rawAvailable, borrowCapRemaining > 0n ? borrowCapRemaining : rawAvailable);
let low = 0n;
let best = 0n;
let bestLtv = null;
let maxAllowedLtvBps = parseBigIntOption(options, "target-ltv-bps", false);
while (low <= high) {
const mid = (low + high) / 2n;
const ltv = client.quote.calculateLtv(
{
collateralPoolId: collateralPool.id,
borrowPoolId: borrowPool.id,
collateralAmount,
borrowAmount: mid,
},
pools,
prices
);
const validationErrors = ltv.validationErrors || [];
const targetBps = maxAllowedLtvBps || toBigInt(ltv.maxAllowedLtvBps);
if (validationErrors.length === 0 && toBigInt(ltv.ltvBps) <= targetBps) {
best = mid;
bestLtv = ltv;
maxAllowedLtvBps = targetBps;
low = mid + 1n;
} else {
high = mid - 1n;
}
}
return {
collateral: summarizeMarketSide(collateralPool, collateralAmount),
borrow: summarizeMarketSide(borrowPool, best),
maxBorrowAmountBaseUnits: best,
maxBorrowAmountDisplay: formatBaseUnits(best, borrowPool.decimals),
currentLtvBps: bestLtv?.ltvBps || "0",
currentLtv: bestLtv ? formatBps(bestLtv.ltvBps) : "0.00%",
maxAllowedLtvBps,
maxAllowedLtv: formatBps(maxAllowedLtvBps || 0),
liquidationThresholdBps: poolRatioToBps(collateralPool.liquidationThreshold),
liquidationThreshold: formatBps(
poolRatioToBps(collateralPool.liquidationThreshold)
),
liquidationEstimate: bestLtv
? estimateLiquidationPrice({ ltv: bestLtv, collateralPool })
: null,
expectedBorrowApy: borrowPool.borrowingRate
? formatScaledRatePercent(borrowPool.borrowingRate, borrowPool.rateDecimals)
: null,
note: "This is based on current SDK market data and is not a guarantee of execution. Requote before creating a loan.",
};
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const command = options._[0];
if (!command || options.help || options.h) {
printHelp();
return;
}
if (!commands.has(command)) {
throw new Error(`Unknown command: ${command}`);
}
if (command === "loan-list") {
return emitJson(listLoanRecords());
}
if (command === "loan-show") {
const ref = stringOption(options, "ref");
return emitJson(readLoanRecord(ref));
}
if (command === "loan-tx") {
const ref = stringOption(options, "ref");
const txid = stringOption(options, "txid");
const kind = stringOption(options, "kind", false) || "collateral";
const chain = stringOption(options, "chain", false);
const record = readLoanRecord(ref);
const entry = {
kind,
txid,
chain,
recordedAt: new Date().toISOString(),
};
record.transactions = [...(record.transactions || []), entry];
record.updatedAt = entry.recordedAt;
const path = writeLoanRecord(record);
return emitJson({ path, transaction: entry });
}
const { LiquidiumClient, LiquidiumError } = ensureClientPackage();
const client = new LiquidiumClient(clientConfig(options));
try {
if (command === "pools") {
const pools = await client.market.listPools();
if (options.json) return emitJson(pools);
for (const pool of pools) {
console.log(
`${pool.asset}${pool.chain ? `/${pool.chain}` : ""} id=${pool.id} decimals=${pool.decimals} frozen=${Boolean(pool.frozen)}`
);
}
return;
}
if (command === "prices") {
return emitJson(await client.market.getAssetPrices());
}
if (command === "max-borrow") {
return emitJson(await buildMaxBorrow(client, options));
}
if (command === "quote") {
const built = await buildInstantRequest(client, {
...options,
"borrow-destination": options["borrow-destination"] || "0x0000000000000000000000000000000000000000",
"refund-destination": options["refund-destination"] || "bc1qplaceholder",
});
const result = {
collateralPool: built.collateralPool,
borrowPool: built.borrowPool,
ltv: built.ltv,
risk: {
currentLtvBps: built.ltv.ltvBps,
currentLtv: formatBps(built.ltv.ltvBps),
maxAllowedLtvBps: built.ltv.maxAllowedLtvBps,
maxAllowedLtv: formatBps(built.ltv.maxAllowedLtvBps),
liquidationThresholdBps: poolRatioToBps(
built.collateralPool.liquidationThreshold
),
liquidationThreshold: formatBps(
poolRatioToBps(built.collateralPool.liquidationThreshold)
),
liquidationEstimate: estimateLiquidationPrice({
ltv: built.ltv,
collateralPool: built.collateralPool,
}),
},
executable: built.validationErrors.length === 0,
validationErrors: built.validationErrors,
};
return emitJson(result);
}
if (command === "instant-create") {
const built = await buildInstantRequest(client, options);
if (built.validationErrors.length > 0) {
emitJson({ ok: false, validationErrors: built.validationErrors });
process.exitCode = 2;
return;
}
const loan = await client.instantLoans.create(built.request);
const details = summarizeLoanDetails(loan, built);
const record = {
type: "instant-loan",
createdAt: details.createdAt,
updatedAt: details.createdAt,
request: built.request,
loan,
...details,
};
const path = options["no-save"] ? null : writeLoanRecord(record);
if (options.json) return emitJson({ ...record, localRecordPath: path });
const summary = details.summary;
console.log(`ref: ${summary.ref}`);
console.log(`local record: ${path || "not saved"}`);
console.log(`status: ${summary.status}`);
console.log(`deposit window seconds: ${details.depositDeadline.depositWindowSeconds ?? ""}`);
console.log(`estimated deposit deadline: ${details.depositDeadline.estimatedDepositDeadlineAt ?? ""}`);
console.log(`deposit amount: ${details.transferInstructions.sendCollateralAmount} ${details.transferInstructions.sendCollateralAsset} on ${details.transferInstructions.sendCollateralChain}`);
console.log(`deposit target: ${details.transferInstructions.sendCollateralTo}`);
console.log(`borrow amount: ${details.transferInstructions.receiveBorrowAmount} ${details.transferInstructions.receiveBorrowAsset} on ${details.transferInstructions.receiveBorrowChain}`);
console.log(`borrow destination: ${details.transferInstructions.receiveBorrowTo}`);
console.log(`refund destination: ${details.transferInstructions.refundCollateralTo}`);
console.log(`current ltv: ${details.risk.currentLtv} (max ${details.risk.maxAllowedLtv})`);
console.log(`liquidation threshold: ${details.risk.liquidationThreshold}`);
if (details.risk.liquidationEstimate) {
console.log(`estimated liquidation ${details.risk.liquidationEstimate.collateralAsset} price: $${details.risk.liquidationEstimate.estimatedLiquidationCollateralPriceUsd}`);
}
console.log(`expected borrow apy: ${details.rate.expectedBorrowApy ?? ""}`);
console.log(`repayment amount: ${summary.repaymentAmount ?? ""}`);
console.log(`repayment target: ${summary.repaymentTarget ?? ""}`);
console.log(`NEXT STEP: ${details.transferInstructions.nextStep}`);
return;
}
if (command === "instant-get") {
const ref = stringOption(options, "ref", false);
const loanId = stringOption(options, "loan-id", false);
if (!ref && !loanId) throw new Error("Pass --ref or --loan-id");
const loan = await client.instantLoans.get(ref ? { ref } : { loanId: BigInt(loanId) });
if (!options["no-save"]) {
const summary = summarizeLoan(loan);
mergeLoanRecord(summary.ref || ref || loanId, {
type: "instant-loan",
updatedAt: new Date().toISOString(),
ref: summary.ref || ref || loanId,
loan,
summary,
});
}
return options.json ? emitJson(loan) : emitJson(summarizeLoan(loan));
}
if (
command === "loan-instructions" ||
command === "repay-instructions" ||
command === "add-collateral-instructions"
) {
const ref = stringOption(options, "ref");
const action =
command === "repay-instructions"
? "repay"
: command === "add-collateral-instructions"
? "add-collateral"
: stringOption(options, "action", false) || "status";
if (!["status", "repay", "add-collateral"].includes(action)) {
throw new Error("--action must be status, repay, or add-collateral");
}
const loan = await client.instantLoans.get({ ref });
const summary = summarizeLoan(loan);
const existing = readLoanRecordIfExists(summary.ref || ref);
const instructions = buildLoanInstructions(loan, action, existing);
const path = options["no-save"]
? null
: mergeLoanRecord(summary.ref || ref, {
type: "instant-loan",
updatedAt: new Date().toISOString(),
ref: summary.ref || ref,
loan,
summary,
latestInstructions: instructions,
});
return emitJson({ ...instructions, localRecordPath: path });
}
if (command === "instant-activities") {
const ref = stringOption(options, "ref");
const filter = stringOption(options, "filter", false);
const activities = await client.activities.list({
shortRef: ref,
...(filter ? { filter } : {}),
});
return emitJson(activities);
}
if (command === "deposit-status") {
const ref = stringOption(options, "ref");
const txid = stringOption(options, "txid", false);
const [loan, activities] = await Promise.all([
client.instantLoans.get({ ref }),
client.activities.list({ shortRef: ref, filter: "all" }),
]);
const summary = summarizeLoan(loan);
const record = readLoanRecordIfExists(summary.ref || ref);
const status = buildDepositStatus({ loan, activities, record, txid });
const updates = {
type: "instant-loan",
updatedAt: new Date().toISOString(),
ref: summary.ref || ref,
loan,
summary,
latestDepositStatus: status,
};
if (txid) {
const existingTransactions =
record && Array.isArray(record.transactions) ? record.transactions : [];
const hasTxid = existingTransactions.some(
(transaction) =>
transaction.txid === txid && (transaction.kind || "collateral") === "collateral"
);
updates.transactions = hasTxid
? existingTransactions
: [
...existingTransactions,
{
kind: "collateral",
txid,
recordedAt: new Date().toISOString(),
},
];
}
const path = options["no-save"]
? null
: mergeLoanRecord(summary.ref || ref, updates);
return emitJson({ ...status, localRecordPath: path });
}
if (command === "instant-find") {
const address = stringOption(options, "address");
const candidates = await client.instantLoans.findByAddress(address);
return emitJson(candidates);
}
if (command === "profile-summary") {
const profileId = stringOption(options, "profile-id");
const [summary, reserves, healthFactor] = await Promise.all([
client.positions.getUserPositionSummary(profileId),
client.positions.getUserReserves(profileId),
client.positions.getHealthFactor(profileId),
]);
return emitJson({ summary, reserves, healthFactor });
}
if (command === "positions") {
const profileId = stringOption(options, "profile-id");
return emitJson(await client.positions.listPositions(profileId));
}
} catch (error) {
if (LiquidiumError && error instanceof LiquidiumError) {
throw new Error(error.message);
}
throw error;
}
}
main().catch((error) => {
console.error(`error: ${error.message}`);
process.exit(1);
});
#!/usr/bin/env sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
exec node "$SCRIPT_DIR/liquidium_cli.mjs" "$@"